Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/lib/classes/lumwalletfactory.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [fromKeyStore](LumWalletFactory.md#fromkeystore)
- [fromLedgerTransport](LumWalletFactory.md#fromledgertransport)
- [fromMnemonic](LumWalletFactory.md#frommnemonic)
- [fromOfflineSigner](LumWalletFactory.md#fromofflinesigner)
- [fromPrivateKey](LumWalletFactory.md#fromprivatekey)

## Constructors
Expand Down Expand Up @@ -81,6 +82,24 @@ Create a LumWallet instance based on a mnemonic and a derivation path

___

### fromOfflineSigner

▸ `Static` **fromOfflineSigner**(`offlineSigner`): `Promise`<[`LumWallet`](LumWallet.md)\>

Create a LumWallet instance based on an OfflineDirectSigner instance compatible with Comsjs based implementations.

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `offlineSigner` | `OfflineDirectSigner` | OfflineDirectSigner instance compatible with Comsjs based implementations |

#### Returns

`Promise`<[`LumWallet`](LumWallet.md)\>

___

### fromPrivateKey

▸ `Static` **fromPrivateKey**(`privateKey`, `addressPrefix?`): `Promise`<[`LumWallet`](LumWallet.md)\>
Expand Down
7 changes: 7 additions & 0 deletions docs/lib/enums/lumconstants.lummessagesigner.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Signing wallets
### Enumeration members

- [LEDGER](LumConstants.LumMessageSigner.md#ledger)
- [OFFLINE](LumConstants.LumMessageSigner.md#offline)
- [PAPER](LumConstants.LumMessageSigner.md#paper)

## Enumeration members
Expand All @@ -19,6 +20,12 @@ Signing wallets

___

### OFFLINE

• **OFFLINE** = `"lum-sdk/offline"`

___

### PAPER

• **PAPER** = `"lum-sdk/paper"`
9 changes: 9 additions & 0 deletions docs/lib/modules/lumconstants.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- [LumBech32PrefixValPub](LumConstants.md#lumbech32prefixvalpub)
- [LumDenom](LumConstants.md#lumdenom)
- [LumExponent](LumConstants.md#lumexponent)
- [LumSignOnlyChainId](LumConstants.md#lumsignonlychainid)
- [LumWalletSigningVersion](LumConstants.md#lumwalletsigningversion)
- [MicroLumDenom](LumConstants.md#microlumdenom)
- [PrivateKeyLength](LumConstants.md#privatekeylength)
Expand Down Expand Up @@ -104,6 +105,14 @@ Lum Exponent

___

### LumSignOnlyChainId

• **LumSignOnlyChainId**: ``"lum-signature-only"``

Chain ID used for message signature by wallet implementations that require one

___

### LumWalletSigningVersion

• **LumWalletSigningVersion**: ``"1"``
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lum-network/sdk-javascript",
"version": "0.5.2",
"version": "0.5.3",
"license": "Apache-2.0",
"description": "Javascript SDK library for NodeJS and Web browsers to interact with the Lum Network.",
"homepage": "https://github.com/lum-network/sdk-javascript#readme",
Expand Down
6 changes: 6 additions & 0 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,10 @@ export const LumWalletSigningVersion = '1';
export enum LumMessageSigner {
PAPER = 'lum-sdk/paper',
LEDGER = 'lum-sdk/ledger',
OFFLINE = 'lum-sdk/offline',
}

/**
* Chain ID used for message signature by wallet implementations that require one
*/
export const LumSignOnlyChainId = 'lum-signature-only';
13 changes: 11 additions & 2 deletions src/utils/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { makeSignBytes } from '@cosmjs/proto-signing';
import { TxRaw, AuthInfo } from '../codec/cosmos/tx/v1beta1/tx';
import { SignMode } from '../codec/cosmos/tx/signing/v1beta1/signing';

import { LumMessageSigner } from '../constants';
import { LumMessageSigner, LumSignOnlyChainId } from '../constants';
import { Fee, Doc, SignDoc, SignMsg, DocSigner } from '../types';
import { LumRegistry } from '../registry';
import { sortJSON } from './commons';
Expand Down Expand Up @@ -125,13 +125,22 @@ export const verifySignMsg = async (msg: SignMsg): Promise<boolean> => {
}
if (msg.signer === LumMessageSigner.PAPER) {
return verifySignature(msg.sig, toAscii(msg.msg), msg.publicKey);
} else if (msg.signer === LumMessageSigner.OFFLINE) {
const signDoc = {
bodyBytes: toAscii(msg.msg),
authInfoBytes: generateAuthInfoBytes([{ accountNumber: 0, sequence: 0, publicKey: msg.publicKey }], { amount: [], gas: '0' }, SignMode.SIGN_MODE_DIRECT),
chainId: LumSignOnlyChainId,
accountNumber: Long.fromNumber(0),
};
const signedBytes = generateSignDocBytes(signDoc);
return verifySignature(msg.sig, signedBytes, msg.publicKey);
} else if (msg.signer === LumMessageSigner.LEDGER) {
// Re-generate ledger required amino payload to sign messages
// This is basically an empty transaction payload
// Same a used in the LumLedgerWallet > signMessage method
const msgToSign = {
'account_number': '0',
'chain_id': 'lum-signature-only',
'chain_id': LumSignOnlyChainId,
'fee': {},
'memo': msg.msg,
'msgs': [],
Expand Down
2 changes: 1 addition & 1 deletion src/wallet/LumLedgerWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export class LumLedgerWallet extends LumWallet {
// that is only provided for basic message signature and verification
const msgToSign = {
'account_number': '0',
'chain_id': 'lum-signature-only',
'chain_id': LumConstants.LumSignOnlyChainId,
'fee': {},
'memo': msg,
'msgs': [],
Expand Down
81 changes: 81 additions & 0 deletions src/wallet/LumOfflineSignerWallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { OfflineDirectSigner } from '@cosmjs/proto-signing';
import { SignMode } from '../codec/cosmos/tx/signing/v1beta1/signing';
import { LumUtils, LumTypes, LumConstants } from '..';
import { LumWallet } from '.';
import Long from 'long';

export class LumOfflineSignerWallet extends LumWallet {
private readonly offlineSigner: OfflineDirectSigner;

/**
* Create a LumOfflineSignerWallet instance based on an OfflineDirectSigner instance compatible with Comsjs based
* implementations.
* This constructor is not intended to be used directly as it does not initialize the underlying key pair
* Better use the provided static LumPaperWallet builders
*
* @param mnemonicOrPrivateKey mnemonic (string) used to derive the private key or private key (Uint8Array)
*/
constructor(offlineSigner: OfflineDirectSigner) {
super();
this.offlineSigner = offlineSigner;
}

signingMode = (): SignMode => {
return SignMode.SIGN_MODE_DIRECT;
};

canChangeAccount = (): boolean => {
return false;
};

useAccount = async (): Promise<boolean> => {
const accounts = await this.offlineSigner.getAccounts();
if (accounts.length === 0) {
throw new Error('No account available.');
}
this.publicKey = accounts[0].pubkey;
this.address = accounts[0].address;
return true;
};

sign = async (): Promise<Uint8Array> => {
throw new Error('Feature not supported.');
};

signTransaction = async (doc: LumTypes.Doc): Promise<Uint8Array> => {
if (!this.address || !this.publicKey) {
throw new Error('No account selected.');
}
const signerIndex = LumUtils.uint8IndexOf(
doc.signers.map((signer) => signer.publicKey),
this.publicKey as Uint8Array,
);
if (signerIndex === -1) {
throw new Error('Signer not found in document');
}
const signDoc = LumUtils.generateSignDoc(doc, signerIndex, this.signingMode());
const response = await this.offlineSigner.signDirect(this.address, signDoc);
return LumUtils.fromBase64(response.signature.signature);
};

signMessage = async (msg: string): Promise<LumTypes.SignMsg> => {
if (!this.address || !this.publicKey) {
throw new Error('No account selected.');
}
const signDoc = {
bodyBytes: LumUtils.toAscii(msg),
authInfoBytes: LumUtils.generateAuthInfoBytes([{ accountNumber: 0, sequence: 0, publicKey: this.getPublicKey() }], { amount: [], gas: '0' }, this.signingMode()),
chainId: LumConstants.LumSignOnlyChainId,
accountNumber: Long.fromNumber(0),
};
const response = await this.offlineSigner.signDirect(this.address, signDoc);
return {
address: this.getAddress(),
publicKey: this.getPublicKey(),
msg: msg,
sig: LumUtils.fromBase64(response.signature.signature),
version: LumConstants.LumWalletSigningVersion,
signer: LumConstants.LumMessageSigner.OFFLINE,
};
};
}
13 changes: 13 additions & 0 deletions src/wallet/LumWalletFactory.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Transport from '@ledgerhq/hw-transport';
import { OfflineDirectSigner } from '@cosmjs/proto-signing';

import { LumWallet } from './LumWallet';
import { LumLedgerWallet } from './LumLedgerWallet';
import { LumPaperWallet } from './LumPaperWallet';
import { LumOfflineSignerWallet } from './LumOfflineSignerWallet';
import { LumConstants, LumUtils } from '../';

export class LumWalletFactory {
Expand Down Expand Up @@ -45,6 +47,17 @@ export class LumWalletFactory {
return wallet;
};

/**
* Create a LumWallet instance based on an OfflineDirectSigner instance compatible with Comsjs based implementations.
*
* @param offlineSigner OfflineDirectSigner instance compatible with Comsjs based implementations
*/
static fromOfflineSigner = async (offlineSigner: OfflineDirectSigner): Promise<LumWallet> => {
const wallet = new LumOfflineSignerWallet(offlineSigner);
await wallet.useAccount();
return wallet;
};

/**
* Create a LumWallet instance based on a ledger transport
*
Expand Down
3 changes: 2 additions & 1 deletion src/wallet/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LumWallet } from './LumWallet';
import { LumPaperWallet } from './LumPaperWallet';
import { LumLedgerWallet } from './LumLedgerWallet';
import { LumOfflineSignerWallet } from './LumOfflineSignerWallet';
import { LumWalletFactory } from './LumWalletFactory';

export { LumWallet, LumPaperWallet, LumLedgerWallet, LumWalletFactory };
export { LumWallet, LumPaperWallet, LumLedgerWallet, LumOfflineSignerWallet, LumWalletFactory };
94 changes: 84 additions & 10 deletions tests/wallet.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,39 @@
import { LumWallet, LumWalletFactory, LumUtils, LumConstants } from '../src';
import { AccountData, DirectSignResponse, OfflineDirectSigner } from '@cosmjs/proto-signing';

import { SignDoc } from '../src/codec/cosmos/tx/v1beta1/tx';
import { LumWallet, LumWalletFactory, LumUtils, LumConstants, LumMessages } from '../src';
import { encodeSecp256k1Signature } from '@cosmjs/amino';

class FakeOfflineSigner implements OfflineDirectSigner {
private readonly privateKey: Uint8Array;

constructor(privateKey: Uint8Array) {
this.privateKey = privateKey;
}

getAccounts = async (): Promise<AccountData[]> => {
const publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
return [
{
pubkey: publicKey,
address: LumUtils.getAddressFromPublicKey(publicKey),
algo: 'secp256k1',
},
];
};

signDirect = async (signerAddress: string, signDoc: SignDoc): Promise<DirectSignResponse> => {
const publicKey = await LumUtils.getPublicKeyFromPrivateKey(this.privateKey);
const signBytes = LumUtils.generateSignDocBytes(signDoc);
const hashedMessage = LumUtils.sha256(signBytes);
const signature = await LumUtils.generateSignature(hashedMessage, this.privateKey);
const stdSig = encodeSecp256k1Signature(publicKey, signature);
return {
signed: signDoc,
signature: stdSig,
};
};
}

describe('LumWallet', () => {
it('Should be identical from mnemonic, privatekey and keystore recovery', async () => {
Expand All @@ -10,15 +45,43 @@ describe('LumWallet', () => {
const w1 = await LumWalletFactory.fromMnemonic(mnemonic);
const w2 = await LumWalletFactory.fromPrivateKey(LumUtils.keyFromHex(privateKey));
const w3 = await LumWalletFactory.fromKeyStore(keystore, 'lumiere');
const w4 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineSigner(LumUtils.keyFromHex(privateKey)));

expect(LumUtils.isAddressValid(w1.getAddress())).toBe(true);
expect(LumUtils.isAddressValid(w1.getAddress(), LumConstants.LumBech32PrefixAccAddr)).toBe(true);
expect(LumUtils.isAddressValid(w1.getAddress(), undefined)).toBe(true);
expect(LumUtils.isAddressValid(w1.getAddress(), 'cosmos')).toBe(false);

// Create a fake document for signature verification purposes
const doc = {
accountNumber: 1,
chainId: 'lumnetwork-testnet',
fee: {
amount: [{ denom: LumConstants.MicroLumDenom, amount: '1' }],
gas: '1',
},
memo: 'Not a real transaction',
messages: [LumMessages.BuildMsgSend(w1.getAddress(), w2.getAddress(), [{ denom: LumConstants.MicroLumDenom, amount: '1' }])],
signers: [
{
accountNumber: 1,
sequence: 1,
publicKey: w1.getPublicKey(),
},
],
};

expect(w1.getAddress()).toEqual(w2.getAddress());
expect(w1.getPublicKey()).toEqual(w2.getPublicKey());
expect(await w1.signTransaction(doc)).toEqual(await w2.signTransaction(doc));

expect(w1.getAddress()).toEqual(w3.getAddress());
expect(w1.getPublicKey()).toEqual(w3.getPublicKey());
expect(await w1.signTransaction(doc)).toEqual(await w3.signTransaction(doc));

expect(w1.getAddress()).toEqual(w4.getAddress());
expect(w1.getPublicKey()).toEqual(w4.getPublicKey());
expect(await w1.signTransaction(doc)).toEqual(await w4.signTransaction(doc));

const randomPrivateKey = LumUtils.generatePrivateKey();
expect(randomPrivateKey).toHaveLength(LumConstants.PrivateKeyLength);
Expand All @@ -33,19 +96,30 @@ describe('LumWallet', () => {
it('Should be able to sign and verify messages', async () => {
const message = 'Lum network is an awesome decentralized protocol';

const privateKey = '0xb8e62c34928025cdd3aef6cbebc68694b5ad9209b2aff6d3891c8e61d22d3a3b';
const mnemonic = 'surround miss nominee dream gap cross assault thank captain prosper drop duty group candy wealth weather scale put';

const w1 = await LumWalletFactory.fromMnemonic(mnemonic);
const w2 = await LumWalletFactory.fromMnemonic(LumUtils.generateMnemonic());
const w3 = await LumWalletFactory.fromOfflineSigner(new FakeOfflineSigner(LumUtils.keyFromHex(privateKey)));

const signedW1 = await w1.signMessage(message);
expect(signedW1.signer).toEqual(LumConstants.LumMessageSigner.PAPER);
expect(signedW1.version).toEqual(LumConstants.LumWalletSigningVersion);
expect(await LumUtils.verifySignMsg(signedW1)).toBeTruthy();
expect(await LumUtils.verifySignMsg(Object.assign({}, signedW1, { msg: 'Wrong message input' }))).toBeFalsy();
expect(await LumUtils.verifySignMsg(Object.assign({}, signedW1, { publicKey: w2.getPublicKey() }))).toBeFalsy();
expect(await LumUtils.verifySignMsg(Object.assign({}, signedW1, { address: w2.getAddress() }))).toBeFalsy();

const signed = await w1.signMessage(message);
const signedW2 = await w2.signMessage(message);
expect(signedW2.signer).toEqual(LumConstants.LumMessageSigner.PAPER);
expect(signedW2.version).toEqual(LumConstants.LumWalletSigningVersion);
expect(LumUtils.toHex(signedW2.sig)).not.toEqual(LumUtils.toHex(signedW1.sig));
expect(await LumUtils.verifySignMsg(signedW2)).toBeTruthy();

const v1 = await LumUtils.verifySignMsg(signed);
expect(v1).toBeTruthy();
const v2 = await LumUtils.verifySignMsg(Object.assign({}, signed, { msg: 'Wrong message input' }));
expect(v2).toBeFalsy();
const v3 = await LumUtils.verifySignMsg(Object.assign({}, signed, { publicKey: w2.getPublicKey() }));
expect(v3).toBeFalsy();
const v4 = await LumUtils.verifySignMsg(Object.assign({}, signed, { address: w2.getAddress() }));
expect(v4).toBeFalsy();
const signedW3 = await w3.signMessage(message);
expect(signedW3.signer).toEqual(LumConstants.LumMessageSigner.OFFLINE);
expect(signedW3.version).toEqual(LumConstants.LumWalletSigningVersion);
expect(await LumUtils.verifySignMsg(signedW3)).toBeTruthy();
});
});