From 2873855f6b3b5019cdebac650cbe2282e7c210ba Mon Sep 17 00:00:00 2001 From: "Felix C. Morency" <1102868+fmorency@users.noreply.github.com> Date: Fri, 10 Jan 2025 09:26:04 -0500 Subject: [PATCH] fix: ledger wallet - Fixes the missing `getOfflineSigner` implementation - Fixes `StdSignDoc` serialization to JSON. The object MUST be serialized in lexicographical key order - Replace the encoding of `pubkey` in `getAccount` from a `TextEncoder` to using the `fromHex` function. --- wallets/ledger/package.json | 12 ++--- wallets/ledger/src/index.ts | 2 +- .../ledger/src/web-usb-hid/chain-wallet.ts | 4 +- wallets/ledger/src/web-usb-hid/client.ts | 44 +++++++++++++++++-- wallets/ledger/src/web-usb-hid/main-wallet.ts | 8 ++-- wallets/ledger/src/web-usb-hid/registry.ts | 1 + wallets/ledger/src/web-usb-hid/utils.ts | 22 +++++----- yarn.lock | 31 +++++++++++-- 8 files changed, 94 insertions(+), 30 deletions(-) diff --git a/wallets/ledger/package.json b/wallets/ledger/package.json index d5c20a6f6..ff45a4c48 100644 --- a/wallets/ledger/package.json +++ b/wallets/ledger/package.json @@ -66,12 +66,14 @@ }, "dependencies": { "@cosmos-kit/core": "^2.15.1", - "@ledgerhq/hw-app-cosmos": "^6.28.1", - "@ledgerhq/hw-transport-webhid": "^6.27.15", - "@ledgerhq/hw-transport-webusb": "^6.27.15" + "@ledgerhq/hw-app-cosmos": "^6.30.4", + "@ledgerhq/hw-transport-webhid": "^6.30.0", + "@ledgerhq/hw-transport-webusb": "^6.29.4" }, "peerDependencies": { - "@cosmjs/amino": ">=0.32.3", - "@cosmjs/proto-signing": ">=0.32.3" + "@cosmjs/amino": ">=0.32.4", + "@cosmjs/crypto": ">=0.32.4", + "@cosmjs/encoding": ">=0.32.4", + "@cosmjs/proto-signing": ">=0.32.4" } } diff --git a/wallets/ledger/src/index.ts b/wallets/ledger/src/index.ts index 2037bc16b..a1b9991fb 100644 --- a/wallets/ledger/src/index.ts +++ b/wallets/ledger/src/index.ts @@ -1,3 +1,3 @@ +export * from './constant'; export * from './ledger'; export * from './web-usb-hid/registry'; -export * from './constant'; diff --git a/wallets/ledger/src/web-usb-hid/chain-wallet.ts b/wallets/ledger/src/web-usb-hid/chain-wallet.ts index e62e83e16..723645ea1 100644 --- a/wallets/ledger/src/web-usb-hid/chain-wallet.ts +++ b/wallets/ledger/src/web-usb-hid/chain-wallet.ts @@ -1,7 +1,7 @@ import { ChainRecord, ChainWalletBase, Wallet } from '@cosmos-kit/core'; -export class LedgerChianWallet extends ChainWalletBase { +export class LedgerChainWallet extends ChainWalletBase { constructor(walletInfo: Wallet, chainInfo: ChainRecord) { super(walletInfo, chainInfo); } -} \ No newline at end of file +} diff --git a/wallets/ledger/src/web-usb-hid/client.ts b/wallets/ledger/src/web-usb-hid/client.ts index a3bffa7a4..856ec07c9 100644 --- a/wallets/ledger/src/web-usb-hid/client.ts +++ b/wallets/ledger/src/web-usb-hid/client.ts @@ -1,9 +1,17 @@ -import { StdSignDoc } from '@cosmjs/amino'; +import { + encodeSecp256k1Signature, + OfflineAminoSigner, + StdSignDoc, +} from '@cosmjs/amino'; +import { sortedJsonStringify } from '@cosmjs/amino/build/signdoc'; +import { Secp256k1Signature } from '@cosmjs/crypto'; +import { fromHex } from '@cosmjs/encoding'; import { Algo } from '@cosmjs/proto-signing'; -import { WalletClient } from '@cosmos-kit/core'; +import { SignType, WalletClient } from '@cosmos-kit/core'; import Cosmos from '@ledgerhq/hw-app-cosmos'; import { ChainIdToBech32Prefix, getCosmosApp, getCosmosPath } from './utils'; + export class LedgerClient implements WalletClient { client: Cosmos; @@ -39,16 +47,44 @@ export class LedgerClient implements WalletClient { username: username ?? path, address, algo: 'secp256k1' as Algo, - pubkey: new TextEncoder().encode(publicKey), + pubkey: fromHex(publicKey), isNanoLedger: true, }; } + getOfflineSigner(chainId: string, preferredSignType?: SignType) { + // Ledger doesn't support direct sign, only Amino sign + if (preferredSignType === 'direct') { + throw new Error('Unsupported sign type: direct'); + } + return this.getOfflineSignerAmino(chainId); + } + + getOfflineSignerAmino(chainId: string): OfflineAminoSigner { + return { + getAccounts: async () => { + return [await this.getAccount(chainId)]; + }, + signAmino: async (_signerAddress, signDoc) => { + const { pubkey } = await this.getAccount(chainId); + const { signature: derSignature } = await this.sign(signDoc); // The signature is in DER format + const signature = Secp256k1Signature.fromDer(derSignature); // Convert the DER signature to fixed length (64 bytes) + return { + signed: signDoc, + signature: encodeSecp256k1Signature( + pubkey, + signature.toFixedLength() + ), + }; + }, + }; + } + async sign(signDoc: StdSignDoc, accountIndex = 0) { if (!this.client) await this.initClient(); return await this.client.sign( getCosmosPath(accountIndex), - JSON.stringify(signDoc) + sortedJsonStringify(signDoc) // signDoc MUST be serialized in lexicographical key order ); } } diff --git a/wallets/ledger/src/web-usb-hid/main-wallet.ts b/wallets/ledger/src/web-usb-hid/main-wallet.ts index 09cb7eea3..38d61252a 100644 --- a/wallets/ledger/src/web-usb-hid/main-wallet.ts +++ b/wallets/ledger/src/web-usb-hid/main-wallet.ts @@ -1,6 +1,6 @@ -import { EndpointOptions, Wallet } from '@cosmos-kit/core'; -import { MainWalletBase } from '@cosmos-kit/core'; -import { LedgerChianWallet } from './chain-wallet'; +import { EndpointOptions, MainWalletBase, Wallet } from '@cosmos-kit/core'; + +import { LedgerChainWallet } from './chain-wallet'; import { LedgerClient } from './client'; import { TransportType } from './utils'; @@ -11,7 +11,7 @@ export class LedgerMainWallet extends MainWalletBase { preferredEndpoints?: EndpointOptions['endpoints'], transportType: TransportType = 'WebUSB' ) { - super(walletInfo, LedgerChianWallet); + super(walletInfo, LedgerChainWallet); this.preferredEndpoints = preferredEndpoints; this.transportType = transportType; } diff --git a/wallets/ledger/src/web-usb-hid/registry.ts b/wallets/ledger/src/web-usb-hid/registry.ts index b5bc6ebad..2b48b1743 100644 --- a/wallets/ledger/src/web-usb-hid/registry.ts +++ b/wallets/ledger/src/web-usb-hid/registry.ts @@ -1,4 +1,5 @@ import { Wallet } from '@cosmos-kit/core'; + import { ICON } from '../constant'; export const LedgerInfo: Wallet = { diff --git a/wallets/ledger/src/web-usb-hid/utils.ts b/wallets/ledger/src/web-usb-hid/utils.ts index 9b3600e75..928e129c9 100644 --- a/wallets/ledger/src/web-usb-hid/utils.ts +++ b/wallets/ledger/src/web-usb-hid/utils.ts @@ -1,25 +1,25 @@ -import { chains } from 'chain-registry' -import Cosmos from "@ledgerhq/hw-app-cosmos"; +import Cosmos from '@ledgerhq/hw-app-cosmos'; +import TransportWebHID from '@ledgerhq/hw-transport-webhid'; import TransportWebUSB from '@ledgerhq/hw-transport-webusb'; -import TransportWebHID from '@ledgerhq/hw-transport-webhid' +import { chains } from 'chain-registry'; -export type TransportType = 'WebUSB' | 'WebHID' +export type TransportType = 'WebUSB' | 'WebHID'; export async function getCosmosApp(type: TransportType = 'WebUSB') { if (type === 'WebUSB') { - return new Cosmos(await TransportWebUSB.create()) + return new Cosmos(await TransportWebUSB.create()); } if (type === 'WebHID') { - return new Cosmos(await TransportWebHID.create()) + return new Cosmos(await TransportWebHID.create()); } - throw new Error(`Unknown transport type: ${type}`) + throw new Error(`Unknown transport type: ${type}`); } export function getCosmosPath(accountIndex = 0) { - return `44'/118'/${accountIndex}'/0/0` + return `44'/118'/${accountIndex}'/0/0`; } -export const ChainIdToBech32Prefix = {} as { [k: string]: string } +export const ChainIdToBech32Prefix = {} as { [k: string]: string }; for (const chain of chains) { - ChainIdToBech32Prefix[chain.chain_id] = chain.bech32_prefix -} \ No newline at end of file + ChainIdToBech32Prefix[chain.chain_id] = chain.bech32_prefix; +} diff --git a/yarn.lock b/yarn.lock index e0a2892d8..5e05c9156 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18024,7 +18024,16 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -18140,7 +18149,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -18168,6 +18177,13 @@ strip-ansi@^5.1.0: dependencies: ansi-regex "^4.1.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -19723,7 +19739,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19741,6 +19757,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"