Web Worker Decrypt (#680)
1. Attempt an empty password every time a keystore is uploaded. 2. Delegate scrypt decryption (ie ethereumjs-wallet.fromV3) to its own web worker and interface with it through an async typescript function that gets handled in the wallet saga. This keeps the UI unblocked when scrypt takes a long time to decrypt. 3. Add logic to show a spinner x number of milliseconds after file upload so the user will understand when a wallet is being decrypted.
This commit is contained in:
parent
a84a6e98fc
commit
af2e0b69e1
|
@ -43,12 +43,25 @@ export function setWallet(value: IWallet): types.SetWalletAction {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setWalletPending(loadingStatus: boolean): types.SetWalletPendingAction {
|
||||||
|
return {
|
||||||
|
type: TypeKeys.WALLET_SET_PENDING,
|
||||||
|
payload: loadingStatus
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function setBalancePending(): types.SetBalancePendingAction {
|
export function setBalancePending(): types.SetBalancePendingAction {
|
||||||
return {
|
return {
|
||||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING
|
type: TypeKeys.WALLET_SET_BALANCE_PENDING
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setPasswordPrompt(): types.SetPasswordPendingAction {
|
||||||
|
return {
|
||||||
|
type: TypeKeys.WALLET_SET_PASSWORD_PENDING
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export type TSetBalance = typeof setBalanceFullfilled;
|
export type TSetBalance = typeof setBalanceFullfilled;
|
||||||
export function setBalanceFullfilled(value: Wei): types.SetBalanceFullfilledAction {
|
export function setBalanceFullfilled(value: Wei): types.SetBalanceFullfilledAction {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -32,6 +32,11 @@ export interface ResetWalletAction {
|
||||||
type: TypeKeys.WALLET_RESET;
|
type: TypeKeys.WALLET_RESET;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetWalletPendingAction {
|
||||||
|
type: TypeKeys.WALLET_SET_PENDING;
|
||||||
|
payload: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/*** Set Balance ***/
|
/*** Set Balance ***/
|
||||||
export interface SetBalancePendingAction {
|
export interface SetBalancePendingAction {
|
||||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
|
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
|
||||||
|
@ -116,10 +121,15 @@ export interface SetWalletConfigAction {
|
||||||
payload: WalletConfig;
|
payload: WalletConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SetPasswordPendingAction {
|
||||||
|
type: TypeKeys.WALLET_SET_PASSWORD_PENDING;
|
||||||
|
}
|
||||||
|
|
||||||
/*** Union Type ***/
|
/*** Union Type ***/
|
||||||
export type WalletAction =
|
export type WalletAction =
|
||||||
| UnlockPrivateKeyAction
|
| UnlockPrivateKeyAction
|
||||||
| SetWalletAction
|
| SetWalletAction
|
||||||
|
| SetWalletPendingAction
|
||||||
| ResetWalletAction
|
| ResetWalletAction
|
||||||
| SetBalancePendingAction
|
| SetBalancePendingAction
|
||||||
| SetBalanceFullfilledAction
|
| SetBalanceFullfilledAction
|
||||||
|
@ -132,4 +142,5 @@ export type WalletAction =
|
||||||
| SetTokenBalanceRejectedAction
|
| SetTokenBalanceRejectedAction
|
||||||
| ScanWalletForTokensAction
|
| ScanWalletForTokensAction
|
||||||
| SetWalletTokensAction
|
| SetWalletTokensAction
|
||||||
| SetWalletConfigAction;
|
| SetWalletConfigAction
|
||||||
|
| SetPasswordPendingAction;
|
||||||
|
|
|
@ -10,11 +10,14 @@ export enum TypeKeys {
|
||||||
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
|
WALLET_SET_TOKEN_BALANCES_PENDING = 'WALLET_SET_TOKEN_BALANCES_PENDING',
|
||||||
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
|
WALLET_SET_TOKEN_BALANCES_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
|
||||||
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
|
WALLET_SET_TOKEN_BALANCES_REJECTED = 'WALLET_SET_TOKEN_BALANCES_REJECTED',
|
||||||
|
WALLET_SET_PENDING = 'WALLET_SET_PENDING',
|
||||||
|
WALLET_SET_NOT_PENDING = 'WALLET_SET_NOT_PENDING',
|
||||||
WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING',
|
WALLET_SET_TOKEN_BALANCE_PENDING = 'WALLET_SET_TOKEN_BALANCE_PENDING',
|
||||||
WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
|
WALLET_SET_TOKEN_BALANCE_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
|
||||||
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
|
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
|
||||||
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
|
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
|
||||||
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
|
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
|
||||||
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',
|
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',
|
||||||
WALLET_RESET = 'WALLET_RESET'
|
WALLET_RESET = 'WALLET_RESET',
|
||||||
|
WALLET_SET_PASSWORD_PENDING = 'WALLET_SET_PASSWORD_PENDING'
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ import {
|
||||||
import { AppState } from 'reducers';
|
import { AppState } from 'reducers';
|
||||||
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
import { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
||||||
import { IWallet } from 'libs/wallet';
|
import { IWallet } from 'libs/wallet';
|
||||||
|
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||||
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
||||||
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
||||||
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
||||||
|
@ -49,10 +50,13 @@ interface Props {
|
||||||
setWallet: TSetWallet;
|
setWallet: TSetWallet;
|
||||||
unlockWeb3: TUnlockWeb3;
|
unlockWeb3: TUnlockWeb3;
|
||||||
resetWallet: TResetWallet;
|
resetWallet: TResetWallet;
|
||||||
|
showNotification: TShowNotification;
|
||||||
wallet: IWallet;
|
wallet: IWallet;
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
offline: boolean;
|
offline: boolean;
|
||||||
disabledWallets?: string[];
|
disabledWallets?: string[];
|
||||||
|
isWalletPending: AppState['wallet']['isWalletPending'];
|
||||||
|
isPasswordPending: AppState['wallet']['isPasswordPending'];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -210,6 +214,15 @@ export class WalletDecrypt extends Component<Props, State> {
|
||||||
value={this.state.value}
|
value={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
onUnlock={this.onUnlock}
|
onUnlock={this.onUnlock}
|
||||||
|
showNotification={this.props.showNotification}
|
||||||
|
isWalletPending={
|
||||||
|
this.state.selectedWalletKey === 'keystore-file' ? this.props.isWalletPending : undefined
|
||||||
|
}
|
||||||
|
isPasswordPending={
|
||||||
|
this.state.selectedWalletKey === 'keystore-file'
|
||||||
|
? this.props.isPasswordPending
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -376,7 +389,9 @@ export class WalletDecrypt extends Component<Props, State> {
|
||||||
function mapStateToProps(state: AppState) {
|
function mapStateToProps(state: AppState) {
|
||||||
return {
|
return {
|
||||||
offline: state.config.offline,
|
offline: state.config.offline,
|
||||||
wallet: state.wallet.inst
|
wallet: state.wallet.inst,
|
||||||
|
isWalletPending: state.wallet.isWalletPending,
|
||||||
|
isPasswordPending: state.wallet.isPasswordPending
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -387,5 +402,6 @@ export default connect(mapStateToProps, {
|
||||||
unlockWeb3,
|
unlockWeb3,
|
||||||
setWallet,
|
setWallet,
|
||||||
resetWallet,
|
resetWallet,
|
||||||
resetTransactionState: reset
|
resetTransactionState: reset,
|
||||||
|
showNotification
|
||||||
})(WalletDecrypt);
|
})(WalletDecrypt);
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
import { isKeystorePassRequired } from 'libs/wallet';
|
import { isKeystorePassRequired } from 'libs/wallet';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import translate, { translateRaw } from 'translations';
|
import translate, { translateRaw } from 'translations';
|
||||||
|
import Spinner from 'components/ui/Spinner';
|
||||||
|
import { TShowNotification } from 'actions/notifications';
|
||||||
|
|
||||||
export interface KeystoreValue {
|
export interface KeystoreValue {
|
||||||
file: string;
|
file: string;
|
||||||
|
@ -18,15 +20,23 @@ function isPassRequired(file: string): boolean {
|
||||||
return passReq;
|
return passReq;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isValidFile(rawFile: File): boolean {
|
||||||
|
const fileType = rawFile.type;
|
||||||
|
return fileType === '' || fileType === 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
export class KeystoreDecrypt extends Component {
|
export class KeystoreDecrypt extends Component {
|
||||||
public props: {
|
public props: {
|
||||||
value: KeystoreValue;
|
value: KeystoreValue;
|
||||||
|
isWalletPending: boolean;
|
||||||
|
isPasswordPending: boolean;
|
||||||
onChange(value: KeystoreValue): void;
|
onChange(value: KeystoreValue): void;
|
||||||
onUnlock(): void;
|
onUnlock(): void;
|
||||||
|
showNotification(level: string, message: string): TShowNotification;
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const { file, password } = this.props.value;
|
const { isWalletPending, isPasswordPending, value: { file, password } } = this.props;
|
||||||
const passReq = isPassRequired(file);
|
const passReq = isPassRequired(file);
|
||||||
const unlockDisabled = !file || (passReq && !password);
|
const unlockDisabled = !file || (passReq && !password);
|
||||||
|
|
||||||
|
@ -44,7 +54,8 @@ export class KeystoreDecrypt extends Component {
|
||||||
{translate('ADD_Radio_2_short')}
|
{translate('ADD_Radio_2_short')}
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
<div className={file.length && passReq ? '' : 'hidden'}>
|
{isWalletPending ? <Spinner /> : ''}
|
||||||
|
<div className={file.length && isPasswordPending ? '' : 'hidden'}>
|
||||||
<p>{translate('ADD_Label_3')}</p>
|
<p>{translate('ADD_Label_3')}</p>
|
||||||
<input
|
<input
|
||||||
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
|
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
|
||||||
|
@ -97,10 +108,15 @@ export class KeystoreDecrypt extends Component {
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
...this.props.value,
|
...this.props.value,
|
||||||
file: keystore,
|
file: keystore,
|
||||||
valid: keystore.length && !passReq
|
valid: keystore.length && !passReq,
|
||||||
|
password: ''
|
||||||
});
|
});
|
||||||
|
this.props.onUnlock();
|
||||||
};
|
};
|
||||||
|
if (isValidFile(inputFile)) {
|
||||||
fileReader.readAsText(inputFile, 'utf-8');
|
fileReader.readAsText(inputFile, 'utf-8');
|
||||||
|
} else {
|
||||||
|
this.props.showNotification('danger', translateRaw('ERROR_3'));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -58,6 +58,10 @@ const isKeystorePassRequired = (file: string): boolean => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getUtcWallet = (file: string, password: string): Promise<IFullWallet> => {
|
||||||
|
return UtcWallet(file, password);
|
||||||
|
};
|
||||||
|
|
||||||
const getPrivKeyWallet = (key: string, password: string) =>
|
const getPrivKeyWallet = (key: string, password: string) =>
|
||||||
key.length === 64
|
key.length === 64
|
||||||
? PrivKeyWallet(Buffer.from(key, 'hex'))
|
? PrivKeyWallet(Buffer.from(key, 'hex'))
|
||||||
|
@ -79,12 +83,16 @@ const getKeystoreWallet = (file: string, password: string) => {
|
||||||
case KeystoreTypes.v2Unencrypted:
|
case KeystoreTypes.v2Unencrypted:
|
||||||
return PrivKeyWallet(Buffer.from(parsed.privKey, 'hex'));
|
return PrivKeyWallet(Buffer.from(parsed.privKey, 'hex'));
|
||||||
|
|
||||||
case KeystoreTypes.utc:
|
|
||||||
return UtcWallet(file, password);
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Error('Unknown wallet');
|
throw Error('Unknown wallet');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { isKeystorePassRequired, getPrivKeyWallet, getKeystoreWallet };
|
export {
|
||||||
|
isKeystorePassRequired,
|
||||||
|
determineKeystoreType,
|
||||||
|
getPrivKeyWallet,
|
||||||
|
getKeystoreWallet,
|
||||||
|
getUtcWallet,
|
||||||
|
KeystoreTypes
|
||||||
|
};
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { fromPrivateKey, fromEthSale, fromV3 } from 'ethereumjs-wallet';
|
import { fromPrivateKey, fromEthSale } from 'ethereumjs-wallet';
|
||||||
import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
import { fromEtherWallet } from 'ethereumjs-wallet/thirdparty';
|
||||||
import { signWrapper } from './helpers';
|
import { signWrapper } from './helpers';
|
||||||
import { decryptPrivKey } from 'libs/decrypt';
|
import { decryptPrivKey } from 'libs/decrypt';
|
||||||
|
import { fromV3 } from 'libs/web-workers/scrypt-wrapper';
|
||||||
import Web3Wallet from './web3';
|
import Web3Wallet from './web3';
|
||||||
import AddressOnlyWallet from './address';
|
import AddressOnlyWallet from './address';
|
||||||
|
|
||||||
|
@ -16,8 +17,7 @@ const MewV1Wallet = (keystore: string, password: string) =>
|
||||||
|
|
||||||
const PrivKeyWallet = (privkey: Buffer) => signWrapper(fromPrivateKey(privkey));
|
const PrivKeyWallet = (privkey: Buffer) => signWrapper(fromPrivateKey(privkey));
|
||||||
|
|
||||||
const UtcWallet = (keystore: string, password: string) =>
|
const UtcWallet = (keystore: string, password: string) => fromV3(keystore, password, true);
|
||||||
signWrapper(fromV3(keystore, password, true));
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
EncryptedPrivateKeyWallet,
|
EncryptedPrivateKeyWallet,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet';
|
||||||
|
import { toBuffer } from 'ethereumjs-util';
|
||||||
|
import Worker from 'worker-loader!./workers/scrypt-worker.worker.ts';
|
||||||
|
|
||||||
|
export const fromV3 = (
|
||||||
|
keystore: string,
|
||||||
|
password: string,
|
||||||
|
nonStrict: boolean
|
||||||
|
): Promise<IFullWallet> => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const scryptWorker = new Worker();
|
||||||
|
scryptWorker.postMessage({ keystore, password, nonStrict });
|
||||||
|
scryptWorker.onmessage = event => {
|
||||||
|
const data: string = event.data;
|
||||||
|
try {
|
||||||
|
const wallet = fromPrivateKey(toBuffer(data));
|
||||||
|
resolve(wallet);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { fromV3, IFullWallet } from 'ethereumjs-wallet';
|
||||||
|
|
||||||
|
const scryptWorker: Worker = self as any;
|
||||||
|
interface DecryptionParameters {
|
||||||
|
keystore: string;
|
||||||
|
password: string;
|
||||||
|
nonStrict: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
scryptWorker.onmessage = (event: MessageEvent) => {
|
||||||
|
const info: DecryptionParameters = event.data;
|
||||||
|
try {
|
||||||
|
const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict);
|
||||||
|
scryptWorker.postMessage(rawKeystore.getPrivateKeyString());
|
||||||
|
} catch (e) {
|
||||||
|
scryptWorker.postMessage(e.message);
|
||||||
|
}
|
||||||
|
};
|
|
@ -4,6 +4,7 @@ import {
|
||||||
SetWalletAction,
|
SetWalletAction,
|
||||||
WalletAction,
|
WalletAction,
|
||||||
SetWalletConfigAction,
|
SetWalletConfigAction,
|
||||||
|
SetWalletPendingAction,
|
||||||
TypeKeys,
|
TypeKeys,
|
||||||
SetTokenBalanceFulfilledAction
|
SetTokenBalanceFulfilledAction
|
||||||
} from 'actions/wallet';
|
} from 'actions/wallet';
|
||||||
|
@ -21,7 +22,9 @@ export interface State {
|
||||||
error: string | null;
|
error: string | null;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
isWalletPending: boolean;
|
||||||
isTokensLoading: boolean;
|
isTokensLoading: boolean;
|
||||||
|
isPasswordPending: boolean;
|
||||||
tokensError: string | null;
|
tokensError: string | null;
|
||||||
hasSavedWalletTokens: boolean;
|
hasSavedWalletTokens: boolean;
|
||||||
}
|
}
|
||||||
|
@ -31,6 +34,8 @@ export const INITIAL_STATE: State = {
|
||||||
config: null,
|
config: null,
|
||||||
balance: { isPending: false, wei: null },
|
balance: { isPending: false, wei: null },
|
||||||
tokens: {},
|
tokens: {},
|
||||||
|
isWalletPending: false,
|
||||||
|
isPasswordPending: false,
|
||||||
isTokensLoading: false,
|
isTokensLoading: false,
|
||||||
tokensError: null,
|
tokensError: null,
|
||||||
hasSavedWalletTokens: true
|
hasSavedWalletTokens: true
|
||||||
|
@ -61,6 +66,14 @@ function setBalanceRejected(state: State): State {
|
||||||
return { ...state, balance: { ...state.balance, isPending: false } };
|
return { ...state, balance: { ...state.balance, isPending: false } };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setWalletPending(state: State, action: SetWalletPendingAction): State {
|
||||||
|
return { ...state, isWalletPending: action.payload };
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPasswordPending(state: State): State {
|
||||||
|
return { ...state, isPasswordPending: true };
|
||||||
|
}
|
||||||
|
|
||||||
function setTokenBalancesPending(state: State): State {
|
function setTokenBalancesPending(state: State): State {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
|
@ -143,6 +156,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
|
||||||
return setBalanceFullfilled(state, action);
|
return setBalanceFullfilled(state, action);
|
||||||
case TypeKeys.WALLET_SET_BALANCE_REJECTED:
|
case TypeKeys.WALLET_SET_BALANCE_REJECTED:
|
||||||
return setBalanceRejected(state);
|
return setBalanceRejected(state);
|
||||||
|
case TypeKeys.WALLET_SET_PENDING:
|
||||||
|
return setWalletPending(state, action);
|
||||||
case TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING:
|
case TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING:
|
||||||
return setTokenBalancesPending(state);
|
return setTokenBalancesPending(state);
|
||||||
case TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED:
|
case TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED:
|
||||||
|
@ -161,6 +176,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
|
||||||
return setWalletTokens(state);
|
return setWalletTokens(state);
|
||||||
case TypeKeys.WALLET_SET_CONFIG:
|
case TypeKeys.WALLET_SET_CONFIG:
|
||||||
return setWalletConfig(state, action);
|
return setWalletConfig(state, action);
|
||||||
|
case TypeKeys.WALLET_SET_PASSWORD_PENDING:
|
||||||
|
return setPasswordPending(state);
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
setTokenBalancesFulfilled,
|
setTokenBalancesFulfilled,
|
||||||
setTokenBalancesRejected,
|
setTokenBalancesRejected,
|
||||||
setWallet,
|
setWallet,
|
||||||
|
setWalletPending,
|
||||||
setWalletConfig,
|
setWalletConfig,
|
||||||
UnlockKeystoreAction,
|
UnlockKeystoreAction,
|
||||||
UnlockMnemonicAction,
|
UnlockMnemonicAction,
|
||||||
|
@ -16,7 +17,8 @@ import {
|
||||||
TypeKeys,
|
TypeKeys,
|
||||||
SetTokenBalancePendingAction,
|
SetTokenBalancePendingAction,
|
||||||
setTokenBalanceFulfilled,
|
setTokenBalanceFulfilled,
|
||||||
setTokenBalanceRejected
|
setTokenBalanceRejected,
|
||||||
|
setPasswordPrompt
|
||||||
} from 'actions/wallet';
|
} from 'actions/wallet';
|
||||||
import { Wei } from 'libs/units';
|
import { Wei } from 'libs/units';
|
||||||
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
|
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
|
||||||
|
@ -27,12 +29,16 @@ import {
|
||||||
MnemonicWallet,
|
MnemonicWallet,
|
||||||
getPrivKeyWallet,
|
getPrivKeyWallet,
|
||||||
getKeystoreWallet,
|
getKeystoreWallet,
|
||||||
|
determineKeystoreType,
|
||||||
|
KeystoreTypes,
|
||||||
|
getUtcWallet,
|
||||||
|
signWrapper,
|
||||||
Web3Wallet,
|
Web3Wallet,
|
||||||
WalletConfig
|
WalletConfig
|
||||||
} from 'libs/wallet';
|
} from 'libs/wallet';
|
||||||
import { NODES, initWeb3Node, Token } from 'config/data';
|
import { NODES, initWeb3Node, Token } from 'config/data';
|
||||||
import { SagaIterator } from 'redux-saga';
|
import { SagaIterator, delay, Task } from 'redux-saga';
|
||||||
import { apply, call, fork, put, select, takeEvery, take } from 'redux-saga/effects';
|
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
|
||||||
import { getNodeLib, getAllTokens } from 'selectors/config';
|
import { getNodeLib, getAllTokens } from 'selectors/config';
|
||||||
import {
|
import {
|
||||||
getTokens,
|
getTokens,
|
||||||
|
@ -168,18 +174,44 @@ export function* unlockPrivateKey(action: UnlockPrivateKeyAction): SagaIterator
|
||||||
yield put(setWallet(wallet));
|
yield put(setWallet(wallet));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function* startLoadingSpinner(): SagaIterator {
|
||||||
|
yield call(delay, 400);
|
||||||
|
yield put(setWalletPending(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function* stopLoadingSpinner(loadingFork: Task | null): SagaIterator {
|
||||||
|
if (loadingFork !== null && loadingFork !== undefined) {
|
||||||
|
yield cancel(loadingFork);
|
||||||
|
}
|
||||||
|
yield put(setWalletPending(false));
|
||||||
|
}
|
||||||
|
|
||||||
export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator {
|
export function* unlockKeystore(action: UnlockKeystoreAction): SagaIterator {
|
||||||
const { file, password } = action.payload;
|
const { file, password } = action.payload;
|
||||||
let wallet: null | IWallet = null;
|
let wallet: null | IWallet = null;
|
||||||
|
let spinnerTask: null | Task = null;
|
||||||
try {
|
try {
|
||||||
|
if (determineKeystoreType(file) === KeystoreTypes.utc) {
|
||||||
|
spinnerTask = yield fork(startLoadingSpinner);
|
||||||
|
wallet = signWrapper(yield call(getUtcWallet, file, password));
|
||||||
|
} else {
|
||||||
wallet = getKeystoreWallet(file, password);
|
wallet = getKeystoreWallet(file, password);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
yield call(stopLoadingSpinner, spinnerTask);
|
||||||
|
if (
|
||||||
|
password === '' &&
|
||||||
|
e.message === 'Private key does not satisfy the curve requirements (ie. it is invalid)'
|
||||||
|
) {
|
||||||
|
yield put(setPasswordPrompt());
|
||||||
|
} else {
|
||||||
yield put(showNotification('danger', translate('ERROR_6')));
|
yield put(showNotification('danger', translate('ERROR_6')));
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
|
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
|
||||||
|
yield call(stopLoadingSpinner, spinnerTask);
|
||||||
yield put(setWallet(wallet));
|
yield put(setWallet(wallet));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
declare module 'worker-loader!*' {
|
||||||
|
class WebpackWorker extends Worker {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
|
export = WebpackWorker;
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
module.exports = Object.create(null);
|
|
@ -5,11 +5,12 @@
|
||||||
},
|
},
|
||||||
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
|
||||||
"moduleDirectories": ["node_modules", "common"],
|
"moduleDirectories": ["node_modules", "common"],
|
||||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"],
|
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "worker.ts"],
|
||||||
"moduleNameMapper": {
|
"moduleNameMapper": {
|
||||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||||
"<rootDir>/jest_config/__mocks__/fileMock.ts",
|
"<rootDir>/jest_config/__mocks__/fileMock.ts",
|
||||||
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts"
|
"\\.(css|scss|less)$": "<rootDir>/jest_config/__mocks__/styleMock.ts",
|
||||||
|
"\\.worker.ts":"<rootDir>/jest_config/__mocks__/workerMock.js"
|
||||||
},
|
},
|
||||||
"testPathIgnorePatterns": ["<rootDir>/common/config"],
|
"testPathIgnorePatterns": ["<rootDir>/common/config"],
|
||||||
"setupFiles": [
|
"setupFiles": [
|
||||||
|
|
|
@ -118,7 +118,8 @@
|
||||||
"webpack": "3.10.0",
|
"webpack": "3.10.0",
|
||||||
"webpack-dev-middleware": "2.0.4",
|
"webpack-dev-middleware": "2.0.4",
|
||||||
"webpack-hot-middleware": "2.21.0",
|
"webpack-hot-middleware": "2.21.0",
|
||||||
"webpack-sources": "1.0.1"
|
"webpack-sources": "1.0.1",
|
||||||
|
"worker-loader": "1.1.0"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
|
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
|
||||||
|
|
|
@ -25,14 +25,17 @@ import {
|
||||||
unlockKeystore,
|
unlockKeystore,
|
||||||
unlockMnemonic,
|
unlockMnemonic,
|
||||||
unlockWeb3,
|
unlockWeb3,
|
||||||
getTokenBalances
|
getTokenBalances,
|
||||||
|
startLoadingSpinner,
|
||||||
|
stopLoadingSpinner
|
||||||
} from 'sagas/wallet';
|
} from 'sagas/wallet';
|
||||||
import { PrivKeyWallet } from 'libs/wallet/non-deterministic';
|
import { getUtcWallet, PrivKeyWallet } from 'libs/wallet';
|
||||||
import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants';
|
import { TypeKeys as ConfigTypeKeys } from 'actions/config/constants';
|
||||||
import Web3Node from 'libs/nodes/web3';
|
import Web3Node from 'libs/nodes/web3';
|
||||||
import { cloneableGenerator } from 'redux-saga/utils';
|
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
||||||
import { showNotification } from 'actions/notifications';
|
import { showNotification } from 'actions/notifications';
|
||||||
import translate from 'translations';
|
import translate from 'translations';
|
||||||
|
import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
||||||
|
|
||||||
// init module
|
// init module
|
||||||
configuredStore.getState();
|
configuredStore.getState();
|
||||||
|
@ -206,6 +209,24 @@ describe('unlockKeystore*', () => {
|
||||||
password: 'testtesttest'
|
password: 'testtesttest'
|
||||||
});
|
});
|
||||||
const gen = unlockKeystore(action);
|
const gen = unlockKeystore(action);
|
||||||
|
const mockTask = createMockTask();
|
||||||
|
const spinnerFork = fork(startLoadingSpinner);
|
||||||
|
|
||||||
|
it('should fork startLoadingSpinner', () => {
|
||||||
|
expect(gen.next().value).toEqual(spinnerFork);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getUtcWallet', () => {
|
||||||
|
expect(gen.next(mockTask).value).toEqual(
|
||||||
|
call(getUtcWallet, action.payload.file, action.payload.password)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
//keystore in this case decrypts quickly, so use fromV3 in ethjs-wallet to avoid testing with promises
|
||||||
|
it('should call stopLoadingSpinner', () => {
|
||||||
|
const mockWallet: IFullWallet = fromV3(action.payload.file, action.payload.password, true);
|
||||||
|
expect(gen.next(mockWallet).value).toEqual(call(stopLoadingSpinner, mockTask));
|
||||||
|
});
|
||||||
|
|
||||||
it('should match put setWallet snapshot', () => {
|
it('should match put setWallet snapshot', () => {
|
||||||
expect(gen.next().value).toMatchSnapshot();
|
expect(gen.next().value).toMatchSnapshot();
|
||||||
|
|
|
@ -35,6 +35,10 @@ const webpackConfig = {
|
||||||
.map(dir => path.resolve(__dirname, `../common/${dir}`))
|
.map(dir => path.resolve(__dirname, `../common/${dir}`))
|
||||||
.concat([path.resolve(__dirname, '../node_modules')])
|
.concat([path.resolve(__dirname, '../node_modules')])
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.worker\.js$/,
|
||||||
|
loader: 'worker-loader'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
include: [
|
include: [
|
||||||
path.resolve(__dirname, '../common/assets'),
|
path.resolve(__dirname, '../common/assets'),
|
||||||
|
|
Loading…
Reference in New Issue