diff --git a/common/actions/deterministicWallets.js b/common/actions/deterministicWallets.js
index c6f277f2..ee30d31c 100644
--- a/common/actions/deterministicWallets.js
+++ b/common/actions/deterministicWallets.js
@@ -14,18 +14,20 @@ export type DeterministicWalletData = {
export type GetDeterministicWalletsAction = {
type: 'DW_GET_WALLETS',
payload: {
+ seed: ?string,
dPath: string,
- publicKey: string,
- chainCode: string,
+ publicKey: ?string,
+ chainCode: ?string,
limit: number,
offset: number
}
};
export type GetDeterministicWalletsArgs = {
+ seed: ?string,
dPath: string,
- publicKey: string,
- chainCode: string,
+ publicKey: ?string,
+ chainCode: ?string,
limit?: number,
offset?: number
};
@@ -33,10 +35,11 @@ export type GetDeterministicWalletsArgs = {
export function getDeterministicWallets(
args: GetDeterministicWalletsArgs
): GetDeterministicWalletsAction {
- const { dPath, publicKey, chainCode, limit, offset } = args;
+ const { seed, dPath, publicKey, chainCode, limit, offset } = args;
return {
type: 'DW_GET_WALLETS',
payload: {
+ seed,
dPath,
publicKey,
chainCode,
diff --git a/common/actions/wallet.js b/common/actions/wallet.js
index 39a9b6b9..aafe9131 100644
--- a/common/actions/wallet.js
+++ b/common/actions/wallet.js
@@ -43,6 +43,28 @@ export function unlockKeystore(
};
}
+/*** Unlock Mnemonic ***/
+export type MnemonicUnlockParams = {
+ phrase: string,
+ pass: string,
+ path: string,
+ address: string
+};
+
+export type UnlockMnemonicAction = {
+ type: 'WALLET_UNLOCK_MNEMONIC',
+ payload: MnemonicUnlockParams
+};
+
+export function unlockMnemonic(
+ value: MnemonicUnlockParams
+): UnlockMnemonicAction {
+ return {
+ type: 'WALLET_UNLOCK_MNEMONIC',
+ payload: value
+ };
+}
+
/*** Set Wallet ***/
export type SetWalletAction = {
type: 'WALLET_SET',
diff --git a/common/components/WalletDecrypt/DeterministicWalletsModal.jsx b/common/components/WalletDecrypt/DeterministicWalletsModal.jsx
index 3fd7b6eb..032441ff 100644
--- a/common/components/WalletDecrypt/DeterministicWalletsModal.jsx
+++ b/common/components/WalletDecrypt/DeterministicWalletsModal.jsx
@@ -36,15 +36,17 @@ type Props = {
walletType: ?string,
dPath: string,
dPaths: { label: string, value: string }[],
- publicKey: string,
- chainCode: string,
+ publicKey: ?string,
+ chainCode: ?string,
+ seed: ?string,
onCancel: () => void,
- onConfirmAddress: string => void,
+ onConfirmAddress: (string, number) => void,
onPathChange: string => void
};
type State = {
selectedAddress: string,
+ selectedAddrIndex: number,
isCustomPath: boolean,
customPath: string,
page: number
@@ -54,6 +56,7 @@ class DeterministicWalletsModal extends React.Component {
props: Props;
state: State = {
selectedAddress: '',
+ selectedAddrIndex: 0,
isCustomPath: false,
customPath: '',
page: 0
@@ -64,20 +67,23 @@ class DeterministicWalletsModal extends React.Component {
}
componentWillReceiveProps(nextProps) {
- const { publicKey, chainCode } = this.props;
+ const { publicKey, chainCode, seed, dPath } = this.props;
if (
nextProps.publicKey !== publicKey ||
- nextProps.chainCode !== chainCode
+ nextProps.chainCode !== chainCode ||
+ nextProps.dPath !== dPath ||
+ nextProps.seed !== seed
) {
this._getAddresses(nextProps);
}
}
_getAddresses(props: Props = this.props) {
- const { dPath, publicKey, chainCode } = props;
+ const { dPath, publicKey, chainCode, seed } = props;
- if (dPath && publicKey && chainCode && isValidPath(dPath)) {
+ if (dPath && ((publicKey && chainCode) || seed) && isValidPath(dPath)) {
this.props.getDeterministicWallets({
+ seed,
dPath,
publicKey,
chainCode,
@@ -116,12 +122,15 @@ class DeterministicWalletsModal extends React.Component {
_handleConfirmAddress = () => {
if (this.state.selectedAddress) {
- this.props.onConfirmAddress(this.state.selectedAddress);
+ this.props.onConfirmAddress(
+ this.state.selectedAddress,
+ this.state.selectedAddrIndex
+ );
}
};
- _selectAddress(selectedAddress) {
- this.setState({ selectedAddress });
+ _selectAddress(selectedAddress, selectedAddrIndex) {
+ this.setState({ selectedAddress, selectedAddrIndex });
}
_nextPage = () => {
@@ -148,7 +157,7 @@ class DeterministicWalletsModal extends React.Component {
return (
{wallet.index + 1}
diff --git a/common/components/WalletDecrypt/Mnemonic.jsx b/common/components/WalletDecrypt/Mnemonic.jsx
index 2c6bb22a..98cc58ce 100644
--- a/common/components/WalletDecrypt/Mnemonic.jsx
+++ b/common/components/WalletDecrypt/Mnemonic.jsx
@@ -1,27 +1,129 @@
import React, { Component } from 'react';
-import translate from 'translations';
+import translate, { translateRaw } from 'translations';
+import { validateMnemonic, mnemonicToSeed } from 'bip39';
+
+import DeterministicWalletsModal from './DeterministicWalletsModal';
+
+import DPATHS from 'config/dpaths.js';
+const DEFAULT_PATH = DPATHS.MNEMONIC[0].value;
+
+type State = {
+ phrase: string,
+ pass: string,
+ seed: string,
+ dPath: string
+};
export default class MnemonicDecrypt extends Component {
+ props: { onUnlock: any => void };
+ state: State = {
+ phrase: '',
+ pass: '',
+ seed: '',
+ dPath: DEFAULT_PATH
+ };
+
render() {
+ const { phrase, seed, dPath, pass } = this.state;
+ const isValidMnemonic = validateMnemonic(phrase);
+
return (
-
- {translate('ADD_Radio_2_alt')}
-
+
+
+ {translate('ADD_Radio_5')}
+
+
+ Password (optional):
+
+
+ {isValidMnemonic &&
+
+
+ }
+
+
);
}
+
+ onPasswordChange = (e: SyntheticInputEvent) => {
+ this.setState({ pass: e.target.value });
+ };
+
+ onMnemonicChange = (e: SyntheticInputEvent) => {
+ this.setState({ phrase: e.target.value });
+ };
+
+ onDWModalOpen = (e: SyntheticInputEvent) => {
+ const { phrase, pass } = this.state;
+
+ if (!validateMnemonic(phrase)) return;
+
+ try {
+ let seed = mnemonicToSeed(phrase.trim(), pass).toString('hex');
+ this.setState({ seed });
+ } catch (err) {
+ console.log(err);
+ }
+ };
+
+ _handleCancel = () => {
+ this.setState({ seed: '' });
+ };
+
+ _handlePathChange = (dPath: string) => {
+ this.setState({ dPath });
+ };
+
+ _handleUnlock = (address, index) => {
+ const { phrase, pass, dPath } = this.state;
+
+ this.props.onUnlock({
+ path: `${dPath}/${index}`,
+ pass,
+ phrase,
+ address
+ });
+
+ this.setState({
+ seed: '',
+ pass: '',
+ phrase: ''
+ });
+ };
}
diff --git a/common/components/WalletDecrypt/Trezor.jsx b/common/components/WalletDecrypt/Trezor.jsx
index df0d111f..dba45ddc 100644
--- a/common/components/WalletDecrypt/Trezor.jsx
+++ b/common/components/WalletDecrypt/Trezor.jsx
@@ -5,7 +5,7 @@ import translate from 'translations';
import TrezorConnect from 'vendor/trezor-connect';
import DeterministicWalletsModal from './DeterministicWalletsModal';
import TrezorWallet from 'libs/wallet/trezor';
-import DPATHS from 'config/dpaths.json';
+import DPATHS from 'config/dpaths.js';
const DEFAULT_PATH = DPATHS.TREZOR[0].value;
type State = {
@@ -65,8 +65,8 @@ export default class TrezorDecrypt extends Component {
});
};
- _handleUnlock = (address: string) => {
- this.props.onUnlock(new TrezorWallet(address, this.state.dPath));
+ _handleUnlock = (address: string, index: number) => {
+ this.props.onUnlock(new TrezorWallet(address, this.state.dPath, index));
};
render() {
diff --git a/common/components/WalletDecrypt/index.jsx b/common/components/WalletDecrypt/index.jsx
index b3919813..4935b81f 100644
--- a/common/components/WalletDecrypt/index.jsx
+++ b/common/components/WalletDecrypt/index.jsx
@@ -9,7 +9,12 @@ import LedgerNanoSDecrypt from './LedgerNano';
import TrezorDecrypt from './Trezor';
import ViewOnlyDecrypt from './ViewOnly';
import map from 'lodash/map';
-import { unlockPrivateKey, unlockKeystore, setWallet } from 'actions/wallet';
+import {
+ unlockPrivateKey,
+ unlockKeystore,
+ unlockMnemonic,
+ setWallet
+} from 'actions/wallet';
import { connect } from 'react-redux';
import isEmpty from 'lodash/isEmpty';
@@ -35,7 +40,8 @@ const WALLETS = {
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt,
- disabled: true
+ initialParams: {},
+ unlock: unlockMnemonic
},
'ledger-nano-s': {
lid: 'x_Ledger',
diff --git a/common/config/dpaths.js b/common/config/dpaths.js
new file mode 100644
index 00000000..b73ba329
--- /dev/null
+++ b/common/config/dpaths.js
@@ -0,0 +1,50 @@
+const ETH_DEFAULT = {
+ label: 'Default (ETH)',
+ value: "m/44'/60'/0'/0"
+};
+
+const ETH_TREZOR = {
+ label: 'TREZOR (ETH)',
+ value: "m/44'/60'/0'/0"
+};
+
+const ETH_LEDGER = {
+ label: 'Ledger (ETH)',
+ value: "m/44'/60'/0'"
+};
+
+const ETC_LEDGER = {
+ label: 'Ledger (ETC)',
+ value: "m/44'/60'/160720'/0'"
+};
+
+const ETC_TREZOR = {
+ label: 'TREZOR (ETC)',
+ value: "m/44'/61'/0'/0"
+};
+
+const TESTNET = {
+ label: 'Testnet',
+ value: "m/44'/1'/0'/0"
+};
+
+const EXPANSE = {
+ label: 'Expanse',
+ value: "m/44'/40'/0'/0"
+};
+
+const TREZOR = [ETH_TREZOR, ETC_TREZOR, TESTNET];
+
+const MNEMONIC = [
+ ETH_DEFAULT,
+ ETH_LEDGER,
+ ETC_LEDGER,
+ ETC_TREZOR,
+ TESTNET,
+ EXPANSE
+];
+
+export default {
+ TREZOR,
+ MNEMONIC
+};
diff --git a/common/config/dpaths.json b/common/config/dpaths.json
deleted file mode 100644
index 10460eb3..00000000
--- a/common/config/dpaths.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "TREZOR": [
- {
- "label": "TREZOR (ETH)",
- "value": "m/44'/60'/0'/0"
- },
- {
- "label": "TREZOR (ETC)",
- "value": "m/44'/61'/0'/0"
- },
- {
- "label": "Testnet",
- "value": "m/44'/1'/0'/0"
- }
- ]
-}
diff --git a/common/libs/decrypt.js b/common/libs/decrypt.js
index 7e60f7e9..d70106c1 100644
--- a/common/libs/decrypt.js
+++ b/common/libs/decrypt.js
@@ -1,6 +1,11 @@
//@flow
import { createHash, createDecipheriv } from 'crypto';
+import { validateMnemonic, mnemonicToSeed } from 'bip39';
+import { fromMasterSeed } from 'hdkey';
+import { stripHexPrefixAndLower } from 'libs/values';
+
+import { privateToAddress } from 'ethereumjs-util';
//adapted from https://github.com/kvhnuke/etherwallet/blob/de536ffebb4f2d1af892a32697e89d1a0d906b01/app/scripts/myetherwallet.js#L230
export function decryptPrivKey(encprivkey: string, password: string): Buffer {
@@ -66,3 +71,28 @@ export function evp_kdf(data: Buffer, salt: Buffer, opts: Object) {
export function decipherBuffer(decipher: Object, data: Buffer): Buffer {
return Buffer.concat([decipher.update(data), decipher.final()]);
}
+
+export function decryptMnemonicToPrivKey(
+ phrase: string,
+ pass: string,
+ path: string,
+ address: string
+): Buffer {
+ phrase = phrase.trim();
+ address = stripHexPrefixAndLower(address);
+
+ if (!validateMnemonic(phrase)) {
+ throw new Error('Invalid mnemonic');
+ }
+
+ const seed = mnemonicToSeed(phrase, pass);
+ const derived = fromMasterSeed(seed).derive(path);
+ const dPrivKey = derived.privateKey;
+ const dAddress = privateToAddress(dPrivKey).toString('hex');
+
+ if (dAddress !== address) {
+ throw new Error(`Derived ${dAddress}, expected ${address}`);
+ }
+
+ return dPrivKey;
+}
diff --git a/common/libs/transaction.js b/common/libs/transaction.js
index ed47705f..1d4c7b2a 100644
--- a/common/libs/transaction.js
+++ b/common/libs/transaction.js
@@ -4,7 +4,7 @@ import translate from 'translations';
import { padToEven, addHexPrefix, toChecksumAddress } from 'ethereumjs-util';
import { isValidETHAddress } from 'libs/validators';
import ERC20 from 'libs/erc20';
-import { stripHex, valueToHex } from 'libs/values';
+import { stripHexPrefixAndLower, valueToHex } from 'libs/values';
import { Wei, Ether, toTokenUnit } from 'libs/units';
import { RPCNode } from 'libs/nodes';
import { TransactionWithoutGas } from 'libs/messages';
@@ -132,12 +132,12 @@ export async function generateCompleteTransactionFromRawTransaction(
}
// Taken from v3's `sanitizeHex`, ensures that the value is a %2 === 0
// prefix'd hex value.
- const cleanHex = hex => addHexPrefix(padToEven(stripHex(hex)));
+ const cleanHex = hex => addHexPrefix(padToEven(stripHexPrefixAndLower(hex)));
const cleanedRawTx = {
nonce: cleanHex(nonce),
gasPrice: cleanHex(gasPrice.toString(16)),
gasLimit: cleanHex(gasLimit.toString(16)),
- to: cleanHex(to),
+ to: toChecksumAddress(cleanHex(to)),
value: token ? '0x00' : cleanHex(value.toString(16)),
data: data ? cleanHex(data) : '',
chainId: chainId || 1
diff --git a/common/libs/validators.js b/common/libs/validators.js
index e4b4cdb9..1ba745ee 100644
--- a/common/libs/validators.js
+++ b/common/libs/validators.js
@@ -140,8 +140,11 @@ export function isValidRawTx(rawTx: RawTransaction): boolean {
return true;
}
-// Full length deterministic wallet paths from BIP32
-// https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki
+// Full length deterministic wallet paths from BIP44
+// https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki
+// normal path length is 4, ledger is the exception at 3
export function isValidPath(dPath: string) {
- return dPath.split("'/").length === 4;
+ //TODO: use a regex to detect proper paths
+ const len = dPath.split("'/").length;
+ return len === 3 || len === 4;
}
diff --git a/common/libs/values.js b/common/libs/values.js
index f7af6b72..f740e72f 100644
--- a/common/libs/values.js
+++ b/common/libs/values.js
@@ -1,7 +1,7 @@
// @flow
import { Ether } from 'libs/units';
-export function stripHex(address: string): string {
+export function stripHexPrefixAndLower(address: string): string {
return address.replace('0x', '').toLowerCase();
}
diff --git a/common/libs/wallet/deterministic.js b/common/libs/wallet/deterministic.js
index ac8ad6d0..d3fae175 100644
--- a/common/libs/wallet/deterministic.js
+++ b/common/libs/wallet/deterministic.js
@@ -4,10 +4,12 @@ import type { IWallet } from './IWallet';
export default class DeterministicWallet implements IWallet {
address: string;
dPath: string;
+ index: number;
- constructor(address: string, dPath: string) {
+ constructor(address: string, dPath: string, index: number) {
this.address = address;
this.dPath = dPath;
+ this.index = index;
}
getAddress(): Promise {
@@ -15,6 +17,6 @@ export default class DeterministicWallet implements IWallet {
}
getPath(): string {
- return this.dPath;
+ return `${this.dPath}/${this.index}`;
}
}
diff --git a/common/libs/wallet/index.js b/common/libs/wallet/index.js
index 80da4005..938340d0 100644
--- a/common/libs/wallet/index.js
+++ b/common/libs/wallet/index.js
@@ -6,3 +6,4 @@ export { default as EncryptedPrivKeyWallet } from './encprivkey';
export { default as PresaleWallet } from './presale';
export { default as MewV1Wallet } from './mewv1';
export { default as UtcWallet } from './utc';
+export { default as MnemonicWallet } from './mnemonic';
diff --git a/common/libs/wallet/mnemonic.js b/common/libs/wallet/mnemonic.js
new file mode 100644
index 00000000..85842683
--- /dev/null
+++ b/common/libs/wallet/mnemonic.js
@@ -0,0 +1,9 @@
+// @flow
+import PrivKeyWallet from './privkey';
+import { decryptMnemonicToPrivKey } from 'libs/decrypt';
+
+export default class MnemonicWallet extends PrivKeyWallet {
+ constructor(phrase: string, pass: string, path: string, address: string) {
+ super(decryptMnemonicToPrivKey(phrase, pass, path, address));
+ }
+}
diff --git a/common/libs/wallet/privkey.js b/common/libs/wallet/privkey.js
index 152f66f9..78f5dcdb 100644
--- a/common/libs/wallet/privkey.js
+++ b/common/libs/wallet/privkey.js
@@ -11,7 +11,7 @@ import { signRawTxWithPrivKey, signMessageWithPrivKey } from 'libs/signing';
import { isValidPrivKey } from 'libs/validators';
import type { RawTransaction } from 'libs/transaction';
import type { UtcKeystore } from 'libs/keystore';
-import { stripHex } from 'libs/values';
+import { stripHexPrefixAndLower } from 'libs/values';
export default class PrivKeyWallet implements IWallet {
privKey: Buffer;
@@ -43,7 +43,7 @@ export default class PrivKeyWallet implements IWallet {
getNakedAddress(): Promise {
return new Promise(resolve => {
this.getAddress().then(address => {
- resolve(stripHex(address));
+ resolve(stripHexPrefixAndLower(address));
});
});
}
diff --git a/common/libs/wallet/trezor.js b/common/libs/wallet/trezor.js
index dcefb1e6..d8145912 100644
--- a/common/libs/wallet/trezor.js
+++ b/common/libs/wallet/trezor.js
@@ -4,8 +4,7 @@ import EthTx from 'ethereumjs-tx';
import Big from 'bignumber.js';
import { addHexPrefix } from 'ethereumjs-util';
import DeterministicWallet from './deterministic';
-import { stripHex } from 'libs/values';
-
+import { stripHexPrefixAndLower } from 'libs/values';
import type { RawTransaction } from 'libs/transaction';
export default class TrezorWallet extends DeterministicWallet {
@@ -14,12 +13,13 @@ export default class TrezorWallet extends DeterministicWallet {
TrezorConnect.ethereumSignTx(
// Args
this.getPath(),
- stripHex(tx.nonce),
- stripHex(tx.gasPrice.toString()),
- stripHex(tx.gasLimit.toString()),
- stripHex(tx.to),
- stripHex(tx.value),
- stripHex(tx.data),
+ // stripHexPrefixAndLower identical to ethFuncs.getNakedAddress
+ stripHexPrefixAndLower(tx.nonce),
+ stripHexPrefixAndLower(tx.gasPrice.toString()),
+ stripHexPrefixAndLower(tx.gasLimit.toString()),
+ stripHexPrefixAndLower(tx.to),
+ stripHexPrefixAndLower(tx.value),
+ stripHexPrefixAndLower(tx.data),
tx.chainId,
// Callback
result => {
diff --git a/common/sagas/deterministicWallets.js b/common/sagas/deterministicWallets.js
index dbae5a64..0ad6ca4a 100644
--- a/common/sagas/deterministicWallets.js
+++ b/common/sagas/deterministicWallets.js
@@ -30,21 +30,34 @@ import { getTokens } from 'selectors/wallet';
import type { INode } from 'libs/nodes/INode';
import type { Token } from 'config/data';
-// TODO: BIP39 for mnemonic wallets?
+import { showNotification } from 'actions/notifications';
+import translate from 'translations';
+
function* getDeterministicWallets(
action?: GetDeterministicWalletsAction
): Generator {
if (!action) return;
- const { publicKey, chainCode, limit, offset } = action.payload;
- const hdk = new HDKey();
- hdk.publicKey = new Buffer(publicKey, 'hex');
- hdk.chainCode = new Buffer(chainCode, 'hex');
+ const { seed, dPath, publicKey, chainCode, limit, offset } = action.payload;
+ let pathBase, hdk;
+
+ //if seed present, treat as mnemonic
+ //if pubKey & chainCode present, treat as HW wallet
+
+ if (seed) {
+ hdk = HDKey.fromMasterSeed(new Buffer(seed, 'hex'));
+ pathBase = dPath;
+ } else if (publicKey && chainCode) {
+ hdk = new HDKey();
+ hdk.publicKey = new Buffer(publicKey, 'hex');
+ hdk.chainCode = new Buffer(chainCode, 'hex');
+ pathBase = 'm';
+ } else return;
const wallets = [];
for (let i = 0; i < limit; i++) {
const index = i + offset;
- const dkey = hdk.derive(`m/${index}`);
+ const dkey = hdk.derive(`${pathBase}/${index}`);
const address = publicToAddress(dkey.publicKey, true).toString('hex');
wallets.push({
index,
@@ -62,16 +75,22 @@ function* getDeterministicWallets(
function* updateWalletValues(): Generator {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
- const calls = wallets.map(w => apply(node, node.getBalance, [w.address]));
- const balances = yield all(calls);
- for (let i = 0; i < wallets.length; i++) {
- yield put(
- updateDeterministicWallet({
- ...wallets[i],
- value: balances[i]
- })
- );
+ try {
+ const calls = wallets.map(w => apply(node, node.getBalance, [w.address]));
+ const balances = yield all(calls);
+
+ for (let i = 0; i < wallets.length; i++) {
+ yield put(
+ updateDeterministicWallet({
+ ...wallets[i],
+ value: balances[i]
+ })
+ );
+ }
+ } catch (err) {
+ console.log(err);
+ yield put(showNotification('danger', translate('ERROR_32')));
}
}
@@ -86,21 +105,27 @@ function* updateWalletTokenValues(): Generator {
const node: INode = yield select(getNodeLib);
const wallets: DeterministicWalletData[] = yield select(getWallets);
- const calls = wallets.map(w => {
- return apply(node, node.getTokenBalance, [w.address, token]);
- });
- const tokenBalances = yield all(calls);
- for (let i = 0; i < wallets.length; i++) {
- yield put(
- updateDeterministicWallet({
- ...wallets[i],
- tokenValues: {
- ...wallets[i].tokenValues,
- [desiredToken]: tokenBalances[i]
- }
- })
- );
+ try {
+ const calls = wallets.map(w => {
+ return apply(node, node.getTokenBalance, [w.address, token]);
+ });
+ const tokenBalances = yield all(calls);
+
+ for (let i = 0; i < wallets.length; i++) {
+ yield put(
+ updateDeterministicWallet({
+ ...wallets[i],
+ tokenValues: {
+ ...wallets[i].tokenValues,
+ [desiredToken]: tokenBalances[i]
+ }
+ })
+ );
+ }
+ } catch (err) {
+ console.log(err);
+ yield put(showNotification('danger', translate('ERROR_32')));
}
}
diff --git a/common/sagas/wallet.js b/common/sagas/wallet.js
index a74cc628..c68ecd3f 100644
--- a/common/sagas/wallet.js
+++ b/common/sagas/wallet.js
@@ -5,7 +5,8 @@ import { takeEvery, call, apply, put, select, fork } from 'redux-saga/effects';
import { setWallet, setBalance, setTokenBalances } from 'actions/wallet';
import type {
UnlockPrivateKeyAction,
- UnlockKeystoreAction
+ UnlockKeystoreAction,
+ UnlockMnemonicAction
} from 'actions/wallet';
import { showNotification } from 'actions/notifications';
import type { BroadcastTxRequestedAction } from 'actions/wallet';
@@ -19,6 +20,7 @@ import {
UtcWallet,
EncryptedPrivKeyWallet,
PrivKeyWallet,
+ MnemonicWallet,
IWallet
} from 'libs/wallet';
import { INode } from 'libs/nodes/INode';
@@ -141,6 +143,25 @@ export function* unlockKeystore(
yield put(setWallet(wallet));
}
+function* unlockMnemonic(
+ action?: UnlockMnemonicAction
+): Generator {
+ if (!action) return;
+
+ let wallet;
+ const { phrase, pass, path, address } = action.payload;
+
+ try {
+ wallet = new MnemonicWallet(phrase, pass, path, address);
+ } catch (err) {
+ // TODO: use better error than 'ERROR_14' (wallet not found)
+ yield put(showNotification('danger', translate('ERROR_14')));
+ return;
+ }
+
+ yield put(setWallet(wallet));
+}
+
function* broadcastTx(
action: BroadcastTxRequestedAction
): Generator {
@@ -176,6 +197,7 @@ export default function* walletSaga(): Generator {
yield [
takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey),
takeEvery('WALLET_UNLOCK_KEYSTORE', unlockKeystore),
+ takeEvery('WALLET_UNLOCK_MNEMONIC', unlockMnemonic),
takeEvery('WALLET_SET', updateBalances),
takeEvery('CUSTOM_TOKEN_ADD', updateTokenBalances),
// $FlowFixMe but how do I specify param types here flow?
diff --git a/package.json b/package.json
index 01c1551f..7498826b 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"dependencies": {
"bignumber.js": "^4.0.2",
"bootstrap-sass": "^3.3.7",
+ "bip39": "^2.4.0",
"classnames": "^2.2.5",
"ethereum-blockies": "git+https://github.com/MyEtherWallet/blockies.git",
"ethereumjs-abi": "^0.6.4",
diff --git a/spec/libs/decrypt.spec.js b/spec/libs/decrypt.spec.js
index d42d0c9b..8135a364 100644
--- a/spec/libs/decrypt.spec.js
+++ b/spec/libs/decrypt.spec.js
@@ -2,7 +2,8 @@ import {
decryptPrivKey,
decodeCryptojsSalt,
evp_kdf,
- decipherBuffer
+ decipherBuffer,
+ decryptMnemonicToPrivKey
} from '../../common/libs/decrypt';
//deconstructed elements of a V1 encrypted priv key
@@ -71,3 +72,120 @@ describe('decipherBuffer', () => {
expect(result.toString()).toEqual(str + '!');
});
});
+
+describe('decryptMnemonicToPrivKey', () => {
+ const mocks = [
+ {
+ phrase:
+ 'first catalog away faculty jelly now life kingdom pigeon raise gain accident',
+ pass: '',
+ path: "m/44'/60'/0'/0/8",
+ address: '0xe2EdC95134bbD88443bc6D55b809F7d0C2f0C854',
+ privKey:
+ '31e97f395cabc6faa37d8a9d6bb185187c35704e7b976c7a110e2f0eab37c344'
+ },
+ {
+ phrase:
+ 'grace near jewel celery divorce unlock thumb segment since photo cushion meat sketch tooth edit',
+ pass: '',
+ path: "m/44'/60'/0'/0/18",
+ address: '0xB20f8aCA62e18f4586aAEf4720daCac23cC29954',
+ privKey:
+ '594ee624ebad54b9469915c3f5eb22127727a5e380a17d24780dbe272996b401'
+ },
+ {
+ phrase:
+ 'airport bid shop easy tiger rule under afraid lobster adapt ranch story elbow album rifle turtle earn witness',
+ pass: '',
+ path: "m/44'/60'/0'/0/24",
+ address: '0xE6D0932fFDDcB45bf0e18dE4716137dEdD2E4c2c',
+ privKey:
+ '6aba8bb6018a85af7cb552325b52e397f83cfb56f68cf8937aa14c3875bbb0aa'
+ },
+ {
+ phrase:
+ 'plug strong practice prize crater private together anchor horror nasty option exhibit position engage pledge giggle soda lecture syrup ocean barrel',
+ pass: '',
+ path: "m/44'/60'/0'/0/0",
+ address: '0xd163f4d95782608b251c4d985846A1754c53D32C',
+ privKey:
+ '88046b4bdbb1c88945662cb0984258ca1b09df0bb0b38fdc55bcb8998f28aad4'
+ },
+ {
+ phrase:
+ 'laptop pool call below prepare car alley wheel bunker valve soul misery buffalo home hobby timber enlist country mind guilt drastic castle cable federal',
+ pass: '',
+ path: "m/44'/60'/0'/0/4",
+ address: '0x04E2df6Fe2a28dd24dbCC49485ff30Fc3ea04822',
+ privKey:
+ 'fc9ad0931a3aee167179c1fd31825b7a7b558b4bb2eb3fb0c04028c98d495907'
+ },
+ {
+ phrase:
+ 'stadium river pigeon midnight grit truck fiscal eight hello rescue destroy eyebrow',
+ pass: 'password',
+ path: "m/44'/60'/0'/0/5",
+ address: '0xe74908668F594f327fd2215A2564Cf79298a136e',
+ privKey:
+ 'b65abfb2660f71b4b46aed98975f0cc1ebe1fcb3835a7a10b236e4012c93f306'
+ },
+ {
+ phrase:
+ 'oval save glimpse regret decline pottery wealth canal post sing congress bounce run unable stove',
+ pass: 'password',
+ path: "m/44'/60'/0'/0/10",
+ address: '0x0d20865AfAE9B8a1F867eCd60684FBCDA3Bd1FA5',
+ privKey:
+ '29eb9ec0f5586d1935bc4c6bd89e6fb3de76b4fad345fa844efc5432885cfe73'
+ },
+ {
+ phrase:
+ 'lecture defy often frog young blush exact tomato culture north urge rescue resemble require bring dismiss actress fog',
+ pass: 'password',
+ path: "m/44'/60'/0'/0/7",
+ address: '0xdd5d6e5dEfD09c3F2BD6d994EE43B59df88c7187',
+ privKey:
+ 'd13404b9b05f6b5bf8e5cf810aa903e4b60ac654b0acf09a8ea0efe174746ae5'
+ },
+ {
+ phrase:
+ 'supreme famous violin such option marriage arctic genius member rare siege circle round field weather humble fame buffalo one control marble',
+ pass: 'password',
+ path: "m/44'/60'/0'/0/11",
+ address: '0x6d95e7cC28113F9491b2Ec6b621575a5565Fd208',
+ privKey:
+ 'a52329aa3d6f2426f8783a1e5f419997e2628ec9a89cc2b7b182d2eaf7f95a24'
+ },
+ {
+ phrase:
+ 'next random ready come great start beyond learn supply chimney include grocery fee phrase margin adult ocean craft topple subject satoshi angry mystery liar',
+ pass: 'password',
+ path: "m/44'/60'/0'/0/4",
+ address: '0x3e583eF3d3cE5Dd483c86A1E00A479cE11Ca21Cf',
+ privKey:
+ '450538d4181c4d8ce076ecb34785198316adebe959d6f9462cfb68a58b1819bc'
+ },
+ {
+ phrase:
+ 'champion pitch profit beyond know imitate weasel gift escape bullet price barely crime renew hurry',
+ pass: 'password123',
+ path: "m/44'/60'/0'/1",
+ address: '0x7545D615643F933c34C3E083E68CC831167F31af',
+ privKey:
+ '0a43098da5ae737843e385b76b44266a9f8f856cb1b943055b5a96188d306d97'
+ }
+ ];
+
+ it('should derive correct private key from variable phrase lengths/passwords/paths', () => {
+ mocks.forEach(mock => {
+ const { phrase, pass, path, privKey, address } = mock;
+ const derivedPrivKey = decryptMnemonicToPrivKey(
+ phrase,
+ pass,
+ path,
+ address
+ );
+ expect(derivedPrivKey.toString('hex')).toEqual(privKey);
+ });
+ });
+});
|