Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Web Worker Decrypt #680

Merged
merged 12 commits into from
Jan 11, 2018
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions common/actions/wallet/actionCreators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ export function setWallet(value: IWallet): types.SetWalletAction {
};
}

export function setWalletLoading(loadingStatus: boolean): types.SetWalletLoadingAction {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Existing nomenclature would be setWalletPending

return {
type: TypeKeys.WALLET_SET_LOADING,
payload: loadingStatus
};
}

export function setBalancePending(): types.SetBalancePendingAction {
return {
type: TypeKeys.WALLET_SET_BALANCE_PENDING
Expand Down
6 changes: 6 additions & 0 deletions common/actions/wallet/actionTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface ResetWalletAction {
type: TypeKeys.WALLET_RESET;
}

export interface SetWalletLoadingAction {
type: TypeKeys.WALLET_SET_LOADING;
payload: boolean;
}

/*** Set Balance ***/
export interface SetBalancePendingAction {
type: TypeKeys.WALLET_SET_BALANCE_PENDING;
Expand Down Expand Up @@ -101,6 +106,7 @@ export interface SetWalletConfigAction {
export type WalletAction =
| UnlockPrivateKeyAction
| SetWalletAction
| SetWalletLoadingAction
| ResetWalletAction
| SetBalancePendingAction
| SetBalanceFullfilledAction
Expand Down
2 changes: 2 additions & 0 deletions common/actions/wallet/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ 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_LOADING = 'WALLET_SET_LOADING',
WALLET_SET_NOT_LOADING = 'WALLET_SET_NOT_LOADING',
WALLET_SCAN_WALLET_FOR_TOKENS = 'WALLET_SCAN_WALLET_FOR_TOKENS',
WALLET_SET_WALLET_TOKENS = 'WALLET_SET_WALLET_TOKENS',
WALLET_SET_CONFIG = 'WALLET_SET_CONFIG',
Expand Down
12 changes: 10 additions & 2 deletions common/components/WalletDecrypt/Keystore.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isKeystorePassRequired } from 'libs/wallet';
import React, { Component } from 'react';
import translate, { translateRaw } from 'translations';
import Spinner from 'components/ui/Spinner';

export interface KeystoreValue {
file: string;
Expand All @@ -21,12 +22,14 @@ function isPassRequired(file: string): boolean {
export default class KeystoreDecrypt extends Component {
public props: {
value: KeystoreValue;
isWalletLoading: boolean;
onChange(value: KeystoreValue): void;
onUnlock(): void;
};

public render() {
const { file, password } = this.props.value;
const isWalletLoading = this.props.isWalletLoading;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not de-structure?

const passReq = isPassRequired(file);

return (
Expand All @@ -46,7 +49,7 @@ export default class KeystoreDecrypt extends Component {
{translate('ADD_Radio_2_short')}
</a>
</label>
<div className={file.length && passReq ? '' : 'hidden'}>
<div className={file.length && passReq && !isWalletLoading ? '' : 'hidden'}>
<p>{translate('ADD_Label_3')}</p>
<input
className={`form-control ${password.length > 0 ? 'is-valid' : 'is-invalid'}`}
Expand All @@ -57,6 +60,7 @@ export default class KeystoreDecrypt extends Component {
type="password"
/>
</div>
{isWalletLoading ? <Spinner /> : ''}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, but you can simplify these boolean checks to {isWalletLoading && <Spinner />}

</div>
</div>
</section>
Expand Down Expand Up @@ -92,8 +96,12 @@ export default class KeystoreDecrypt extends Component {
this.props.onChange({
...this.props.value,
file: keystore,
valid: keystore.length && !passReq
valid: keystore.length && !passReq,
password: ''
});
if (isPassRequired(keystore)) {
this.props.onUnlock();
}
};

fileReader.readAsText(inputFile, 'utf-8');
Expand Down
13 changes: 11 additions & 2 deletions common/components/WalletDecrypt/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ interface Props {
offline: boolean;
allowReadOnly?: boolean;
disabledWallets?: string[];
isWalletLoading: AppState['wallet']['isWalletLoading'];
}

interface State {
Expand Down Expand Up @@ -125,7 +126,14 @@ export class WalletDecrypt extends Component<Props, State> {
return null;
}
return (
<selectedWallet.component value={value} onChange={this.onChange} onUnlock={this.onUnlock} />
<selectedWallet.component
value={value}
onChange={this.onChange}
onUnlock={this.onUnlock}
isWalletLoading={
selectedWalletKey === 'keystore-file' ? this.props.isWalletLoading : undefined
}
/>
);
}

Expand Down Expand Up @@ -235,7 +243,8 @@ export class WalletDecrypt extends Component<Props, State> {
function mapStateToProps(state: AppState) {
return {
offline: state.config.offline,
wallet: state.wallet.inst
wallet: state.wallet.inst,
isWalletLoading: state.wallet.isWalletLoading
};
}

Expand Down
16 changes: 12 additions & 4 deletions common/libs/wallet/non-deterministic/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'))
Expand All @@ -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
};
6 changes: 3 additions & 3 deletions common/libs/wallet/non-deterministic/wallets.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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,
Expand Down
22 changes: 22 additions & 0 deletions common/libs/web-workers/scrypt-wrapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { IFullWallet, fromPrivateKey } from 'ethereumjs-wallet';
import { toBuffer } from 'ethereumjs-util';
const Worker = require('worker-loader!./workers/scrypt-worker.worker.ts');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should setup typing for this, as outlined here: https://github.com/webpack-contrib/worker-loader#integrating-with-typescript. Right now it's an implicit any.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still typed as any, but

import Worker from 'worker-loader!./workers/scrypt-worker.worker.ts';

seems to fix it!


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 => {
try {
const wallet = fromPrivateKey(toBuffer(event.data));
resolve(wallet);
} catch (e) {
reject(e);
}
};
});
};
18 changes: 18 additions & 0 deletions common/libs/web-workers/workers/scrypt-worker.worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { fromV3, IFullWallet } from 'ethereumjs-wallet';

declare var postMessage: any;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From https://github.com/webpack-contrib/worker-loader#integrating-with-typescript:

const ctx: Worker = self as any;
ctx.postMessage({ foo: "foo" });

for typing

interface DecryptionParameters {
keystore: string;
password: string;
nonStrict: boolean;
}

onmessage = event => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

event could use typing

const info: DecryptionParameters = event.data;
try {
const rawKeystore: IFullWallet = fromV3(info.keystore, info.password, info.nonStrict);
postMessage(rawKeystore.getPrivateKeyString());
} catch (e) {
postMessage(e.message);
}
};
9 changes: 9 additions & 0 deletions common/reducers/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
SetWalletAction,
WalletAction,
SetWalletConfigAction,
SetWalletLoadingAction,
TypeKeys
} from 'actions/wallet';
import { TokenValue } from 'libs/units';
Expand All @@ -20,6 +21,7 @@ export interface State {
error: string | null;
};
};
isWalletLoading: boolean;
isTokensLoading: boolean;
tokensError: string | null;
hasSavedWalletTokens: boolean;
Expand All @@ -30,6 +32,7 @@ export const INITIAL_STATE: State = {
config: null,
balance: { isPending: false, wei: null },
tokens: {},
isWalletLoading: false,
isTokensLoading: false,
tokensError: null,
hasSavedWalletTokens: true
Expand Down Expand Up @@ -60,6 +63,10 @@ function setBalanceRejected(state: State): State {
return { ...state, balance: { ...state.balance, isPending: false } };
}

function setWalletLoading(state: State, action: SetWalletLoadingAction): State {
return { ...state, isWalletLoading: action.payload };
}

function setTokenBalancesPending(state: State): State {
return {
...state,
Expand Down Expand Up @@ -118,6 +125,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_LOADING:
return setWalletLoading(state, action);
case TypeKeys.WALLET_SET_TOKEN_BALANCES_PENDING:
return setTokenBalancesPending(state);
case TypeKeys.WALLET_SET_TOKEN_BALANCES_FULFILLED:
Expand Down
40 changes: 35 additions & 5 deletions common/sagas/wallet/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
setTokenBalancesFulfilled,
setTokenBalancesRejected,
setWallet,
setWalletLoading,
setWalletConfig,
UnlockKeystoreAction,
UnlockMnemonicAction,
Expand All @@ -24,12 +25,16 @@ import {
MnemonicWallet,
getPrivKeyWallet,
getKeystoreWallet,
determineKeystoreType,
KeystoreTypes,
getUtcWallet,
signWrapper,
Web3Wallet,
WalletConfig
} from 'libs/wallet';
import { NODES, initWeb3Node } 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 } from 'selectors/config';
import {
getTokens,
Expand Down Expand Up @@ -141,18 +146,43 @@ export function* unlockPrivateKey(action: UnlockPrivateKeyAction): SagaIterator
yield put(setWallet(wallet));
}

export function* startLoadingSpinner(): SagaIterator {
yield call(delay, 1000);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd probably shorten this to 300-500, a full second before having the site respond is a little long.

yield put(setWalletLoading(true));
}

export function* stopLoadingSpinner(loadingFork: Task | null): SagaIterator {
if (loadingFork !== null) {
yield cancel(loadingFork);
}
yield put(setWalletLoading(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 {
yield call(stopLoadingSpinner, spinnerTask);
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(showNotification('danger', translate('ERROR_6')));
}
return;
}

// TODO: provide a more descriptive error than the two 'ERROR_6' (invalid pass) messages above
yield put(setWalletLoading(false));
yield put(setWallet(wallet));
}

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@
"webpack": "3.10.0",
"webpack-dev-middleware": "2.0.3",
"webpack-hot-middleware": "2.21.0",
"webpack-sources": "1.0.1"
"webpack-sources": "1.0.1",
"worker-loader": "^1.1.0"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lock down version # (We try to avoid ^ versions to keep builds consistent.)

},
"scripts": {
"freezer": "webpack --config=./webpack_config/webpack.freezer.js && node ./dist/freezer.js",
Expand Down
4 changes: 4 additions & 0 deletions webpack_config/webpack.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ module.exports = {
},
module: {
loaders: [
{
test: /\.worker\.js$/,
loader: 'worker-loader'
},
{
test: /\.(ts|tsx)$/,
loaders: [
Expand Down