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:
aitrean 2018-01-11 01:44:13 -05:00 committed by Daniel Ternyak
parent a84a6e98fc
commit af2e0b69e1
17 changed files with 219 additions and 28 deletions

View File

@ -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 {

View File

@ -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;

View File

@ -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'
} }

View File

@ -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);

View File

@ -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'));
}
}; };
} }

View File

@ -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
};

View File

@ -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,

View File

@ -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);
}
};
});
};

View File

@ -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);
}
};

View File

@ -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;
} }

View File

@ -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 {
wallet = getKeystoreWallet(file, password); if (determineKeystoreType(file) === KeystoreTypes.utc) {
spinnerTask = yield fork(startLoadingSpinner);
wallet = signWrapper(yield call(getUtcWallet, file, password));
} else {
wallet = getKeystoreWallet(file, password);
}
} catch (e) { } catch (e) {
yield put(showNotification('danger', translate('ERROR_6'))); 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')));
}
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));
} }

6
common/typescript/worker-loader.d.ts vendored Normal file
View File

@ -0,0 +1,6 @@
declare module 'worker-loader!*' {
class WebpackWorker extends Worker {
constructor();
}
export = WebpackWorker;
}

View File

@ -0,0 +1 @@
module.exports = Object.create(null);

View File

@ -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": [

View File

@ -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",

View File

@ -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();

View File

@ -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'),