Skip to content

Commit

Permalink
chore: ledger-v backport
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolasbrugneaux committed Nov 15, 2024
1 parent 7826b08 commit 80966eb
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 67 deletions.
3 changes: 2 additions & 1 deletion packages/viem-account-ledger/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
lib/
lib-es/
tmp/
.tmp/
.env
.env
20 changes: 11 additions & 9 deletions packages/viem-account-ledger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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"
},
Expand Down
209 changes: 168 additions & 41 deletions packages/viem-account-ledger/src/ledger-to-account.test.ts
Original file line number Diff line number Diff line change
@@ -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<ReturnType<typeof ledgerToAccount>>
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', () => {
Expand All @@ -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<ReturnType<typeof ledgerToAccount>>
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: {
Expand Down
2 changes: 1 addition & 1 deletion packages/viem-account-ledger/src/ledger-to-account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LedgerAccount> {
Expand Down
6 changes: 3 additions & 3 deletions packages/viem-account-ledger/src/test-utils.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -90,7 +90,7 @@ const TYPED_DATA = {
},
}

interface Config extends Partial<Awaited<ReturnType<Eth.default['getAppConfiguration']>>> {}
interface Config extends Partial<Awaited<ReturnType<Eth['getAppConfiguration']>>> {}

export const mockLedger = (config?: Config) => {
const _ledger = {
Expand Down Expand Up @@ -201,6 +201,6 @@ export const mockLedger = (config?: Config) => {
}
return verified
},
} as unknown as Eth.default
} as unknown as Eth
return _ledger
}
4 changes: 2 additions & 2 deletions packages/viem-account-ledger/src/tokens.ts
Original file line number Diff line number Diff line change
@@ -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'

/**
Expand All @@ -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 {
Expand Down
14 changes: 8 additions & 6 deletions packages/viem-account-ledger/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}> {
Expand All @@ -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
}
2 changes: 0 additions & 2 deletions packages/viem-account-ledger/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
"compilerOptions": {
"rootDir": "src",
"outDir": "lib",
"moduleResolution": "Node16",
"module": "Node16",
"declaration": true,
"types": ["vitest/globals"]
},
Expand Down
Loading

0 comments on commit 80966eb

Please sign in to comment.