Skip to content

Commit d29b7e1

Browse files
author
Fabrice Bascoulergue
committed
Refactor signature generation and add ledger support
1 parent 2fcdbf4 commit d29b7e1

File tree

11 files changed

+197
-46
lines changed

11 files changed

+197
-46
lines changed

docs/README.md

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ A couple examples to help you get started.
1212

1313
```typescript
1414
import {
15-
LumWallet,
15+
LumWalletFactory,
1616
LumClient,
1717
LumTypes,
1818
LumUtils,
@@ -21,15 +21,15 @@ import {
2121
} from '@lum-network/sdk-javascript'
2222
```
2323

24-
### Create a wallet
24+
### Software wallets
2525

2626
#### Mnemonic
2727
```typescript
2828
// Create a new cryptographically secure random mnemonic
2929
const mnemonic = LumUtils.generateMnemonic(12);
3030

3131
// Create a wallet instance based on this fresh mnemonic
32-
const wallet = await LumWallet.fromMnemonic(mnemonic);
32+
const wallet = await LumWalletFactory.fromMnemonic(mnemonic);
3333
```
3434

3535
#### Private key
@@ -38,12 +38,12 @@ const wallet = await LumWallet.fromMnemonic(mnemonic);
3838
const privateKey = LumUtils.generatePrivateKey();
3939

4040
// Create a wallet instance based on this fresh private key
41-
const wallet = await LumWallet.fromPrivateKey(mnemonic);
41+
const wallet = await LumWalletFactory.fromPrivateKey(mnemonic);
4242
console.log(`Wallet address: ${wallet.address}`);
4343

4444
// Create a wallet instance based on an hexadecimal private key (ex: user input - 0x is optional)
4545
const hexPrivateKey = '0xb8e62c34928025cdd3aef6cbebc68694b5ad9209b2aff6d3891c8e61d22d3a3b';
46-
const existingWallet = await LumWallet.fromPrivateKey(LumUtils.keyFromHex(hexPrivateKey));
46+
const existingWallet = await LumWalletFactory.fromPrivateKey(LumUtils.keyFromHex(hexPrivateKey));
4747
console.log(`Existing wallet address: ${wallet.address}`);
4848
```
4949

@@ -53,10 +53,49 @@ console.log(`Existing wallet address: ${wallet.address}`);
5353
const privateKey = LumUtils.generatePrivateKey();
5454
// Create a keystore (or consume user input)
5555
const keystore = LumUtils.generateKeyStore(privateKey, 'some-password');
56-
const wallet = await LumWallet.fromKeyStore(keystore, 'some-password');
56+
const wallet = await LumWalletFactory.fromKeyStore(keystore, 'some-password');
5757
console.log(`Wallet address: ${wallet.address}`);
5858
```
5959

60+
### Hardware wallets
61+
62+
**IMPORTANT NOTES:**
63+
- Transaction signature using Hardware devices is currently work in progress, therefore broadcasting a transaction is not possible at the moment.
64+
- Derivation path using the Cosmos Ledger application cannot be set to the default Lum Path for now `m/44'/118'/0'/*/*` and must remain on the Cosmos path `m/44'/'837/0'/*/*`
65+
66+
#### Ledger
67+
68+
The SDK only provides access to the Ledger API using a provided Transport.
69+
Ledger transport must be initialized and handled by the code using the SDK.
70+
71+
See [LedgerHQ/ledgerjs documentation](https://github.com/LedgerHQ/ledgerjs) for more information.
72+
73+
```typescript
74+
import TransportNodeHid from '@ledgerhq/hw-transport-node-hid';
75+
76+
// Connect your ledger device
77+
// Unlock it
78+
// Open the Cosmos application
79+
80+
// Create a Node HID transport
81+
const transport = await TransportNodeHid.create();
82+
83+
// Create the ledger based wallet instance
84+
const wallet = await LumWalletFactory.fromLedgerTransport(transport, `m/44'/118'/0'/0/0`, 'lum');
85+
86+
// Change account to 1 and wallet to 1 (optional)
87+
await wallet.useAccount(`m/44'/118'/0'/1/1`, 'lum');
88+
89+
// Get account information
90+
const account = await testnetClient.getAccount(wallet.getAddress());
91+
if (account === null) {
92+
console.log('Account: not found');
93+
} else {
94+
console.log(`Account: ${account.address}, ${account.accountNumber}, ${account.sequence}`);
95+
}
96+
```
97+
98+
6099
### Connect to the testnet
61100

62101
```typescript
@@ -78,7 +117,7 @@ if (account === null) {
78117

79118
#### Get account balances
80119
```typescript
81-
const balances = await testnetClient.getBalancesUnverified(wallet.address);
120+
const balances = await testnetClient.getAllBalancesUnverified(wallet.address);
82121
if (balances.length === 0) {
83122
console.log('Balances: empty account');
84123
} else {
@@ -115,8 +154,19 @@ const fee = {
115154
amount: [{ denom: LumConstants.LumDenom, amount: '1' }],
116155
gas: '100000',
117156
};
157+
// Fetch account number and sequence
158+
const account = await testnetClient.getAccount(wallet.address);
159+
// Create the transaction document
160+
const doc = {
161+
accountNumber: account.accountNumber,
162+
chainId,
163+
fee: fee,
164+
memo: 'my transaction memo',
165+
messages: [sendMsg],
166+
sequence: account.sequence,
167+
};
118168
// Sign and broadcast the transaction using the client
119-
const broadcastResult = await clt.signAndBroadcastTx(w1, [sendMsg], fee, 'hello memo!');
169+
const broadcastResult = await clt.signAndBroadcastTx(w1, doc);
120170
// Verify the transaction was succesfully broadcasted and made it into a block
121171
console.log(`Broadcast success: ${LumUtils.broadcastTxCommitSuccess(broadcastResult)}`);
122172
```

jest.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ module.exports = {
1313
tsconfig: 'tsconfig.spec.json',
1414
},
1515
},
16-
testTimeout: 10000,
16+
testTimeout: 30000,
1717
};

src/client/LumClient.ts

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -243,21 +243,15 @@ export class LumClient {
243243
* Signs the messages using the provided wallet and builds the transaction
244244
*
245245
* @param wallet signing wallet
246-
* @param messages messages to sign
247-
* @param fee requested fee
248-
* @param memo optional memo for the transaction
246+
* @param doc document to sign
249247
*/
250-
signTx = async (wallet: LumWallet, messages: LumMessages.Message[], fee: LumTypes.Fee, memo?: string): Promise<Uint8Array> => {
248+
signTx = async (wallet: LumWallet, doc: LumTypes.Doc): Promise<Uint8Array> => {
251249
const account = await this.getAccount(wallet.getAddress());
252250
if (!account) {
253251
throw new Error('Account not found');
254252
}
255-
const { accountNumber, sequence } = account;
256-
const chainId = await this.getChainId();
257-
258-
const authInfo = LumUtils.generateAuthInfo(wallet.getPublicKey(), fee, sequence);
259-
const signDoc = LumUtils.generateSignDoc(messages, memo, authInfo, chainId, accountNumber);
260-
const signature = await wallet.signTransaction(signDoc);
253+
const signDoc = LumUtils.generateSignDoc(doc, wallet.getPublicKey());
254+
const signature = await wallet.signTransaction(doc);
261255
return LumUtils.generateTxBytes(signDoc, signature);
262256
};
263257

@@ -276,12 +270,10 @@ export class LumClient {
276270
* Signs and broadcast the transaction using the specified wallet and messages
277271
*
278272
* @param wallet signing wallet
279-
* @param messages messages to sign
280-
* @param fee requested fee
281-
* @param memo optional memo for the transaction
273+
* @param doc document to sign and broadcast as a transaction
282274
*/
283-
signAndBroadcastTx = async (wallet: LumWallet, messages: LumMessages.Message[], fee: LumTypes.Fee, memo?: string): Promise<LumTypes.BroadcastTxCommitResponse> => {
284-
const signedTx = await this.signTx(wallet, messages, fee, memo);
275+
signAndBroadcastTx = async (wallet: LumWallet, doc: LumTypes.Doc): Promise<LumTypes.BroadcastTxCommitResponse> => {
276+
const signedTx = await this.signTx(wallet, doc);
285277
return this.broadcastTx(signedTx);
286278
};
287279
}

src/types/Doc.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { Message } from '../messages';
2+
import { Fee } from './Fee';
3+
4+
export interface Doc {
5+
/** account_number is the account number of the account in state */
6+
accountNumber: number;
7+
/**
8+
* chain_id is the unique identifier of the chain this transaction targets.
9+
* It prevents signed transactions from being used on another chain by an
10+
* attacker
11+
*/
12+
chainId: string;
13+
/**
14+
* Transaction requested Fee
15+
*/
16+
fee: Fee;
17+
/**
18+
* Transaction memo
19+
*/
20+
memo?: string;
21+
/**
22+
* Transactions messages
23+
*/
24+
messages: Message[];
25+
/**
26+
* Transction sequence number
27+
*/
28+
sequence: number;
29+
}

src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ export * from './Fee';
66
export * from './Description';
77
export * from './Commission';
88
export * from './CommissionRates';
9+
export * from './Doc';
910
export * from './SignDoc';
1011
export * from './Log';

src/utils/commons.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,26 @@
11
export { isNonNullObject, isUint8Array } from '@cosmjs/utils';
2+
3+
/**
4+
* Sorts an object properties recursively.
5+
*
6+
* @param jsonObj object to sort
7+
* @returns a new object with keys sorted alphabetically
8+
*/
9+
export const sortJSON = <T>(jsonObj: T): T => {
10+
if (jsonObj instanceof Array) {
11+
for (let i = 0; i < jsonObj.length; i++) {
12+
jsonObj[i] = sortJSON(jsonObj[i]);
13+
}
14+
return jsonObj;
15+
} else if (typeof jsonObj !== 'object') {
16+
return jsonObj;
17+
}
18+
19+
let keys = Object.keys(jsonObj) as Array<keyof T>;
20+
keys = keys.sort();
21+
const newObject: Partial<T> = {};
22+
for (let i = 0; i < keys.length; i++) {
23+
newObject[keys[i]] = sortJSON(jsonObj[keys[i]]);
24+
}
25+
return newObject as T;
26+
};

src/utils/transactions.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import { makeAuthInfoBytes, makeSignBytes } from '@cosmjs/proto-signing';
66
import { TxRaw } from '../codec/cosmos/tx/v1beta1/tx';
77

88
import { sha256 } from './encoding';
9-
import { Message } from '../messages';
10-
import { Fee, SignDoc } from '../types';
9+
import { Fee, Doc, SignDoc } from '../types';
1110
import { publicKeyToProto } from './keys';
1211
import { LumRegistry } from '../registry';
1312

@@ -18,7 +17,7 @@ import { LumRegistry } from '../registry';
1817
* @param fee requested fee
1918
* @param sequence account sequence number
2019
*/
21-
export const generateAuthInfo = (publicKey: Uint8Array, fee: Fee, sequence: number): Uint8Array => {
20+
export const generateAuthInfoBytes = (publicKey: Uint8Array, fee: Fee, sequence: number): Uint8Array => {
2221
const pubkeyAny = publicKeyToProto(publicKey);
2322
const gasLimit = Int53.fromString(fee.gas).toNumber();
2423
return makeAuthInfoBytes([pubkeyAny], fee.amount, gasLimit, sequence);
@@ -27,16 +26,13 @@ export const generateAuthInfo = (publicKey: Uint8Array, fee: Fee, sequence: numb
2726
/**
2827
* Generate transaction doc to be signed
2928
*
30-
* @param messages Transaction messages
31-
* @param memo optional memo for the transaction
32-
* @param authInfoBytes info bytes (as generated by the generateAuthInfo function)
33-
* @param chainId chain id
34-
* @param accountNumber account number
29+
* @param doc document to create the sign version
30+
* @param publicKey public key used for signature
3531
*/
36-
export const generateSignDoc = (messages: Message[], memo: string | undefined, authInfoBytes: Uint8Array, chainId: string, accountNumber: number): SignDoc => {
32+
export const generateSignDoc = (doc: Doc, publicKey: Uint8Array): SignDoc => {
3733
const txBody = {
38-
messages: messages,
39-
memo: memo,
34+
messages: doc.messages,
35+
memo: doc.memo,
4036
};
4137
const bodyBytes = LumRegistry.encode({
4238
typeUrl: '/cosmos.tx.v1beta1.TxBody',
@@ -45,9 +41,9 @@ export const generateSignDoc = (messages: Message[], memo: string | undefined, a
4541

4642
return {
4743
bodyBytes,
48-
authInfoBytes,
49-
chainId,
50-
accountNumber: Long.fromNumber(accountNumber),
44+
authInfoBytes: generateAuthInfoBytes(publicKey, doc.fee, doc.sequence),
45+
chainId: doc.chainId,
46+
accountNumber: Long.fromNumber(doc.accountNumber),
5147
};
5248
};
5349

src/wallet/LumLedgerWallet.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,29 @@ export class LumLedgerWallet extends LumWallet {
2525
return true;
2626
};
2727

28-
signTransaction = async (doc: LumTypes.SignDoc): Promise<Uint8Array> => {
28+
signTransaction = async (doc: LumTypes.Doc): Promise<Uint8Array> => {
2929
if (!this.hdPath) {
3030
throw new Error('No account selected.');
3131
}
32-
// Implementation is delayed. We are not sure if we will be able to sign transactions using the new protobuf implementation
32+
// TODO: does not work as intented - signature "works" but not valid for broadcast using the client
33+
// Implementation not working. We are not sure if we will be able to sign transactions using the new protobuf implementation
3334
// with the current cosmos ledger application
3435
//
3536
// Useful doc & code:
3637
// sign call: https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-cosmos/src/Cosmos.js
3738
// Expected tx format: https://github.com/cosmos/ledger-cosmos/blob/master/docs/TXSPEC.md
38-
throw new Error('Not implemented.');
39+
const msg = {
40+
'account_number': doc.accountNumber,
41+
'chain_id': doc.chainId,
42+
'fee': doc.fee,
43+
'memo': doc.memo,
44+
'msgs': doc.messages,
45+
'sequence': doc.sequence,
46+
};
47+
const { signature, return_code } = await this.cosmosApp.sign(this.hdPath, JSON.stringify(LumUtils.sortJSON(msg)));
48+
if (!signature || return_code === 0) {
49+
throw new Error(`Failed to sign message: error code ${return_code}`);
50+
}
51+
return new Uint8Array(signature);
3952
};
4053
}

src/wallet/LumPaperWallet.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { LumUtils, LumConstants, LumMessages, LumTypes } from '..';
1+
import { LumUtils, LumConstants, LumTypes } from '..';
22
import { LumWallet } from '.';
33

44
export class LumPaperWallet extends LumWallet {
@@ -39,11 +39,12 @@ export class LumPaperWallet extends LumWallet {
3939
throw new Error('No available mnemonic or private key.');
4040
};
4141

42-
signTransaction = async (doc: LumTypes.SignDoc): Promise<Uint8Array> => {
42+
signTransaction = async (doc: LumTypes.Doc): Promise<Uint8Array> => {
4343
if (!this.privateKey || !this.publicKey) {
4444
throw new Error('No account selected.');
4545
}
46-
const signBytes = LumUtils.generateSignDocBytes(doc);
46+
const signDoc = LumUtils.generateSignDoc(doc, this.getPublicKey());
47+
const signBytes = LumUtils.generateSignDocBytes(signDoc);
4748
const hashedMessage = LumUtils.sha256(signBytes);
4849
const signature = await LumUtils.generateSignature(hashedMessage, this.privateKey);
4950
return signature;

src/wallet/LumWallet.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,9 @@ export abstract class LumWallet {
4646
abstract useAccount(hdPath: string, addressPrefix: string): Promise<boolean>;
4747

4848
/**
49-
* Sign a transaction using a LumWallet
49+
* Sign a transaction document using a LumWallet
5050
*
5151
* @param doc document to sign
5252
*/
53-
abstract signTransaction(doc: LumTypes.SignDoc): Promise<Uint8Array>;
53+
abstract signTransaction(doc: LumTypes.Doc): Promise<Uint8Array>;
5454
}

0 commit comments

Comments
 (0)