Skip to content

Commit 2fcdbf4

Browse files
author
Fabrice Bascoulergue
committed
Revamp wallet implementation and add base ledger wallet
1 parent 1d31add commit 2fcdbf4

File tree

15 files changed

+718
-117
lines changed

15 files changed

+718
-117
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ docs/
33
node_modules/
44
proto/
55
scripts/
6+
tests/
67
*.d.ts

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@
4747
"@cosmjs/stargate": "^0.24.0-alpha.26",
4848
"@cosmjs/tendermint-rpc": "^0.24.0-alpha.26",
4949
"@cosmjs/utils": "^0.24.0-alpha.26",
50+
"@ledgerhq/hw-app-cosmos": "^5.46.0",
51+
"@ledgerhq/hw-transport": "^5.46.0",
5052
"@types/uuid": "^8.3.0",
5153
"crypto-browserify": "^3.12.0",
5254
"crypto-js": "^4.0.0",
@@ -61,8 +63,11 @@
6163
"@babel/plugin-transform-runtime": "^7.12.10",
6264
"@babel/preset-env": "^7.8.3",
6365
"@babel/preset-typescript": "^7.8.3",
66+
"@ledgerhq/hw-transport-node-hid": "^5.46.0",
6467
"@types/crypto-js": "^4.0.1",
6568
"@types/jest": "^26.0.20",
69+
"@types/ledgerhq__hw-transport": "^4.21.3",
70+
"@types/ledgerhq__hw-transport-node-hid": "^4.22.2",
6671
"cross-env": "^7.0.3",
6772
"eslint": "^7.19.0",
6873
"gts": "^3.1.0",

src/client/LumClient.ts

Lines changed: 22 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import {
1313
DistributionExtension,
1414
} from '@cosmjs/stargate';
1515

16-
import { LumWallet } from '../wallet';
17-
import { Message } from '../messages';
18-
import { sha256, generateAuthInfo, generateSignDoc, generateSignDocBytes, generateTxBytes } from '../utils';
19-
import { BroadcastTxCommitResponse, TxResponse, TxSearchParams, BlockResponse, Account, Coin, Fee } from '../types';
16+
import { LumWallet, LumUtils, LumTypes, LumMessages } from '..';
2017

2118
export class LumClient {
2219
readonly tmClient: Tendermint34Client;
@@ -102,7 +99,7 @@ export class LumClient {
10299
*
103100
* @param height block height to get (default to current height)
104101
*/
105-
getBlock = async (height?: number): Promise<BlockResponse> => {
102+
getBlock = async (height?: number): Promise<LumTypes.BlockResponse> => {
106103
const response = await this.tmClient.block(height);
107104
return response;
108105
};
@@ -112,7 +109,7 @@ export class LumClient {
112109
*
113110
* @param address wallet address
114111
*/
115-
getAccount = async (address: string): Promise<Account | null> => {
112+
getAccount = async (address: string): Promise<LumTypes.Account | null> => {
116113
const account = await this.queryClient.auth.account(address);
117114
if (!account) {
118115
return null;
@@ -129,7 +126,7 @@ export class LumClient {
129126
*
130127
* @param address wallet address
131128
*/
132-
getAccountUnverified = async (address: string): Promise<Account | null> => {
129+
getAccountUnverified = async (address: string): Promise<LumTypes.Account | null> => {
133130
const account = await this.queryClient.auth.unverified.account(address);
134131
if (!account) {
135132
return null;
@@ -147,7 +144,7 @@ export class LumClient {
147144
* @param address wallet address
148145
* @param searchDenom Coin denomination (ex: lum)
149146
*/
150-
getBalance = async (address: string, searchDenom: string): Promise<Coin | null> => {
147+
getBalance = async (address: string, searchDenom: string): Promise<LumTypes.Coin | null> => {
151148
const balance = await this.queryClient.bank.balance(address, searchDenom);
152149
return balance ? coinFromProto(balance) : null;
153150
};
@@ -158,7 +155,7 @@ export class LumClient {
158155
* @param address wallet address
159156
* @param searchDenom Coin denomination (ex: lum)
160157
*/
161-
getBalanceUnverified = async (address: string, searchDenom: string): Promise<Coin | null> => {
158+
getBalanceUnverified = async (address: string, searchDenom: string): Promise<LumTypes.Coin | null> => {
162159
const balance = await this.queryClient.bank.unverified.balance(address, searchDenom);
163160
return balance ? coinFromProto(balance) : null;
164161
};
@@ -168,7 +165,7 @@ export class LumClient {
168165
*
169166
* @param address wallet address
170167
*/
171-
getAllBalancesUnverified = async (address: string): Promise<Coin[]> => {
168+
getAllBalancesUnverified = async (address: string): Promise<LumTypes.Coin[]> => {
172169
const balances = await this.queryClient.bank.unverified.allBalances(address);
173170
return balances.map(coinFromProto);
174171
};
@@ -178,15 +175,15 @@ export class LumClient {
178175
*
179176
* @param searchDenom Coin denomination (ex: lum)
180177
*/
181-
getSupply = async (searchDenom: string): Promise<Coin | null> => {
178+
getSupply = async (searchDenom: string): Promise<LumTypes.Coin | null> => {
182179
const supply = await this.queryClient.bank.unverified.supplyOf(searchDenom);
183180
return supply ? coinFromProto(supply) : null;
184181
};
185182

186183
/**
187184
* Get all coins supplies
188185
*/
189-
getAllSupplies = async (): Promise<Coin[]> => {
186+
getAllSupplies = async (): Promise<LumTypes.Coin[]> => {
190187
const supplies = await this.queryClient.bank.unverified.totalSupply();
191188
return supplies.map(coinFromProto);
192189
};
@@ -197,7 +194,7 @@ export class LumClient {
197194
* @param hash transaction hash to retrieve
198195
* @param includeProof whether or not to include proof of the transaction inclusion in the block
199196
*/
200-
getTx = async (hash: Uint8Array, includeProof?: boolean): Promise<TxResponse | null> => {
197+
getTx = async (hash: Uint8Array, includeProof?: boolean): Promise<LumTypes.TxResponse | null> => {
201198
const result = await this.tmClient.tx({ hash: hash, prove: includeProof });
202199
return result;
203200
};
@@ -216,10 +213,10 @@ export class LumClient {
216213
* @param perPage results per pages (default to 30)
217214
* @param includeProof whether or not to include proofs of the transactions inclusion in the block
218215
*/
219-
searchTx = async (queries: string[], page = 1, perPage = 30, includeProof?: boolean): Promise<TxResponse[]> => {
216+
searchTx = async (queries: string[], page = 1, perPage = 30, includeProof?: boolean): Promise<LumTypes.TxResponse[]> => {
220217
const results = await Promise.all(queries.map((q) => this.txsQuery({ query: q, page: page, per_page: perPage, prove: includeProof })));
221218
const seenHashes: Uint8Array[] = [];
222-
const uniqueResults: TxResponse[] = [];
219+
const uniqueResults: LumTypes.TxResponse[] = [];
223220
for (let r = 0; r < results.length; r++) {
224221
for (let t = 0; t < results[r].length; t++) {
225222
const tx = results[r][t];
@@ -237,9 +234,9 @@ export class LumClient {
237234
*
238235
* @param params Search params
239236
*/
240-
private txsQuery = async (params: TxSearchParams): Promise<TxResponse[]> => {
237+
private txsQuery = async (params: LumTypes.TxSearchParams): Promise<LumTypes.TxResponse[]> => {
241238
const results = await this.tmClient.txSearch(params);
242-
return results.txs as TxResponse[];
239+
return results.txs as LumTypes.TxResponse[];
243240
};
244241

245242
/**
@@ -250,20 +247,18 @@ export class LumClient {
250247
* @param fee requested fee
251248
* @param memo optional memo for the transaction
252249
*/
253-
signTx = async (wallet: LumWallet, messages: Message[], fee: Fee, memo?: string): Promise<Uint8Array> => {
254-
const account = await this.getAccount(wallet.address);
250+
signTx = async (wallet: LumWallet, messages: LumMessages.Message[], fee: LumTypes.Fee, memo?: string): Promise<Uint8Array> => {
251+
const account = await this.getAccount(wallet.getAddress());
255252
if (!account) {
256253
throw new Error('Account not found');
257254
}
258255
const { accountNumber, sequence } = account;
259256
const chainId = await this.getChainId();
260257

261-
const authInfo = generateAuthInfo(wallet.publicKey, fee, sequence);
262-
const signDoc = generateSignDoc(messages, memo, authInfo, chainId, accountNumber);
263-
const signBytes = generateSignDocBytes(signDoc);
264-
const hashedMessage = sha256(signBytes);
265-
const signature = await wallet.signTransaction(hashedMessage);
266-
return generateTxBytes(signDoc, signature);
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);
261+
return LumUtils.generateTxBytes(signDoc, signature);
267262
};
268263

269264
/**
@@ -272,7 +267,7 @@ export class LumClient {
272267
*
273268
* @param tx signed transaction to broadcast
274269
*/
275-
broadcastTx = async (tx: Uint8Array): Promise<BroadcastTxCommitResponse> => {
270+
broadcastTx = async (tx: Uint8Array): Promise<LumTypes.BroadcastTxCommitResponse> => {
276271
const response = await this.tmClient.broadcastTxCommit({ tx });
277272
return response;
278273
};
@@ -285,7 +280,7 @@ export class LumClient {
285280
* @param fee requested fee
286281
* @param memo optional memo for the transaction
287282
*/
288-
signAndBroadcastTx = async (wallet: LumWallet, messages: Message[], fee: Fee, memo?: string): Promise<BroadcastTxCommitResponse> => {
283+
signAndBroadcastTx = async (wallet: LumWallet, messages: LumMessages.Message[], fee: LumTypes.Fee, memo?: string): Promise<LumTypes.BroadcastTxCommitResponse> => {
289284
const signedTx = await this.signTx(wallet, messages, fee, memo);
290285
return this.broadcastTx(signedTx);
291286
};

src/constants/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ export const LumBech32PrefixConsPub = 'lumvalconspub';
3939
* @see https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
4040
* @see https://github.com/satoshilabs/slips/blob/master/slip-0044.md
4141
*/
42-
export const HDPath = "m/44'/837'/0'/0/";
42+
export const HDPath = "m/44'/837'/0'/";
4343

4444
/**
4545
* Get a Lum Network HDPath for a specified account index
4646
*
4747
* @param accountIndex appended at the end of the default Lum derivation path
4848
*/
49-
export const getLumHdPath = (accountIndex = 0): string => {
50-
return HDPath + accountIndex.toString();
49+
export const getLumHdPath = (accountIndex = 0, walletIndex = 0): string => {
50+
return HDPath + accountIndex.toString() + '/' + walletIndex.toString();
5151
};
5252

5353
/**

src/declarations.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,39 @@
33
* Only used by the KeyStore feature therefore not properly declared as it is a minor, almost deprecated feature.
44
*/
55
declare module 'crypto-browserify';
6+
7+
/**
8+
* Add ledger missing cosmos app types declarations
9+
*/
10+
declare module '@ledgerhq/hw-app-cosmos' {
11+
export default class Cosmos {
12+
transport: import('@ledgerhq/hw-transport').default<*>;
13+
14+
constructor(transport: import('@ledgerhq/hw-transport').default<*>, scrambleKey: string);
15+
getAppConfiguration(): {
16+
test_mode: boolean;
17+
version: string;
18+
device_locked: boolean;
19+
major: string;
20+
};
21+
22+
serializePath(path: Buffer): Buffer;
23+
24+
serializeHRP(hrp: string): Buffer;
25+
26+
/**
27+
* get Cosmos address for a given BIP 32 path.
28+
* @param path a path in BIP 32 format
29+
* @param hrp usually cosmos
30+
* @option boolDisplay optionally enable or not the display
31+
* @return an object with a publicKey, address and (optionally) chainCode
32+
* @example
33+
* cosmos.getAddress("44'/60'/0'/0/0", "cosmos").then(o => o.address)
34+
*/
35+
getAddress(path: string, hrp: string, boolDisplay?: boolean): Promise<{ publicKey: string; address: string }>;
36+
37+
foreach<T, A>(arr: T[], callback: (arg0: T, arg1: number) => Promise<A>): Promise<A[]>;
38+
39+
sign(path: string, message: string): Promise<{ signature: null | Buffer; return_code: number }>;
40+
}
41+
}

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as LumConstants from './constants';
33
import * as LumTypes from './types';
44
import * as LumMessages from './messages';
55
import { LumRegistry } from './registry';
6-
import { LumWallet } from './wallet';
6+
import { LumWallet, LumWalletFactory } from './wallet';
77
import { LumClient } from './client';
88

9-
export { LumWallet, LumClient, LumTypes, LumUtils, LumConstants, LumMessages, LumRegistry };
9+
export { LumWallet, LumWalletFactory, LumClient, LumTypes, LumUtils, LumConstants, LumMessages, LumRegistry };

src/wallet/LumLedgerWallet.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Transport from '@ledgerhq/hw-transport';
2+
import Cosmos from '@ledgerhq/hw-app-cosmos';
3+
4+
import { LumUtils, LumTypes, LumRegistry } from '..';
5+
import { LumWallet } from '.';
6+
7+
export class LumLedgerWallet extends LumWallet {
8+
cosmosApp: Cosmos;
9+
private hdPath?: string;
10+
11+
constructor(transport: Transport) {
12+
super();
13+
this.cosmosApp = new Cosmos(transport, 'CSM'); // TODO: CSM identifier should either be LUM or dynamic depending on our ledger implementation
14+
}
15+
16+
canChangeAccount = () => {
17+
return true;
18+
};
19+
20+
useAccount = async (hdPath: string, addressPrefix: string): Promise<boolean> => {
21+
const { address, publicKey } = await this.cosmosApp.getAddress(hdPath, addressPrefix);
22+
this.hdPath = hdPath;
23+
this.address = address;
24+
this.publicKey = LumUtils.fromHex(publicKey);
25+
return true;
26+
};
27+
28+
signTransaction = async (doc: LumTypes.SignDoc): Promise<Uint8Array> => {
29+
if (!this.hdPath) {
30+
throw new Error('No account selected.');
31+
}
32+
// Implementation is delayed. We are not sure if we will be able to sign transactions using the new protobuf implementation
33+
// with the current cosmos ledger application
34+
//
35+
// Useful doc & code:
36+
// sign call: https://github.com/LedgerHQ/ledgerjs/blob/master/packages/hw-app-cosmos/src/Cosmos.js
37+
// Expected tx format: https://github.com/cosmos/ledger-cosmos/blob/master/docs/TXSPEC.md
38+
throw new Error('Not implemented.');
39+
};
40+
}

src/wallet/LumPaperWallet.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { LumUtils, LumConstants, LumMessages, LumTypes } from '..';
2+
import { LumWallet } from '.';
3+
4+
export class LumPaperWallet extends LumWallet {
5+
private readonly mnemonic?: string;
6+
private privateKey?: Uint8Array;
7+
8+
/**
9+
* Create a LumPaperWallet instance based on a mnemonic or a private key
10+
* This constructor is not intended to be used directly as it does not initialize the underlying key pair
11+
* Better use the provided static LumPaperWallet builders
12+
*
13+
* @param mnemonicOrPrivateKey mnemonic (string) used to derive the private key or private key (Uint8Array)
14+
*/
15+
constructor(mnemonicOrPrivateKey: string | Uint8Array) {
16+
super();
17+
if (LumUtils.isUint8Array(mnemonicOrPrivateKey)) {
18+
this.privateKey = mnemonicOrPrivateKey;
19+
} else {
20+
this.mnemonic = mnemonicOrPrivateKey;
21+
}
22+
}
23+
24+
canChangeAccount = (): boolean => {
25+
return !!this.mnemonic;
26+
};
27+
28+
useAccount = async (hdPath = LumConstants.getLumHdPath(0, 0), addressPrefix = LumConstants.LumBech32PrefixAccAddr): Promise<boolean> => {
29+
if (this.mnemonic) {
30+
this.privateKey = await LumUtils.getPrivateKeyFromMnemonic(this.mnemonic, hdPath);
31+
this.publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
32+
this.address = LumUtils.getAddressFromPublicKey(this.publicKey, addressPrefix);
33+
return true;
34+
} else if (this.privateKey) {
35+
this.publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
36+
this.address = LumUtils.getAddressFromPublicKey(this.publicKey, addressPrefix);
37+
return false;
38+
}
39+
throw new Error('No available mnemonic or private key.');
40+
};
41+
42+
signTransaction = async (doc: LumTypes.SignDoc): Promise<Uint8Array> => {
43+
if (!this.privateKey || !this.publicKey) {
44+
throw new Error('No account selected.');
45+
}
46+
const signBytes = LumUtils.generateSignDocBytes(doc);
47+
const hashedMessage = LumUtils.sha256(signBytes);
48+
const signature = await LumUtils.generateSignature(hashedMessage, this.privateKey);
49+
return signature;
50+
};
51+
}

0 commit comments

Comments
 (0)