unlock wip

This commit is contained in:
crptm 2017-06-30 03:03:11 +04:00
parent dbb90d0d5c
commit 63e7eb89e4
22 changed files with 454 additions and 182 deletions

View File

@ -1,9 +1,7 @@
{
"extends": ["eslint:recommended", "plugin:react/recommended"],
"parser": "babel-eslint",
"plugins": [
"react"
],
"plugins": ["react"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true,
@ -19,7 +17,7 @@
},
"rules": {
"comma-dangle": 1,
"quotes": [ 1, "single" ],
"quotes": [1, "single"],
"no-undef": 1,
"global-strict": 0,
"no-extra-semi": 1,
@ -32,6 +30,7 @@
"react/jsx-uses-react": 1
},
"globals": {
"SyntheticInputEvent": false
"SyntheticInputEvent": false,
"SyntheticKeyboardEvent": false
}
}

29
common/actions/wallet.js Normal file
View File

@ -0,0 +1,29 @@
// @flow
import type { PrivateKeyUnlockParams } from 'libs/wallet/privkey';
import BaseWallet from 'libs/wallet/base';
export type UnlockPrivateKeyAction = {
type: 'WALLET_UNLOCK_PRIVATE_KEY',
payload: PrivateKeyUnlockParams
};
export type SaveWalletAction = {
type: 'WALLET_SAVE',
payload: BaseWallet
};
export type WalletAction = UnlockPrivateKeyAction | SaveWalletAction;
export function unlockPrivateKey(value: PrivateKeyUnlockParams): UnlockPrivateKeyAction {
return {
type: 'WALLET_UNLOCK_PRIVATE_KEY',
payload: value
};
}
export function saveWallet(value: BaseWallet): SaveWalletAction {
return {
type: 'WALLET_SAVE',
payload: value
};
}

View File

@ -0,0 +1,95 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
import { isValidPrivKey } from 'libs/validators';
import type { PrivateKeyUnlockParams } from 'libs/wallet/privkey';
export type PrivateKeyValue = PrivateKeyUnlockParams & {
valid: boolean
};
function fixPkey(key) {
if (key.indexOf('0x') === 0) {
return key.slice(2);
}
return key;
}
export default class PrivateKeyDecrypt extends Component {
props: {
value: PrivateKeyUnlockParams,
onChange: (value: PrivateKeyUnlockParams) => void,
onUnlock: () => void
};
render() {
const { key, password } = this.props.value;
const fixedPkey = fixPkey(key);
const isValid = isValidPrivKey(fixedPkey.length);
const isPassRequired = fixedPkey.length > 64;
return (
<section className="col-md-4 col-sm-6">
<div id="selectedTypeKey">
<h4>
{translate('ADD_Radio_3')}
</h4>
<div className="form-group">
<textarea
id="aria-private-key"
className={`form-control ${isValid ? 'is-valid' : 'is-invalid'}`}
value={key}
onChange={this.onPkeyChange}
onKeyDown={this.onKeyDown}
placeholder={translate('x_PrivKey2')}
rows="4"
/>
</div>
{isValid &&
isPassRequired &&
<div className="form-group">
<p>
{translate('ADD_Label_3')}
</p>
<input
className={`form-control ${password.length > 0
? 'is-valid'
: 'is-invalid'}`}
value={password}
onChange={this.onPasswordChange}
onKeyDown={this.onKeyDown}
placeholder={translate('x_Password')}
type="password"
/>
</div>}
</div>
</section>
);
}
onPkeyChange = (e: SyntheticInputEvent) => {
const fixedPkey = fixPkey(e.target.value);
const isValid = isValidPrivKey(fixedPkey.length);
const isPassRequired = fixedPkey.length > 64;
const valid = isValid && (isPassRequired ? this.props.value.password.length > 0 : true);
this.props.onChange({ ...this.props.value, key: e.target.value, valid });
};
onPasswordChange = (e: SyntheticInputEvent) => {
const fixedPkey = fixPkey(this.props.value.key);
const isValid = isValidPrivKey(fixedPkey.length);
const isPassRequired = fixedPkey.length > 64;
const valid = isValid && (isPassRequired ? e.target.value.length > 0 : true);
this.props.onChange({ ...this.props.value, password: e.target.value, valid });
};
onKeyDown = (e: SyntheticKeyboardEvent) => {
if (e.keyCode === 13) {
e.preventDefault();
e.stopPropagation();
this.props.onUnlock();
}
};
}

View File

@ -0,0 +1,161 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
import KeystoreDecrypt from './Keystore';
import PrivateKeyDecrypt from './PrivateKey';
import type { PrivateKeyValue } from './PrivateKey';
import MnemonicDecrypt from './Mnemonic';
import LedgerNanoSDecrypt from './LedgerNano';
import TrezorDecrypt from './Trezor';
import ViewOnlyDecrypt from './ViewOnly';
import map from 'lodash/map';
import { unlockPrivateKey } from 'actions/wallet';
import { connect } from 'react-redux';
const WALLETS = {
'keystore-file': {
lid: 'x_Keystore2',
component: KeystoreDecrypt,
initialParams: {}
},
'private-key': {
lid: 'x_PrivKey2',
component: PrivateKeyDecrypt,
initialParams: {
key: '',
password: ''
},
unlock: unlockPrivateKey
},
'mnemonic-phrase': {
lid: 'x_Mnemonic',
component: MnemonicDecrypt
},
'ledger-nano-s': {
lid: 'x_Ledger',
component: LedgerNanoSDecrypt
},
trezor: {
lid: 'x_Trezor',
component: TrezorDecrypt
},
'view-only': {
lid: 'View with Address Only',
component: ViewOnlyDecrypt
}
};
type UnlockParams = {} | PrivateKeyValue;
type State = {
selectedWalletKey: string,
value: UnlockParams
};
export class WalletDecrypt extends Component {
props: {
// FIXME
dispatch: (action: any) => void
};
state: State = {
selectedWalletKey: 'keystore-file',
value: WALLETS['keystore-file'].initialParams
};
getDecryptionComponent() {
const { selectedWalletKey, value } = this.state;
const selectedWallet = WALLETS[selectedWalletKey];
if (!selectedWallet) {
return null;
}
return (
<selectedWallet.component
value={value}
onChange={this.onChange}
onUnlock={this.onUnlock}
/>
);
}
buildWalletOptions() {
return map(WALLETS, (wallet, key) => {
const isSelected = this.state.selectedWalletKey === key;
return (
<label className="radio" key={key}>
<input
aria-flowto={`aria-${key}`}
aria-labelledby={`${key}-label`}
type="radio"
name="decryption-choice-radio-group"
value={key}
checked={isSelected}
onChange={this.handleDecryptionChoiceChange}
/>
<span id={`${key}-label`}>
{translate(wallet.lid)}
</span>
</label>
);
});
}
handleDecryptionChoiceChange = (event: SyntheticInputEvent) => {
const wallet = WALLETS[event.target.value];
if (!wallet) {
return;
}
this.setState({
selectedWalletKey: event.target.value,
value: wallet.initialParams
});
};
render() {
const decryptionComponent = this.getDecryptionComponent();
return (
<article className="well decrypt-drtv row">
<section className="col-md-4 col-sm-6">
<h4>
{translate('decrypt_Access')}
</h4>
{this.buildWalletOptions()}
</section>
{decryptionComponent}
{!!this.state.value.valid &&
<section className="col-md-4 col-sm-6">
<h4 id="uploadbtntxt-wallet">
{translate('ADD_Label_6')}
</h4>
<div className="form-group">
<a
tabIndex="0"
role="button"
className="btn btn-primary btn-block"
onClick={this.onUnlock}
>
{translate('ADD_Label_6_short')}
</a>
</div>
</section>}
</article>
);
}
onChange = (value: UnlockParams) => {
this.setState({ value });
};
onUnlock = () => {
this.props.dispatch(WALLETS[this.state.selectedWalletKey].unlock(this.state.value));
};
}
export default connect()(WalletDecrypt);

View File

@ -2,11 +2,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import translate from 'translations';
import WalletDecrypt from 'components/WalletDecrypt';
import BaseWallet from 'libs/wallet/base';
import { connect } from 'react-redux';
import type { State } from 'reducers';
export default class UnlockHeader extends React.Component {
props: {
title: string
};
type Props = {
title: string,
wallet: BaseWallet
};
export class UnlockHeader extends React.Component {
props: Props;
static propTypes = {
title: PropTypes.string.isRequired
};
@ -17,17 +24,33 @@ export default class UnlockHeader extends React.Component {
expanded: true
};
componentDidUpdate(prevProps: Props) {
if (this.props.wallet && this.props.wallet !== prevProps.wallet) {
this.setState({ expanded: false });
}
// not sure if could happen
if (!this.props.wallet && this.props.wallet !== prevProps.wallet) {
this.setState({ expanded: true });
}
}
render() {
return (
<article className="collapse-container">
<div onClick={this.toggleExpanded}>
<a className="collapse-button">
<span>{this.state.expanded ? '-' : '+'}</span>
<span>
{this.state.expanded ? '-' : '+'}
</span>
</a>
<h1>{translate(this.props.title)}</h1>
<h1>
{translate(this.props.title)}
</h1>
</div>
{this.state.expanded &&
<div>
<WalletDecrypt />
{/* @@if (site === 'cx' ) { <cx-wallet-decrypt-drtv></cx-wallet-decrypt-drtv> }
@@if (site === 'mew' ) { <wallet-decrypt-drtv></wallet-decrypt-drtv> } */}
</div>}
@ -43,3 +66,11 @@ export default class UnlockHeader extends React.Component {
});
};
}
function mapStateToProps(state: State) {
return {
wallet: state.wallet
};
}
export default connect(mapStateToProps)(UnlockHeader);

View File

@ -13,6 +13,9 @@ import {
AddressField
} from './components';
import pickBy from 'lodash/pickBy';
import type { State as AppState } from 'reducers';
import { connect } from 'react-redux';
import BaseWallet from 'libs/wallet/base';
// import type { Transaction } from './types';
import customMessages from './messages';
@ -49,7 +52,8 @@ export class SendTransaction extends React.Component {
query: {
[string]: string
}
}
},
wallet: BaseWallet
};
state: State = {
hasQueryString: false,
@ -73,7 +77,7 @@ export class SendTransaction extends React.Component {
}
render() {
const unlocked = true; //wallet != null
const unlocked = !!this.props.wallet;
const unitReadable = 'UNITREADABLE';
const nodeUnit = 'NODEUNIT';
const hasEnoughBalance = false;
@ -87,7 +91,6 @@ export class SendTransaction extends React.Component {
<section className="container" style={{ minHeight: '50%' }}>
<div className="tab-content">
<main className="tab-pane active" ng-controller="sendTxCtrl">
{hasQueryString &&
<div className="alert alert-info">
<p>
@ -116,8 +119,7 @@ export class SendTransaction extends React.Component {
<strong>
Warning! You do not have enough funds to
complete this swap.
</strong>
{' '}
</strong>{' '}
<br />
Please add more funds or access a different wallet.
</div>
@ -187,7 +189,6 @@ export class SendTransaction extends React.Component {
{' '}Send Transaction{' '}
</a>
</div>
</section>
{'' /* <!-- / Content --> */}
{
@ -266,5 +267,11 @@ export class SendTransaction extends React.Component {
});
};
}
// export connected version
export default SendTransaction;
function mapStateToProps(state: AppState) {
return {
wallet: state.wallet
};
}
export default connect(mapStateToProps)(SendTransaction);

View File

@ -0,0 +1,26 @@
// @flow
import React, { Component } from 'react';
import translate from 'translations';
export default class ViewWallet extends Component {
render() {
return (
<section className="container">
<div className="tab-content">
<article className="tab-pane active">
<article className="collapse-container">
<div>
<h1>View Wallet Info</h1>
</div>
<div>
<p>
{translate('VIEWWALLET_Subtitle')}
</p>
</div>
</article>
</article>
</div>
</section>
);
}
}

View File

@ -1,21 +0,0 @@
import React, {Component} from 'react';
import translate from 'translations';
export default class PrivateKeyDecrypt extends Component {
render() {
return (
<section className="col-md-4 col-sm-6">
<div id="selectedUploadKey">
<h4>{translate('ADD_Radio_2_alt')}</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>
</div>
</div>
</section>
);
}
}

View File

@ -1,129 +0,0 @@
import React, {Component} from 'react';
import translate from 'translations';
import KeystoreDecrypt from './KeystoreDecrypt';
import PrivateKeyDecrypt from './PrivateKeyDecrypt';
import MnemonicDecrypt from './MnemonicDecrypt';
import LedgerNanoSDecrypt from './LedgerNanoSDecrypt';
import TrezorDecrypt from './TrezorDecrypt';
import ViewOnlyDecrypt from './ViewOnlyDecrypt';
export default class WalletDecrypt extends Component {
constructor(props) {
super(props);
this.decryptionChoices = [
{
name: 'keystore-file',
lid: 'x_Keystore2',
component: KeystoreDecrypt
},
{
name: 'private-key',
lid: 'x_PrivKey2',
component: PrivateKeyDecrypt
},
{
name: 'mnemonic-phrase',
lid: 'x_Mnemonic',
component: MnemonicDecrypt
},
{
name: 'ledger-nano-s',
lid: 'x_Ledger',
component: LedgerNanoSDecrypt
},
{
name: 'trezor',
lid: 'x_Trezor',
component: TrezorDecrypt
},
{
name: 'view-only',
lid: 'View with Address Only',
component: ViewOnlyDecrypt
}
];
this.state = {
decryptionChoice: this.decryptionChoices[0].name // auto-select first option.
};
}
getDecryptionComponent() {
const selectedDecryptionChoice = this.decryptionChoices.find((decryptionChoice) => {
return this.state.decryptionChoice === decryptionChoice.name;
});
if (!selectedDecryptionChoice) {
return null;
}
return <selectedDecryptionChoice.component />;
}
buildDecryptionChoices() {
return this.decryptionChoices.map((decryptionChoice, idx) => {
const isSelected = this.state.decryptionChoice === decryptionChoice.name;
return (
<label className="radio" key={decryptionChoice.name}>
<input
aria-flowto={`aria${idx}`}
aria-labelledby={`${decryptionChoice.name}-label`}
type="radio"
name="decryption-choice-radio-group"
value={decryptionChoice.name}
checked={isSelected}
onChange={this.handleDecryptionChoiceChange}
/>
<span id={`${decryptionChoice.name}-label`}>
{translate(decryptionChoice.lid)}
</span>
</label>
);
});
}
handleDecryptionChoiceChange = (event) => {
const choiceObject = this.decryptionChoices.find(decryptionChoice => {
return (decryptionChoice.name === event.target.value) ? decryptionChoice.component : false;
});
const decryptionChoice = choiceObject.name;
this.setState({
decryptionChoice
});
}
render() {
const decryptionComponent = this.getDecryptionComponent();
return (
<section className="container">
<div className="tab-content">
<article className="tab-pane active">
<article className="collapse-container">
<div>
<h1>View Wallet Info</h1>
</div>
<div>
<p>{translate('VIEWWALLET_Subtitle')}</p>
<article className="well decrypt-drtv row">
<section className="col-md-4 col-sm-6">
<h4>{translate('decrypt_Access')}</h4>
{this.buildDecryptionChoices()}
</section>
{decryptionComponent}
</article>
</div>
</article>
</article>
</div>
</section>
);
}
}

View File

@ -11,31 +11,32 @@ import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import notificationsSaga from './sagas/notifications';
import ensSaga from './sagas/ens';
import walletSaga from './sagas/wallet';
// application styles
import 'assets/styles/etherwallet-master.less';
const sagaMiddleware = createSagaMiddleware();
const configureStore = () => {
let sagaApplied = applyMiddleware(sagaMiddleware);
const logger = createLogger({
collapsed: true
});
const sagaMiddleware = createSagaMiddleware();
let store;
let middleware;
if (process.env.NODE_ENV !== 'production') {
window.Perf = Perf;
sagaApplied = composeWithDevTools(sagaApplied);
const logger = createLogger({
collapsed: true
});
middleware = applyMiddleware(routerMiddleware(history), logger);
middleware = composeWithDevTools(
applyMiddleware(sagaMiddleware, logger, routerMiddleware(history))
);
} else {
middleware = applyMiddleware(routerMiddleware(history));
middleware = applyMiddleware(sagaMiddleware, routerMiddleware(history));
}
store = createStore(RootReducer, sagaApplied, middleware);
store = createStore(RootReducer, void 0, middleware);
sagaMiddleware.run(notificationsSaga);
sagaMiddleware.run(ensSaga);
sagaMiddleware.run(walletSaga);
return store;
};

View File

@ -65,3 +65,7 @@ function validateEtherAddress(address: string): boolean {
return true;
else return isChecksumAddress(address);
}
export function isValidPrivKey(length: number): boolean {
return length === 64 || length === 128 || length === 132;
}

View File

@ -0,0 +1,7 @@
// @flow
export default class BaseWallet {
getAddress(): string {
throw 'Implement me';
}
}

View File

@ -0,0 +1,24 @@
// @flow
import BaseWallet from './base';
import { privateToPublic, publicToAddress, toChecksumAddress } from 'ethereumjs-util';
export type PrivateKeyUnlockParams = {
key: string,
password: string
};
export default class PrivKeyWallet extends BaseWallet {
privKey: Buffer;
pubKey: Buffer;
address: Buffer;
constructor(params: PrivateKeyUnlockParams) {
super();
this.privKey = Buffer.from(params.key, 'hex');
this.pubKey = privateToPublic(this.privKey);
this.address = publicToAddress(this.pubKey);
}
getAddress() {
return toChecksumAddress(`0x${this.address.toString('hex')}`);
}
}

View File

@ -13,6 +13,9 @@ import type { State as NotificationsState } from './notifications';
import * as ens from './ens';
import type { State as EnsState } from './ens';
import * as wallet from './wallet';
import type { State as WalletState } from './wallet';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
@ -21,7 +24,8 @@ export type State = {
generateWallet: GenerateWalletState,
conig: ConfigState,
notifications: NotificationsState,
ens: EnsState
ens: EnsState,
wallet: WalletState
};
export default combineReducers({
@ -30,6 +34,7 @@ export default combineReducers({
...swap,
...notifications,
...ens,
...wallet,
form: formReducer,
routing: routerReducer
});

20
common/reducers/wallet.js Normal file
View File

@ -0,0 +1,20 @@
// @flow
import type { WalletAction, SaveWalletAction } from 'actions/wallet';
import BaseWallet from 'libs/wallet/base';
export type State = ?BaseWallet;
const initialState: State = null;
function saveWallet(state: State, action: SaveWalletAction): State {
return action.payload;
}
export function wallet(state: State = initialState, action: WalletAction): State {
switch (action.type) {
case 'WALLET_SAVE':
return saveWallet(state, action);
default:
return state;
}
}

View File

@ -3,7 +3,6 @@ import { browserHistory, Redirect, Route } from 'react-router';
import { useBasename } from 'history';
import { App } from 'containers';
import GenerateWallet from 'containers/Tabs/GenerateWallet';
import WalletDecrypt from 'containers/Tabs/WalletDecrypt';
import ViewWallet from 'containers/Tabs/ViewWallet';
import Help from 'containers/Tabs/Help';
import Swap from 'containers/Tabs/Swap';
@ -13,7 +12,7 @@ export const history = getHistory();
export const Routing = () =>
<Route name="App" path="" component={App}>
<Route name="GenerateWallet" path="/" component={GenerateWallet} />
<Route name="ViewWallet" path="/view-wallet" component={WalletDecrypt} />
<Route name="ViewWallet" path="/view-wallet" component={ViewWallet} />
<Route name="Help" path="/help" component={Help} />
<Route name="Swap" path="/swap" component={Swap} />
<Route name="Send" path="/send-transaction" component={SendTransaction} />

14
common/sagas/wallet.js Normal file
View File

@ -0,0 +1,14 @@
// @flow
import { takeEvery, call, put, select } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { saveWallet } from 'actions/wallet';
import type { UnlockPrivateKeyAction } from 'actions/wallet';
import PrivKeyWallet from 'libs/wallet/privkey';
function* unlockPrivateKey(action: UnlockPrivateKeyAction) {
yield put(saveWallet(new PrivKeyWallet(action.payload)));
}
export default function* notificationsSaga() {
yield takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey);
}