diff --git a/.changeset/twenty-humans-beg.md b/.changeset/twenty-humans-beg.md new file mode 100644 index 000000000..06d454b81 --- /dev/null +++ b/.changeset/twenty-humans-beg.md @@ -0,0 +1,6 @@ +--- +'@celo/wallet-ledger': patch +'@celo/viem-account-ledger': patch +--- + +Safer handling of v from device diff --git a/.prettierignore b/.prettierignore index f267b654b..a608710df 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,6 +7,7 @@ package.json packages/**/dist packages/**/lib +packages/**/lib-es # Needed because we have packages/celotool/src/lib !packages/celotool/src/** diff --git a/codecov.yml b/codecov.yml index a2f39e5f1..0f05aa022 100644 --- a/codecov.yml +++ b/codecov.yml @@ -2,11 +2,9 @@ coverage: status: patch: default: - # basic - target: auto - threshold: 0% - base: auto + target: 80% only_pulls: true + comment: layout: 'header, diff, flags, components' diff --git a/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md b/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md index 6f43b9f26..763253de9 100644 --- a/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md +++ b/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md @@ -72,7 +72,7 @@ Signer.computeSharedSecret #### Defined in -[wallet-ledger/src/ledger-signer.ts:190](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L190) +[wallet-ledger/src/ledger-signer.ts:207](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L207) ___ @@ -96,7 +96,7 @@ Signer.decrypt #### Defined in -[wallet-ledger/src/ledger-signer.ts:184](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L184) +[wallet-ledger/src/ledger-signer.ts:201](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L201) ___ @@ -138,7 +138,7 @@ Signer.signPersonalMessage #### Defined in -[wallet-ledger/src/ledger-signer.ts:79](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L79) +[wallet-ledger/src/ledger-signer.ts:96](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L96) ___ @@ -187,4 +187,4 @@ Signer.signTypedData #### Defined in -[wallet-ledger/src/ledger-signer.ts:99](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L99) +[wallet-ledger/src/ledger-signer.ts:116](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L116) diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts index c6b1ec7b1..453da5d06 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts @@ -49,16 +49,33 @@ export class LedgerSigner implements Signer { try { const validatedDerivationPath = await this.getValidatedDerivationPath() await this.checkForKnownToken(encodedTx) - const signature = await this.ledger!.signTransaction( + let { + r, + s, + v: _v, + } = await this.ledger!.signTransaction( validatedDerivationPath, trimLeading0x(encodedTx.rlpEncode), // the ledger requires the rlpEncode without the leading 0x null ) + if (typeof _v === 'string' && (_v === '' || _v === '0x')) { + console.warn( + `ledger-signer@signTransaction: signature \`v\` was malformed \`${_v}\`. Replaced with "0x0"` + ) + _v = '0x0' + } + const v = typeof _v === 'string' ? parseInt(ensureLeading0x(_v), 16) : _v + if (isNaN(v)) { + throw new Error( + `ledger-signer@signTransaction: signature \`v\` was malformed and was parsed to NaN \`${_v}\`` + ) + } + return { - v: parseInt(signature.v, 16), - r: ethUtil.toBuffer(ensureLeading0x(signature.r)), - s: ethUtil.toBuffer(ensureLeading0x(signature.s)), + v, + r: ethUtil.toBuffer(ensureLeading0x(r)), + s: ethUtil.toBuffer(ensureLeading0x(s)), } } catch (error: unknown) { if (error instanceof TransportStatusError) { 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..68ecd1fc8 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,41 @@ +import { recoverMessageSigner, recoverTransaction } from '@celo/wallet-base' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { describe, expect, it, test, vi } from 'vitest' +import { recoverMessageAddress } from 'viem' +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 { mockLedger, TEST_CHAIN_ID, test_ledger } from './test-utils.js' +import { generateLedger } from './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 + +const transport = USE_PHYSICAL_LEDGER + ? 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(() => Promise.resolve(mockLedger())), + generateLedger: vi.fn((...args) => + // @ts-expect-error + USE_PHYSICAL_LEDGER ? module.generateLedger(...args) : Promise.resolve(mockLedger()) + ), } }) -const transport = - process.env.USE_PHYSICAL_LEDGER === 'true' - ? TransportNodeHid.open('') - : Promise.resolve(undefined as unknown as TransportNodeHid) +syntheticDescribe('ledgerToAccount (mocked ledger)', () => { + let account: Awaited> + beforeAll(async () => { + account = await ledgerToAccount({ + transport: await transport, + }) + }) -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 +48,218 @@ describe('ledgerToAccount', () => { maxPriorityFeePerGas: BigInt(100), } as const - test('eip1559', async () => { - const account = await ledgerToAccount({ - transport: await transport, + describe('eip1559', () => { + 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).toBe(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).toBe(1) }) - await expect(account.signTransaction(txData)).resolves.toMatchInlineSnapshot( - `"0x02f86282aef32a6464809412345678901234567890123456789012345678907b80c080a05e130d8edb38e3ee8ab283af7c03a2579598b9a77807d7d796060358787d4707a07219dd22fe3bf3fe57682041d8f80dc9909cd70d903163b077d19625c4cd6e67"` - ) }) - test('cip64', async () => { - const account = await ledgerToAccount({ - transport: await transport, + describe('cip64', () => { + 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).toBe(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).toBe(1) + }) + }) + + describe('malformed v values', () => { + test('recoverable formats', async () => { + const test_vs_0_and_1 = [ + [0, '', '00', '0x', '0x0', '0x00', '0x1b', 27], // yParity 0 + [1, '1', '0x1', '0x01', '01', '0x1c', 28], // vParity 1 + ] + for (const expectedyParity in test_vs_0_and_1) { + const test_vs = test_vs_0_and_1[+expectedyParity] + for (const v of test_vs) { + vi.spyOn(test_ledger, 'signTransaction').mockImplementationOnce(() => + // @ts-expect-error + Promise.resolve({ + v, + r: '0x1', + s: '0x1', + }) + ) + const txHash = await account.signTransaction(txData) + const [recovered] = recoverTransaction(txHash) + // @ts-expect-error + expect(recovered.yParity).toBe(+expectedyParity) + } + } + }) + test('unrecoverable', async () => { + const test_vs = [NaN, 'asdf', null, undefined, {}] + for (const v of test_vs) { + vi.spyOn(test_ledger, 'signTransaction').mockImplementationOnce(() => + // @ts-expect-error + Promise.resolve({ + v, + r: '0x1', + s: '0x1', + }) + ) + await expect(account.signTransaction(txData)).rejects.toThrowError( + "Ledger signature `v` was malformed and couldn't be parsed" + ) + } + }) }) }) it('signs messages', async () => { - const account = await ledgerToAccount({ - transport: await transport, - }) - await expect(account.signMessage({ message: 'Hello World' })).resolves.toMatchInlineSnapshot( - `"0x2f9a547e69592e98114263c08c6f7a6e6cd2f991fc29f442947179419233fe9641c8e4c86975a2722b54313e47768d2ffe2608c497ff9fe7f8c61b12e6257e571c"` + const message = 'Hello World clabs' + const signedMessage = await account.signMessage({ message }) + expect((await recoverMessageAddress({ message, signature: signedMessage })).toLowerCase()).toBe( + account.address.toLowerCase() ) + expect( + recoverMessageSigner(`0x${Buffer.from(message).toString('hex')}`, signedMessage).toLowerCase() + ).toBe(account.address.toLowerCase()) }) 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, nonce: 5 }) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) + // @ts-expect-error + expect(decoded.yParity).toBe(0) + }, 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).toBe(1) + }, 20_000) + }) + + describe('cip64', async () => { + test('v=0', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + // NOTE: this is device-specific + // play with the nonce to produce a different tx with a yParity==0 + 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).toBe(0) + }, 20_000) + test('v=1', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + // NOTE: this is device-specific + // play with the nonce to produce a different tx with a yParity==1 + 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).toBe(1) + }, 20_000) + }) + }) + + it('signs messages', async () => { + const message = 'Hello World clabs' + const signedMessage = await account.signMessage({ message }) + expect((await recoverMessageAddress({ message, signature: signedMessage })).toLowerCase()).toBe( + account.address.toLowerCase() + ) + expect( + recoverMessageSigner(`0x${Buffer.from(message).toString('hex')}`, signedMessage).toLowerCase() + ).toBe(account.address.toLowerCase()) + }, 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..7205a6cbb 100644 --- a/packages/viem-account-ledger/src/ledger-to-account.ts +++ b/packages/viem-account-ledger/src/ledger-to-account.ts @@ -1,7 +1,7 @@ import { CELO_DERIVATION_PATH_BASE, trimLeading0x } from '@celo/base' import { ensureLeading0x } from '@celo/base/lib/address.js' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { hashMessage, serializeSignature } from 'viem' +import { serializeSignature } from 'viem' import { LocalAccount, toAccount } from 'viem/accounts' import { CeloTransactionSerializable, serializeTransaction } from 'viem/celo' @@ -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 { @@ -46,17 +46,30 @@ export async function ledgerToAccount({ }) const hash = serializeTransaction(transaction) - const { r, s, v } = await ledger!.signTransaction(derivationPath, trimLeading0x(hash), null) + let { r, s, v: _v } = await ledger!.signTransaction(derivationPath, trimLeading0x(hash), null) + if (typeof _v === 'string' && (_v === '' || _v === '0x')) { + _v = '0x0' + } + let v: bigint + try { + v = BigInt(typeof _v === 'string' ? ensureLeading0x(_v) : _v) + } catch (err) { + throw new Error( + `Ledger signature \`v\` was malformed and couldn't be parsed \`${_v}\` (Original error: ${err})` + ) + } return serializeTransaction(transaction, { r: ensureLeading0x(r), s: ensureLeading0x(s), - v: BigInt(ensureLeading0x(v)), + v, }) }, async signMessage({ message }) { - const hash = hashMessage(message) - const { r, s, v } = await ledger!.signPersonalMessage(derivationPath, trimLeading0x(hash)) + const { r, s, v } = await ledger!.signPersonalMessage( + derivationPath, + Buffer.from(message as string).toString('hex') + ) return serializeSignature({ r: ensureLeading0x(r), s: ensureLeading0x(s), diff --git a/packages/viem-account-ledger/src/test-utils.ts b/packages/viem-account-ledger/src/test-utils.ts index 1a4305e9a..0cb06a118 100644 --- a/packages/viem-account-ledger/src/test-utils.ts +++ b/packages/viem-account-ledger/src/test-utils.ts @@ -1,13 +1,13 @@ 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' -import { Hex } from 'viem' -import { privateKeyToAccount, privateKeyToAddress } from 'viem/accounts' +import { Hex, parseSignature } from 'viem' +import { privateKeyToAccount, privateKeyToAddress, signMessage } from 'viem/accounts' import { legacyLedgerPublicKeyHex } from './data.js' import { DEFAULT_DERIVATION_PATH } from './ledger-to-account.js' import { meetsVersionRequirements, MIN_VERSION_EIP1559 } from './utils.js' @@ -90,117 +90,122 @@ const TYPED_DATA = { }, } -interface Config extends Partial>> {} +interface Config extends Partial>> {} -export const mockLedger = (config?: Config) => { - const _ledger = { - getAddress: async (derivationPath: string) => { - if (ledgerAddresses[derivationPath]) { - const { address, privateKey } = ledgerAddresses[derivationPath] - return { - address, - derivationPath, - publicKey: privateKeyToAccount(privateKey).publicKey, - } - } +export const test_ledger = { + isMock: true, + getAddress: async (derivationPath: string) => { + if (ledgerAddresses[derivationPath]) { + const { address, privateKey } = ledgerAddresses[derivationPath] return { - address: '', + address, derivationPath, - publicKey: '', + publicKey: privateKeyToAccount(privateKey).publicKey, } - }, - signTransaction: async (derivationPath: string, data: string) => { - if (ledgerAddresses[derivationPath]) { - const hash = getHashFromEncoded(ensureLeading0x(data)) - const { r, s, v } = signTransaction(hash, ledgerAddresses[derivationPath].privateKey) + } + return { + address: '', + derivationPath, + publicKey: '', + } + }, + signTransaction: async (derivationPath: string, data: string) => { + if (ledgerAddresses[derivationPath]) { + const hash = getHashFromEncoded(ensureLeading0x(data)) + const { r, s, v } = signTransaction(hash, ledgerAddresses[derivationPath].privateKey) - return { - v: v.toString(16), - r: r.toString('hex'), - s: s.toString('hex'), - } + return { + v: v.toString(16), + r: r.toString('hex'), + s: s.toString('hex'), } - throw new Error('Invalid Path') - }, - signPersonalMessage: async (derivationPath: string, data: string) => { - if (ledgerAddresses[derivationPath]) { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) + } + throw new Error('Invalid Path') + }, + signPersonalMessage: async (derivationPath: string, data: string) => { + if (ledgerAddresses[derivationPath]) { + const signedMessage = await signMessage({ + privateKey: ledgerAddresses[derivationPath].privateKey, + message: { raw: ensureLeading0x(data) }, + }) + return parseSignature(signedMessage) + } + throw new Error('Invalid Path') + }, + signEIP712HashedMessage: async ( + derivationPath: string, + _domainSeparator: string, + _structHash: string + ) => { + const messageHash = generateTypedDataHash(TYPED_DATA) - const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) - const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(msgHashBuff, pkBuffer) - return { - v: Number(signature.v), - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), - } - } - throw new Error('Invalid Path') - }, - signEIP712HashedMessage: async ( - derivationPath: string, - _domainSeparator: string, - _structHash: string - ) => { - const messageHash = generateTypedDataHash(TYPED_DATA) + const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) + const pkBuffer = Buffer.from(trimmedKey, 'hex') + const signature = ethUtil.ecsign(messageHash, pkBuffer) + return { + v: Number(signature.v), + r: signature.r.toString('hex'), + s: signature.s.toString('hex'), + } + }, + getAppConfiguration: async () => { + return { + arbitraryDataEnabled: 1, + version: MIN_VERSION_EIP1559, + erc20ProvisioningNecessary: 1, + starkEnabled: 1, + starkv2Supported: 1, + } + }, + provideERC20TokenInformation: async (tokenData: string) => { + let pubkey: VerifyPublicKeyInput + const version = (await test_ledger.getAppConfiguration()).version + if ( + meetsVersionRequirements(version, { + minimum: MIN_VERSION_EIP1559, + }) + ) { + // verify with new pubkey + const pubDir = dirname(require.resolve('@celo/ledger-token-signer')) + pubkey = { key: readFileSync(join(pubDir, 'pubkey.pem')).toString() } + } else { + // verify with oldpubkey + pubkey = { key: legacyLedgerPublicKeyHex } + } - const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) - const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(messageHash, pkBuffer) - return { - v: Number(signature.v), - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), - } - }, - getAppConfiguration: async () => { - return { - arbitraryDataEnabled: config?.arbitraryDataEnabled ?? 1, - version: config?.version ?? MIN_VERSION_EIP1559, - erc20ProvisioningNecessary: config?.erc20ProvisioningNecessary ?? 1, - starkEnabled: config?.starkEnabled ?? 1, - starkv2Supported: config?.starkv2Supported ?? 1, - } - }, - provideERC20TokenInformation: async (tokenData: string) => { - let pubkey: VerifyPublicKeyInput - const version = (await _ledger.getAppConfiguration()).version - if ( - meetsVersionRequirements(version, { - minimum: MIN_VERSION_EIP1559, - }) - ) { - // verify with new pubkey - const pubDir = dirname(require.resolve('@celo/ledger-token-signer')) - pubkey = { key: readFileSync(join(pubDir, 'pubkey.pem')).toString() } - } else { - // verify with oldpubkey - pubkey = { key: legacyLedgerPublicKeyHex } - } + const verify = createVerify('sha256') + const tokenDataBuf = Buffer.from(trimLeading0x(tokenData), 'hex') + const BASE_DATA_LENGTH = + 20 + // contract address, 20 bytes + 4 + // decimals, uint32, 4 bytes + 4 // chainId, uint32, 4 bytes + // first byte of data is the ticker length, so we add that to base data length + const dataLen = BASE_DATA_LENGTH + tokenDataBuf.readUint8(0) - const verify = createVerify('sha256') - const tokenDataBuf = Buffer.from(trimLeading0x(tokenData), 'hex') - const BASE_DATA_LENGTH = - 20 + // contract address, 20 bytes - 4 + // decimals, uint32, 4 bytes - 4 // chainId, uint32, 4 bytes - // first byte of data is the ticker length, so we add that to base data length - const dataLen = BASE_DATA_LENGTH + tokenDataBuf.readUint8(0) + // start at 1 since the first byte was just informative + const data = tokenDataBuf.slice(1, dataLen + 1) + verify.update(data) + verify.end() + // read from end of data til the end + const signature = tokenDataBuf.slice(dataLen + 1) + const verified = verify.verify(pubkey, signature) - // start at 1 since the first byte was just informative - const data = tokenDataBuf.slice(1, dataLen + 1) - verify.update(data) - verify.end() - // read from end of data til the end - const signature = tokenDataBuf.slice(dataLen + 1) - const verified = verify.verify(pubkey, signature) + if (!verified) { + throw new Error('couldnt verify data sent to MockLedger') + } + return verified + }, +} as unknown as Eth - if (!verified) { - throw new Error('couldnt verify data sent to MockLedger') - } - return verified - }, - } as unknown as Eth.default - return _ledger +export const mockLedger = (config?: Config) => { + test_ledger.getAppConfiguration = () => + Promise.resolve({ + arbitraryDataEnabled: config?.arbitraryDataEnabled ?? 1, + version: config?.version ?? MIN_VERSION_EIP1559, + erc20ProvisioningNecessary: config?.erc20ProvisioningNecessary ?? 1, + starkEnabled: config?.starkEnabled ?? 1, + starkv2Supported: config?.starkv2Supported ?? 1, + }) + + return test_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..828c30a48 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,24 @@ 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) { + 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/packages/viem-account-ledger/vitest.config.ts b/packages/viem-account-ledger/vitest.config.ts index 5b8d2de3f..49497d65f 100644 --- a/packages/viem-account-ledger/vitest.config.ts +++ b/packages/viem-account-ledger/vitest.config.ts @@ -5,7 +5,7 @@ export default defineConfig({ test: { coverage: { reporter: ['json', 'clover', 'lcov'], - exclude: ['**/data**', '**/tokens**', ...coverageConfigDefaults.exclude], + exclude: ['**/data**', '**/tokens**', '**/test-utils**', ...coverageConfigDefaults.exclude], }, }, }) diff --git a/yarn.lock b/yarn.lock index 32711be60..cbabe6a69 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"