Merge pull request #12 from MyEtherWallet/wallet-decrypt

WIP: Wallet decrypt
This commit is contained in:
Daniel Ternyak 2017-07-03 22:16:14 -05:00 committed by GitHub
commit 51899e8f4e
39 changed files with 1632 additions and 681 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,8 @@
"react/jsx-uses-react": 1
},
"globals": {
"SyntheticInputEvent": false
"SyntheticInputEvent": false,
"SyntheticKeyboardEvent": false,
"Generator": false
}
}

View File

@ -1,18 +1,30 @@
// @flow
import { setLanguage } from 'translations';
export const CONFIG_LANGUAGE_CHANGE = 'CONFIG_LANGUAGE_CHANGE';
export const CONFIG_NODE_CHANGE = 'CONFIG_NODE_CHANGE';
export const CHANGE_LANGUAGE = (value: any) => {
setLanguage(value.sign);
return {
type: CONFIG_LANGUAGE_CHANGE,
value
};
export type ChangeNodeAction = {
type: 'CONFIG_NODE_CHANGE',
// FIXME $keyof?
value: string
};
export const CHANGE_NODE = (value: any) =>
Object({
type: CONFIG_NODE_CHANGE,
export type ChangeLanguageAction = {
type: 'CONFIG_LANGUAGE_CHANGE',
value: string
};
export type ConfigAction = ChangeNodeAction | ChangeLanguageAction;
export function changeLanguage(sign: string) {
setLanguage(sign);
return {
type: 'CONFIG_LANGUAGE_CHANGE',
value: sign
};
}
export function changeNode(value: string): ChangeNodeAction {
return {
type: 'CONFIG_NODE_CHANGE',
value
});
};
}

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

@ -0,0 +1,39 @@
// @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 InitWalletAction = {
type: 'WALLET_INIT'
};
export type WalletAction = UnlockPrivateKeyAction | SaveWalletAction | InitWalletAction;
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
};
}
export function initWallet(): InitWalletAction {
return {
type: 'WALLET_INIT'
};
}

View File

@ -1,26 +1,23 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TabsOptions from './components/TabsOptions';
import { Link } from 'react-router';
import { Dropdown } from 'components/ui';
import { languages, nodeList } from '../../config/data';
import { languages, NODES } from '../../config/data';
export default class Header extends Component {
static propTypes = {
location: PropTypes.object,
props: {
languageSelection: string,
nodeSelection: string,
// Language DropDown
changeLanguage: PropTypes.func,
languageSelection: PropTypes.object,
// Node Dropdown
changeNode: PropTypes.func,
nodeSelection: PropTypes.object
changeLanguage: (sign: string) => any,
changeNode: (key: string) => any
};
render() {
const { languageSelection, changeLanguage, changeNode, nodeSelection } = this.props;
const { languageSelection, changeNode, nodeSelection } = this.props;
const selectedLanguage = languages.find(l => l.sign === languageSelection) || languages[0];
const selectedNode = NODES[nodeSelection];
return (
<div>
@ -41,10 +38,10 @@ export default class Header extends Component {
</span>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change language. current language ${languageSelection.name}`}
ariaLabel={`change language. current language ${selectedLanguage.name}`}
options={languages}
formatTitle={o => o.name}
value={languageSelection}
value={selectedLanguage}
extra={[
<li key={'separator'} role="separator" className="divider" />,
<li key={'disclaimer'}>
@ -53,23 +50,23 @@ export default class Header extends Component {
</a>
</li>
]}
onChange={changeLanguage}
onChange={this.changeLanguage}
/>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change node. current node ${nodeSelection.name} node by ${nodeSelection.service}`}
options={nodeList}
ariaLabel={`change node. current node ${selectedNode.network} node by ${selectedNode.service}`}
options={Object.keys(NODES)}
formatTitle={o => [
o.name,
NODES[o].network,
' ',
<small key="service">({o.service})</small>
<small key="service">
({NODES[o].service})
</small>
]}
value={nodeSelection}
extra={
<li>
<a onClick={() => {}}>
Add Custom Node
</a>
<a onClick={() => {}}>Add Custom Node</a>
</li>
}
onChange={changeNode}
@ -79,8 +76,11 @@ export default class Header extends Component {
</section>
<TabsOptions {...this.props} />
</div>
);
}
changeLanguage = (value: { sign: string }) => {
this.props.changeLanguage(value.sign);
};
}

View File

@ -0,0 +1,46 @@
import React, {Component} from 'react';
import translate from 'translations';
import wallet from 'ethereumjs-wallet';
import ethUtil from 'ethereumjs-util';
export default class KeystoreDecrypt extends Component {
constructor(props) {
super(props);
}
handleFileSelection = (event) => {
const fileReader = new FileReader();
const inputFile = event.target.files[0];
fileReader.onload = () => {
try {
const keyStoreString = fileReader.result;
const decryptedWallet = wallet.fromV3(keyStoreString, 'asdfasdfasdf', true);
const privateHex = ethUtil.bufferToHex(decryptedWallet._privKey);
const publicHex = ethUtil.bufferToHex(ethUtil.privateToAddress(decryptedWallet._privKey));
console.log(privateHex, publicHex); // TODO: Remove console log, it's only here to let Travis pass
} catch (e) {
console.error('Could not parse Keystore file.', e);
}
};
fileReader.readAsText(inputFile, 'utf-8');
}
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" onChange={this.handleFileSelection} />
<label htmlFor="fselector">
<a className="btn-file marg-v-sm" id="aria1" tabIndex="0" role="button">{translate('ADD_Radio_2_short')}</a>
</label>
</div>
</div>
</section>
);
}
}

View File

@ -0,0 +1,21 @@
import React, {Component} from 'react';
import translate from 'translations';
export default class LedgerNanoSDecrypt 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

@ -0,0 +1,21 @@
import React, {Component} from 'react';
import translate from 'translations';
export default class MnemonicDecrypt 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

@ -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,21 @@
import React, {Component} from 'react';
import translate from 'translations';
export default class TrezorDecrypt 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

@ -0,0 +1,21 @@
import React, {Component} from 'react';
import translate from 'translations';
export default class ViewOnlyDecrypt 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

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

@ -1,3 +1,5 @@
export Header from './Header';
export Footer from './Footer';
export Root from './Root';
// @flow
export { default as Header } from './Header';
export { default as Footer } from './Footer';
export { default as Root } from './Root';

View File

@ -1,26 +1,21 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
export default class DropdownComponent extends Component {
static propTypes = {
value: PropTypes.object.isRequired,
options: PropTypes.arrayOf(PropTypes.object).isRequired,
ariaLabel: PropTypes.string.isRequired,
formatTitle: PropTypes.func.isRequired,
extra: PropTypes.node,
onChange: PropTypes.func.isRequired
};
type Props<T> = {
value: T,
options: T[],
ariaLabel: string,
formatTitle: (option: T) => any,
extra?: any,
onChange: (value: T) => void
}
// FIXME
props: {
value: any,
options: any[],
ariaLabel: string,
formatTitle: (option: any) => any,
extra?: any,
onChange: (value: any) => void
};
type State = {
expanded: boolean
}
export default class DropdownComponent<T: *> extends Component<void, Props<T>, State> {
props: Props<T>
state = {
expanded: false

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

@ -1,168 +1,126 @@
// @flow
import { RPCNode } from 'libs/nodes';
export const DONATION_ADDRESSES_MAP = {
BTC: '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6',
ETH: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
REP: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8'
BTC: '1MEWT2SGbqtz6mPCgFcnea8XmWV5Z4Wc6',
ETH: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
REP: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8'
};
export const donationAddressMap = DONATION_ADDRESSES_MAP;
export const languages = [
{
sign: 'en',
name: 'English'
},
{
sign: 'de',
name: 'Deutsch'
},
{
sign: 'el',
name: 'Ελληνικά'
},
{
sign: 'es',
name: 'Español'
},
{
sign: 'fi',
name: 'Suomi'
},
{
sign: 'fr',
name: 'Français'
},
{
sign: 'hu',
name: 'Magyar'
},
{
sign: 'id',
name: 'Indonesian'
},
{
sign: 'it',
name: 'Italiano'
},
{
sign: 'ja',
name: '日本語'
},
{
sign: 'nl',
name: 'Nederlands'
},
{
sign: 'no',
name: 'Norsk Bokmål'
},
{
sign: 'pl',
name: 'Polski'
},
{
sign: 'pt',
name: 'Português'
},
{
sign: 'ru',
name: 'Русский'
},
{
sign: 'ko',
name: 'Korean'
},
// {
// 'sign': 'sk',
// 'name': 'Slovenčina'
// },
// {
// 'sign': 'sl',
// 'name': 'Slovenščina'
// },
// {
// 'sign': 'sv',
// 'name': 'Svenska'
// },
{
sign: 'tr',
name: 'Türkçe'
},
{
sign: 'vi',
name: 'Tiếng Việt'
},
{
sign: 'zhcn',
name: '简体中文'
},
{
sign: 'zhtw',
name: '繁體中文'
}
{
sign: 'en',
name: 'English'
},
{
sign: 'de',
name: 'Deutsch'
},
{
sign: 'el',
name: 'Ελληνικά'
},
{
sign: 'es',
name: 'Español'
},
{
sign: 'fi',
name: 'Suomi'
},
{
sign: 'fr',
name: 'Français'
},
{
sign: 'hu',
name: 'Magyar'
},
{
sign: 'id',
name: 'Indonesian'
},
{
sign: 'it',
name: 'Italiano'
},
{
sign: 'ja',
name: '日本語'
},
{
sign: 'nl',
name: 'Nederlands'
},
{
sign: 'no',
name: 'Norsk Bokmål'
},
{
sign: 'pl',
name: 'Polski'
},
{
sign: 'pt',
name: 'Português'
},
{
sign: 'ru',
name: 'Русский'
},
{
sign: 'ko',
name: 'Korean'
},
// {
// 'sign': 'sk',
// 'name': 'Slovenčina'
// },
// {
// 'sign': 'sl',
// 'name': 'Slovenščina'
// },
// {
// 'sign': 'sv',
// 'name': 'Svenska'
// },
{
sign: 'tr',
name: 'Türkçe'
},
{
sign: 'vi',
name: 'Tiếng Việt'
},
{
sign: 'zhcn',
name: '简体中文'
},
{
sign: 'zhtw',
name: '繁體中文'
}
];
export const nodeList = [
{
name: 'ETH',
blockExplorerTX: 'https://etherscan.io/tx/[[txHash]]',
blockExplorerAddr: 'https://etherscan.io/address/[[address]]',
// 'type': nodes.nodeTypes.ETH,
eip155: true,
chainId: 1,
// 'tokenList': require('./tokens/ethTokens.json'),
// 'abiList': require('./abiDefinitions/ethAbi.json'),
estimateGas: true,
service: 'MyEtherWallet'
// 'lib': new nodes.customNode('https://api.myetherapi.com/eth', '')
},
{
name: 'ETH',
blockExplorerTX: 'https://etherscan.io/tx/[[txHash]]',
blockExplorerAddr: 'https://etherscan.io/address/[[address]]',
// 'type': nodes.nodeTypes.ETH,
eip155: true,
chainId: 1,
// 'tokenList': require('./tokens/ethTokens.json'),
// 'abiList': require('./abiDefinitions/ethAbi.json'),
estimateGas: false,
service: 'Etherscan.io'
// 'lib': require('./nodeHelpers/etherscan')
},
{
name: 'Ropsten',
// 'type': nodes.nodeTypes.Ropsten,
blockExplorerTX: 'https://ropsten.etherscan.io/tx/[[txHash]]',
blockExplorerAddr: 'https://ropsten.etherscan.io/address/[[address]]',
eip155: true,
chainId: 3,
// 'tokenList': require('./tokens/ropstenTokens.json'),
// 'abiList': require('./abiDefinitions/ropstenAbi.json'),
estimateGas: false,
service: 'MyEtherWallet'
// 'lib': new nodes.customNode('https://api.myetherapi.com/rop', '')
},
{
name: 'Kovan',
// 'type': nodes.nodeTypes.Kovan,
blockExplorerTX: 'https://kovan.etherscan.io/tx/[[txHash]]',
blockExplorerAddr: 'https://kovan.etherscan.io/address/[[address]]',
eip155: true,
chainId: 42,
// 'tokenList': require('./tokens/kovanTokens.json'),
// 'abiList': require('./abiDefinitions/kovanAbi.json'),
estimateGas: false,
service: 'Etherscan.io'
// 'lib': require('./nodeHelpers/etherscanKov')
},
{
name: 'ETC',
blockExplorerTX: 'https://gastracker.io/tx/[[txHash]]',
blockExplorerAddr: 'https://gastracker.io/addr/[[address]]',
// 'type': nodes.nodeTypes.ETC,
eip155: true,
chainId: 61,
// 'tokenList': require('./tokens/etcTokens.json'),
// 'abiList': require('./abiDefinitions/etcAbi.json'),
estimateGas: false,
service: 'Epool.io'
// 'lib': new nodes.customNode('https://mewapi.epool.io', '')
}
];
export const NETWORKS = {
ETH: {
name: 'ETH',
blockExplorerTX: 'https://etherscan.io/tx/[[txHash]]',
blockExplorerAddr: 'https://etherscan.io/address/[[address]]',
chainId: 1
}
};
export const NODES = {
eth_mew: {
network: 'ETH',
lib: new RPCNode('https://api.myetherapi.com/eth'),
service: 'MyEtherWallet',
estimateGas: true,
eip155: true
// 'tokenList': require('./tokens/ethTokens.json'),
// 'abiList': require('./abiDefinitions/ethAbi.json'),
}
};

View File

@ -2,32 +2,23 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Footer, Header } from 'components';
import PropTypes from 'prop-types';
import Notifications from './Notifications';
import { CHANGE_LANGUAGE, CHANGE_NODE } from 'actions/config';
import * as actions from 'actions/config';
class App extends Component {
constructor(props) {
super(props);
}
props: {
// FIXME
children: any,
location: any,
router: any,
isMobile: boolean,
static propTypes = {
children: PropTypes.node.isRequired,
location: PropTypes.object,
handleWindowResize: PropTypes.func,
languageSelection: string,
nodeSelection: string,
router: PropTypes.object,
isMobile: PropTypes.bool,
// BEGIN ACTUAL
languageSelection: PropTypes.object,
changeLanguage: PropTypes.func,
changeNode: PropTypes.func,
nodeSelection: PropTypes.object,
showNotification: PropTypes.func
changeLanguage: typeof actions.changeLanguage,
changeNode: typeof actions.changeNode,
handleWindowResize: () => void
};
render() {
@ -72,16 +63,4 @@ function mapStateToProps(state) {
};
}
function mapDispatchToProps(dispatch) {
return {
// FIXME replace with actual types
changeNode: (i: any) => {
dispatch(CHANGE_NODE(i));
},
changeLanguage: (i: any) => {
dispatch(CHANGE_LANGUAGE(i));
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
export default connect(mapStateToProps, actions)(App);

View File

@ -5,275 +5,272 @@ import PropTypes from 'prop-types';
import translate from 'translations';
import { UnlockHeader } from 'components/ui';
import {
Donate,
DataField,
CustomMessage,
GasField,
AmountField,
AddressField
Donate,
DataField,
CustomMessage,
GasField,
AmountField,
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';
import { donationAddressMap } from 'config/data';
type State = {
hasQueryString: boolean,
readOnly: boolean,
to: string,
value: string,
unit: string,
gasLimit: string,
data: string,
gasChanged: boolean
hasQueryString: boolean,
readOnly: boolean,
to: string,
value: string,
unit: string,
gasLimit: string,
data: string,
gasChanged: boolean
};
function getParam(query: { [string]: string }, key: string) {
const keys = Object.keys(query);
const index = keys.findIndex(k => k.toLowerCase() === key.toLowerCase());
if (index === -1) {
return null;
}
const keys = Object.keys(query);
const index = keys.findIndex(k => k.toLowerCase() === key.toLowerCase());
if (index === -1) {
return null;
}
return query[keys[index]];
return query[keys[index]];
}
// TODO query string
// TODO how to handle DATA?
export class SendTransaction extends React.Component {
static propTypes = {
location: PropTypes.object.isRequired
};
props: {
location: {
query: {
[string]: string
}
}
};
state: State = {
hasQueryString: false,
readOnly: false,
// FIXME use correct defaults
to: '',
value: '999.11',
unit: 'ether',
gasLimit: '21000',
data: '',
gasChanged: false
};
static propTypes = {
location: PropTypes.object.isRequired
};
props: {
location: {
query: {
[string]: string
}
},
wallet: BaseWallet
};
state: State = {
hasQueryString: false,
readOnly: false,
// FIXME use correct defaults
to: '',
value: '999.11',
unit: 'ether',
gasLimit: '21000',
data: '',
gasChanged: false
};
componentDidMount() {
const queryPresets = pickBy(this.parseQuery());
if (Object.keys(queryPresets).length) {
this.setState({ ...queryPresets, hasQueryString: true });
componentDidMount() {
const queryPresets = pickBy(this.parseQuery());
if (Object.keys(queryPresets).length) {
this.setState({ ...queryPresets, hasQueryString: true });
}
}
this.setState(pickBy(queryPresets));
}
render() {
const unlocked = !!this.props.wallet;
const unitReadable = 'UNITREADABLE';
const nodeUnit = 'NODEUNIT';
const hasEnoughBalance = false;
const { to, value, unit, gasLimit, data, readOnly, hasQueryString } = this.state;
const customMessage = customMessages.find(m => m.to === to);
render() {
const unlocked = true; //wallet != null
const unitReadable = 'UNITREADABLE';
const nodeUnit = 'NODEUNIT';
const hasEnoughBalance = false;
const {
to,
value,
unit,
gasLimit,
data,
readOnly,
hasQueryString
} = this.state;
const customMessage = customMessages.find(m => m.to === to);
// tokens
// ng-show="token.balance!=0 && token.balance!='loading' || token.type!=='default' || tokenVisibility=='shown'"
// tokens
// ng-show="token.balance!=0 && token.balance!='loading' || token.type!=='default' || tokenVisibility=='shown'"
return (
<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>
{translate('WARN_Send_Link')}
</p>
</div>}
return (
<section className="container" style={{ minHeight: '50%' }}>
<div className="tab-content">
<main className="tab-pane active">
<UnlockHeader title={'NAV_SendEther'} />
{hasQueryString &&
<div className="alert alert-info">
<p>
{translate('WARN_Send_Link')}
</p>
</div>}
{unlocked &&
<article className="row">
{'' /* <!-- Sidebar --> */}
<section className="col-sm-4">
<div style={{ maxWidth: 350 }}>
{'' /* <wallet-balance-drtv /> */}
<hr />
<Donate onDonate={this.onNewTx} />
</div>
</section>
<UnlockHeader title={'NAV_SendEther'} />
<section className="col-sm-8">
{readOnly &&
!hasEnoughBalance &&
<div className="row form-group">
<div className="alert alert-danger col-xs-12 clearfix">
<strong>
Warning! You do not have enough funds to
complete this swap.
</strong>{' '}
<br />
Please add more funds or access a different wallet.
</div>
</div>}
{unlocked &&
<article className="row">
{'' /* <!-- Sidebar --> */}
<section className="col-sm-4">
<div style={{ maxWidth: 350 }}>
{'' /* <wallet-balance-drtv /> */}
<hr />
<Donate onDonate={this.onNewTx} />
</div>
</section>
<div className="row form-group">
<h4 className="col-xs-12">
{translate('SEND_trans')}
</h4>
</div>
<AddressField
placeholder={donationAddressMap.ETH}
value={this.state.to}
onChange={readOnly ? null : this.onAddressChange}
/>
<AmountField
value={value}
unit={unit}
onChange={readOnly ? void 0 : this.onAmountChange}
/>
<GasField
value={gasLimit}
onChange={readOnly ? void 0 : this.onGasChange}
/>
{unit === 'ether' &&
<DataField
value={data}
onChange={readOnly ? void 0 : this.onDataChange}
/>}
<CustomMessage message={customMessage} />
<section className="col-sm-8">
{readOnly &&
!hasEnoughBalance &&
<div className="row form-group">
<div className="alert alert-danger col-xs-12 clearfix">
<strong>
Warning! You do not have enough funds to
complete this swap.
</strong>
{' '}
<br />
Please add more funds or access a different wallet.
</div>
</div>}
<div className="row form-group">
<div className="col-xs-12 clearfix">
<a
className="btn btn-info btn-block"
onClick={this.generateTx}
>
{translate('SEND_generate')}
</a>
</div>
</div>
<div className="row form-group">
<h4 className="col-xs-12">
{translate('SEND_trans')}
</h4>
</div>
<AddressField
placeholder={donationAddressMap.ETH}
value={this.state.to}
onChange={readOnly ? null : this.onAddressChange}
/>
<AmountField
value={value}
unit={unit}
onChange={readOnly ? void 0 : this.onAmountChange}
/>
<GasField
value={gasLimit}
onChange={readOnly ? void 0 : this.onGasChange}
/>
{unit === 'ether' &&
<DataField
value={data}
onChange={readOnly ? void 0 : this.onDataChange}
/>}
<CustomMessage message={customMessage} />
<div className="row form-group" ng-show="showRaw">
<div className="col-sm-6">
<label translate="SEND_raw"> Raw Transaction </label>
<textarea className="form-control" rows="4" readOnly>
{'' /*rawTx*/}
</textarea>
</div>
<div className="col-sm-6">
<label translate="SEND_signed">
{' '}Signed Transaction{' '}
</label>
<textarea className="form-control" rows="4" readOnly>
{'' /*signedTx*/}
</textarea>
</div>
</div>
<div className="row form-group">
<div className="col-xs-12 clearfix">
<a
className="btn btn-info btn-block"
onClick={this.generateTx}
>
{translate('SEND_generate')}
</a>
</div>
</div>
<div className="row form-group" ng-show="showRaw">
<div className="col-sm-6">
<label translate="SEND_raw"> Raw Transaction </label>
<textarea className="form-control" rows="4" readOnly>
{'' /*rawTx*/}
</textarea>
</div>
<div className="col-sm-6">
<label translate="SEND_signed">
{' '}Signed Transaction{' '}
</label>
<textarea className="form-control" rows="4" readOnly>
{'' /*signedTx*/}
</textarea>
</div>
</div>
<div className="form-group" ng-show="showRaw">
<a
className="btn btn-primary btn-block col-sm-11"
data-toggle="modal"
data-target="#sendTransaction"
translate="SEND_trans"
>
{' '}Send Transaction{' '}
</a>
</div>
</section>
{'' /* <!-- / Content --> */}
{
'' /* @@if (site === 'mew' ) { @@include( './sendTx-content.tpl', { "site": "mew" } ) }
<div className="form-group" ng-show="showRaw">
<a
className="btn btn-primary btn-block col-sm-11"
data-toggle="modal"
data-target="#sendTransaction"
translate="SEND_trans"
>
{' '}Send Transaction{' '}
</a>
</div>
</section>
{'' /* <!-- / Content --> */}
{
'' /* @@if (site === 'mew' ) { @@include( './sendTx-content.tpl', { "site": "mew" } ) }
@@if (site === 'cx' ) { @@include( './sendTx-content.tpl', { "site": "cx" } ) }
@@if (site === 'mew' ) { @@include( './sendTx-modal.tpl', { "site": "mew" } ) }
@@if (site === 'cx' ) { @@include( './sendTx-modal.tpl', { "site": "cx" } ) } */
}
</article>}
</main>
</div>
</section>
);
}
parseQuery() {
const query = this.props.location.query;
const to = getParam(query, 'to');
const data = getParam(query, 'data');
// FIXME validate token against presets
const unit = getParam(query, 'tokenSymbol');
const value = getParam(query, 'value');
let gasLimit = getParam(query, 'gas');
if (gasLimit === null) {
gasLimit = getParam(query, 'limit');
}
</article>}
</main>
</div>
</section>
);
}
const readOnly = getParam(query, 'readOnly') == null ? false : true;
return { to, data, value, unit, gasLimit, readOnly };
}
parseQuery() {
const query = this.props.location.query;
const to = getParam(query, 'to');
const data = getParam(query, 'data');
// FIXME validate token against presets
const unit = getParam(query, 'tokenSymbol');
const value = getParam(query, 'value');
let gasLimit = getParam(query, 'gas');
if (gasLimit === null) {
gasLimit = getParam(query, 'limit');
}
const readOnly = getParam(query, 'readOnly') == null ? false : true;
// FIXME use mkTx instead or something that could take care of default gas/data and whatnot,
// FIXME also should it reset gasChanged?
onNewTx = (
address: string,
amount: string,
unit: string,
data: string = '',
gasLimit: string = '21000'
) => {
this.setState({
to: address,
value: amount,
unit,
data,
gasLimit,
gasChanged: false
});
};
onAddressChange = (value: string) => {
this.setState({
to: value
});
};
onDataChange = (value: string) => {
if (this.state.unit !== 'ether') {
return;
return { to, data, value, unit, gasLimit, readOnly };
}
this.setState({
...this.state,
data: value
});
};
onGasChange = (value: string) => {
this.setState({ gasLimit: value, gasChanged: true });
};
// FIXME use mkTx instead or something that could take care of default gas/data and whatnot,
// FIXME also should it reset gasChanged?
onNewTx = (
address: string,
amount: string,
unit: string,
data: string = '',
gasLimit: string = '21000'
) => {
this.setState({
to: address,
value: amount,
unit,
data,
gasLimit,
gasChanged: false
});
};
onAmountChange = (value: string, unit: string) => {
this.setState({
value,
unit
});
};
onAddressChange = (value: string) => {
this.setState({
to: value
});
};
onDataChange = (value: string) => {
if (this.state.unit !== 'ether') {
return;
}
this.setState({
...this.state,
data: value
});
};
onGasChange = (value: string) => {
this.setState({ gasLimit: value, gasChanged: true });
};
onAmountChange = (value: string, unit: string) => {
this.setState({
value,
unit
});
};
}
// export connected version
export default SendTransaction;
function mapStateToProps(state: AppState) {
return {
wallet: state.wallet.inst
};
}
export default connect(mapStateToProps)(SendTransaction);

View File

@ -1,126 +0,0 @@
import React, {Component} from 'react';
export default class ViewWallet extends Component {
constructor(props) {
super(props)
}
render() {
return (
<section className="container" style={{minHeight: '50%'}}>
<div className="tab-content">
<article className="tab-pane active ng-scope"
// ng-if="globalService.currentTab==globalService.tabs.viewWalletInfo.id"
// ng-controller="viewWalletCtrl"
>
<article className="collapse-container">
<div
// ng-click="wd = !wd"
>
{/*<a className="collapse-button"><span ng-show="wd" className="ng-hide">+</span><span*/}
{/*ng-show="!wd">-</span></a>*/}
<h1 className="ng-scope">View Wallet Info</h1>
</div>
<div
// ng-show="!wd"
>
<p className="ng-scope">This allows you to download different versions
of
private keys and re-print your paper wallet. You may want to do this in order to
<a
target="_blank"
href="http://ethereum.stackexchange.com/questions/465/how-to-import-a-plain-private-key-into-geth/">
import
your account into Geth/Mist</a>. If you want to check your balance, we
recommend
using a blockchain explorer like <a target="_blank" href="http://etherscan.io/">etherscan.io</a>.
</p>
{/*<wallet-decrypt-drtv/><article className="well decrypt-drtv row ng-scope" ng-controller="decryptWalletCtrl as $crtl">*/}
<article className="well decrypt-drtv row ng-scope"
// ng-controller="decryptWalletCtrl as $crtl"
>
<section className="col-md-4 col-sm-6">
<h4 className="ng-scope">How would you like to access your
wallet?</h4>
<label className="radio">
<input aria-flowto="aria1"
aria-label="Keystore JSON file"
type="radio"
// ng-model="walletType"
value="fileupload"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="133"/>
<span
// translate=""
className="ng-scope">Keystore File (UTC / JSON)</span></label>
<label className="radio"><input aria-flowto="aria2" aria-label="private key"
type="radio"
// ng-model="walletType"
value="pasteprivkey"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="135"/><span
// translate=""
className="ng-scope">Private Key</span></label>
<label className="radio">
<input aria-flowto="aria3" aria-label="mnemonic phrase"
type="radio"
// ng-model="walletType"
value="pastemnemonic"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="137"/><span
// translate=""
className="ng-scope">Mnemonic Phrase</span></label>
<label className="radio">
<input aria-flowto="aria4" aria-label="parity phrase"
type="radio"
// ng-model="walletType"
value="parityBWallet"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="139"/><span
// translate=""
className="ng-scope">Parity Phrase</span></label>
<label className="radio"
// ng-hide="globalService.currentTab==globalService.tabs.signMsg.id"
>
<input
aria-flowto="aria5" type="radio"
aria-label="Ledger Nano S hardware wallet"
// ng-model="walletType"
value="ledger"
className="ng-pristine ng-untouched ng-valid ng-empty" name="141"/>Ledger
Nano S</label>
<label className="radio"
// ng-hide="globalService.currentTab==globalService.tabs.signMsg.id"
>
<input
aria-flowto="aria6" type="radio" aria-label="Trezor hardware wallet"
// ng-model="walletType"
value="trezor"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="142"/>TREZOR</label>
<label className="radio"
// ng-hide="globalService.currentTab!==globalService.tabs.viewWalletInfo.id"
>
<input
aria-label="address" type="radio"
// ng-model="walletType"
value="addressOnly"
className="ng-pristine ng-untouched ng-valid ng-empty"
name="143"/><span>View with Address Only</span></label>
</section>
</article>
</div>
</article>
</article>
</div>
</section>
)
}
}

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,2 +1,2 @@
export ViewWallet from './ViewWallet'
export WalletDecrypt from './WalletDecrypt'
export GenerateWallet from './GenerateWallet'

View File

@ -11,31 +11,33 @@ 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();
let store;
const configureStore = () => {
let sagaApplied = applyMiddleware(sagaMiddleware);
const logger = createLogger({
collapsed: true
});
const sagaMiddleware = createSagaMiddleware();
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;
};
@ -52,6 +54,6 @@ renderRoot(Root);
if (module.hot) {
module.hot.accept('reducers/index', () =>
store.replaceReducer(require('reducers/index').default)
);
store.replaceReducer(require('reducers/index').default)
);
}

View File

@ -0,0 +1,8 @@
// @flow
export default class BaseNode {
// FIXME bignumber?
queryBalance(address: string): Promise<number> {
throw 'Implement me';
}
}

View File

@ -0,0 +1,3 @@
// @flow
export { default as BaseNode } from './base';
export { default as RPCNode } from './rpc';

10
common/libs/nodes/rpc.js Normal file
View File

@ -0,0 +1,10 @@
// @flow
import BaseNode from './base';
export default class RPCNode extends BaseNode {
endpoint: string;
constructor(endpoint: string) {
super();
this.endpoint = endpoint;
}
}

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

@ -1,36 +1,39 @@
// @flow
import {
CONFIG_LANGUAGE_CHANGE,
CONFIG_NODE_CHANGE
} from 'actions/config';
import {languages, nodeList} from '../config/data';
import type { ConfigAction, ChangeNodeAction, ChangeLanguageAction } from 'actions/config';
import { languages, NODES } from '../config/data';
export type State = {
// FIXME
languageSelection: string,
nodeSelection: string
};
const initialState: State = {
languageSelection: languages[0].sign,
nodeSelection: Object.keys(NODES)[0]
};
function changeLanguage(state: State, action: ChangeLanguageAction): State {
return {
...state,
languageSelection: action.value
};
}
const initialState = {
languageSelection: languages[0],
nodeSelection: nodeList[0]
function changeNode(state: State, action: ChangeNodeAction): State {
return {
...state,
nodeSelection: action.value
};
}
export function config(state: State = initialState, action): State {
export function config(state: State = initialState, action: ConfigAction): State {
switch (action.type) {
case CONFIG_LANGUAGE_CHANGE: {
return {
...state,
languageSelection: action.value
}
}
case CONFIG_NODE_CHANGE: {
return {
...state,
nodeSelection: action.value
}
}
case 'CONFIG_LANGUAGE_CHANGE':
return changeLanguage(state, action);
case 'CONFIG_NODE_CHANGE':
return changeNode(state, action);
default:
return state
return state;
}
}

View File

@ -13,15 +13,19 @@ 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';
export type State = {
generateWallet: GenerateWalletState,
conig: ConfigState,
config: 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
});

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

@ -0,0 +1,36 @@
// @flow
import type { WalletAction, SaveWalletAction, InitWalletAction } from 'actions/wallet';
import BaseWallet from 'libs/wallet/base';
export type State = {
inst: ?BaseWallet,
balance: number,
tokens: {
[string]: number
}
};
const initialState: State = {
inst: null,
balance: 0,
tokens: {}
};
function saveWallet(state: State, action: SaveWalletAction): State {
return { ...state, inst: action.payload };
}
function initWallet(state: State): State {
return { ...state, balance: 0, tokens: {} };
}
export function wallet(state: State = initialState, action: WalletAction): State {
switch (action.type) {
case 'WALLET_SAVE':
return saveWallet(state, action);
case 'WALLET_INIT':
return initWallet(state);
default:
return state;
}
}

View File

@ -1,24 +1,26 @@
// @flow
import { takeEvery, call, put, select } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import type { Effect } from 'redux-saga/effects';
import { cacheEnsAddress } from 'actions/ens';
import type { ResolveEnsNameAction } from 'actions/ens';
import { getEnsAddress } from 'selectors/ens';
import { donationAddressMap } from 'config/data';
function* resolveEns(action: ResolveEnsNameAction) {
const ensName = action.payload;
// FIXME Add resolve logic
//// _ens.getAddress(scope.addressDrtv.ensAddressField, function(data) {
// if (data.error) uiFuncs.notifier.danger(data.msg);
// else if (data.data == '0x0000000000000000000000000000000000000000' || data.data == '0x') {
// setValue('0x0000000000000000000000000000000000000000');
// scope.addressDrtv.derivedAddress = '0x0000000000000000000000000000000000000000';
// scope.addressDrtv.showDerivedAddress = true;
// } else {
// setValue(data.data);
// scope.addressDrtv.derivedAddress = ethUtil.toChecksumAddress(data.data);
// scope.addressDrtv.showDerivedAddress = true;
function* resolveEns(action?: ResolveEnsNameAction) {
if (!action) return;
const ensName = action.payload;
// FIXME Add resolve logic
//// _ens.getAddress(scope.addressDrtv.ensAddressField, function(data) {
// if (data.error) uiFuncs.notifier.danger(data.msg);
// else if (data.data == '0x0000000000000000000000000000000000000000' || data.data == '0x') {
// setValue('0x0000000000000000000000000000000000000000');
// scope.addressDrtv.derivedAddress = '0x0000000000000000000000000000000000000000';
// scope.addressDrtv.showDerivedAddress = true;
// } else {
// setValue(data.data);
// scope.addressDrtv.derivedAddress = ethUtil.toChecksumAddress(data.data);
// scope.addressDrtv.showDerivedAddress = true;
const cachedEnsAddress = yield select(getEnsAddress, ensName);
@ -29,6 +31,6 @@ function* resolveEns(action: ResolveEnsNameAction) {
yield put(cacheEnsAddress(ensName, donationAddressMap.ETH));
}
export default function* notificationsSaga() {
yield takeEvery('ENS_RESOLVE', resolveEns);
export default function* notificationsSaga(): Generator<Effect, void, any> {
yield takeEvery('ENS_RESOLVE', resolveEns);
}

View File

@ -1,10 +1,12 @@
// @flow
import { takeEvery, put, call } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import type { Effect } from 'redux-saga/effects';
import { closeNotification } from 'actions/notifications';
import type { ShowNotificationAction } from 'actions/notifications';
function* handleNotification(action: ShowNotificationAction) {
function* handleNotification(action?: ShowNotificationAction) {
if (!action) return;
const { duration } = action.payload;
// show forever
if (duration === 0) {
@ -16,6 +18,6 @@ function* handleNotification(action: ShowNotificationAction) {
yield put(closeNotification(action.payload));
}
export default function* notificationsSaga() {
export default function* notificationsSaga(): Generator<Effect, void, any> {
yield takeEvery('SHOW_NOTIFICATION', handleNotification);
}

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

@ -0,0 +1,26 @@
// @flow
import { takeEvery, call, put, select } from 'redux-saga/effects';
import type { Effect } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { saveWallet, initWallet } from 'actions/wallet';
import type { UnlockPrivateKeyAction } from 'actions/wallet';
import PrivKeyWallet from 'libs/wallet/privkey';
function* init() {
yield put(initWallet());
// const node = select(getNode);
// yield call();
// fetch balance,
// fetch tokens
yield delay(100);
}
function* unlockPrivateKey(action?: UnlockPrivateKeyAction) {
if (!action) return;
yield put(saveWallet(new PrivKeyWallet(action.payload)));
yield call(init);
}
export default function* notificationsSaga(): Generator<Effect, void, any> {
yield takeEvery('WALLET_UNLOCK_PRIVATE_KEY', unlockPrivateKey);
}

View File

@ -0,0 +1,8 @@
// @flow
import type { State } from 'reducers';
import { BaseNode } from 'libs/nodes';
import { NODES } from 'config/data';
export function getNodeLib(state: State): BaseNode {
return NODES[state.config.nodeSelection].lib;
}

View File

@ -37,7 +37,9 @@ export function setLanguage(code: string) {
export default function translate(key: string) {
return markupToReact(
repository[activeLanguage][key] || repository[fallbackLanguage][key] || key
(repository[activeLanguage] && repository[activeLanguage][key]) ||
repository[fallbackLanguage][key] ||
key
);
}

508
flow-typed/redux-saga_v0.14.x.js vendored Normal file
View File

@ -0,0 +1,508 @@
declare type ReduxSaga$Predicate<T> = (arg: T) => boolean;
declare interface ReduxSaga$Task {
isRunning(): boolean,
isCancelled(): boolean,
result(): any,
result<T>(): T,
error(): any,
done: Promise<any>,
cancel(): void
}
declare interface ReduxSaga$Buffer<T> {
isEmpty(): boolean,
put(message: T): void,
take(): T
}
declare interface ReduxSaga$Channel<T> {
take(cb: (message: T) => void, matcher?: ReduxSaga$Predicate<T>): void,
put(message: T): void,
close(): void
}
declare module 'redux-saga/effects' {
declare type Predicate<T> = ReduxSaga$Predicate<T>;
declare type Task = ReduxSaga$Task;
declare type Buffer<T> = ReduxSaga$Buffer<T>;
declare type Channel<T> = ReduxSaga$Channel<T>;
declare type Action = { type: $Subtype<string> };
declare type Pattern<T> = string | Predicate<T> | (string | Predicate<T>)[];
declare type Effect =
| TakeEffect<any>
| PutEffect<any>
| RaceEffect
| CallEffect
| CpsEffect
| ForkEffect
| JoinEffect
| CancelEffect
| SelectEffect
| ActionChannelEffect<any>
| CancelledEffect
| FlushEffect<any>;
// take
declare interface TakeEffectDescriptor<T> {
pattern: Pattern<T>,
channel: Channel<T>,
maybe?: boolean
}
declare interface TakeEffect<T> {
TAKE: TakeEffectDescriptor<T>
}
declare var take: {
<T>(pattern: Pattern<T>): TakeEffect<T>,
<T>(channel: Channel<T>): TakeEffect<T>,
maybe: {
<T>(pattern: Pattern<T>): TakeEffect<T>,
<T>(channel: Channel<T>): TakeEffect<T>
}
};
declare var takem: void;
// put
declare interface PutEffectDescriptor<T> {
action: T,
channel: Channel<T>
}
declare interface PutEffect<T> {
PUT: PutEffectDescriptor<T>
}
declare var put: {
<T: Action>(action: T): PutEffect<T>,
<T: Action>(channel: Channel<T>, action: T): PutEffect<T>,
resolve: {
<T: Action>(action: T): PutEffect<T>,
<T: Action>(channel: Channel<T>, action: T): PutEffect<T>
},
sync: void
};
// race
declare type RaceEffectDescriptor = { [key: string]: Effect };
declare interface RaceEffect {
RACE: RaceEffectDescriptor
}
declare function race(effects: { [key: string]: Effect }): RaceEffect;
// call & apply
declare interface CallEffectDescriptor {
context: any,
fn: Function,
args: any[]
}
declare type Collable0 = () => any;
declare type Collable1<A> = (a: A) => any;
declare type Collable2<A, B> = (a: A, b: B) => any;
declare type Collable3<A, B, C> = (a: A, b: B, c: C) => any;
declare type Collable4<A, B, C, D> = (a: A, b: B, c: C, d: D) => any;
declare type Collable5<A, B, C, D, E> = (a: A, b: B, c: C, d: D, e: E) => any;
declare type CollableR = (...args: mixed[]) => any;
declare type CallEffectArg<F> = F | [any, F] | { context: any, fn: F };
declare interface CallEffect {
CALL: CallEffectDescriptor
}
declare type CallEffectFactory<R> = {
(fn: CallEffectArg<Collable0>): R,
<A>(fn: CallEffectArg<Collable1<A>>, a: A): R,
<A, B>(fn: CallEffectArg<Collable2<A, B>>, a: A, b: B): R,
<A, B, C>(fn: CallEffectArg<Collable3<A, B, C>>, a: A, b: B, c: C): R,
<A, B, C, D>(fn: CallEffectArg<Collable4<A, B, C, D>>, a: A, b: B, c: C, d: D): R,
<A, B, C, D, E>(
fn: CallEffectArg<Collable5<A, B, C, D, E>>,
a: A,
b: B,
c: C,
d: D,
e: E
): R,
(fn: CallEffectArg<CollableR>, ...args: any[]): R
};
declare var call: CallEffectFactory<CallEffect>;
declare var apply: {
(context: any, fn: Collable0): CallEffect,
<A>(context: any, fn: Collable1<A>, args: [A]): CallEffect,
<A, B>(context: any, fn: Collable2<A, B>, args: [A, B]): CallEffect,
<A, B, C>(context: any, fn: Collable3<A, B, C>, args: [A, B, C]): CallEffect,
<A, B, C, D>(context: any, fn: Collable4<A, B, C, D>, args: [A, B, C, D]): CallEffect,
<A, B, C, D, E>(
context: any,
fn: Collable5<A, B, C, D, E>,
args: [A, B, C, D, E]
): CallEffect,
(context: any, fn: CollableR, args: any[]): CallEffect
};
// cps
declare interface CpsEffect {
CPS: CallEffectDescriptor
}
declare type CpsCallback = (error: any, result: any) => void;
declare var cps: {
(fn: CallEffectArg<Collable1<CpsCallback>>): CpsEffect,
<A>(fn: CallEffectArg<Collable2<A, CpsCallback>>, a: A): CpsEffect,
<A, B>(fn: CallEffectArg<Collable3<A, B, CpsCallback>>, a: A, b: B): CpsEffect,
<A, B, C>(fn: CallEffectArg<Collable4<A, B, C, CpsCallback>>, a: A, b: B, c: C): CpsEffect,
<A, B, C, D>(
fn: CallEffectArg<Collable5<A, B, C, D, CpsCallback>>,
a: A,
b: B,
c: C,
d: D
): CpsEffect
};
// fork & spawn
declare interface ForkEffectDescriptor extends CallEffectDescriptor {
detached?: boolean
}
declare interface ForkEffect {
FORK: ForkEffectDescriptor
}
declare var fork: CallEffectFactory<ForkEffect>;
declare var spawn: CallEffectFactory<ForkEffect>;
// join
declare interface JoinEffect {
JOIN: Task
}
declare function join(task: Task): JoinEffect;
// cancel
declare interface CancelEffect {
CANCEL: Task
}
declare function cancel(task: Task): CancelEffect;
// select
declare interface SelectEffectDescriptor {
selector(state: any, ...args: any[]): any,
args: any[]
}
declare interface SelectEffect {
SELECT: SelectEffectDescriptor
}
declare var select: {
(): SelectEffect,
<S>(selector: Collable1<S>): SelectEffect,
<S, A>(selector: Collable2<S, A>, a: A): SelectEffect,
<S, A, B>(selector: Collable3<S, A, B>, a: A, b: B): SelectEffect,
<S, A, B, C>(selector: Collable4<S, A, B, C>, a: A, b: B, c: C): SelectEffect,
<S, A, B, C, D>(selector: Collable5<S, A, B, C, D>, a: A, b: B, c: C, d: D): SelectEffect,
(selector: CollableR, ...rest: any[]): SelectEffect
};
// actionChannel
declare interface ActionChannelEffectDescriptor<T> {
pattern: Pattern<T>,
buffer: Buffer<T>
}
declare interface ActionChannelEffect<T> {
ACTION_CHANNEL: ActionChannelEffectDescriptor<T>
}
declare function actionChannel<T>(
pattern: Pattern<T>,
buffer?: Buffer<T>
): ActionChannelEffect<T>;
// actionChannel
declare interface CancelledEffect {
CANCELLED: {}
}
declare function cancelled(): CancelledEffect;
// flush
declare interface FlushEffect<T> {
FLUSH: Channel<T>
}
declare function flush<T>(channel: Channel<T>): FlushEffect<T>;
// takeEvery & takeLatest
declare type Workable0<A> = (action?: A) => any;
declare type Workable1<A, B> = (b: B, action?: A) => any;
declare type Workable2<A, B, C> = (b: B, c: C, action?: A) => any;
declare type Workable3<A, B, C, D> = (b: B, c: C, d: D, action?: A) => any;
declare type Workable4<A, B, C, D, E> = (b: B, c: C, d: D, e: E, action?: A) => any;
declare type WorkableR<A, B, C, D, E, F> = (
b: B,
c: C,
d: D,
e: E,
f: F,
...args: mixed[]
) => any;
declare interface TakeHelper {
<A>(pattern: Pattern<A>, worker: Workable0<A>): ForkEffect,
<A, B>(pattern: Pattern<A>, worker: Workable1<A, B>, b: B): ForkEffect,
<A, B, C>(pattern: Pattern<A>, worker: Workable2<A, B, C>, b: B, c: C): ForkEffect,
<A, B, C, D>(
pattern: Pattern<A>,
worker: Workable3<A, B, C, D>,
b: B,
c: C,
d: D
): ForkEffect,
<A, B, C, D, E>(
pattern: Pattern<A>,
worker: Workable4<A, B, C, D, E>,
b: B,
c: C,
d: D,
e: E
): ForkEffect,
<A, B, C, D, E, F>(
pattern: Pattern<A>,
worker: WorkableR<A, B, C, D, E, F>,
b: B,
c: C,
d: D,
e: E,
f: F,
...rest: any[]
): ForkEffect
}
declare var takeEvery: TakeHelper;
declare var takeLatest: TakeHelper;
// throttle
declare var throttle: {
<A>(ms: number, pattern: Pattern<A>, worker: Workable0<A>): ForkEffect,
<A, B>(ms: number, pattern: Pattern<A>, worker: Workable1<A, B>, b: B): ForkEffect,
<A, B, C>(
ms: number,
pattern: Pattern<A>,
worker: Workable2<A, B, C>,
b: B,
c: C
): ForkEffect,
<A, B, C, D>(
ms: number,
pattern: Pattern<A>,
worker: Workable3<A, B, C, D>,
b: B,
c: C,
d: D
): ForkEffect,
<A, B, C, D, E>(
ms: number,
pattern: Pattern<A>,
worker: Workable4<A, B, C, D, E>,
b: B,
c: C,
d: D,
e: E
): ForkEffect,
<A, B, C, D, E, F>(
ms: number,
pattern: Pattern<A>,
worker: WorkableR<A, B, C, D, E, F>,
b: B,
c: C,
d: D,
e: E,
f: F,
...rest: any[]
): ForkEffect
};
}
declare module 'redux-saga/utils' {
import type {
Effect,
TakeEffectDescriptor,
PutEffectDescriptor,
RaceEffectDescriptor,
CallEffectDescriptor,
ForkEffectDescriptor,
SelectEffectDescriptor,
ActionChannelEffectDescriptor
} from 'redux-saga/effects';
declare type Task = ReduxSaga$Task;
declare type Channel<T> = ReduxSaga$Channel<T>;
declare type Is = ReduxSaga$Predicate<*>;
declare interface Deferred<R> {
resolve(result: R): void,
reject(error: any): void,
promise: Promise<R>
}
declare interface MockTask extends Task {
setRunning(running: boolean): void,
setResult(result: any): void,
setError(error: any): void
}
declare var TASK: '@@redux-saga/TASK';
declare var SAGA_ACTION: '@@redux-saga/SAGA_ACTION';
declare function noop(): void;
declare var is: {
undef: Is,
notUndef: Is,
func: Is,
number: Is,
array: Is,
promise: Is,
iterator: Is,
task: Is,
observable: Is,
buffer: Is,
pattern: Is,
channel: Is,
helper: Is,
stringableFunc: Is
};
declare function deferred<T, R>(props?: T): T & Deferred<R>;
declare function arrayOfDeffered<T>(length: number): Deferred<T>[];
declare function createMockTask(): MockTask;
declare var asEffect: {
take<T>(effect: Effect): ?TakeEffectDescriptor<T>,
put<T>(effect: Effect): ?PutEffectDescriptor<T>,
race(effect: Effect): ?RaceEffectDescriptor,
call(effect: Effect): ?CallEffectDescriptor,
cps(effect: Effect): ?CallEffectDescriptor,
fork(effect: Effect): ?ForkEffectDescriptor,
join(effect: Effect): ?Task,
cancel(effect: Effect): ?Task,
select(effect: Effect): ?SelectEffectDescriptor,
actionChannel<T>(effect: Effect): ?ActionChannelEffectDescriptor<T>,
cancelled(effect: Effect): ?{},
flush<T>(effect: Effect): ?Channel<T>
};
declare var CHANNEL_END: {
toString(): '@@redux-saga/CHANNEL_END'
};
}
declare module 'redux-saga' {
import type { Middleware } from 'redux';
import type { Effect } from 'redux-saga/effects';
import typeof * as Effects from 'redux-saga/effects';
import typeof * as Utils from 'redux-saga/utils';
declare export type Predicate<T> = ReduxSaga$Predicate<T>;
declare export type Task = ReduxSaga$Task;
declare export type Buffer<T> = ReduxSaga$Buffer<T>;
declare export type Channel<T> = ReduxSaga$Channel<T>;
declare export interface SagaMonitor {
effectTriggered(options: {
effectId: number,
parentEffectId: number,
label: string,
root?: boolean,
effect: Effect
}): void,
effectResolved(effectId: number, result: any): void,
effectRejected(effectId: number, err: any): void,
effectCancelled(effectId: number): void,
actionDispatched<A>(action: A): void
}
declare type Saga0 = () => Generator<*, *, *>;
declare type Saga1<A> = (a: A) => Generator<*, *, *>;
declare type Saga2<A, B> = (a: A, b: B) => Generator<*, *, *>;
declare type Saga3<A, B, C> = (a: A, b: B, c: C) => Generator<*, *, *>;
declare type Saga4<A, B, C, D> = (a: A, b: B, c: C, d: D) => Generator<*, *, *>;
declare type SagaR = (...args: mixed[]) => Generator<*, *, *>;
declare export type SagaMiddleware<S, A> = Middleware<S, A> & {
run(saga: Saga0): Task,
run<A>(saga: Saga1<A>, a: A): Task,
run<A, B>(saga: Saga2<A, B>, a: A, B: B): Task,
run<A, B, C>(saga: Saga3<A, B, C>, a: A, B: B, c: C): Task,
run<A, B, C, T4>(saga: Saga4<A, B, C, T4>, a: A, B: B, c: C, d: T4): Task,
run(saga: SagaR, ...args: any[]): Task
};
declare export type Emit<T> = (input: T) => void;
declare export default function createSagaMiddleware<T>(options?: {
sagaMonitor?: SagaMonitor,
emitter: (emit: Emit<T>) => Emit<T>
}): SagaMiddleware<*, *>;
declare export type Unsubscribe = () => void;
declare export type Subscribe<T> = (cb: (input: T) => void) => Unsubscribe;
declare export type Logger = (level: 'info' | 'warning' | 'error', ...args: Array<any>) => void;
declare export function runSaga<S, SA, DA>(
saga: Generator<*, *, *>,
io: {
subscribe?: Subscribe<SA>,
dispatch?: (input: DA) => any,
getState?: () => S,
sagaMonitor?: SagaMonitor,
logger?: Logger,
onError?: void
}
): Task;
declare export var END: { type: '@@redux-saga/CHANNEL_END' };
declare export function eventChannel<T>(
subscribe: Subscribe<T>,
buffer?: Buffer<T>,
matcher?: Predicate<T>
): Channel<T>;
declare export function channel<T>(buffer?: Buffer<T>): Channel<T>;
declare export var buffers: {
none<T>(): Buffer<T>,
fixed<T>(limit?: number): Buffer<T>,
dropping<T>(limit?: number): Buffer<T>,
sliding<T>(limit?: number): Buffer<T>,
expanding<T>(limit?: number): Buffer<T>
};
// deprecate
declare export var takeEvery: void;
declare export var takeLatest: void;
declare export var throttle: void;
declare export function delay(ms: number, rest: void): Promise<boolean>;
declare export function delay<T>(ms: number, val: T): Promise<T>;
declare export var CANCEL: '@@redux-saga/cancelPromise';
declare export var effects: Effects;
declare export var utils: Utils;
}

View File

@ -10,6 +10,7 @@
"^routing": "<rootDir>/common/routing",
"^components$": "<rootDir>/common/components",
"^containers$": "<rootDir>/common/containers",
"^translations(.*)": "<rootDir>/common/translations$1"
"^translations(.*)": "<rootDir>/common/translations$1",
"^libs(.*)": "<rootDir>/common/libs$1"
}
}

View File

@ -9,6 +9,7 @@
"ethereumjs-util": "^5.1.2",
"idna-uts46": "^1.1.0",
"lodash": "^4.17.4",
"ethereumjs-wallet": "^0.6.0",
"prop-types": "^15.5.8",
"react": "^15.4.2",
"react-dom": "^15.4.2",

View File

@ -1,12 +1,12 @@
import { CHANGE_LANGUAGE, CONFIG_LANGUAGE_CHANGE } from '../../common/actions/config';
import { changeLanguage } from '../../common/actions/config';
describe('actions', () => {
it('should create an action to change language to index', () => {
const value = { name: 'English', sign: 'en' };
const value = 'en' ;
const expectedAction = {
type: CONFIG_LANGUAGE_CHANGE,
type: 'CONFIG_LANGUAGE_CHANGE',
value
};
expect(CHANGE_LANGUAGE(value)).toEqual(expectedAction);
expect(changeLanguage(value)).toEqual(expectedAction);
});
});