Skip to content

Commit f4380ac

Browse files
authored
Implement full offline signer capability (#37)
* Improve abstraction to handle amino offline signer * Bump version to 0.7.2 * Remove useless double check which might cause reflection issues * Fix offline signing Co-authored-by: Fabrice Bascoulergue <lebascou@users.noreply.github.com>
1 parent fd126a4 commit f4380ac

File tree

5 files changed

+114
-31
lines changed

5 files changed

+114
-31
lines changed

docs/lib/classes/lumwalletfactory.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ Create a LumWallet instance based on an OfflineDirectSigner instance compatible
9292

9393
| Name | Type | Description |
9494
| :------ | :------ | :------ |
95-
| `offlineSigner` | `OfflineDirectSigner` | OfflineDirectSigner instance compatible with Comsjs based implementations |
95+
| `offlineSigner` | `OfflineSigner` | OfflineDirectSigner instance compatible with Comsjs based implementations |
9696

9797
#### Returns
9898

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@lum-network/sdk-javascript",
3-
"version": "0.7.1",
3+
"version": "0.7.2",
44
"license": "Apache-2.0",
55
"description": "Javascript SDK library for NodeJS and Web browsers to interact with the Lum Network.",
66
"homepage": "https://github.com/lum-network/sdk-javascript#readme",

src/wallet/LumOfflineSignerWallet.ts

Lines changed: 48 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1-
import { OfflineDirectSigner } from '@cosmjs/proto-signing';
1+
import Long from 'long';
2+
import { OfflineAminoSigner } from '@cosmjs/amino';
3+
import { OfflineSigner, OfflineDirectSigner } from '@cosmjs/proto-signing';
24
import { SignMode } from '../codec/cosmos/tx/signing/v1beta1/signing';
3-
import { LumUtils, LumTypes, LumConstants } from '..';
5+
import { LumUtils, LumTypes, LumConstants, LumAminoRegistry } from '..';
46
import { LumWallet } from '.';
5-
import Long from 'long';
67

78
export class LumOfflineSignerWallet extends LumWallet {
8-
private readonly offlineSigner: OfflineDirectSigner;
9+
private readonly offlineSigner: OfflineSigner;
910

1011
/**
11-
* Create a LumOfflineSignerWallet instance based on an OfflineDirectSigner instance compatible with Comsjs based
12+
* Create a LumOfflineSignerWallet instance based on an OfflineSigner instance compatible with Comsjs based
1213
* implementations.
1314
* This constructor is not intended to be used directly as it does not initialize the underlying key pair
1415
* Better use the provided static LumPaperWallet builders
1516
*
1617
* @param mnemonicOrPrivateKey mnemonic (string) used to derive the private key or private key (Uint8Array)
1718
*/
18-
constructor(offlineSigner: OfflineDirectSigner) {
19+
constructor(offlineSigner: OfflineSigner) {
1920
super();
2021
this.offlineSigner = offlineSigner;
2122
}
2223

2324
signingMode = (): SignMode => {
24-
return SignMode.SIGN_MODE_DIRECT;
25+
if (typeof (this.offlineSigner as OfflineAminoSigner).signAmino === 'function') {
26+
return SignMode.SIGN_MODE_LEGACY_AMINO_JSON;
27+
} else if (typeof (this.offlineSigner as OfflineDirectSigner).signDirect === 'function') {
28+
return SignMode.SIGN_MODE_DIRECT;
29+
}
30+
throw 'Unknown offline signer mode';
2531
};
2632

2733
canChangeAccount = (): boolean => {
@@ -53,9 +59,27 @@ export class LumOfflineSignerWallet extends LumWallet {
5359
if (signerIndex === -1) {
5460
throw new Error('Signer not found in document');
5561
}
56-
const signDoc = LumUtils.generateSignDoc(doc, signerIndex, this.signingMode());
57-
const response = await this.offlineSigner.signDirect(this.address, signDoc);
58-
return [response.signed, LumUtils.fromBase64(response.signature.signature)];
62+
if (this.signingMode() === SignMode.SIGN_MODE_DIRECT) {
63+
const signDoc = LumUtils.generateSignDoc(doc, signerIndex, this.signingMode());
64+
const response = await (this.offlineSigner as OfflineDirectSigner).signDirect(this.address, signDoc);
65+
return [response.signed, LumUtils.fromBase64(response.signature.signature)];
66+
} else if (this.signingMode() === SignMode.SIGN_MODE_LEGACY_AMINO_JSON) {
67+
const response = await (this.offlineSigner as OfflineAminoSigner).signAmino(this.address, {
68+
'account_number': doc.signers[signerIndex].accountNumber.toString(),
69+
'chain_id': doc.chainId,
70+
'fee': doc.fee,
71+
'memo': doc.memo || '',
72+
'msgs': doc.messages.map((msg) => LumAminoRegistry.toAmino(msg)),
73+
'sequence': doc.signers[signerIndex].sequence.toString(),
74+
});
75+
if (response.signed) {
76+
// Fees and memo could have been edited by the offline signer
77+
doc.fee = response.signed.fee;
78+
doc.memo = response.signed.memo;
79+
}
80+
return [LumUtils.generateSignDoc(doc, signerIndex, this.signingMode()), LumUtils.fromBase64(response.signature.signature)];
81+
}
82+
throw 'Unknown offline signer mode';
5983
};
6084

6185
signMessage = async (msg: string): Promise<LumTypes.SignMsg> => {
@@ -68,14 +92,19 @@ export class LumOfflineSignerWallet extends LumWallet {
6892
chainId: LumConstants.LumSignOnlyChainId,
6993
accountNumber: Long.fromNumber(0),
7094
};
71-
const response = await this.offlineSigner.signDirect(this.address, signDoc);
72-
return {
73-
address: this.getAddress(),
74-
publicKey: this.getPublicKey(),
75-
msg: msg,
76-
sig: LumUtils.fromBase64(response.signature.signature),
77-
version: LumConstants.LumWalletSigningVersion,
78-
signer: LumConstants.LumMessageSigner.OFFLINE,
79-
};
95+
if (this.signingMode() === SignMode.SIGN_MODE_DIRECT) {
96+
const response = await (this.offlineSigner as OfflineDirectSigner).signDirect(this.address, signDoc);
97+
return {
98+
address: this.getAddress(),
99+
publicKey: this.getPublicKey(),
100+
msg: msg,
101+
sig: LumUtils.fromBase64(response.signature.signature),
102+
version: LumConstants.LumWalletSigningVersion,
103+
signer: LumConstants.LumMessageSigner.OFFLINE,
104+
};
105+
} else if (typeof (this.offlineSigner as OfflineAminoSigner).signAmino === 'function') {
106+
throw 'Feature not available for amino signers';
107+
}
108+
throw 'Unknown offline signer mode';
80109
};
81110
}

src/wallet/LumWalletFactory.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Transport from '@ledgerhq/hw-transport';
2-
import { OfflineDirectSigner } from '@cosmjs/proto-signing';
2+
import { OfflineSigner } from '@cosmjs/proto-signing';
33

44
import { LumWallet } from './LumWallet';
55
import { LumLedgerWallet } from './LumLedgerWallet';
@@ -52,7 +52,7 @@ export class LumWalletFactory {
5252
*
5353
* @param offlineSigner OfflineDirectSigner instance compatible with Comsjs based implementations
5454
*/
55-
static fromOfflineSigner = async (offlineSigner: OfflineDirectSigner): Promise<LumWallet> => {
55+
static fromOfflineSigner = async (offlineSigner: OfflineSigner): Promise<LumWallet> => {
5656
const wallet = new LumOfflineSignerWallet(offlineSigner);
5757
await wallet.useAccount();
5858
return wallet;

tests/wallet.test.ts

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { AccountData, DirectSignResponse, OfflineDirectSigner } from '@cosmjs/proto-signing';
22

33
import { SignDoc } from '../src/codec/cosmos/tx/v1beta1/tx';
4-
import { LumWallet, LumWalletFactory, LumUtils, LumConstants, LumMessages } from '../src';
5-
import { encodeSecp256k1Signature } from '@cosmjs/amino';
4+
import { LumWallet, LumWalletFactory, LumUtils, LumConstants, LumMessages, LumRegistry, LumAminoRegistry } from '../src';
5+
import { AminoSignResponse, encodeSecp256k1Signature, OfflineAminoSigner, StdSignDoc } from '@cosmjs/amino';
6+
import { SignMode } from '../src/codec/cosmos/tx/signing/v1beta1/signing';
67

7-
class FakeOfflineSigner implements OfflineDirectSigner {
8+
class FakeOfflineDirectSigner implements OfflineDirectSigner {
89
private readonly privateKey: Uint8Array;
910

1011
constructor(privateKey: Uint8Array) {
@@ -35,6 +36,48 @@ class FakeOfflineSigner implements OfflineDirectSigner {
3536
};
3637
}
3738

39+
class FakeOfflineAminoSigner implements OfflineAminoSigner {
40+
private readonly privateKey: Uint8Array;
41+
42+
constructor(privateKey: Uint8Array) {
43+
this.privateKey = privateKey;
44+
}
45+
46+
getAccounts = async (): Promise<AccountData[]> => {
47+
const publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
48+
return [
49+
{
50+
pubkey: publicKey,
51+
address: LumUtils.getAddressFromPublicKey(publicKey),
52+
algo: 'secp256k1',
53+
},
54+
];
55+
};
56+
57+
signAmino = async (signerAddress: string, stdSignDoc: StdSignDoc): Promise<AminoSignResponse> => {
58+
const publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
59+
const signDoc = LumUtils.generateSignDoc(
60+
{
61+
chainId: stdSignDoc.chain_id,
62+
fee: stdSignDoc.fee,
63+
memo: stdSignDoc.memo,
64+
messages: stdSignDoc.msgs.map((aminoMsg) => LumAminoRegistry.fromAmino(aminoMsg)),
65+
signers: [{ accountNumber: parseInt(stdSignDoc.account_number), sequence: parseInt(stdSignDoc.sequence), publicKey: publicKey }],
66+
},
67+
0,
68+
SignMode.SIGN_MODE_DIRECT, // Simulated to enable signature comparison during tests
69+
);
70+
const signBytes = LumUtils.generateSignDocBytes(signDoc);
71+
const hashedMessage = LumUtils.sha256(signBytes);
72+
const signature = await LumUtils.generateSignature(hashedMessage, this.privateKey);
73+
const stdSig = encodeSecp256k1Signature(publicKey, signature);
74+
return {
75+
signed: stdSignDoc,
76+
signature: stdSig,
77+
};
78+
};
79+
}
80+
3881
describe('LumWallet', () => {
3982
it('Should be identical from mnemonic, privatekey and keystore recovery', async () => {
4083
const mnemonic = 'surround miss nominee dream gap cross assault thank captain prosper drop duty group candy wealth weather scale put';
@@ -45,7 +88,8 @@ describe('LumWallet', () => {
4588
const w1 = await LumWalletFactory.fromMnemonic(mnemonic, `m/44'/837'/0'/0/0`);
4689
const w2 = await LumWalletFactory.fromPrivateKey(LumUtils.keyFromHex(privateKey));
4790
const w3 = await LumWalletFactory.fromKeyStore(keystore, 'lumiere');
48-
const w4 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineSigner(LumUtils.keyFromHex(privateKey)));
91+
const w4 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineDirectSigner(LumUtils.keyFromHex(privateKey)));
92+
const w5 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineAminoSigner(LumUtils.keyFromHex(privateKey)));
4993

5094
expect(LumUtils.isAddressValid(w1.getAddress())).toBe(true);
5195
expect(LumUtils.isAddressValid(w1.getAddress(), LumConstants.LumBech32PrefixAccAddr)).toBe(true);
@@ -71,17 +115,27 @@ describe('LumWallet', () => {
71115
],
72116
};
73117

118+
const w1Response = await w1.signTransaction(doc);
119+
expect(LumUtils.verifySignature(w1Response[1], LumUtils.sha256(LumUtils.generateSignDocBytes(w1Response[0])), w5.getPublicKey()));
120+
74121
expect(w1.getAddress()).toEqual(w2.getAddress());
75122
expect(w1.getPublicKey()).toEqual(w2.getPublicKey());
76-
expect(await w1.signTransaction(doc)).toEqual(await w2.signTransaction(doc));
123+
expect(w1Response).toEqual(await w2.signTransaction(doc));
77124

78125
expect(w1.getAddress()).toEqual(w3.getAddress());
79126
expect(w1.getPublicKey()).toEqual(w3.getPublicKey());
80-
expect(await w1.signTransaction(doc)).toEqual(await w3.signTransaction(doc));
127+
expect(w1Response).toEqual(await w3.signTransaction(doc));
81128

82129
expect(w1.getAddress()).toEqual(w4.getAddress());
83130
expect(w1.getPublicKey()).toEqual(w4.getPublicKey());
84-
expect(await w1.signTransaction(doc)).toEqual(await w4.signTransaction(doc));
131+
expect(w1Response).toEqual(await w4.signTransaction(doc));
132+
133+
expect(w1.getAddress()).toEqual(w5.getAddress());
134+
expect(w1.getPublicKey()).toEqual(w5.getPublicKey());
135+
// Signature will differ due to the SignMode use but should still be valid
136+
const w5Response = await w5.signTransaction(doc);
137+
expect(w1Response).not.toEqual(w5Response);
138+
expect(LumUtils.verifySignature(w5Response[1], LumUtils.sha256(LumUtils.generateSignDocBytes(w5Response[0])), w5.getPublicKey()));
85139

86140
const randomPrivateKey = LumUtils.generatePrivateKey();
87141
expect(randomPrivateKey).toHaveLength(LumConstants.PrivateKeyLength);
@@ -101,7 +155,7 @@ describe('LumWallet', () => {
101155

102156
const w1 = await LumWalletFactory.fromMnemonic(mnemonic);
103157
const w2 = await LumWalletFactory.fromMnemonic(LumUtils.generateMnemonic());
104-
const w3 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineSigner(LumUtils.keyFromHex(privateKey)));
158+
const w3 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineDirectSigner(LumUtils.keyFromHex(privateKey)));
105159

106160
const signedW1 = await w1.signMessage(message);
107161
expect(signedW1.signer).toEqual(LumConstants.LumMessageSigner.PAPER);

0 commit comments

Comments
 (0)