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: ledger-v backport #443

Merged
merged 11 commits into from
Nov 19, 2024
44 changes: 29 additions & 15 deletions packages/viem-account-ledger/src/ledger-to-account.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { recoverTransaction } from '@celo/wallet-base'
import { recoverMessageSigner, recoverTransaction } from '@celo/wallet-base'
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
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, test_ledger } from './test-utils.js'
Expand Down Expand Up @@ -56,7 +57,7 @@ syntheticDescribe('ledgerToAccount (mocked ledger)', () => {
const [decoded, signer] = recoverTransaction(txHash)
expect(signer.toLowerCase()).toBe(account.address.toLowerCase())
// @ts-expect-error
expect(decoded.yParity).toMatchInlineSnapshot(`0`)
expect(decoded.yParity).toBe(0)
})
test('v=1', async () => {
const txHash = await account.signTransaction({ ...txData, nonce: 100 })
Expand All @@ -66,7 +67,7 @@ syntheticDescribe('ledgerToAccount (mocked ledger)', () => {
const [decoded, signer] = recoverTransaction(txHash)
expect(signer.toLowerCase()).toBe(account.address.toLowerCase())
// @ts-expect-error
expect(decoded.yParity).toMatchInlineSnapshot(`1`)
expect(decoded.yParity).toBe(1)
})
})

Expand All @@ -83,7 +84,7 @@ syntheticDescribe('ledgerToAccount (mocked ledger)', () => {
const [decoded, signer] = recoverTransaction(txHash)
expect(signer.toLowerCase()).toBe(account.address.toLowerCase())
// @ts-expect-error
expect(decoded.yParity).toMatchInlineSnapshot(`0`)
expect(decoded.yParity).toBe(0)
})
test('v=1', async () => {
const account = await ledgerToAccount({
Expand All @@ -97,7 +98,7 @@ syntheticDescribe('ledgerToAccount (mocked ledger)', () => {
const [decoded, signer] = recoverTransaction(txHash)
expect(signer.toLowerCase()).toBe(account.address.toLowerCase())
// @ts-expect-error
expect(decoded.yParity).toMatchInlineSnapshot(`1`)
expect(decoded.yParity).toBe(1)
})
})

Expand Down Expand Up @@ -145,9 +146,14 @@ syntheticDescribe('ledgerToAccount (mocked ledger)', () => {
})

it('signs messages', async () => {
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())
})
aaronmgdr marked this conversation as resolved.
Show resolved Hide resolved

it('signs typed data', async () => {
Expand Down Expand Up @@ -197,18 +203,18 @@ hardwareDescribe('ledgerToAccount (device ledger)', () => {

describe('eip1559', async () => {
test('v=0', async () => {
const txHash = await account.signTransaction(txData)
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).toMatchInlineSnapshot(`1`)
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).toMatchInlineSnapshot(`1`)
expect(decoded.yParity).toBe(1)
}, 20_000)
})

Expand All @@ -218,31 +224,39 @@ hardwareDescribe('ledgerToAccount (device ledger)', () => {
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).toMatchInlineSnapshot(`0`)
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).toMatchInlineSnapshot(`1`)
expect(decoded.yParity).toBe(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"`
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 () => {
Expand Down
8 changes: 5 additions & 3 deletions packages/viem-account-ledger/src/ledger-to-account.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -66,8 +66,10 @@ export async function ledgerToAccount({
},

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),
Expand Down
20 changes: 7 additions & 13 deletions packages/viem-account-ledger/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import * as ethUtil from '@ethereumjs/util'
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'
Expand Down Expand Up @@ -124,17 +124,11 @@ export const test_ledger = {
},
signPersonalMessage: async (derivationPath: string, data: string) => {
if (ledgerAddresses[derivationPath]) {
const dataBuff = ethUtil.toBuffer(ensureLeading0x(data))
const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff)

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'),
}
const signedMessage = await signMessage({
privateKey: ledgerAddresses[derivationPath].privateKey,
message: { raw: ensureLeading0x(data) },
})
return parseSignature(signedMessage)
}
throw new Error('Invalid Path')
},
Expand Down
Loading