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 {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export function setPasswordPrompt(): types.SetPasswordPendingAction {
|
||||
return {
|
||||
type: TypeKeys.WALLET_SET_PASSWORD_PENDING
|
||||
};
|
||||
}
|
||||
|
||||
export type TSetBalance = typeof setBalanceFullfilled;
|
||||
export function setBalanceFullfilled(value: Wei): types.SetBalanceFullfilledAction {
|
||||
return {
|
||||
|
|
|
@ -32,6 +32,11 @@ export interface ResetWalletAction {
|
|||
type: TypeKeys.WALLET_RESET;
|
||||
}
|
||||
|
||||
export interface SetWalletPendingAction {
|
||||
type: TypeKeys.WALLET_SET_PENDING;
|
||||
payload: boolean;
|
||||
}
|
||||
|
||||
/*** Set Balance ***/
|
||||
export interface SetBalancePendingAction {
|
||||
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
|
||||
|
@ -116,10 +121,15 @@ export interface SetWalletConfigAction {
|
|||
payload: WalletConfig;
|
||||
}
|
||||
|
||||
export interface SetPasswordPendingAction {
|
||||
type: TypeKeys.WALLET_SET_PASSWORD_PENDING;
|
||||
}
|
||||
|
||||
/*** Union Type ***/
|
||||
export type WalletAction =
|
||||
| UnlockPrivateKeyAction
|
||||
| SetWalletAction
|
||||
| SetWalletPendingAction
|
||||
| ResetWalletAction
|
||||
| SetBalancePendingAction
|
||||
| SetBalanceFullfilledAction
|
||||
|
@ -132,4 +142,5 @@ export type WalletAction =
|
|||
| SetTokenBalanceRejectedAction
|
||||
| ScanWalletForTokensAction
|
||||
| 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_FULFILLED = 'WALLET_SET_TOKEN_BALANCES_FULFILLED',
|
||||
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_FULFILLED = 'WALLET_SET_TOKEN_BALANCE_FULFILLED',
|
||||
WALLET_SET_TOKEN_BALANCE_REJECTED = 'WALLET_SET_TOKEN_BALANCE_REJECTED',
|
||||
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
|
||||
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
|
||||
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 { knowledgeBaseURL, isWeb3NodeAvailable } from 'config/data';
|
||||
import { IWallet } from 'libs/wallet';
|
||||
import { showNotification, TShowNotification } from 'actions/notifications';
|
||||
import DigitalBitboxIcon from 'assets/images/wallets/digital-bitbox.svg';
|
||||
import LedgerIcon from 'assets/images/wallets/ledger.svg';
|
||||
import MetamaskIcon from 'assets/images/wallets/metamask.svg';
|
||||
|
@ -49,10 +50,13 @@ interface Props {
|
|||
setWallet: TSetWallet;
|
||||
unlockWeb3: TUnlockWeb3;
|
||||
resetWallet: TResetWallet;
|
||||
showNotification: TShowNotification;
|
||||
wallet: IWallet;
|
||||
hidden?: boolean;
|
||||
offline: boolean;
|
||||
disabledWallets?: string[];
|
||||
isWalletPending: AppState['wallet']['isWalletPending'];
|
||||
isPasswordPending: AppState['wallet']['isPasswordPending'];
|
||||
}
|
||||
|
||||
interface State {
|
||||
|
@ -210,6 +214,15 @@ export class WalletDecrypt extends Component<Props, State> {
|
|||
value={this.state.value}
|
||||
onChange={this.onChange}
|
||||
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) {
|
||||
return {
|
||||
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,
|
||||
setWallet,
|
||||
resetWallet,
|
||||
resetTransactionState: reset
|
||||
resetTransactionState: reset,
|
||||
showNotification
|
||||
})(WalletDecrypt);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import { isKeystorePassRequired } from 'libs/wallet';
|
||||
import React, { Component } from 'react';
|
||||
import translate, { translateRaw } from 'translations';
|
||||
import Spinner from 'components/ui/Spinner';
|
||||
import { TShowNotification } from 'actions/notifications';
|
||||
|
||||
export interface KeystoreValue {
|
||||
file: string;
|
||||
|
@ -18,15 +20,23 @@ function isPassRequired(file: string): boolean {
|
|||
return passReq;
|
||||
}
|
||||
|
||||
function isValidFile(rawFile: File): boolean {
|
||||
const fileType = rawFile.type;
|
||||
return fileType === '' || fileType === 'application/json';
|
||||
}
|
||||
|
||||
export class KeystoreDecrypt extends Component {
|
||||
public props: {
|
||||
value: KeystoreValue;
|
||||
isWalletPending: boolean;
|
||||
isPasswordPending: boolean;
|
||||
onChange(value: KeystoreValue): void;
|
||||
onUnlock(): void;
|
||||
showNotification(level: string, message: string): TShowNotification;
|
||||
};
|
||||
|
||||
public render() {
|
||||
const { file, password } = this.props.value;
|
||||
const { isWalletPending, isPasswordPending, value: { file, password } } = this.props;
|
||||
const passReq = isPassRequired(file);
|
||||
const unlockDisabled = !file || (passReq && !password);
|
||||
|
||||
|
@ -44,7 +54,8 @@ export class KeystoreDecrypt extends Component {
|
|||
{translate('ADD_Radio_2_short')}
|
||||
</a>
|
||||
</label>
|
||||
<div className={file.length && passReq ? '' : 'hidden'}>
|
||||
{isWalletPending ? <Spinner /> : ''}
|
||||
<div className={file.length && isPasswordPending ? '' : 'hidden'}>
|
||||
<p>{translate('ADD_Label_3')}</p>
|
||||
<input
|
||||
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
|
||||
|
@ -97,10 +108,15 @@ export class KeystoreDecrypt extends Component {
|
|||
this.props.onChange({
|
||||
...this.props.value,
|
||||
file: keystore,
|
||||
valid: keystore.length && !passReq
|
||||
valid: keystore.length && !passReq,
|
||||
password: ''
|
||||
});
|
||||
this.props.onUnlock();
|
||||
};
|
||||
|
||||
fileReader.readAsText(inputFile, 'utf-8');
|
||||
if (isValidFile(inputFile)) {
|
||||
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) =>
|
||||
key.length === 64
|
||||
? PrivKeyWallet(Buffer.from(key, 'hex'))
|
||||
|
@ -79,12 +83,16 @@ const getKeystoreWallet = (file: string, password: string) => {
|
|||
case KeystoreTypes.v2Unencrypted:
|
||||
return PrivKeyWallet(Buffer.from(parsed.privKey, 'hex'));
|
||||
|
||||
case KeystoreTypes.utc:
|
||||
return UtcWallet(file, password);
|
||||
|
||||
default:
|
||||
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 { signWrapper } from './helpers';
|
||||
import { decryptPrivKey } from 'libs/decrypt';
|
||||
import { fromV3 } from 'libs/web-workers/scrypt-wrapper';
|
||||
import Web3Wallet from './web3';
|
||||
import AddressOnlyWallet from './address';
|
||||
|
||||
|
@ -16,8 +17,7 @@ const MewV1Wallet = (keystore: string, password: string) =>
|
|||
|
||||
const PrivKeyWallet = (privkey: Buffer) => signWrapper(fromPrivateKey(privkey));
|
||||
|
||||
const UtcWallet = (keystore: string, password: string) =>
|
||||
signWrapper(fromV3(keystore, password, true));
|
||||
const UtcWallet = (keystore: string, password: string) => fromV3(keystore, password, true);
|
||||
|
||||
export {
|
||||
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,
|
||||
WalletAction,
|
||||
SetWalletConfigAction,
|
||||
SetWalletPendingAction,
|
||||
TypeKeys,
|
||||
SetTokenBalanceFulfilledAction
|
||||
} from 'actions/wallet';
|
||||
|
@ -21,7 +22,9 @@ export interface State {
|
|||
error: string | null;
|
||||
};
|
||||
};
|
||||
isWalletPending: boolean;
|
||||
isTokensLoading: boolean;
|
||||
isPasswordPending: boolean;
|
||||
tokensError: string | null;
|
||||
hasSavedWalletTokens: boolean;
|
||||
}
|
||||
|
@ -31,6 +34,8 @@ export const INITIAL_STATE: State = {
|
|||
config: null,
|
||||
balance: { isPending: false, wei: null },
|
||||
tokens: {},
|
||||
isWalletPending: false,
|
||||
isPasswordPending: false,
|
||||
isTokensLoading: false,
|
||||
tokensError: null,
|
||||
hasSavedWalletTokens: true
|
||||
|
@ -61,6 +66,14 @@ function setBalanceRejected(state: State): State {
|
|||
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 {
|
||||
return {
|
||||
...state,
|
||||
|
@ -143,6 +156,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
|
|||
return setBalanceFullfilled(state, action);
|
||||
case TypeKeys.WALLET_SET_BALANCE_REJECTED:
|
||||
return setBalanceRejected(state);
|
||||
case TypeKeys.WALLET_SET_PENDING:
|
||||
return setWalletPending(state, action);
|
||||
case TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING:
|
||||
return setTokenBalancesPending(state);
|
||||
case TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED:
|
||||
|
@ -161,6 +176,8 @@ export function wallet(state: State = INITIAL_STATE, action: WalletAction): Stat
|
|||
return setWalletTokens(state);
|
||||
case TypeKeys.WALLET_SET_CONFIG:
|
||||
return setWalletConfig(state, action);
|
||||
case TypeKeys.WALLET_SET_PASSWORD_PENDING:
|
||||
return setPasswordPending(state);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import {
|
|||
setTokenBalancesFulfilled,
|
||||
setTokenBalancesRejected,
|
||||
setWallet,
|
||||
setWalletPending,
|
||||
setWalletConfig,
|
||||
UnlockKeystoreAction,
|
||||
UnlockMnemonicAction,
|
||||
|
@ -16,7 +17,8 @@ import {
|
|||
TypeKeys,
|
||||
SetTokenBalancePendingAction,
|
||||
setTokenBalanceFulfilled,
|
||||
setTokenBalanceRejected
|
||||
setTokenBalanceRejected,
|
||||
setPasswordPrompt
|
||||
} from 'actions/wallet';
|
||||
import { Wei } from 'libs/units';
|
||||
import { changeNodeIntent, web3UnsetNode, TypeKeys as ConfigTypeKeys } from 'actions/config';
|
||||
|
@ -27,12 +29,16 @@ import {
|
|||
MnemonicWallet,
|
||||
getPrivKeyWallet,
|
||||
getKeystoreWallet,
|
||||
determineKeystoreType,
|
||||
KeystoreTypes,
|
||||
getUtcWallet,
|
||||
signWrapper,
|
||||
Web3Wallet,
|
||||
WalletConfig
|
||||
} from 'libs/wallet';
|
||||
import { NODES, initWeb3Node, Token } from 'config/data';
|
||||
import { SagaIterator } from 'redux-saga';
|
||||
import { apply, call, fork, put, select, takeEvery, take } from 'redux-saga/effects';
|
||||
import { SagaIterator, delay, Task } from 'redux-saga';
|
||||
import { apply, call, fork, put, select, takeEvery, take, cancel } from 'redux-saga/effects';
|
||||
import { getNodeLib, getAllTokens } from 'selectors/config';
|
||||
import {
|
||||
getTokens,
|
||||
|
@ -168,18 +174,44 @@ export function* unlockPrivateKey(action: UnlockPrivateKeyAction): SagaIterator
|
|||
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 {
|
||||
const { file, password } = action.payload;
|
||||
let wallet: null | IWallet = null;
|
||||
|
||||
let spinnerTask: null | Task = null;
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
|
||||
// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
|
||||
yield call(stopLoadingSpinner, spinnerTask);
|
||||
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?)$",
|
||||
"moduleDirectories": ["node_modules", "common"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json"],
|
||||
"moduleFileExtensions": ["ts", "tsx", "js", "jsx", "json", "worker.ts"],
|
||||
"moduleNameMapper": {
|
||||
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$":
|
||||
"<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"],
|
||||
"setupFiles": [
|
||||
|
|
|
@ -118,7 +118,8 @@
|
|||
"webpack": "3.10.0",
|
||||
"webpack-dev-middleware": "2.0.4",
|
||||
"webpack-hot-middleware": "2.21.0",
|
||||
"webpack-sources": "1.0.1"
|
||||
"webpack-sources": "1.0.1",
|
||||
"worker-loader": "1.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
|
||||
|
|
|
@ -25,14 +25,17 @@ import {
|
|||
unlockKeystore,
|
||||
unlockMnemonic,
|
||||
unlockWeb3,
|
||||
getTokenBalances
|
||||
getTokenBalances,
|
||||
startLoadingSpinner,
|
||||
stopLoadingSpinner
|
||||
} 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 Web3Node from 'libs/nodes/web3';
|
||||
import { cloneableGenerator } from 'redux-saga/utils';
|
||||
import { cloneableGenerator, createMockTask } from 'redux-saga/utils';
|
||||
import { showNotification } from 'actions/notifications';
|
||||
import translate from 'translations';
|
||||
import { IFullWallet, fromV3 } from 'ethereumjs-wallet';
|
||||
|
||||
// init module
|
||||
configuredStore.getState();
|
||||
|
@ -206,6 +209,24 @@ describe('unlockKeystore*', () => {
|
|||
password: 'testtesttest'
|
||||
});
|
||||
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', () => {
|
||||
expect(gen.next().value).toMatchSnapshot();
|
||||
|
|
|
@ -35,6 +35,10 @@ const webpackConfig = {
|
|||
.map(dir => path.resolve(__dirname, `../common/${dir}`))
|
||||
.concat([path.resolve(__dirname, '../node_modules')])
|
||||
},
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
loader: 'worker-loader'
|
||||
},
|
||||
{
|
||||
include: [
|
||||
path.resolve(__dirname, '../common/assets'),
|
||||
|
|
Loading…
Reference in New Issue