From 80966eb85e828b20386d5485292889526c3ca430 Mon Sep 17 00:00:00 2001 From: Nicolas Brugneaux Date: Fri, 15 Nov 2024 14:43:59 +0100 Subject: [PATCH] chore: ledger-v backport --- packages/viem-account-ledger/.gitignore | 3 +- packages/viem-account-ledger/package.json | 20 +- .../src/ledger-to-account.test.ts | 209 ++++++++++++++---- .../src/ledger-to-account.ts | 2 +- .../viem-account-ledger/src/test-utils.ts | 6 +- packages/viem-account-ledger/src/tokens.ts | 4 +- packages/viem-account-ledger/src/utils.ts | 14 +- packages/viem-account-ledger/tsconfig.json | 2 - yarn.lock | 37 +++- 9 files changed, 230 insertions(+), 67 deletions(-) diff --git a/packages/viem-account-ledger/.gitignore b/packages/viem-account-ledger/.gitignore index 7fabe89f6..35b15fe6c 100644 --- a/packages/viem-account-ledger/.gitignore +++ b/packages/viem-account-ledger/.gitignore @@ -1,4 +1,5 @@ lib/ +lib-es/ tmp/ .tmp/ -.env \ No newline at end of file +.env diff --git a/packages/viem-account-ledger/package.json b/packages/viem-account-ledger/package.json index 71daa7cf5..66e238324 100644 --- a/packages/viem-account-ledger/package.json +++ b/packages/viem-account-ledger/package.json @@ -3,10 +3,9 @@ "version": "1.0.0-beta.1", "description": "Helper library to make ledger<->viem interactions easier", "type": "module", - "exports": { - ".": "./lib/index.js" - }, - "types": "./lib/index.d.ts", + "main": "lib/index.js", + "module": "lib-es/index.js", + "types": "lib/index.d.ts", "author": "cLabs", "license": "Apache-2.0", "homepage": "https://docs.celo.org/developer/tools", @@ -18,12 +17,14 @@ "ledger" ], "scripts": { - "build": "yarn run --top-level tsc -b .", - "clean": "yarn run --top-level tsc -b . --clean", - "docs": "yarn run --top-level typedoc", + "build": "yarn build:cjs && yarn build:esm", + "build:cjs": "yarn --top-level run tsc -m commonjs", + "build:esm": "yarn --top-level run tsc -m ES6 --outDir lib-es", + "clean": "yarn --top-level run tsc -b . --clean && yarn run rimraf lib lib-es", + "docs": "yarn --top-level run typedoc", "test": "yarn run vitest", - "lint": "yarn run --top-level eslint -c .eslintrc.cjs ", - "prepublishOnly": "yarn build" + "lint": "yarn --top-level run eslint -c .eslintrc.cjs ", + "prepublishOnly": "yarn clean && yarn build" }, "peerDependencies": { "@ledgerhq/hw-transport-node-hid": "^6.x", @@ -45,6 +46,7 @@ "@ledgerhq/hw-transport-node-hid": "^6.29.5", "@vitest/coverage-v8": "2.1.2", "dotenv": "^8.2.0", + "rimraf": "^4.4.1", "viem": "^2.21.14", "vitest": "^2.1.2" }, diff --git a/packages/viem-account-ledger/src/ledger-to-account.test.ts b/packages/viem-account-ledger/src/ledger-to-account.test.ts index 1dce8f0fa..1788614ed 100644 --- a/packages/viem-account-ledger/src/ledger-to-account.test.ts +++ b/packages/viem-account-ledger/src/ledger-to-account.test.ts @@ -1,30 +1,40 @@ +import { recoverTransaction } from '@celo/wallet-base' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { describe, expect, it, test, vi } from 'vitest' +import { beforeAll, describe, expect, it, test, vi } from 'vitest' import { ledgerToAccount } from './ledger-to-account.js' import { mockLedger, TEST_CHAIN_ID } from './test-utils.js' +import { generateLedger } from './utils.js' -vi.mock('./utils.js', async () => { - const module = await vi.importActual('./utils.js') +const USE_PHYSICAL_LEDGER = process.env.USE_PHYSICAL_LEDGER === 'true' +const hardwareDescribe = USE_PHYSICAL_LEDGER ? describe : describe.skip +const syntheticDescribe = USE_PHYSICAL_LEDGER ? describe.skip : describe - return { - ...module, - generateLedger: vi.fn(() => Promise.resolve(mockLedger())), - } -}) +const transport = USE_PHYSICAL_LEDGER + ? TransportNodeHid.open('') + : Promise.resolve(undefined as unknown as TransportNodeHid) + +syntheticDescribe('ledgerToAccount (mocked ledger)', () => { + let account: Awaited> + beforeAll(async () => { + account = await ledgerToAccount({ + transport: await transport, + }) -const transport = - process.env.USE_PHYSICAL_LEDGER === 'true' - ? TransportNodeHid.open('') - : Promise.resolve(undefined as unknown as TransportNodeHid) + vi.mock('./utils.js', async () => { + const module = await vi.importActual('./utils.js') + + return { + ...module, + generateLedger: vi.fn((...args) => + // @ts-expect-error + USE_PHYSICAL_LEDGER ? module.generateLedger(...args) : Promise.resolve(mockLedger()) + ), + } + }) + }) -describe('ledgerToAccount', () => { it('can be setup', async () => { - await expect( - ledgerToAccount({ - transport: await transport, - }) - ).resolves.not.toBe(undefined) - // expect((generateLedger as ReturnType<(typeof jest)['fn']>).mock.calls.length).toBe(1) + expect((generateLedger as ReturnType<(typeof jest)['fn']>).mock.calls.length).toBe(1) }) describe('signs txs', () => { @@ -37,46 +47,163 @@ describe('ledgerToAccount', () => { maxPriorityFeePerGas: BigInt(100), } as const - test('eip1559', async () => { - const account = await ledgerToAccount({ - transport: await transport, + describe('eip1559', async () => { + test('v=0', async () => { + const txHash = await account.signTransaction(txData) + expect(txHash).toMatchInlineSnapshot( + `"0x02f86282aef32a6464809412345678901234567890123456789012345678907b80c080a05e130d8edb38e3ee8ab283af7c03a2579598b9a77807d7d796060358787d4707a07219dd22fe3bf3fe57682041d8f80dc9909cd70d903163b077d19625c4cd6e67"` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`0`) + }) + test('v=1', async () => { + const txHash = await account.signTransaction({ ...txData, nonce: 100 }) + expect(txHash).toMatchInlineSnapshot( + `"0x02f86282aef3646464809412345678901234567890123456789012345678907b80c001a05d166032c75a416c4e552223b9288a7a280d47909d7f526c2884d21d05a28747a047b32b31eb8a9f035b73218ab2b8b8f3211713fc44ef9b9965e268b6ae064cfc"` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`1`) }) - await expect(account.signTransaction(txData)).resolves.toMatchInlineSnapshot( - `"0x02f86282aef32a6464809412345678901234567890123456789012345678907b80c080a05e130d8edb38e3ee8ab283af7c03a2579598b9a77807d7d796060358787d4707a07219dd22fe3bf3fe57682041d8f80dc9909cd70d903163b077d19625c4cd6e67"` - ) }) - test('cip64', async () => { - const account = await ledgerToAccount({ - transport: await transport, + describe('cip64', async () => { + test('v=0', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ ...txData, feeCurrency: cUSDa }) + expect(txHash).toMatchInlineSnapshot( + `"0x7bf87782aef32a6464809412345678901234567890123456789012345678907b80c094874069fa1eb16d44d622f2e0ca25eea172369bc180a017d8df83b40dc645b60142280613467ca92438ff5aa0811a6ceff399fe66d661a02efe4eea14146f41d4f776bec1ededc486ddee37cea8304d297a69dbf27c4089"` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`0`) }) - const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' - await expect( - account.signTransaction({ - ...txData, - - feeCurrency: cUSDa, + test('v=1', async () => { + const account = await ledgerToAccount({ + transport: await transport, }) - ).resolves.toMatchInlineSnapshot( - `"0x7bf87782aef32a6464809412345678901234567890123456789012345678907b80c094874069fa1eb16d44d622f2e0ca25eea172369bc180a017d8df83b40dc645b60142280613467ca92438ff5aa0811a6ceff399fe66d661a02efe4eea14146f41d4f776bec1ededc486ddee37cea8304d297a69dbf27c4089"` - ) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ ...txData, feeCurrency: cUSDa, nonce: 100 }) + expect(txHash).toMatchInlineSnapshot( + `"0x7bf87782aef3646464809412345678901234567890123456789012345678907b80c094874069fa1eb16d44d622f2e0ca25eea172369bc101a02425b4eed4b98f3e0b206ca0bc6d6eb7d144ab6a676dc46bb02a243f3b810b84a00364b83eebbb23cbc9a76406842166e0d78086a820388adb1e249f9ed9753474"` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`1`) + }) }) }) it('signs messages', async () => { - const account = await ledgerToAccount({ - transport: await transport, - }) await expect(account.signMessage({ message: 'Hello World' })).resolves.toMatchInlineSnapshot( `"0x2f9a547e69592e98114263c08c6f7a6e6cd2f991fc29f442947179419233fe9641c8e4c86975a2722b54313e47768d2ffe2608c497ff9fe7f8c61b12e6257e571c"` ) }) it('signs typed data', async () => { - const account = await ledgerToAccount({ + await expect( + account.signTypedData({ + domain: { + name: 'foo', + version: '0.0.0', + chainId: BigInt(42), + verifyingContract: '0x123', + }, + primaryType: 'EIP712Domain', + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + }, + }) + ).rejects.toMatchInlineSnapshot(`[Error: Not implemented as of this release.]`) + }) +}) + +hardwareDescribe('ledgerToAccount (device ledger)', () => { + let account: Awaited> + beforeAll(async () => { + account = await ledgerToAccount({ transport: await transport, }) + }) + + it('can be setup', async () => { + expect((generateLedger as ReturnType<(typeof jest)['fn']>).mock.calls.length).toBe(1) + }) + + describe('signs txs', () => { + const txData = { + to: '0x1234567890123456789012345678901234567890', + value: BigInt(123), + chainId: TEST_CHAIN_ID, + nonce: 42, + maxFeePerGas: BigInt(100), + maxPriorityFeePerGas: BigInt(100), + } as const + + describe('eip1559', async () => { + test('v=0', async () => { + const txHash = await account.signTransaction(txData) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`1`) + }, 20_000) + test('v=1', async () => { + const txHash = await account.signTransaction({ ...txData, nonce: 100 }) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`1`) + }, 20_000) + }) + + describe('cip64', async () => { + test('v=0', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ ...txData, feeCurrency: cUSDa, nonce: 0 }) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`0`) + }, 20_000) + test('v=1', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ ...txData, feeCurrency: cUSDa, nonce: 100 }) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toMatchInlineSnapshot(`1`) + }, 20_000) + }) + }) + + it('signs messages', async () => { + // TODO: refactor to check signer rather than snapshot to be device-agnostic + await expect(account.signMessage({ message: 'Hello World' })).resolves.toMatchInlineSnapshot( + `"0x15859cf38f7b58d98e330b1d105add503a31c88f02104d43b4f9cc7cb08dbb483e51abc79881d0f8562073ad57dc60a4a567ddedc1176bf94d4dccc69fce09c51c"` + ) + }, 20_000) + it('signs typed data', async () => { await expect( account.signTypedData({ domain: { diff --git a/packages/viem-account-ledger/src/ledger-to-account.ts b/packages/viem-account-ledger/src/ledger-to-account.ts index c621270a7..99f55424a 100644 --- a/packages/viem-account-ledger/src/ledger-to-account.ts +++ b/packages/viem-account-ledger/src/ledger-to-account.ts @@ -27,7 +27,7 @@ export async function ledgerToAccount({ derivationPathIndex = 0, baseDerivationPath = DEFAULT_DERIVATION_PATH, }: { - transport: TransportNodeHid.default + transport: TransportNodeHid derivationPathIndex?: number | string baseDerivationPath?: string }): Promise { diff --git a/packages/viem-account-ledger/src/test-utils.ts b/packages/viem-account-ledger/src/test-utils.ts index 1a4305e9a..e3320812b 100644 --- a/packages/viem-account-ledger/src/test-utils.ts +++ b/packages/viem-account-ledger/src/test-utils.ts @@ -1,8 +1,8 @@ import { ensureLeading0x, normalizeAddressWith0x, trimLeading0x } from '@celo/base' +import Eth from '@celo/hw-app-eth' import { generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils.js' import { getHashFromEncoded, signTransaction } from '@celo/wallet-base' import * as ethUtil from '@ethereumjs/util' -import Eth from '@celo/hw-app-eth' import { createVerify, VerifyPublicKeyInput } from 'node:crypto' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' @@ -90,7 +90,7 @@ const TYPED_DATA = { }, } -interface Config extends Partial>> {} +interface Config extends Partial>> {} export const mockLedger = (config?: Config) => { const _ledger = { @@ -201,6 +201,6 @@ export const mockLedger = (config?: Config) => { } return verified }, - } as unknown as Eth.default + } as unknown as Eth return _ledger } diff --git a/packages/viem-account-ledger/src/tokens.ts b/packages/viem-account-ledger/src/tokens.ts index 01dbf55cb..2fde3dcca 100644 --- a/packages/viem-account-ledger/src/tokens.ts +++ b/packages/viem-account-ledger/src/tokens.ts @@ -1,6 +1,6 @@ // Copied from '@ledgerhq/hw-app-eth/erc20' because we need to change the path of the blob and support for address+chainId import { Address, normalizeAddressWith0x } from '@celo/base/lib/address.js' -import { default as blob } from '@celo/ledger-token-signer' +import blob from '@celo/ledger-token-signer' import blobLegacy from './data.js' /** @@ -21,7 +21,7 @@ export const legacyTokenInfoByAddressAndChainId = ( /** * list all the ERC20 tokens informations */ -export const list = (): TokenInfo[] => get(blob.default).list() +export const list = (): TokenInfo[] => get(blob).list() export const listLegacy = (): TokenInfo[] => get(blobLegacy).list() export interface TokenInfo { diff --git a/packages/viem-account-ledger/src/utils.ts b/packages/viem-account-ledger/src/utils.ts index 8b552eb21..1408c9605 100644 --- a/packages/viem-account-ledger/src/utils.ts +++ b/packages/viem-account-ledger/src/utils.ts @@ -24,7 +24,7 @@ export function meetsVersionRequirements( return min && max } -export async function assertCompat(ledger: Eth.default): Promise<{ +export async function assertCompat(ledger: Eth): Promise<{ arbitraryDataEnabled: number version: string }> { @@ -44,24 +44,26 @@ export async function assertCompat(ledger: Eth.default): Promise<{ } export async function checkForKnownToken( - ledger: Eth.default, + ledger: Eth, { to, chainId, feeCurrency }: { to: string; chainId: number; feeCurrency?: Hex } ) { const tokenInfo = tokenInfoByAddressAndChainId(to, chainId) if (tokenInfo) { - await ledger.provideERC20TokenInformation(`0x${tokenInfo.data.toString('hex')}`) + await ledger.provideERC20TokenInformation(tokenInfo.data.toString('hex')) } if (!feeCurrency || feeCurrency === '0x') return const feeTokenInfo = tokenInfoByAddressAndChainId(feeCurrency, chainId) if (feeTokenInfo) { - await ledger.provideERC20TokenInformation(`0x${feeTokenInfo.data.toString('hex')}`) + await ledger.provideERC20TokenInformation(feeTokenInfo.data.toString('hex')) } } -export async function generateLedger(transport: TransportNodeHid.default) { - const ledger = new Eth.default(transport) +export async function generateLedger(transport: TransportNodeHid) { + // NOTE: for some reason when running it with the + // test runner + real ledger, I need to call `Eth(transport)` + const ledger = new Eth(transport) await assertCompat(ledger) return ledger } diff --git a/packages/viem-account-ledger/tsconfig.json b/packages/viem-account-ledger/tsconfig.json index bb170fe17..756f81753 100644 --- a/packages/viem-account-ledger/tsconfig.json +++ b/packages/viem-account-ledger/tsconfig.json @@ -3,8 +3,6 @@ "compilerOptions": { "rootDir": "src", "outDir": "lib", - "moduleResolution": "Node16", - "module": "Node16", "declaration": true, "types": ["vitest/globals"] }, diff --git a/yarn.lock b/yarn.lock index a8b0b00e8..5b0b91870 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2184,6 +2184,7 @@ __metadata: "@ledgerhq/hw-transport-node-hid": "npm:^6.29.5" "@vitest/coverage-v8": "npm:2.1.2" dotenv: "npm:^8.2.0" + rimraf: "npm:^4.4.1" semver: "npm:^7.6.0" viem: "npm:^2.21.14" vitest: "npm:^2.1.2" @@ -11849,6 +11850,18 @@ __metadata: languageName: node linkType: hard +"glob@npm:^9.2.0": + version: 9.3.5 + resolution: "glob@npm:9.3.5" + dependencies: + fs.realpath: "npm:^1.0.0" + minimatch: "npm:^8.0.2" + minipass: "npm:^4.2.4" + path-scurry: "npm:^1.6.1" + checksum: e5fa8a58adf53525bca42d82a1fad9e6800032b7e4d372209b80cfdca524dd9a7dbe7d01a92d7ed20d89c572457f12c250092bc8817cb4f1c63efefdf9b658c0 + languageName: node + linkType: hard + "global@npm:~4.4.0": version: 4.4.0 resolution: "global@npm:4.4.0" @@ -14965,6 +14978,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^8.0.2": + version: 8.0.4 + resolution: "minimatch@npm:8.0.4" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: aef05598ee565e1013bc8a10f53410ac681561f901c1a084b8ecfd016c9ed919f58f4bbd5b63e05643189dfb26e8106a84f0e1ff12e4a263aa37e1cae7ce9828 + languageName: node + linkType: hard + "minimatch@npm:^9.0.4, minimatch@npm:^9.0.5": version: 9.0.5 resolution: "minimatch@npm:9.0.5" @@ -15072,7 +15094,7 @@ __metadata: languageName: node linkType: hard -"minipass@npm:^4.0.0": +"minipass@npm:^4.0.0, minipass@npm:^4.2.4": version: 4.2.8 resolution: "minipass@npm:4.2.8" checksum: e148eb6dcb85c980234cad889139ef8ddf9d5bdac534f4f0268446c8792dd4c74f4502479be48de3c1cce2f6450f6da4d0d4a86405a8a12be04c1c36b339569a @@ -16572,7 +16594,7 @@ __metadata: languageName: node linkType: hard -"path-scurry@npm:^1.11.1": +"path-scurry@npm:^1.11.1, path-scurry@npm:^1.6.1": version: 1.11.1 resolution: "path-scurry@npm:1.11.1" dependencies: @@ -17686,6 +17708,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^4.4.1": + version: 4.4.1 + resolution: "rimraf@npm:4.4.1" + dependencies: + glob: "npm:^9.2.0" + bin: + rimraf: dist/cjs/src/bin.js + checksum: 218ef9122145ccce9d0a71124d36a3894537de46600b37fae7dba26ccff973251eaa98aa63c2c5855a05fa04bca7cbbd7a92d4b29f2875d2203e72530ecf6ede + languageName: node + linkType: hard + "rimraf@npm:~2.4.0": version: 2.4.5 resolution: "rimraf@npm:2.4.5"