Skip to content

Commit e77c352

Browse files
committed
support ledger cosmos signing in staking-cli
1 parent 7f66e10 commit e77c352

File tree

11 files changed

+906
-61
lines changed

11 files changed

+906
-61
lines changed

package-lock.json

+833-27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cosmos/src/staker.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -309,9 +309,10 @@ export class CosmosStaker {
309309
const acc = await getAccount(cosmosClient, this.networkConfig.lcdUrl, signerAddress)
310310
const signDoc = await genSignableTx(this.networkConfig, chainID, tx, acc.accountNumber, acc.sequence, memo ?? '')
311311

312-
const { sig, pk } = await genSignDocSignature(signer, acc, signDoc)
312+
const isEVM = this.networkConfig.isEVM ?? false
313+
const { sig, pk } = await genSignDocSignature(signer, acc, signDoc, isEVM)
313314

314-
const pkType = this.networkConfig.isEVM ? acc.pubkey?.type ?? undefined : undefined
315+
const pkType = isEVM ? acc.pubkey?.type ?? undefined : undefined
315316
const signedTx = genSignedTx(signDoc, sig, pk, pkType)
316317

317318
// IMPORTANT: verify that signer address matches derived address from signature

packages/cosmos/src/tx.ts

+9-6
Original file line numberDiff line numberDiff line change
@@ -216,15 +216,18 @@ export async function genSignableTx (
216216
export async function genSignDocSignature (
217217
signer: Signer,
218218
signerAccount: Account,
219-
signDoc: StdSignDoc
219+
signDoc: StdSignDoc,
220+
isEVM: boolean
220221
): Promise<{ sig: Signature; pk: Uint8Array }> {
221-
if (signerAccount.pubkey === null) {
222-
throw new Error('Signer account pubkey is not set')
222+
// The LCD doesn't have to return a public key for an acocunt, therefore
223+
// we do a best effort check to assert the pubkey type is ethsecp256k1
224+
if (isEVM && signerAccount.pubkey !== undefined) {
225+
if (!signerAccount.pubkey?.type.toLowerCase().includes('ethsecp256k1')) {
226+
throw new Error('signer account pubkey type is not ethsecp256k1')
227+
}
223228
}
224229

225-
const msg = signerAccount.pubkey.type.toLowerCase().includes('ethsecp256k1')
226-
? keccak256(serializeSignDoc(signDoc))
227-
: new Sha256(serializeSignDoc(signDoc)).digest()
230+
const msg = isEVM ? keccak256(serializeSignDoc(signDoc)) : new Sha256(serializeSignDoc(signDoc)).digest()
228231
const message = Buffer.from(msg).toString('hex')
229232
const note = SafeJSONStringify(signDoc, 2)
230233

packages/signer-ledger-cosmos/package.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"dependencies": {
3333
"@chorus-one/signer": "^1.0.0",
3434
"@chorus-one/utils": "^1.0.0",
35-
"@ledgerhq/hw-app-cosmos": "^6.7.0",
36-
"@ledgerhq/hw-transport": "^6.31.0"
35+
"@ledgerhq/hw-transport": "^6.31.0",
36+
"@zondax/ledger-cosmos-js": "^4.0.0",
37+
"@noble/curves": "^1.4.2"
3738
}
3839
}

packages/signer-ledger-cosmos/src/signer.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { nopLogger } from '@chorus-one/utils'
2-
import type { Logger } from '@chorus-one/utils'
2+
import type { Logger, sortObjectByKeys } from '@chorus-one/utils'
33
import type { LedgerCosmosSignerConfig } from './types'
44
import type { Signature, SignerData } from '@chorus-one/signer'
5-
import Cosmos from '@ledgerhq/hw-app-cosmos'
5+
import { secp256k1 } from '@noble/curves/secp256k1'
6+
import CosmosApp from '@zondax/ledger-cosmos-js'
67
import Transport from '@ledgerhq/hw-transport'
78

89
/**
@@ -16,7 +17,7 @@ export class LedgerCosmosSigner {
1617
private readonly config: LedgerCosmosSignerConfig
1718
private readonly transport: Transport
1819
private accounts: Map<string, { hdPath: string; publicKey: Uint8Array }>
19-
private app?: Cosmos
20+
private app?: CosmosApp
2021
private logger: Logger
2122

2223
/**
@@ -43,15 +44,15 @@ export class LedgerCosmosSigner {
4344
* @returns A promise that resolves once the initialization is complete.
4445
*/
4546
async init (): Promise<void> {
46-
const app = new Cosmos(this.transport)
47+
const app = new CosmosApp(this.transport)
4748
this.app = app
4849

4950
this.config.accounts.forEach(async (account: { hdPath: string }) => {
50-
const response = await app.getAddress(account.hdPath, this.config.bechPrefix)
51+
const response = await app.getAddressAndPubKey(account.hdPath, this.config.bechPrefix)
5152

52-
this.accounts.set(response.address.toLowerCase(), {
53+
this.accounts.set(response.bech32_address.toLowerCase(), {
5354
hdPath: account.hdPath,
54-
publicKey: Buffer.from(response.publicKey, 'hex')
55+
publicKey: response.compressed_pk
5556
})
5657
})
5758
}
@@ -75,16 +76,23 @@ export class LedgerCosmosSigner {
7576
}
7677

7778
const account = this.getAccount(signerAddress)
78-
const { signature, return_code } = await this.app.sign(account.hdPath, signerData.message)
7979

80-
if (signature === null) {
81-
throw new Error(`failed to sign message: ${return_code}`)
80+
const { signature } = await this.app.sign(
81+
account.hdPath,
82+
Buffer.from(JSON.stringify(sortObjectByKeys(signerData.data.signDoc)), 'utf-8'),
83+
this.config.bechPrefix,
84+
0 // 0 for JSON, 1 for TEXTUAL
85+
)
86+
87+
if (signature.length === 0) {
88+
throw new Error(`failed to sign message`)
8289
}
8390

91+
const secpsig = secp256k1.Signature.fromDER(signature)
8492
const sig = {
85-
fullSig: Buffer.from(signature).toString('hex'),
86-
r: Buffer.from(signature.subarray(0, 32)).toString('hex'),
87-
s: Buffer.from(signature.subarray(32, 64)).toString('hex'),
93+
fullSig: secpsig.toCompactHex(),
94+
r: Buffer.from(secpsig.toCompactRawBytes().subarray(0, 32)).toString('hex'),
95+
s: Buffer.from(secpsig.toCompactRawBytes().subarray(32, 64)).toString('hex'),
8896
v: 0
8997
}
9098

packages/staking-cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"@chorus-one/signer": "^1.0.0",
3333
"@chorus-one/signer-fireblocks": "^1.0.0",
3434
"@chorus-one/signer-ledger-ton": "^1.0.0",
35+
"@chorus-one/signer-ledger-cosmos": "^1.0.0",
3536
"@chorus-one/signer-local": "^1.0.0",
3637
"@chorus-one/solana": "^1.0.0",
3738
"@chorus-one/substrate": "^1.0.1",

packages/staking-cli/src/signer.ts

+15-10
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { FireblocksSigner } from '@chorus-one/signer-fireblocks'
77
import { LocalSigner } from '@chorus-one/signer-local'
88
import { Logger } from '@chorus-one/utils'
99
import { LedgerTonSigner } from '@chorus-one/signer-ledger-ton'
10+
import { LedgerCosmosSigner } from '@chorus-one/signer-ledger-cosmos'
1011
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid'
1112

1213
export async function newSigner (
@@ -53,17 +54,21 @@ export async function newSigner (
5354
})
5455
}
5556
case SignerType.LEDGER: {
56-
if (config.networkType !== 'ton') {
57-
throw new Error('Ledger signer is only supported for TON network')
57+
switch (config.networkType) {
58+
case 'ton':
59+
return new LedgerTonSigner({
60+
transport: await TransportNodeHid.create(),
61+
logger: options.logger,
62+
...config.ledger
63+
})
64+
case 'cosmos':
65+
return new LedgerCosmosSigner({
66+
transport: await TransportNodeHid.create(),
67+
logger: options.logger,
68+
...config.ledger
69+
})
5870
}
59-
60-
const transport = await TransportNodeHid.create()
61-
62-
return new LedgerTonSigner({
63-
transport: transport,
64-
logger: options.logger,
65-
...config.ledger
66-
})
71+
throw new Error(`Ledger signer is not supported for ${config.networkType} network type`)
6772
}
6873
}
6974
}

packages/staking-cli/src/types.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export interface Config {
3636
localsigner: LocalSignerCliConfig
3737

3838
// use ledger hardware wallet as signer
39-
ledger: LedgerTonSignerConfig // | LedgerCosmosSignerConfig | ...
39+
ledger: LedgerTonSignerConfig | LedgerCosmosSignerConfig
4040

4141
// the network type to interact with
4242
networkType: NetworkType

packages/staking-cli/tsconfig.cjs.json

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
{ "path": "../near" },
1313
{ "path": "../signer-fireblocks" },
1414
{ "path": "../signer-local" },
15+
{ "path": "../signer-ledger-ton" },
16+
{ "path": "../signer-ledger-cosmos" },
1517
{ "path": "../signer" },
1618
{ "path": "../solana" },
1719
{ "path": "../substrate" },

packages/staking-cli/tsconfig.json

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"@chorus-one/near": ["../near/src"],
1111
"@chorus-one/signer-fireblocks": ["../signer-fireblocks/src"],
1212
"@chorus-one/signer-local": ["../signer-local/src"],
13+
"@chorus-one/signer-ledger-ton": ["../signer-ledger-ton/src"],
14+
"@chorus-one/signer-ledger-cosmos": ["../signer-ledger-cosmos/src"],
1315
"@chorus-one/signer": ["../signer/src"],
1416
"@chorus-one/solana": ["../solana/src"],
1517
"@chorus-one/substrate": ["../substrate/src"],
@@ -26,6 +28,8 @@
2628
{ "path": "../near" },
2729
{ "path": "../signer-fireblocks" },
2830
{ "path": "../signer-local" },
31+
{ "path": "../signer-ledger-ton" },
32+
{ "path": "../signer-ledger-cosmos" },
2933
{ "path": "../signer" },
3034
{ "path": "../solana" },
3135
{ "path": "../substrate" },

packages/utils/src/index.ts

+14
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,17 @@ export function checkMaxDecimalPlaces (denomMultiplier: string) {
3434
throw new Error(`denomMultiplier ${denomMultiplier} exceeds maximum decimal precision: ${MAX_DECIMAL_PLACES}`)
3535
}
3636
}
37+
38+
export function sortObjectByKeys<T> (obj: T): T {
39+
if (Array.isArray(obj)) {
40+
return obj.map(sortObjectByKeys) as T
41+
} else if (obj !== null && typeof obj === 'object') {
42+
return Object.keys(obj)
43+
.sort()
44+
.reduce((acc, key) => {
45+
;(acc as any)[key] = sortObjectByKeys((obj as any)[key])
46+
return acc
47+
}, {} as T)
48+
}
49+
return obj
50+
}

0 commit comments

Comments
 (0)