mirror of
https://github.com/status-im/MyCrypto.git
synced 2025-02-01 22:06:00 +00:00
Wallet Decrypt - Mnemonic (#180)
* add 'bip39' package * add mnemonic decrypt, wallet wrapper * add mnemonic path config * add mnemonic support to deterministic components * add mnemonic support * accomodate for ledger ETH path * remove comments * update comments regarding path length * rename modal open handler * make several props optional * add basic tests for mnemonic decrypt * make flow happy, add user error notifications * convert dpaths to js file, update references * add ledger path to test * Trezor DPath Fix (#196) * Match v3 more closely. * Require wallet index on deterministic wallets, update trezor to send index. * remove redundent stripAndLower function and rename existing stripHex to stripHexPrefixAndLower
This commit is contained in:
parent
7a460960d7
commit
c88e96d603
@ -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,
|
||||
|
@ -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',
|
||||
|
@ -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 (
|
||||
<tr
|
||||
key={wallet.address}
|
||||
onClick={this._selectAddress.bind(this, wallet.address)}
|
||||
onClick={this._selectAddress.bind(this, wallet.address, wallet.index)}
|
||||
>
|
||||
<td>
|
||||
{wallet.index + 1}
|
||||
|
@ -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 (
|
||||
<section className="col-md-4 col-sm-6">
|
||||
<div id="selectedUploadKey">
|
||||
<h4>{translate('ADD_Radio_2_alt')}</h4>
|
||||
|
||||
<div id="selectedTypeKey">
|
||||
<h4>
|
||||
{translate('ADD_Radio_5')}
|
||||
</h4>
|
||||
<div className="form-group">
|
||||
<input type="file" id="fselector" />
|
||||
|
||||
<a
|
||||
className="btn-file marg-v-sm"
|
||||
id="aria1"
|
||||
tabIndex="0"
|
||||
role="button"
|
||||
>
|
||||
{translate('ADD_Radio_2_short')}
|
||||
</a>
|
||||
<textarea
|
||||
id="aria-private-key"
|
||||
className={`form-control ${isValidMnemonic
|
||||
? 'is-valid'
|
||||
: 'is-invalid'}`}
|
||||
value={phrase}
|
||||
onChange={this.onMnemonicChange}
|
||||
placeholder={translateRaw('x_Mnemonic')}
|
||||
rows="4"
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<p>Password (optional):</p>
|
||||
<input
|
||||
className="form-control"
|
||||
value={pass}
|
||||
onChange={this.onPasswordChange}
|
||||
placeholder={translateRaw('x_Password')}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
{isValidMnemonic &&
|
||||
<div className="form-group">
|
||||
<button
|
||||
style={{ width: '100%' }}
|
||||
onClick={this.onDWModalOpen}
|
||||
className="btn btn-primary btn-lg"
|
||||
>
|
||||
{translate('Choose Address')}
|
||||
</button>
|
||||
</div>}
|
||||
</div>
|
||||
|
||||
<DeterministicWalletsModal
|
||||
isOpen={!!seed}
|
||||
seed={seed}
|
||||
dPath={dPath}
|
||||
dPaths={DPATHS.MNEMONIC}
|
||||
onCancel={this._handleCancel}
|
||||
onConfirmAddress={this._handleUnlock}
|
||||
onPathChange={this._handlePathChange}
|
||||
walletType={translateRaw('x_Mnemonic')}
|
||||
/>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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: ''
|
||||
});
|
||||
};
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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',
|
||||
|
50
common/config/dpaths.js
Normal file
50
common/config/dpaths.js
Normal file
@ -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
|
||||
};
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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<string> {
|
||||
@ -15,6 +17,6 @@ export default class DeterministicWallet implements IWallet {
|
||||
}
|
||||
|
||||
getPath(): string {
|
||||
return this.dPath;
|
||||
return `${this.dPath}/${this.index}`;
|
||||
}
|
||||
}
|
||||
|
@ -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';
|
||||
|
9
common/libs/wallet/mnemonic.js
Normal file
9
common/libs/wallet/mnemonic.js
Normal file
@ -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));
|
||||
}
|
||||
}
|
@ -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<string> {
|
||||
return new Promise(resolve => {
|
||||
this.getAddress().then(address => {
|
||||
resolve(stripHex(address));
|
||||
resolve(stripHexPrefixAndLower(address));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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<Yield, Return, Next> {
|
||||
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<Yield, Return, Next> {
|
||||
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<Yield, Return, Next> {
|
||||
|
||||
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')));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<Yield, Return, Next> {
|
||||
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<Yield, Return, Next> {
|
||||
@ -176,6 +197,7 @@ export default function* walletSaga(): Generator<Yield, Return, Next> {
|
||||
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?
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user