diff --git a/docs/sdk/wallet-ledger/README.md b/docs/sdk/wallet-ledger/README.md index be7dbb496..5834defc1 100644 --- a/docs/sdk/wallet-ledger/README.md +++ b/docs/sdk/wallet-ledger/README.md @@ -11,4 +11,5 @@ - [ledger-signer](modules/ledger_signer.md) - [ledger-utils](modules/ledger_utils.md) - [ledger-wallet](modules/ledger_wallet.md) +- [test-utils](modules/test_utils.md) - [tokens](modules/tokens.md) diff --git a/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md b/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md index 763253de9..2f53ed705 100644 --- a/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md +++ b/docs/sdk/wallet-ledger/classes/ledger_signer.LedgerSigner.md @@ -39,6 +39,7 @@ Signs the EVM transaction with a Ledger device | `derivationPath` | `string` | | `ledgerAddressValidation` | [`AddressValidation`](../enums/ledger_wallet.AddressValidation.md) | | `appConfiguration` | `Object` | +| `appConfiguration.appName` | `string` | | `appConfiguration.arbitraryDataEnabled` | `number` | | `appConfiguration.version` | `string` | @@ -72,7 +73,7 @@ Signer.computeSharedSecret #### Defined in -[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) +[wallet-ledger/src/ledger-signer.ts:212](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L212) ___ @@ -96,7 +97,7 @@ Signer.decrypt #### Defined in -[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) +[wallet-ledger/src/ledger-signer.ts:206](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L206) ___ @@ -114,7 +115,7 @@ Signer.getNativeKey #### Defined in -[wallet-ledger/src/ledger-signer.ts:41](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L41) +[wallet-ledger/src/ledger-signer.ts:42](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L42) ___ @@ -138,7 +139,7 @@ Signer.signPersonalMessage #### Defined in -[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) +[wallet-ledger/src/ledger-signer.ts:97](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L97) ___ @@ -163,7 +164,7 @@ Signer.signTransaction #### Defined in -[wallet-ledger/src/ledger-signer.ts:45](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L45) +[wallet-ledger/src/ledger-signer.ts:46](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L46) ___ @@ -187,4 +188,4 @@ Signer.signTypedData #### Defined in -[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) +[wallet-ledger/src/ledger-signer.ts:117](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts#L117) diff --git a/docs/sdk/wallet-ledger/classes/test_utils.TestLedger.md b/docs/sdk/wallet-ledger/classes/test_utils.TestLedger.md new file mode 100644 index 000000000..1540cd095 --- /dev/null +++ b/docs/sdk/wallet-ledger/classes/test_utils.TestLedger.md @@ -0,0 +1,230 @@ +[@celo/wallet-ledger](../README.md) / [test-utils](../modules/test_utils.md) / TestLedger + +# Class: TestLedger + +[test-utils](../modules/test_utils.md).TestLedger + +## Table of contents + +### Constructors + +- [constructor](test_utils.TestLedger.md#constructor) + +### Properties + +- [config](test_utils.TestLedger.md#config) +- [isMock](test_utils.TestLedger.md#ismock) +- [mockForceValidation](test_utils.TestLedger.md#mockforcevalidation) +- [transport](test_utils.TestLedger.md#transport) + +### Methods + +- [getAddress](test_utils.TestLedger.md#getaddress) +- [getAppConfiguration](test_utils.TestLedger.md#getappconfiguration) +- [getName](test_utils.TestLedger.md#getname) +- [provideERC20TokenInformation](test_utils.TestLedger.md#provideerc20tokeninformation) +- [signEIP712HashedMessage](test_utils.TestLedger.md#signeip712hashedmessage) +- [signPersonalMessage](test_utils.TestLedger.md#signpersonalmessage) +- [signTransaction](test_utils.TestLedger.md#signtransaction) + +## Constructors + +### constructor + +• **new TestLedger**(`mockForceValidation`, `config?`): [`TestLedger`](test_utils.TestLedger.md) + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `mockForceValidation` | () => `void` | +| `config?` | `Config` | + +#### Returns + +[`TestLedger`](test_utils.TestLedger.md) + +#### Defined in + +[wallet-ledger/src/test-utils.ts:113](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L113) + +## Properties + +### config + +• `Optional` `Readonly` **config**: `Config` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:113](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L113) + +___ + +### isMock + +• **isMock**: `boolean` = `true` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:110](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L110) + +___ + +### mockForceValidation + +• `Readonly` **mockForceValidation**: () => `void` + +#### Type declaration + +▸ (): `void` + +##### Returns + +`void` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:113](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L113) + +___ + +### transport + +• **transport**: `default` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:111](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L111) + +## Methods + +### getAddress + +▸ **getAddress**(`derivationPath`, `forceValidation?`): `Promise`\<\{ `address`: `string` = ''; `derivationPath`: `string` ; `publicKey`: `string` = '' }\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `derivationPath` | `string` | +| `forceValidation?` | `boolean` | + +#### Returns + +`Promise`\<\{ `address`: `string` = ''; `derivationPath`: `string` ; `publicKey`: `string` = '' }\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:142](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L142) + +___ + +### getAppConfiguration + +▸ **getAppConfiguration**(): `Promise`\<\{ `arbitraryDataEnabled`: `number` ; `erc20ProvisioningNecessary`: `number` ; `starkEnabled`: `number` ; `starkv2Supported`: `number` ; `version`: `string` }\> + +#### Returns + +`Promise`\<\{ `arbitraryDataEnabled`: `number` ; `erc20ProvisioningNecessary`: `number` ; `starkEnabled`: `number` ; `starkv2Supported`: `number` ; `version`: `string` }\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:132](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L132) + +___ + +### getName + +▸ **getName**(): `undefined` \| `string` + +#### Returns + +`undefined` \| `string` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:128](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L128) + +___ + +### provideERC20TokenInformation + +▸ **provideERC20TokenInformation**(`tokenData`): `Promise`\<``true``\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `tokenData` | `string` | + +#### Returns + +`Promise`\<``true``\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:211](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L211) + +___ + +### signEIP712HashedMessage + +▸ **signEIP712HashedMessage**(`derivationPath`, `_domainSeparator`, `_structHash`): `Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `number` }\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `derivationPath` | `string` | +| `_domainSeparator` | `string` | +| `_structHash` | `string` | + +#### Returns + +`Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `number` }\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:194](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L194) + +___ + +### signPersonalMessage + +▸ **signPersonalMessage**(`derivationPath`, `data`): `Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `number` }\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `derivationPath` | `string` | +| `data` | `string` | + +#### Returns + +`Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `number` }\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:177](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L177) + +___ + +### signTransaction + +▸ **signTransaction**(`derivationPath`, `data`): `Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `string` }\> + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `derivationPath` | `string` | +| `data` | `string` | + +#### Returns + +`Promise`\<\{ `r`: `string` ; `s`: `string` ; `v`: `string` }\> + +#### Defined in + +[wallet-ledger/src/test-utils.ts:160](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L160) diff --git a/docs/sdk/wallet-ledger/modules/test_utils.md b/docs/sdk/wallet-ledger/modules/test_utils.md new file mode 100644 index 000000000..afb0f89bd --- /dev/null +++ b/docs/sdk/wallet-ledger/modules/test_utils.md @@ -0,0 +1,114 @@ +[@celo/wallet-ledger](../README.md) / test-utils + +# Module: test-utils + +## Table of contents + +### Classes + +- [TestLedger](../classes/test_utils.TestLedger.md) + +### Variables + +- [ACCOUNT\_ADDRESS1](test_utils.md#account_address1) +- [ACCOUNT\_ADDRESS2](test_utils.md#account_address2) +- [ACCOUNT\_ADDRESS3](test_utils.md#account_address3) +- [ACCOUNT\_ADDRESS4](test_utils.md#account_address4) +- [ACCOUNT\_ADDRESS5](test_utils.md#account_address5) +- [ACCOUNT\_ADDRESS\_NEVER](test_utils.md#account_address_never) +- [TEST\_CHAIN\_ID](test_utils.md#test_chain_id) + +### Functions + +- [mockLedgerImplementation](test_utils.md#mockledgerimplementation) + +## Variables + +### ACCOUNT\_ADDRESS1 + +• `Const` **ACCOUNT\_ADDRESS1**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:27](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L27) + +___ + +### ACCOUNT\_ADDRESS2 + +• `Const` **ACCOUNT\_ADDRESS2**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:29](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L29) + +___ + +### ACCOUNT\_ADDRESS3 + +• `Const` **ACCOUNT\_ADDRESS3**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:31](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L31) + +___ + +### ACCOUNT\_ADDRESS4 + +• `Const` **ACCOUNT\_ADDRESS4**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:33](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L33) + +___ + +### ACCOUNT\_ADDRESS5 + +• `Const` **ACCOUNT\_ADDRESS5**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:35](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L35) + +___ + +### ACCOUNT\_ADDRESS\_NEVER + +• `Const` **ACCOUNT\_ADDRESS\_NEVER**: \`0x$\{string}\` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:37](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L37) + +___ + +### TEST\_CHAIN\_ID + +• `Const` **TEST\_CHAIN\_ID**: ``44787`` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:63](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L63) + +## Functions + +### mockLedgerImplementation + +▸ **mockLedgerImplementation**(`mockForceValidation`, `config?`): `default` + +#### Parameters + +| Name | Type | +| :------ | :------ | +| `mockForceValidation` | () => `void` | +| `config` | `Config` | + +#### Returns + +`default` + +#### Defined in + +[wallet-ledger/src/test-utils.ts:251](https://github.com/celo-org/developer-tooling/blob/master/packages/sdk/wallets/wallet-ledger/src/test-utils.ts#L251) diff --git a/packages/docs/viem-account-ledger/modules/ledger_to_account.md b/packages/docs/viem-account-ledger/modules/ledger_to_account.md index f88e9f991..403d737ce 100644 --- a/packages/docs/viem-account-ledger/modules/ledger_to_account.md +++ b/packages/docs/viem-account-ledger/modules/ledger_to_account.md @@ -12,7 +12,6 @@ - [CELO\_BASE\_DERIVATION\_PATH](ledger_to_account.md#celo_base_derivation_path) - [DEFAULT\_DERIVATION\_PATH](ledger_to_account.md#default_derivation_path) -- [ETH\_DERIVATION\_PATH\_BASE](ledger_to_account.md#eth_derivation_path_base) ### Functions @@ -26,7 +25,7 @@ #### Defined in -[ledger-to-account.ts:10](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L10) +[ledger-to-account.ts:16](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L16) ## Variables @@ -36,7 +35,7 @@ #### Defined in -[ledger-to-account.ts:13](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L13) +[ledger-to-account.ts:19](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L19) ___ @@ -46,17 +45,7 @@ ___ #### Defined in -[ledger-to-account.ts:14](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L14) - -___ - -### ETH\_DERIVATION\_PATH\_BASE - -• `Const` **ETH\_DERIVATION\_PATH\_BASE**: ``"m/44'/60'/0'"`` - -#### Defined in - -[ledger-to-account.ts:12](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L12) +[ledger-to-account.ts:20](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L20) ## Functions @@ -83,4 +72,4 @@ a viem LocalAccount<"ledger"> #### Defined in -[ledger-to-account.ts:25](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L25) +[ledger-to-account.ts:37](https://github.com/celo-org/developer-tooling/blob/master/packages/viem-account-ledger/src/ledger-to-account.ts#L37) diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts index 453da5d06..0d0e2a98f 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts @@ -21,15 +21,16 @@ export class LedgerSigner implements Signer { private derivationPath: string private validated: boolean = false private ledgerAddressValidation: AddressValidation - private appConfiguration: { arbitraryDataEnabled: number; version: string } + private appConfiguration: { arbitraryDataEnabled: number; version: string; appName: string } constructor( ledger: Ledger, derivationPath: string, ledgerAddressValidation: AddressValidation, - appConfiguration: { arbitraryDataEnabled: number; version: string } = { + appConfiguration: { arbitraryDataEnabled: number; version: string; appName: string } = { arbitraryDataEnabled: 0, version: '0.0.0', + appName: 'unknown', } ) { this.ledger = ledger @@ -114,6 +115,10 @@ export class LedgerSigner implements Signer { } async signTypedData(typedData: EIP712TypedData): Promise<{ v: number; r: Buffer; s: Buffer }> { + if (this.appConfiguration.appName === 'celo') { + throw new Error('Not implemented as of this release.') + } + try { const domainSeparator = structHash('EIP712Domain', typedData.domain, typedData.types) const hashStructMessage = structHash( diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts index 2fd35a997..f940602e4 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts @@ -1,33 +1,23 @@ import { ETHEREUM_DERIVATION_PATH } from '@celo/base' -import { - StrongAddress, - ensureLeading0x, - normalizeAddressWith0x, - trimLeading0x, -} from '@celo/base/lib/address' -import { CeloTx, EncodedTransaction, Hex } from '@celo/connect' +import { StrongAddress, normalizeAddressWith0x } from '@celo/base/lib/address' +import { CeloTx, EncodedTransaction } from '@celo/connect' import { StableToken, newKit } from '@celo/contractkit' -import Ledger from '@celo/hw-app-eth' -import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' -import { generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { verifySignature } from '@celo/utils/lib/signatureUtils' import { chainIdTransformationForSigning, - determineTXType, - getHashFromEncoded, recoverTransaction, - signTransaction, verifyEIP712TypedDataSigner, } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { VerifyPublicKeyInput, createVerify } from 'crypto' -import { readFileSync } from 'fs' -import { dirname, join } from 'path' import Web3 from 'web3' -import { legacyLedgerPublicKeyHex } from './data' import { meetsVersionRequirements } from './ledger-utils' import { AddressValidation, LedgerWallet } from './ledger-wallet' +import { + ACCOUNT_ADDRESS1, + ACCOUNT_ADDRESS2, + ACCOUNT_ADDRESS_NEVER, + mockLedgerImplementation, +} from './test-utils' // Update this variable when testing using a physical device const USE_PHYSICAL_LEDGER = process.env.USE_PHYSICAL_LEDGER === 'true' @@ -36,59 +26,6 @@ const syntheticDescribe = USE_PHYSICAL_LEDGER ? describe.skip : describe // Increase timeout to give developer time to respond on device const TEST_TIMEOUT_IN_MS = USE_PHYSICAL_LEDGER ? 30 * 1000 : 1 * 1000 -const PRIVATE_KEY1 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' -const ACCOUNT_ADDRESS1 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY1)) -const PRIVATE_KEY2 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fdeccc' -const ACCOUNT_ADDRESS2 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY2)) -const PRIVATE_KEY3 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff1' -const ACCOUNT_ADDRESS3 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY3)) -const PRIVATE_KEY4 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff2' -const ACCOUNT_ADDRESS4 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY4)) -const PRIVATE_KEY5 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff3' -const ACCOUNT_ADDRESS5 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY5)) -const PRIVATE_KEY_NEVER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890ffffff' -const ACCOUNT_ADDRESS_NEVER = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY_NEVER)) - -const ledgerAddresses: { [myKey: string]: { address: Hex; privateKey: Hex } } = { - "44'/52752'/0'/0/0": { - address: ACCOUNT_ADDRESS1, - privateKey: PRIVATE_KEY1, - }, - "44'/52752'/0'/0/1": { - address: ACCOUNT_ADDRESS2, - privateKey: PRIVATE_KEY2, - }, - "44'/52752'/0'/0/2": { - address: ACCOUNT_ADDRESS3, - privateKey: PRIVATE_KEY3, - }, - "44'/52752'/0'/0/3": { - address: ACCOUNT_ADDRESS4, - privateKey: PRIVATE_KEY4, - }, - "44'/52752'/0'/0/4": { - address: ACCOUNT_ADDRESS5, - privateKey: PRIVATE_KEY5, - }, - // change addresses - "44'/52752'/0'/1/0": { - address: '0x3c21B4d5b7E945149F30B078c261F2c349A5A195', - privateKey: '0xf879c2b42fb2e945123ba0340227c20123bceba5ebb4873a6999921afe938de5', - }, - "44'/52752'/0'/1/1": { - address: '0x4CD07D5b5E35a06c3B8077dF6a43572077BB0E28', - privateKey: '0x361a3334abb2555c1de8319ffd143efea08306c5b490179e3c74155adf683dc0', - }, - "44'/52752'/0'/2/0": { - address: '0xDE27268d1672Abd7F7493399cD0348273Bd76C93', - privateKey: '0xc49318225cfcf96f8297b24507284f1815b7625dd8f035c2e3f495e6c61bce1b', - }, - "44'/52752'/0'/2/1": { - address: '0xE81Bc4b00fe4913418BBC747620e1f1e7fb8E5cE', - privateKey: '0xe2faeec7b7fcd599c5c6ed8921d48d982c75bcbc9fc8b8aabf9b0c68d4bcb95c', - }, -} - const CHAIN_ID = 44787 // Sample data from the official EIP-712 example: @@ -131,146 +68,16 @@ const TYPED_DATA = { }, } -interface ILedger { - getAddress: typeof Ledger.prototype.getAddress - signTransaction: typeof Ledger.prototype.signTransaction - signPersonalMessage: typeof Ledger.prototype.signPersonalMessage - signEIP712HashedMessage: typeof Ledger.prototype.signEIP712HashedMessage - getAppConfiguration: typeof Ledger.prototype.getAppConfiguration - provideERC20TokenInformation: typeof Ledger.prototype.provideERC20TokenInformation -} - -const mockLedgerImplementation = (mockForceValidation: () => void, version: string): ILedger => { - const _ledger = { - getAddress: jest.fn(async (derivationPath: string, forceValidation?: boolean) => { - if (forceValidation) { - mockForceValidation() - } - if (ledgerAddresses[derivationPath]) { - return { - address: ledgerAddresses[derivationPath].address, - derivationPath, - publicKey: privateKeyToPublicKey(ledgerAddresses[derivationPath].privateKey), - } - } - return { - address: '', - derivationPath, - publicKey: '', - } - }), - signTransaction: async (derivationPath: string, data: string) => { - if (ledgerAddresses[derivationPath]) { - const type = determineTXType(ensureLeading0x(data)) - // replicate logic from wallet-base/src/wallet-base.ts - const addToV = type === 'celo-legacy' ? chainIdTransformationForSigning(CHAIN_ID) : 0 - const hash = getHashFromEncoded(ensureLeading0x(data)) - const { r, s, v } = signTransaction( - hash, - ledgerAddresses[derivationPath].privateKey, - addToV - ) - - 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) - - 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: version, - erc20ProvisioningNecessary: 1, - starkEnabled: 1, - starkv2Supported: 1, - } - }, - provideERC20TokenInformation: async (tokenData: string) => { - let pubkey: VerifyPublicKeyInput - const version = (await _ledger.getAppConfiguration()).version - if ( - meetsVersionRequirements(version, { - minimum: LedgerWallet.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(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.readInt8(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) - - if (!verified) { - throw new Error('couldnt verify data sent to MockLedger') - } - return verified - }, - } - return _ledger -} - function mockLedger( wallet: LedgerWallet, mockForceValidation: () => void, - version = LedgerWallet.MIN_VERSION_EIP1559 + { version = LedgerWallet.MIN_VERSION_EIP1559, name = 'celo' } = {} ) { return jest .spyOn(wallet, 'generateNewLedger') .mockClear() - .mockImplementation((_transport: any): ILedger => { - return mockLedgerImplementation(mockForceValidation, version) + .mockImplementation((_transport: any) => { + return mockLedgerImplementation(mockForceValidation, { version, name }) }) } @@ -345,7 +152,7 @@ describe('LedgerWallet class', () => { await expect( wallet.signTransaction(celoTransaction) ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Cannot read properties of undefined (reading 'getAppConfiguration')"` + `"Cannot read properties of undefined (reading 'transport')"` ) }) @@ -423,6 +230,8 @@ describe('LedgerWallet class', () => { }) describe('after initializing', () => { + let currentAppName: string + beforeEach(async () => { if (USE_PHYSICAL_LEDGER) { wallet = hardwareWallet @@ -432,6 +241,10 @@ describe('LedgerWallet class', () => { knownAddress = wallet.getAccounts()[0] as StrongAddress otherAddress = wallet.getAccounts()[1] as StrongAddress } + + // @ts-expect-error + currentAppName = await wallet.retrieveAppName() + console.log(currentAppName) }, TEST_TIMEOUT_IN_MS) test('starts 5 accounts', () => { @@ -567,7 +380,9 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) + mockLedger(wallet, mockForceValidation, { + version: LedgerWallet.MIN_VERSION_TOKEN_DATA, + }) await wallet.init() expect( @@ -731,7 +546,9 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) + mockLedger(wallet, mockForceValidation, { + version: LedgerWallet.MIN_VERSION_TOKEN_DATA, + }) await wallet.init() await expect( @@ -759,7 +576,9 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) + mockLedger(wallet, mockForceValidation, { + version: LedgerWallet.MIN_VERSION_TOKEN_DATA, + }) await wallet.init() } @@ -787,7 +606,9 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, LedgerWallet.MIN_VERSION_TOKEN_DATA) + mockLedger(wallet, mockForceValidation, { + version: LedgerWallet.MIN_VERSION_TOKEN_DATA, + }) await wallet.init() }) @@ -1068,10 +889,16 @@ describe('LedgerWallet class', () => { test( 'succeeds', async () => { - const signedMessage = await wallet.signTypedData(knownAddress, TYPED_DATA) - expect(signedMessage).not.toBeUndefined() - const valid = verifyEIP712TypedDataSigner(TYPED_DATA, signedMessage, knownAddress) - expect(valid).toBeTruthy() + if (currentAppName === 'celo') { + await expect( + wallet.signTypedData(knownAddress, TYPED_DATA) + ).rejects.toMatchInlineSnapshot(`[Error: Not implemented as of this release.]`) + } else { + const signedMessage = await wallet.signTypedData(knownAddress, TYPED_DATA) + expect(signedMessage).not.toBeUndefined() + const valid = verifyEIP712TypedDataSigner(TYPED_DATA, signedMessage, knownAddress) + expect(valid).toBeTruthy() + } }, TEST_TIMEOUT_IN_MS ) @@ -1117,7 +944,7 @@ describe('LedgerWallet class', () => { mockForceValidation = jest.fn((): void => { // do nothing }) - mockLedger(wallet, mockForceValidation, '0.0.0') + mockLedger(wallet, mockForceValidation, { version: '0.0.0' }) Promise.resolve(123) }) diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts index 4e377a353..4eb9ca680 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts @@ -125,7 +125,10 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly const version = new SemVer(deviceApp.version) // if the app is of minimum version it doesnt matter if chain is cel2 or not - if (meetsVersionRequirements(version, { minimum: LedgerWallet.MIN_VERSION_EIP1559 })) { + if ( + deviceApp.appName !== 'celo' || + meetsVersionRequirements(version, { minimum: LedgerWallet.MIN_VERSION_EIP1559 }) + ) { if (txParams.gasPrice && txParams.feeCurrency && txParams.feeCurrency !== '0x') { throw new Error( `celo ledger app above ${LedgerWallet.MIN_VERSION_EIP1559} cannot serialize legacy celo transactions. Replace "gasPrice" with "maxFeePerGas".` @@ -140,7 +143,13 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly // by deleting/ adding properties instead of throwing. // TODO when cip66 is implemented ensure it is not that // @ts-expect-error -- 66 isnt in this branch but will be in the release so future proof - if (isEIP1559(txParams) || (isCIP64(txParams) && !isPresent(txParams.maxFeePerFeeCurrency))) { + const isCeloSpecificTx = isCIP64(txParams) && !isPresent(txParams.maxFeePerFeeCurrency) + if (isEIP1559(txParams) || isCeloSpecificTx) { + if (isCeloSpecificTx && deviceApp.appName !== 'celo') { + throw new Error( + 'To submit celo-specific transactions you must use the celo app on your ledger device.' + ) + } return rlpEncodedTx(txParams) } else { throw new Error( @@ -221,11 +230,23 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly private async retrieveAppConfiguration(): Promise<{ arbitraryDataEnabled: number version: string + appName: string }> { + const appName = await this.retrieveAppName() const appConfiguration = await this.ledger!.getAppConfiguration() - if (new SemVer(appConfiguration.version).compare(LedgerWallet.MIN_VERSION_SUPPORTED) === -1) { + if (appName === 'celo') { + if (new SemVer(appConfiguration.version).compare(LedgerWallet.MIN_VERSION_SUPPORTED) === -1) { + throw new Error( + `Due to technical issues, we require the users to update their ledger celo-app to >= ${LedgerWallet.MIN_VERSION_SUPPORTED}. You can do this on ledger-live by updating the celo-app in the app catalog.` + ) + } + } else if (appName === 'ethereum') { + console.warn( + `Beware, you opened the Ethereum app instead of the Celo app. Some features may not work correctly, including token transfers.` + ) + } else { throw new Error( - `Due to technical issues, we require the users to update their ledger celo-app to >= ${LedgerWallet.MIN_VERSION_SUPPORTED}. You can do this on ledger-live by updating the celo-app in the app catalog.` + `Beware, you opened the ${appName} app instead of the Celo app. We cannot ensure the safety of using this SDK with ${appName}.` ) } if (!appConfiguration.arbitraryDataEnabled) { @@ -233,9 +254,31 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly 'Beware, your ledger does not allow the use of contract data. Some features may not work correctly, including token transfers. You can enable it from the ledger app settings.' ) } - return appConfiguration + return { ...appConfiguration, appName } + } + + private async retrieveAppName(): Promise { + // acl ins p1 p2 + const response = await this.ledger!.transport.send(0xb0, 0x01, 0x00, 0x00) + try { + let results = [] // (name, version) + let i = 1 + while (i < response.length + 1) { + const len = response[i] + i += 1 + const bufValue = response.subarray(i, i + len) + i += len + results.push(bufValue.toString('ascii').trim()) + } + + return results[0]!.toLowerCase() + } catch (err) { + console.error('The appName couldnt be infered from the device') + throw err + } } } + function validateIndexes(indexes: number[], label: string = 'address index') { if (indexes.length === 0) { throw new Error(`ledger-wallet: No ${label} provided`) diff --git a/packages/sdk/wallets/wallet-ledger/src/test-utils.ts b/packages/sdk/wallets/wallet-ledger/src/test-utils.ts new file mode 100644 index 000000000..b6221ab51 --- /dev/null +++ b/packages/sdk/wallets/wallet-ledger/src/test-utils.ts @@ -0,0 +1,259 @@ +import { + CELO_DERIVATION_PATH_BASE, + ensureLeading0x, + normalizeAddressWith0x, + trimLeading0x, +} from '@celo/base' +import Eth from '@celo/hw-app-eth' +import { generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' +import { + chainIdTransformationForSigning, + determineTXType, + getHashFromEncoded, + signTransaction, +} from '@celo/wallet-base' +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 '@celo/connect' +import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' +import { legacyLedgerPublicKeyHex } from './data' +import { meetsVersionRequirements } from './ledger-utils' +import { LedgerWallet } from './ledger-wallet' + +const PRIVATE_KEY1 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' +export const ACCOUNT_ADDRESS1 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY1)) +const PRIVATE_KEY2 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fdeccc' +export const ACCOUNT_ADDRESS2 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY2)) +const PRIVATE_KEY3 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff1' +export const ACCOUNT_ADDRESS3 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY3)) +const PRIVATE_KEY4 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff2' +export const ACCOUNT_ADDRESS4 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY4)) +const PRIVATE_KEY5 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890fffff3' +export const ACCOUNT_ADDRESS5 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY5)) +const PRIVATE_KEY_NEVER = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890ffffff' +export const ACCOUNT_ADDRESS_NEVER = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY_NEVER)) + +const DEFAULT_DERIVATION_PATH = `${CELO_DERIVATION_PATH_BASE.slice(2)}/0` +const ledgerAddresses: { [myKey: string]: { address: Hex; privateKey: Hex } } = { + [`${DEFAULT_DERIVATION_PATH}/0`]: { + address: ACCOUNT_ADDRESS1, + privateKey: PRIVATE_KEY1, + }, + [`${DEFAULT_DERIVATION_PATH}/1`]: { + address: ACCOUNT_ADDRESS2, + privateKey: PRIVATE_KEY2, + }, + [`${DEFAULT_DERIVATION_PATH}/2`]: { + address: ACCOUNT_ADDRESS3, + privateKey: PRIVATE_KEY3, + }, + [`${DEFAULT_DERIVATION_PATH}/3`]: { + address: ACCOUNT_ADDRESS4, + privateKey: PRIVATE_KEY4, + }, + [`${DEFAULT_DERIVATION_PATH}/4`]: { + address: ACCOUNT_ADDRESS5, + privateKey: PRIVATE_KEY5, + }, +} + +export const TEST_CHAIN_ID = 44787 + +// Sample data from the official EIP-712 example: +// https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js +const TYPED_DATA = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: 1, + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + from: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + contents: 'Hello, Bob!', + }, +} + +interface Config extends Partial>> { + name?: string +} + +export class TestLedger { + isMock = true + transport: Eth['transport'] + + constructor(readonly mockForceValidation: () => void, readonly config?: Config) { + this.transport = { + send: async (_cla: number, ins: number, _p1: number, _p2: number): Promise => { + if (ins === 0x01) { + // get app information INS + const version = Buffer.from((await this.getAppConfiguration()).version, 'ascii') + const name = Buffer.from(this.getName() ?? 'Celo', 'ascii') + return Buffer.from([0x01, name.byteLength, ...name, version.byteLength, ...version]) + } + + throw new Error('Unsupport INS ' + ins + ' in mock') + }, + } as Eth['transport'] + } + + getName() { + return this.config?.name + } + + getAppConfiguration() { + return Promise.resolve({ + arbitraryDataEnabled: this.config?.arbitraryDataEnabled ?? 1, + version: this.config?.version ?? LedgerWallet.MIN_VERSION_EIP1559, + erc20ProvisioningNecessary: this.config?.erc20ProvisioningNecessary ?? 1, + starkEnabled: this.config?.starkEnabled ?? 1, + starkv2Supported: this.config?.starkv2Supported ?? 1, + }) + } + + async getAddress(derivationPath: string, forceValidation?: boolean) { + if (forceValidation) { + this.mockForceValidation() + } + if (ledgerAddresses[derivationPath]) { + return { + address: ledgerAddresses[derivationPath].address, + derivationPath, + publicKey: privateKeyToPublicKey(ledgerAddresses[derivationPath].privateKey), + } + } + return { + address: '', + derivationPath, + publicKey: '', + } + } + + async signTransaction(derivationPath: string, data: string) { + if (ledgerAddresses[derivationPath]) { + const type = determineTXType(ensureLeading0x(data)) + // replicate logic from wallet-base/src/wallet-base.ts + const addToV = type === 'celo-legacy' ? chainIdTransformationForSigning(TEST_CHAIN_ID) : 0 + const hash = getHashFromEncoded(ensureLeading0x(data)) + const { r, s, v } = signTransaction(hash, ledgerAddresses[derivationPath].privateKey, addToV) + + return { + v: v.toString(16), + r: r.toString('hex'), + s: s.toString('hex'), + } + } + throw new Error('Invalid Path') + } + + async signPersonalMessage(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'), + } + } + throw new Error('Invalid Path') + } + + async signEIP712HashedMessage( + 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'), + } + } + + async provideERC20TokenInformation(tokenData: string) { + let pubkey: VerifyPublicKeyInput + const version = (await this.getAppConfiguration()).version + if ( + meetsVersionRequirements(version, { + minimum: LedgerWallet.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) + + // 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 + } +} + +export const mockLedgerImplementation = ( + mockForceValidation: () => void, + config: Config = {} +): Eth => { + const testLedger = new TestLedger(mockForceValidation, config) as unknown as Eth + + jest.spyOn(testLedger, 'getAddress') + return testLedger +} 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 68ecd1fc8..b87adf6e8 100644 --- a/packages/viem-account-ledger/src/ledger-to-account.test.ts +++ b/packages/viem-account-ledger/src/ledger-to-account.test.ts @@ -1,10 +1,11 @@ +import Eth from '@celo/hw-app-eth' 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' -import { generateLedger } from './utils.js' +import { mockLedger, TEST_CHAIN_ID, TestLedger } from './test-utils.js' +import { generateLedger, readAppName } from './utils.js' const USE_PHYSICAL_LEDGER = process.env.USE_PHYSICAL_LEDGER === 'true' const hardwareDescribe = USE_PHYSICAL_LEDGER ? describe : describe.skip @@ -26,165 +27,204 @@ vi.mock('./utils.js', async () => { } }) +// Sample data from the official EIP-712 example: +// https://github.com/ethereum/EIPs/blob/master/assets/eip-712/Example.js +const TYPED_DATA = { + types: { + EIP712Domain: [ + { name: 'name', type: 'string' }, + { name: 'version', type: 'string' }, + { name: 'chainId', type: 'uint256' }, + { name: 'verifyingContract', type: 'address' }, + ], + Person: [ + { name: 'name', type: 'string' }, + { name: 'wallet', type: 'address' }, + ], + Mail: [ + { name: 'from', type: 'Person' }, + { name: 'to', type: 'Person' }, + { name: 'contents', type: 'string' }, + ], + }, + primaryType: 'Mail', + domain: { + name: 'Ether Mail', + version: '1', + chainId: BigInt(1), + verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + message: { + from: { + name: 'Cow', + wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + to: { + name: 'Bob', + wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + contents: 'Hello, Bob!', + }, +} as const + syntheticDescribe('ledgerToAccount (mocked 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', () => { - 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) - }) - }) - - describe('cip64', () => { - test('v=0', async () => { - const account = await ledgerToAccount({ + for (const supportedApp of ['celo', 'ethereum']) { + describe(supportedApp, () => { + beforeAll(async () => { + 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) + vi.spyOn(TestLedger.prototype, 'getName').mockReturnValue(supportedApp) }) - test('v=1', async () => { - const account = await ledgerToAccount({ - transport: await transport, + + describe('signs txs', () => { + 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()) }) - 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', - }) + it('signs typed data', async () => { + if (supportedApp === 'celo') { + await expect(account.signTypedData(TYPED_DATA)).rejects.toMatchInlineSnapshot( + `[Error: Not implemented as of this release.]` + ) + } else if (supportedApp === 'ethereum') { + await expect(account.signTypedData(TYPED_DATA)).resolves.toMatchInlineSnapshot( + `"0x51a454925c2ff4cad0a09cc64fc970685a17f39b2c3a843323f0cc08942d413d15e1ee8c7ff2e12e85eaf1f887cadfbb20b270a579f0945f30de2a73cad4d8ce1c"` ) + } + }) + const txData = { + to: '0x1234567890123456789012345678901234567890', + value: BigInt(123), + chainId: TEST_CHAIN_ID, + nonce: 42, + maxFeePerGas: BigInt(100), + maxPriorityFeePerGas: BigInt(100), + } as const + + describe('eip1559', () => { + test('v=0', async () => { const txHash = await account.signTransaction(txData) - const [recovered] = recoverTransaction(txHash) + expect(txHash).toEqual( + `0x02f86282aef32a6464809412345678901234567890123456789012345678907b80c080a05e130d8edb38e3ee8ab283af7c03a2579598b9a77807d7d796060358787d4707a07219dd22fe3bf3fe57682041d8f80dc9909cd70d903163b077d19625c4cd6e67` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) // @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(() => + expect(decoded.yParity).toBe(0) + }) + test('v=1', async () => { + const txHash = await account.signTransaction({ ...txData, nonce: 100 }) + expect(txHash).toEqual( + `0x02f86282aef3646464809412345678901234567890123456789012345678907b80c001a05d166032c75a416c4e552223b9288a7a280d47909d7f526c2884d21d05a28747a047b32b31eb8a9f035b73218ab2b8b8f3211713fc44ef9b9965e268b6ae064cfc` + ) + const [decoded, signer] = recoverTransaction(txHash) + expect(signer.toLowerCase()).toBe(account.address.toLowerCase()) // @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" - ) - } - }) - }) - }) + expect(decoded.yParity).toBe(1) + }) + }) - 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()) - }) + describe('cip64', () => { + const _test = supportedApp === 'celo' ? test : test.fails - it('signs typed data', async () => { - 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' }, - ], - }, + _test('v=0', async () => { + const account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ ...txData, feeCurrency: cUSDa }) + expect(txHash).toEqual( + `0x7bf87782aef32a6464809412345678901234567890123456789012345678907b80c094874069fa1eb16d44d622f2e0ca25eea172369bc180a017d8df83b40dc645b60142280613467ca92438ff5aa0811a6ceff399fe66d661a02efe4eea14146f41d4f776bec1ededc486ddee37cea8304d297a69dbf27c4089` + ) + 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 account = await ledgerToAccount({ + transport: await transport, + }) + const cUSDa = '0x874069fa1eb16d44d622f2e0ca25eea172369bc1' + const txHash = await account.signTransaction({ + ...txData, + feeCurrency: cUSDa, + nonce: 100, + }) + expect(txHash).toEqual( + `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(TestLedger.prototype, 'signTransaction').mockImplementationOnce(() => + Promise.resolve({ + v: v as string, + 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(TestLedger.prototype, 'signTransaction').mockImplementationOnce(() => + Promise.resolve({ + v: v as string, + r: '0x1', + s: '0x1', + }) + ) + await expect(account.signTransaction(txData)).rejects.toThrowError( + "Ledger signature `v` was malformed and couldn't be parsed" + ) + } + }) + }) }) - ).rejects.toMatchInlineSnapshot(`[Error: Not implemented as of this release.]`) - }) + }) + } }) hardwareDescribe('ledgerToAccount (device ledger)', () => { let account: Awaited> + let currentApp: string beforeAll(async () => { account = await ledgerToAccount({ transport: await transport, }) + currentApp = await readAppName({ transport: await transport } as unknown as Eth) }) it('can be setup', async () => { @@ -219,32 +259,46 @@ hardwareDescribe('ledgerToAccount (device ledger)', () => { }) 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) + const _test = currentApp === 'celo' ? test : test.fails + + _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 + ) }) }) @@ -260,24 +314,12 @@ hardwareDescribe('ledgerToAccount (device ledger)', () => { }, 20_000) it('signs typed data', async () => { - 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.]`) + if (currentApp === 'celo') { + await expect(account.signTypedData(TYPED_DATA)).rejects.toMatchInlineSnapshot( + `[Error: Not implemented as of this release.]` + ) + } else if (currentApp === 'ethereum') { + await expect(account.signTypedData(TYPED_DATA)).resolves.toMatch(/0x[0-9a-fA-F]{130}/) + } }, 20_000) }) diff --git a/packages/viem-account-ledger/src/ledger-to-account.ts b/packages/viem-account-ledger/src/ledger-to-account.ts index 7205a6cbb..529bffd35 100644 --- a/packages/viem-account-ledger/src/ledger-to-account.ts +++ b/packages/viem-account-ledger/src/ledger-to-account.ts @@ -1,17 +1,29 @@ -import { CELO_DERIVATION_PATH_BASE, trimLeading0x } from '@celo/base' +import { CELO_DERIVATION_PATH_BASE, ETHEREUM_DERIVATION_PATH, trimLeading0x } from '@celo/base' import { ensureLeading0x } from '@celo/base/lib/address.js' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import { serializeSignature } from 'viem' +import { + getTypesForEIP712Domain, + hashDomain, + hashStruct, + HashTypedDataParameters, + serializeSignature, +} from 'viem' import { LocalAccount, toAccount } from 'viem/accounts' import { CeloTransactionSerializable, serializeTransaction } from 'viem/celo' -import { checkForKnownToken, generateLedger } from './utils.js' +import { checkForKnownToken, generateLedger, readAppName } from './utils.js' export type LedgerAccount = LocalAccount<'ledger'> -export const ETH_DERIVATION_PATH_BASE = "m/44'/60'/0'" as const +const CIP64_PREFIX = '0x7b' 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 const DEFAULT_DERIVATION_PATH = `${ETHEREUM_DERIVATION_PATH.slice(2)}/0` + +// not exported from viem... +interface MessageTypeProperty { + name: string + type: string +} /** * A function to create a ledger account for viem @@ -34,18 +46,25 @@ export async function ledgerToAccount({ 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 ledgerAppName = await readAppName(ledger) const hash = serializeTransaction(transaction) + if (hash.startsWith(CIP64_PREFIX) && ledgerAppName !== 'celo') { + throw new Error( + 'To submit celo-specific transactions you must use the celo app on your ledger device.' + ) + } + if (ledgerAppName === 'celo') { + await checkForKnownToken(ledger, { + to: transaction.to!, + chainId: transaction.chainId!, + feeCurrency: transaction.feeCurrency, + }) + } + let { r, s, v: _v } = await ledger!.signTransaction(derivationPath, trimLeading0x(hash), null) if (typeof _v === 'string' && (_v === '' || _v === '0x')) { _v = '0x0' @@ -77,8 +96,39 @@ export async function ledgerToAccount({ }) }, - async signTypedData(_parameters) { - throw new Error('Not implemented as of this release.') + async signTypedData(parameters) { + const ledgerAppName = await readAppName(ledger) + if (ledgerAppName === 'celo') { + throw new Error('Not implemented as of this release.') + } + + const { domain = {}, message, primaryType } = parameters as HashTypedDataParameters + const types = { + EIP712Domain: getTypesForEIP712Domain({ domain }), + ...parameters.types, + } + + const domainSeperator = hashDomain({ + domain, + types: types as Record, + }) + const messageHash = hashStruct({ + data: message, + primaryType, + types: types as Record, + }) + + const { r, s, v } = await ledger.signEIP712HashedMessage( + derivationPath, + domainSeperator, + messageHash + ) + + return serializeSignature({ + r: ensureLeading0x(r), + s: ensureLeading0x(s), + v: BigInt(v), + }) }, }) diff --git a/packages/viem-account-ledger/src/test-utils.ts b/packages/viem-account-ledger/src/test-utils.ts index 0cb06a118..27ed99a37 100644 --- a/packages/viem-account-ledger/src/test-utils.ts +++ b/packages/viem-account-ledger/src/test-utils.ts @@ -90,11 +90,44 @@ const TYPED_DATA = { }, } -interface Config extends Partial>> {} +interface Config extends Partial>> { + name?: string +} + +export class TestLedger { + isMock = true + transport: Eth['transport'] + + constructor(readonly config?: Config) { + this.transport = { + send: async (_cla: number, ins: number, _p1: number, _p2: number): Promise => { + if (ins === 0x01) { + // get app information INS + const version = Buffer.from((await this.getAppConfiguration()).version, 'ascii') + const name = Buffer.from(this.getName() ?? 'Celo', 'ascii') + return Buffer.from([0x01, name.byteLength, ...name, version.byteLength, ...version]) + } -export const test_ledger = { - isMock: true, - getAddress: async (derivationPath: string) => { + throw new Error('Unsupport INS ' + ins + ' in mock') + }, + } as Eth['transport'] + } + + getName() { + return this.config?.name + } + + getAppConfiguration() { + return Promise.resolve({ + arbitraryDataEnabled: this.config?.arbitraryDataEnabled ?? 1, + version: this.config?.version ?? MIN_VERSION_EIP1559, + erc20ProvisioningNecessary: this.config?.erc20ProvisioningNecessary ?? 1, + starkEnabled: this.config?.starkEnabled ?? 1, + starkv2Supported: this.config?.starkv2Supported ?? 1, + }) + } + + async getAddress(derivationPath: string) { if (ledgerAddresses[derivationPath]) { const { address, privateKey } = ledgerAddresses[derivationPath] return { @@ -108,8 +141,9 @@ export const test_ledger = { derivationPath, publicKey: '', } - }, - signTransaction: async (derivationPath: string, data: string) => { + } + + async signTransaction(derivationPath: string, data: string) { if (ledgerAddresses[derivationPath]) { const hash = getHashFromEncoded(ensureLeading0x(data)) const { r, s, v } = signTransaction(hash, ledgerAddresses[derivationPath].privateKey) @@ -121,8 +155,9 @@ export const test_ledger = { } } throw new Error('Invalid Path') - }, - signPersonalMessage: async (derivationPath: string, data: string) => { + } + + async signPersonalMessage(derivationPath: string, data: string) { if (ledgerAddresses[derivationPath]) { const signedMessage = await signMessage({ privateKey: ledgerAddresses[derivationPath].privateKey, @@ -131,12 +166,13 @@ export const test_ledger = { return parseSignature(signedMessage) } throw new Error('Invalid Path') - }, - signEIP712HashedMessage: async ( + } + + async signEIP712HashedMessage( derivationPath: string, _domainSeparator: string, _structHash: string - ) => { + ) { const messageHash = generateTypedDataHash(TYPED_DATA) const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) @@ -147,19 +183,11 @@ export const test_ledger = { 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) => { + } + + async provideERC20TokenInformation(tokenData: string) { let pubkey: VerifyPublicKeyInput - const version = (await test_ledger.getAppConfiguration()).version + const version = (await this.getAppConfiguration()).version if ( meetsVersionRequirements(version, { minimum: MIN_VERSION_EIP1559, @@ -194,18 +222,9 @@ export const test_ledger = { throw new Error('couldnt verify data sent to MockLedger') } return verified - }, -} as unknown as Eth - -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 +export const mockLedger = (config: Config = {}): Eth => { + return new TestLedger(config) as unknown as Eth } diff --git a/packages/viem-account-ledger/src/utils.test.ts b/packages/viem-account-ledger/src/utils.test.ts index b630919e3..d88d59c9b 100644 --- a/packages/viem-account-ledger/src/utils.test.ts +++ b/packages/viem-account-ledger/src/utils.test.ts @@ -4,6 +4,7 @@ import { assertCompat, checkForKnownToken, meetsVersionRequirements, + readAppName, transportErrorFriendlyMessage, } from './utils.js' @@ -50,6 +51,20 @@ describe('utils', () => { }) }) + describe('readAppName', () => { + it('works', async () => { + await expect(readAppName(mockLedger({ name: 'unknown' }))).resolves.toMatchInlineSnapshot( + `"unknown"` + ) + await expect( + readAppName(mockLedger({ version: '1.0.0', name: 'Ethereum' })) + ).resolves.toMatchInlineSnapshot(`"ethereum"`) + await expect(readAppName(mockLedger({ name: 'Celo' }))).resolves.toMatchInlineSnapshot( + `"celo"` + ) + }) + }) + describe('assertCompat', () => { it("throws if it doesn't meet the requirements", async () => { await expect(assertCompat(mockLedger({ version: '1.0.0' }))).rejects.toMatchInlineSnapshot( @@ -65,6 +80,15 @@ describe('utils', () => { ] `) }) + it('warns if it using Ethereum', async () => { + const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined) + await expect(assertCompat(mockLedger({ name: 'ethereum' }))).resolves.toBeTruthy() + expect(warn.mock.lastCall).toMatchInlineSnapshot(` + [ + "Beware, you opened the Ethereum app instead of the Celo app. Some features may not work correctly, including token transfers.", + ] + `) + }) it('works', async () => { await expect(assertCompat(mockLedger())).resolves.toBeTruthy() }) diff --git a/packages/viem-account-ledger/src/utils.ts b/packages/viem-account-ledger/src/utils.ts index 828c30a48..cc37e036b 100644 --- a/packages/viem-account-ledger/src/utils.ts +++ b/packages/viem-account-ledger/src/utils.ts @@ -24,15 +24,46 @@ export function meetsVersionRequirements( return min && max } +export async function readAppName(ledger: Eth): Promise { + const response = await ledger.transport.send(0xb0, 0x01, 0x00, 0x00) + try { + let results = [] // (name, version) + let i = 1 + while (i < response.length + 1) { + const len = response[i] + i += 1 + const bufValue = response.subarray(i, i + len) + i += len + results.push(bufValue.toString('ascii').trim()) + } + + return results[0]!.toLowerCase() + } catch (err) { + console.error('The appName couldnt be infered from the device') + throw err + } +} + export async function assertCompat(ledger: Eth): Promise<{ arbitraryDataEnabled: number version: string }> { - // TODO: check version only for CELO and not ETH if we wanna be eth compatible const appConfiguration = await ledger.getAppConfiguration() - if (!meetsVersionRequirements(appConfiguration.version, { minimum: MIN_VERSION_EIP1559 })) { + const appName = await readAppName(ledger) + if (appName === 'celo') { + // only check version for Celo + if (!meetsVersionRequirements(appConfiguration.version, { minimum: MIN_VERSION_EIP1559 })) { + throw new Error( + `Due to technical issues, we require the users to update their ledger celo-app to >= ${MIN_VERSION_EIP1559}. You can do this on ledger-live by updating the celo-app in the app catalog.` + ) + } + } else if (appName === 'ethereum') { + console.warn( + `Beware, you opened the Ethereum app instead of the Celo app. Some features may not work correctly, including token transfers.` + ) + } else { throw new Error( - `Due to technical issues, we require the users to update their ledger celo-app to >= ${MIN_VERSION_EIP1559}. You can do this on ledger-live by updating the celo-app in the app catalog.` + `Beware, you opened the ${appName} app instead of the Celo app. We cannot ensure the safety of using this SDK with ${appName}.` ) } if (!appConfiguration.arbitraryDataEnabled) {