Skip to content

Commit

Permalink
feat @celo/viem-accounts/ledger (#344)
Browse files Browse the repository at this point in the history
<!-- start pr-codex -->

## PR-Codex overview
This PR introduces the `@celo/viem-account-ledger` library, facilitating
interactions between Ledger devices and the `viem` framework. It
includes type definitions, configurations, and initial implementations
for ledger account management.

### Detailed summary
- Added `Hex` type definition in `src/types.ts`.
- Created initial release notes in `CHANGELOG.md`.
- Configured ESLint and TypeScript settings.
- Implemented `ledgerToAccount` function in `src/ledger-to-account.ts`.
- Added utility functions in `src/utils.ts`.
- Created test files for ledger interactions.
- Updated `README.md` with installation and usage instructions.

> The following files were skipped due to too many changes: `yarn.lock`

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your
question}`

<!-- end pr-codex -->
  • Loading branch information
nicolasbrugneaux authored Oct 4, 2024
1 parent d3b88fd commit 6bba5e3
Show file tree
Hide file tree
Showing 21 changed files with 2,114 additions and 12 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-gifts-love.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@celo/viem-account-ledger': major
---

Initial release
3 changes: 3 additions & 0 deletions packages/viem-account-ledger/.eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
extends: '../../.eslintrc.js',
}
4 changes: 4 additions & 0 deletions packages/viem-account-ledger/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
lib/
tmp/
.tmp/
.env
16 changes: 16 additions & 0 deletions packages/viem-account-ledger/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/.devchain/
/.devchain.tar.gz
/coverage/
/node_modules/
/src/
/tmp/
/.tmp/

/tsconfig.*
/jest.config.*
*.tgz

/src

/lib/**/*.test.*
/lib/test-utils
Empty file.
55 changes: 55 additions & 0 deletions packages/viem-account-ledger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# @celo/viem-account-ledger

This library aims to ease the usage of a ledger device with (viem)[https://viem.sh/].

## Installation

```bash
npm install @celo/viem-account-ledger viem@2 @ledgerhq/[email protected]
# or yarn or bun or ...
```

> [!IMPORTANT] > `viem` is a peer dependency and MUST be installed alongside this library.
> `@ledgerhq/transport-node-hid` is a peer dependency and MUST be installed alongside this library.
## Usage

```ts
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { createWalletClient } from 'viem'
import { celo, celoAlfajores } from 'viem/chains'
import { ledgerToAccount } from '@celo/viem-account-ledger'

async function main() => {
const account = await ledgerToAccount({
transport: await TransportNodeHid.open(''),
})
const client = createWalletClient({
account,
chain: celo,
transport: http()
});
await client.sendTransaction({
to: '0x123...',
value: 10n,
feeCurrency: '0x123...'
});
}
```

You can also use the `account` directly eg:

```ts
const account = await ledgerToAccount({
transport: await TransportNodeHid.open(''),
})
account.signTransaction({
to: '0x123...',
value: 123n,
chainId: celoAlfajores.id,
nonce: 42,
maxFeePerGas: 100n,
maxPriorityFeePerGas: 100n,
feeCurrency: '0x123...',
})
```
4 changes: 4 additions & 0 deletions packages/viem-account-ledger/eslint.tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": []
}
54 changes: 54 additions & 0 deletions packages/viem-account-ledger/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"name": "@celo/viem-account-ledger",
"version": "0.0.1",
"description": "Helper library to make ledger<->viem interactions easier",
"type": "module",
"exports": {
".": "./lib/index.js"
},
"types": "./lib/index.d.ts",
"author": "cLabs",
"license": "Apache-2.0",
"homepage": "https://docs.celo.org/developer/tools",
"repository": "https://github.com/celo-org/developer-tooling/tree/master/packages/viem-account-ledger",
"keywords": [
"celo",
"blockchain",
"viem",
"ledger"
],
"scripts": {
"build": "yarn run --top-level tsc -b .",
"clean": "yarn run --top-level tsc -b . --clean",
"docs": "yarn run --top-level typedoc",
"test": "yarn run vitest",
"lint": "yarn run --top-level eslint -c .eslintrc.cjs ",
"prepublishOnly": "yarn build"
},
"peerDependencies": {
"@ledgerhq/hw-transport-node-hid": "^6.x",
"viem": "2.x"
},
"dependencies": {
"@celo/base": "^6.1.0",
"@celo/ledger-token-signer": "^0.4.0",
"@ledgerhq/errors": "^6.16.4",
"@ledgerhq/hw-app-eth": "git+https://github.com:celo-org/ledgerjs-hw-app-eth.git",
"semver": "^7.6.0"
},
"devDependencies": {
"@celo/typescript": "workspace:^",
"@celo/utils": "workspace:^",
"@celo/wallet-base": "workspace:^",
"@celo/wallet-remote": "workspace:^",
"@ethereumjs/util": "8.0.5",
"@ledgerhq/hw-transport-node-hid": "^6.29.5",
"@vitest/coverage-v8": "2.1.2",
"dotenv": "^8.2.0",
"viem": "^2.21.14",
"vitest": "^2.1.2"
},
"engines": {
"node": ">=18"
}
}
19 changes: 19 additions & 0 deletions packages/viem-account-ledger/src/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// export default 'AAAAaARDRUxPRx7ON1DaI3+TuOM5xTaYm4l4pDgAAAASAACk7DBFAiEA5rECRg94+fCoIvoG9/5qWh62zl2C6Y+aFuuZrFe4CtcCIEJbRrkL3gqwT/Jj+7L3neazgpVCCTZZ3HX9JXXg5vleAAAAaARjVVNEdl3oFoRYYedaJfyhIrtomLixKCoAAAASAACk7DBFAiEApwQFHNBKXp+V2jq8BMD2y/5AwC9bhPQ2H4hT/vMl/B4CIFalOVtBFGREUKMU/F5vDlJLeQrTn6GQeDertpB2FpMvAAAAaARjRVVS2HY8uidqNzjm3oW0s79f3tbWynMAAAASAACk7DBFAiEAh2UeP1+SI2Ed5SiAjpJF6MkMrVa94gUwjJztyBlzhWMCIHfaOrEsxdxAGx+P+hxuSNO4zcw6KRLfJkkuic1V/CrHAAAAagZiIENFTE/dyb5X9VP+dXUtYWBrlMvX4CZO+AAAABIAAPNwMEUCIQCi62KsBfuNcfX0MriiRZ7a5DKERhtIz7sZ1SqBT7ruhgIgVrfmavyWzxzDW4AQeHn++A4qPjB1pQKoHvNXo8Hf1SMAAABpBmIgY1VTRGJJKmRKWI/ZBCcL7QatUrmr/qGuAAAAEgAA83AwRAIgGDYx4oB/gkYUqLeXqvEZXx9nOxVHzTe2ajyd2wnehxgCICQBe/rBPcXiaQJj3pdoXxroct/hV6r3G2G7y79EOEAPAAAAaQZiIGNFVVL57OMBJHrSziGJSUGDCiRw9Od0ygAAABIAAPNwMEQCIEdcFWP+HxEUoF1sCGVd34QGS0hL5cVUdrWdqVm3bYTgAiBCMA+Rg3Ubc3xla/35wzZesPlbeSMEPcr4uqL+8PeydwAAAGoGYSBDRUxP8ZSv31CwPmm9fQV8GqnhDJlU5MkAAAASAACu8zBFAiEAk/o0FBus2/QCrunFGEyoneQIRaMRC+y5L6Dvar8MU/kCIByJt2ziRhDG3AAbyXBIuJfZQujSHFcSJL3xF0xIlcPdAAAAaQZhIGNVU0SHQGn6HrFtRNYi8uDKJe6hcjabwQAAABIAAK7zMEQCIClrH2xgE3WMbD+hgQ7t5SiAcVG5WiUZ655voqCszKEoAiA/cO8UVgNY891MNJ5yeDk8w47WO0E1DQecrK71LR8g8gAAAGoGYSBjRVVSEMiSpuxDpT5F0LkWtLfTg7G3jA8AAAASAACu8zBFAiEAgpktbB1ZxyAwMJwKTSbZ30n8zgRuW0twbXoZxlsUAswCIHek4l4CIbjVMG2HVr0Ml9/8kA4F9dr69JBMaoSUkdKl'

// This was signed by Marek Olszewski on Wed 18 June 2024
export default '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'

// How did we get this? By following these steps:
// 1 - get pubkey from https://github.com/blooo-io/app-celo-spender/pull/7/files#diff-e0cf5b28d9b6b600f0af2bc78e8fd30ec675fd731a5da86f0c4283ffc0e40176L75-L83
// 2 - now you've got to trust me
// 3 - run `ASN1_PREFIX=3056301006072a8648ce3d020106052b8104000a034200`
// 4 - remove spaces, colons, 0xs from the pubkey of step 1, I've done that for you :)
// 5 - and store it in key `KEY=04b06cf5d8f7ed71d8bd9b9dc37944a1c6d240f69bb0be3621dddbb6ac0eccd1508bcc2ea46227e43b941e2c6f1b1cd0ae68e54b185e2cabef3455580604bd45b8`
// 5 - finally run `echo $ASN1_PREFIX$KEY | xxd -r -p - | openssl ec -inform der -pubin -pubout`
// 6 - enjoy
export const legacyLedgerPublicKeyHex = [
`-----BEGIN PUBLIC KEY-----`,
`MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEsGz12Pftcdi9m53DeUShxtJA9puwvjYh`,
`3du2rA7M0VCLzC6kYifkO5QeLG8bHNCuaOVLGF4sq+80VVgGBL1FuA==`,
`-----END PUBLIC KEY-----`,
].join('\n')
5 changes: 5 additions & 0 deletions packages/viem-account-ledger/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export {
CELO_BASE_DERIVATION_PATH,
DEFAULT_DERIVATION_PATH,
ledgerToAccount,
} from './ledger-to-account.js'
100 changes: 100 additions & 0 deletions packages/viem-account-ledger/src/ledger-to-account.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
import { describe, expect, it, test, vi } from 'vitest'
import { ledgerToAccount } from './ledger-to-account.js'
import { mockLedger, TEST_CHAIN_ID } from './test-utils.js'

vi.mock('./utils.js', async () => {
const module = await vi.importActual('./utils.js')

return {
...module,
generateLedger: vi.fn(() => Promise.resolve(mockLedger())),
}
})

const transport =
process.env.USE_PHYSICAL_LEDGER === 'true'
? TransportNodeHid.open('')
: Promise.resolve(undefined as unknown as TransportNodeHid)

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)
})

describe('signs txs', () => {
const txData = {
to: '0x1234567890123456789012345678901234567890',
value: BigInt(123),
chainId: TEST_CHAIN_ID,
nonce: 42,
maxFeePerGas: BigInt(100),
maxPriorityFeePerGas: BigInt(100),
} as const

test('eip1559', async () => {
const account = await ledgerToAccount({
transport: await transport,
})
await expect(account.signTransaction(txData)).resolves.toMatchInlineSnapshot(
`"0x02f86282aef32a6464809412345678901234567890123456789012345678907b80c080a05e130d8edb38e3ee8ab283af7c03a2579598b9a77807d7d796060358787d4707a07219dd22fe3bf3fe57682041d8f80dc9909cd70d903163b077d19625c4cd6e67"`
)
})

test('cip64', async () => {
const account = await ledgerToAccount({
transport: await transport,
})
const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1'
await expect(
account.signTransaction({
...txData,

feeCurrency: cUSDa,
})
).resolves.toMatchInlineSnapshot(
`"0x7bf87782aef32a6464809412345678901234567890123456789012345678907b80c094874069fa1eb16d44d622f2e0ca25eea172369bc180a017d8df83b40dc645b60142280613467ca92438ff5aa0811a6ceff399fe66d661a02efe4eea14146f41d4f776bec1ededc486ddee37cea8304d297a69dbf27c4089"`
)
})
})

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({
transport: await transport,
})

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.]`)
}, 20_000)
})
68 changes: 68 additions & 0 deletions packages/viem-account-ledger/src/ledger-to-account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
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 { LocalAccount, toAccount } from 'viem/accounts'
import { CeloTransactionSerializable, serializeTransaction } from 'viem/celo'

import { checkForKnownToken, generateLedger } from './utils.js'

type LedgerAccount = LocalAccount<'ledger'>

export const ETH_DERIVATION_PATH_BASE = "m/44'/60'/0'" as const
export const CELO_BASE_DERIVATION_PATH = `${CELO_DERIVATION_PATH_BASE.slice(2)}/0`
export const DEFAULT_DERIVATION_PATH = `${ETH_DERIVATION_PATH_BASE.slice(2)}/0`

export async function ledgerToAccount({
transport,
derivationPathIndex = 0,
baseDerivationPath = DEFAULT_DERIVATION_PATH,
}: {
transport: TransportNodeHid.default
derivationPathIndex?: number | string
baseDerivationPath?: string
}): Promise<LedgerAccount> {
const derivationPath = `${baseDerivationPath}/${derivationPathIndex}`
const ledger = await generateLedger(transport)
const { address, publicKey } = await ledger.getAddress(derivationPath, true)

const account = toAccount({
address: ensureLeading0x(address),

async signTransaction(transaction: CeloTransactionSerializable) {
await checkForKnownToken(ledger, {
to: transaction.to!,
chainId: transaction.chainId!,
feeCurrency: transaction.feeCurrency,
})

const hash = serializeTransaction(transaction)
const { r, s, v } = await ledger!.signTransaction(derivationPath, trimLeading0x(hash), null)
return serializeTransaction(transaction, {
r: ensureLeading0x(r),
s: ensureLeading0x(s),
v: BigInt(ensureLeading0x(v)),
})
},

async signMessage({ message }) {
const hash = hashMessage(message)
const { r, s, v } = await ledger!.signPersonalMessage(derivationPath, trimLeading0x(hash))
return serializeSignature({
r: ensureLeading0x(r),
s: ensureLeading0x(s),
v: BigInt(v),
})
},

async signTypedData(_parameters) {
throw new Error('Not implemented as of this release.')
},
})

return {
...account,
publicKey: ensureLeading0x(publicKey),
source: 'ledger',
}
}
Loading

0 comments on commit 6bba5e3

Please sign in to comment.