Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chore/celo upgrade rebased #8185

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-boats-chew.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ledgerhq/live-common": patch
---

Upgrade @celo/\* dependencies for compatibility with Celo as Layer 2
12 changes: 1 addition & 11 deletions .pnpmfile.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -63,19 +63,9 @@ function readPackage(pkg, context) {
webpack: "*",
}),
/* @celo/* packages */
addDependencies(/@celo\/(?!base)+/, { "@celo/base": `^${pkg.version}` }),
addDependencies("@celo/connect", {
"@celo/base": `^${pkg.version}`,
"web3-eth-contract": pkg.peerDependencies?.web3 ?? "*",
"web3": pkg.peerDependencies?.web3 ?? "1.10",
}),
addDependencies("@celo/contractkit", {
"web3-utils": pkg.dependencies?.["web3"],
}),
addDependencies("@celo/utils", {
"fp-ts": "*",
rlp: "*",
}),
/* @cosmjs/* packages */
addDependencies("@cosmjs/proto-signing", {
"@cosmjs/crypto": pkg.version,
"@cosmjs/encoding": pkg.version,
Expand Down
6 changes: 6 additions & 0 deletions apps/ledger-live-desktop/jest.polyfills.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Object.defineProperties(globalThis, {
});

const { Blob, File } = require("node:buffer");
// Note: this polyfill depends on the patch buffer@6.0.3 which adds the Uint8
// subarray logic. It's the same as in ledger-live-mobile
// Furthermore, importing 'buffer' gets translated to 'node:buffer' so we're
// using a relative path here
const { Buffer } = require("./node_modules/buffer");
const { fetch, Headers, FormData, Request, Response } = require("undici");

Object.defineProperties(globalThis, {
Expand All @@ -20,4 +25,5 @@ Object.defineProperties(globalThis, {
FormData: { value: FormData },
Request: { value: Request },
Response: { value: Response },
Buffer: { value: Buffer },
});
1 change: 1 addition & 0 deletions apps/ledger-live-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"@xstate/react": "1.6.3",
"allure-commandline": "2.28.0",
"bignumber.js": "9.1.2",
"buffer": "6.0.3",
"chart.js": "2.9.4",
"color": "4.2.3",
"dotenv": "16.4.5",
Expand Down
3 changes: 3 additions & 0 deletions apps/ledger-live-desktop/src/internal/jsdom.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test("has Buffer & Uint8array equivalency", () => {
expect(Buffer.from("").subarray() instanceof Uint8Array).toBeTruthy();
});
17 changes: 17 additions & 0 deletions libs/ledger-live-common/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,29 @@ if (process.env.CI) {
reporters.push("github-actions");
}

// interface Uint8ArrayAlloc extends Uint8Array {
// alloc(size: number): Uint8Array;
// }
// class U8a extends Uint8Array {
// alloc(size: number) {
// return new Uint8Array(size);
// }
// }
// Object.defineProperty(Uint8Array, "alloc", {
// writable: false,
// value: function (size: number) {
// return new Uint8Array(size);
// },
// });
Object.setPrototypeOf(Buffer, Uint8Array);

const defaultConfig = {
preset: "ts-jest",
globals: {
"ts-jest": {
isolatedModules: true,
},
// Buffer: U8a,
},
testEnvironment: "node",
reporters,
Expand Down
10 changes: 5 additions & 5 deletions libs/ledger-live-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -121,11 +121,11 @@
"dependencies": {
"@blooo/hw-app-acre": "^1.0.1",
"@cardano-foundation/ledgerjs-hw-app-cardano": "^7.1.2",
"@celo/connect": "^3.0.1",
"@celo/contractkit": "^3.0.1",
"@celo/utils": "^3.0.1",
"@celo/wallet-base": "^3.0.1",
"@celo/wallet-ledger": "^3.0.1",
"@celo/connect": "^6.0.2",
"@celo/contractkit": "^8.3.0",
"@celo/utils": "^7.0.0",
"@celo/wallet-base": "^6.0.2",
"@celo/wallet-ledger": "^6.0.2",
"@crypto-org-chain/chain-jslib": "1.1.2",
"@dfinity/agent": "^0.21.0",
"@dfinity/candid": "^0.21.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,6 @@ const buildTransaction = async (account: CeloAccount, transaction: Transaction)
...celoTransaction,
chainId: await kit.connection.chainId(),
nonce: await kit.connection.nonce(account.freshAddress),
gasPrice: await kit.connection.gasPrice(),
};

return tx;
Expand Down
111 changes: 84 additions & 27 deletions libs/ledger-live-common/src/families/celo/hw-app-celo.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,92 @@
import type Transport from "@ledgerhq/hw-transport";
/* eslint-disable no-console */
import Eth from "@ledgerhq/hw-app-eth";
import {
tokenInfoByAddressAndChainId,
legacyTokenInfoByAddressAndChainId,
} from "@celo/wallet-ledger/lib/tokens";
import {
encode_deprecated_celo_legacy_type_only_for_temporary_ledger_compat,
rlpEncodedTx,
LegacyEncodedTx,
} from "@celo/wallet-base";
import type { CeloTx, RLPEncodedTx } from "@celo/connect";
import { celoKit } from "./api/sdk";
import { decode, encode } from "rlp";

/**
* Heavily inspiried by celo-web-wallet
* https://github.com/celo-tools/celo-web-wallet/blob/master/src/features/ledger/CeloLedgerApp.ts
*/
export class CeloApp {
transport: Transport;
import SemVer from "semver";
import { LedgerEthTransactionResolution } from "@ledgerhq/hw-app-eth/lib/services/types";

constructor(transport: any) {
this.transport = transport;
}
export default class Celo extends Eth {
private config?: Promise<{ version: string }>;

async signTransaction(
path: string,
rawTxHex: string,
resolution?: LedgerEthTransactionResolution | null,
): Promise<{ s: string; v: string; r: string }> {
if (await this.isAppModern()) {
return super.signTransaction(path, rawTxHex, resolution);
}
return this.__dangerous__signTransactionLegacy(path, rawTxHex);
}

// celo-spender-app below version 1.2.3 used a different private key to validate erc20 token info.
// this legacy version of the app also only supported celo type 0 transactions.
// if you are reading this after celo moved to op based L2 those celo type 0 transactions will no longer work
// so you can safely remove all the legacy paths.
async verifyTokenInfo(to: string, chainId: number): Promise<void> {
const isModern = await this.isAppModern();
const tokenInfo = isModern
? tokenInfoByAddressAndChainId(to!, chainId!)
: legacyTokenInfoByAddressAndChainId(to!, chainId!);

if (tokenInfo) {
// celo-spender-app below version 1.2.3 expected unprefixed hex strings only
const dataString = isModern
? `0x${tokenInfo.data.toString("hex")}`
: tokenInfo.data.toString("hex");
await this.provideERC20TokenInformation(dataString);
}
}

async determinePrice(txParams: CeloTx): Promise<void> {
const isModern = await this.isAppModern();
const {
connection: { setFeeMarketGas, gasPrice },
} = celoKit();

if (isModern) {
await setFeeMarketGas(txParams);
} else {
txParams.gasPrice = await gasPrice();
}
}

async rlpEncodedTxForLedger(txParams: CeloTx): Promise<RLPEncodedTx | LegacyEncodedTx> {
const isModern = await this.isAppModern();

// if the app is of minimum version it doesnt matter if chain is cel2 or not
if (isModern) {
return rlpEncodedTx(txParams);
} else {
return encode_deprecated_celo_legacy_type_only_for_temporary_ledger_compat(txParams);
}
}

async isAppModern(): Promise<boolean> {
if (!this.config) {
this.config = this.getAppConfiguration();
}

return SemVer.satisfies((await this.config).version, ">= 1.2.3");
}

// this works for celo-legacy
// this is code written a long time ago in a galaxy far far away
// do not touch (pretty please)
private async __dangerous__signTransactionLegacy(
path: string,
rawTxHex: string,
): Promise<{
s: string;
v: string;
Expand All @@ -27,6 +99,7 @@ export class CeloApp {

const rlpTx = decode(rawTx);
let rlpOffset = 0;
// this seem specific to tx type
if (rlpTx.length > 6) {
const rlpVrs = encode(rlpTx.slice(-3));
rlpOffset = rawTx.length - (rlpVrs.length - 1);
Expand All @@ -50,7 +123,7 @@ export class CeloApp {
} else {
rawTx.copy(buffer, 0, offset, offset + chunkSize);
}

console.info("buffer buffer buffer", buffer.toString());
response = await this.transport
.send(0xe0, 0x04, first ? 0x00 : 0x80, 0x00, buffer)
.catch(e => {
Expand All @@ -65,10 +138,6 @@ export class CeloApp {
const s = response.slice(1 + 32, 1 + 32 + 32).toString("hex");
return { v, r, s };
}

provideERC20TokenInformation({ data }: { data: Buffer }): Promise<boolean> {
return provideERC20TokenInformation(this.transport, data);
}
}

function splitPath(path: string): number[] {
Expand All @@ -86,15 +155,3 @@ function splitPath(path: string): number[] {
});
return result;
}

function provideERC20TokenInformation(transport: Transport, data: Buffer): Promise<boolean> {
return transport.send(0xe0, 0x0a, 0x00, 0x00, data).then(
() => true,
e => {
if (e && e.statusCode === 0x6d00) {
return false;
}
throw e;
},
);
}
65 changes: 53 additions & 12 deletions libs/ledger-live-common/src/families/celo/signOperation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
/* eslint-disable no-console */
import { BigNumber } from "bignumber.js";
import { Observable } from "rxjs";
import { FeeNotLoaded } from "@ledgerhq/errors";
import type { AccountBridge } from "@ledgerhq/types-live";
import { rlpEncodedTx, encodeTransaction } from "@celo/wallet-base";
import { tokenInfoByAddressAndChainId } from "@celo/wallet-ledger/lib/tokens";
import { encodeTransaction, recoverTransaction } from "@celo/wallet-base";

import { buildOptimisticOperation } from "./buildOptimisticOperation";
import type { Transaction, CeloAccount } from "./types";
import { withDevice } from "../../hw/deviceAccess";
import buildTransaction from "./buildTransaction";
import { CeloApp } from "./hw-app-celo";
import Celo from "./hw-app-celo";

/**
* Sign Transaction with Ledger hardware
Expand All @@ -28,31 +29,42 @@ export const signOperation: AccountBridge<Transaction, CeloAccount>["signOperati
throw new FeeNotLoaded();
}

const celo = new CeloApp(transport);
const celo = new Celo(transport);
const unsignedTransaction = await buildTransaction(account, transaction);
const { chainId, to } = unsignedTransaction;
const rlpEncodedTransaction = rlpEncodedTx(unsignedTransaction);

const tokenInfo = tokenInfoByAddressAndChainId(to!, chainId!);
if (tokenInfo) {
await celo.provideERC20TokenInformation(tokenInfo);
}
await Promise.all([
celo.verifyTokenInfo(to!, chainId!),
celo.determinePrice(unsignedTransaction),
]);

o.next({ type: "device-signature-requested" });
const rlpEncodedTransaction = await celo.rlpEncodedTxForLedger(unsignedTransaction);

o.next({ type: "device-signature-requested" });
// celo.signTransaction already does the eip155 v chain stuff
// TODO should we be using clearSignTransaction like evm fam does?
const response = await celo.signTransaction(
account.freshAddressPath,
trimLeading0x(rlpEncodedTransaction.rlpEncode),
);
// freshAddressPath is actually a derivation path
const { address } = await celo.getAddress(account.freshAddressPath);

if (cancelled) return;

const signature = parseSigningResponse(response, chainId!);
const signature = parseSigningResponse(response, chainId!, await celo.isAppModern());

o.next({ type: "device-signature-granted" });

const encodedTransaction = await encodeTransaction(rlpEncodedTransaction, signature);

const [_, recoveredAddress] = recoverTransaction(encodedTransaction.raw);
if (recoveredAddress !== address) {
throw new Error(
"celo: there was a signing error, the recovered address doesn't match the your ledger address, the operation was cancelled",
);
}

const operation = buildOptimisticOperation(
account,
transaction,
Expand Down Expand Up @@ -81,13 +93,18 @@ export const signOperation: AccountBridge<Transaction, CeloAccount>["signOperati

const trimLeading0x = (input: string) => (input.startsWith("0x") ? input.slice(2) : input);

// this is where it all goes wrong
// we need to to the conversion from string to number / Buffer. the v seems clearly wrong.
// Buffers im less sure of. might need to do 0x prefix before converting
// v keeps on parsing as 8.123437e1341 or some bs. which seems wrong. it should be an INT. not a decimal.
const parseSigningResponse = (
response: {
s: string;
v: string;
r: string;
},
chainId: number,
isModern: boolean,
): {
s: Buffer;
v: number;
Expand All @@ -96,7 +113,12 @@ const parseSigningResponse = (
// EIP155
const sigV = parseInt(response.v, 16);
let eip155V = chainId * 2 + 35;
if (sigV !== eip155V && (sigV & eip155V) !== sigV) {

if (isModern) {
// eip1559 and other enveloppes txs dont need to modify V
// just use what the ledger device returns
eip155V = sigV;
} else if (sigV !== eip155V && (sigV & eip155V) !== sigV) {
eip155V += 1;
}

Expand All @@ -108,3 +130,22 @@ const parseSigningResponse = (
};

export default signOperation;

// copied from hw-eth
export const applyEIP155 = (vAsHex: string, chainId: number): number => {
const v = parseInt(vAsHex, 16);

if (v === 0 || v === 1) {
// if v is 0 or 1, it's already representing parity
return chainId * 2 + 35 + v;
} else if (v === 27 || v === 28) {
const parity = v - 27; // transforming v into 0 or 1 to become the parity
return chainId * 2 + 35 + parity;
}
// When chainId is lower than 109, hw-app-eth *can* return a v with EIP155 already applied
// e.g. bsc's chainId is 56 -> v then equals to 147/148
// optimism's chainId is 10 -> v equals to 55/56
// ethereum's chainId is 1 -> v equals to 0/1
// goerli's chainId is 5 -> v equals to 0/1
return v;
};
Loading
Loading