Merge feat/send-page

This commit is contained in:
Daniel Ternyak 2017-07-02 00:49:06 -05:00
commit 7cf3d26bbe
37 changed files with 1774 additions and 454 deletions

View File

@ -30,5 +30,8 @@
"no-unreachable": 1,
"no-alert": 0,
"react/jsx-uses-react": 1
},
"globals": {
"SyntheticInputEvent": false
}
}

36
common/actions/ens.js Normal file
View File

@ -0,0 +1,36 @@
// @flow
export type ResolveEnsNameAction = {
type: 'ENS_RESOLVE',
payload: string
};
export type CacheEnsAddressAction = {
type: 'ENS_CACHE',
payload: {
ensName: string,
address: string
}
};
export type EnsAction = ResolveEnsNameAction | CacheEnsAddressAction;
export function resolveEnsName(name: string): ResolveEnsNameAction {
return {
type: 'ENS_RESOLVE',
payload: name
};
}
export function cacheEnsAddress(
ensName: string,
address: string
): CacheEnsAddressAction {
return {
type: 'ENS_CACHE',
payload: {
ensName,
address
}
};
}

View File

@ -1,98 +1,108 @@
import React, {Component} from 'react';
import {Link} from 'react-router';
import React, { Component } from 'react';
import { Link } from 'react-router';
import translate from 'translations';
import PropTypes from 'prop-types';
const tabs = [
{
name: 'NAV_GenerateWallet',
link: '/'
},
{
name: 'NAV_SendEther'
},
{
name: 'NAV_Swap',
link: 'swap'
},
{
name: 'NAV_Offline'
},
{
name: 'NAV_Contracts'
},
{
name: 'NAV_ViewWallet',
link: 'view-wallet'
},
{
name: 'NAV_Help',
link: 'help'
}
{
name: 'NAV_GenerateWallet',
link: '/'
},
{
name: 'NAV_SendEther',
link: 'send-transaction'
},
{
name: 'NAV_Swap',
link: 'swap'
},
{
name: 'NAV_Offline'
},
{
name: 'NAV_Contracts'
},
{
name: 'NAV_ViewWallet',
link: 'view-wallet'
},
{
name: 'NAV_Help',
link: 'help'
}
];
export default class TabsOptions extends Component {
constructor(props) {
super(props);
this.state = {
showLeftArrow: false,
showRightArrow: false
}
}
static propTypes = {
location: PropTypes.object
constructor(props) {
super(props);
this.state = {
showLeftArrow: false,
showRightArrow: false
};
}
tabClick() {
}
static propTypes = {
location: PropTypes.object
};
scrollLeft() {
}
tabClick() {}
scrollRight() {
}
scrollLeft() {}
render() {
const {location} = this.props;
return (
<div>
<nav role='navigation' aria-label='main navigation' className='container nav-container overflowing'>
{
this.state.showLeftArrow && <a aria-hidden='true'
className='nav-arrow-left'
onClick={() => this.scrollLeft(100)}>&#171;</a>
}
<div className='nav-scroll'>
<ul className='nav-inner'>
{
tabs.map((object, i) => {
// if the window pathname is the same or similar to the tab objects name, set the active toggle
const activeOrNot = (location.pathname === object.link || location.pathname.substring(1) === object.link) ? 'active' : '';
return (
<li className={`nav-item ${activeOrNot}`}
key={i} onClick={this.tabClick(i)}>
<Link to={object.link}
aria-label={`nav item: ${translate(object.name)}`}>
{translate(object.name)}
</Link>
</li>
)
}
)
}
</ul>
</div>
{
this.state.showRightArrow &&
<a aria-hidden='true'
className='nav-arrow-right'
onClick={() => this.scrollRight(100)}>&#187;</a>
}
</nav>
</div>
scrollRight() {}
)
}
render() {
const { location } = this.props;
return (
<div>
<nav
role="navigation"
aria-label="main navigation"
className="container nav-container overflowing"
>
{this.state.showLeftArrow &&
<a
aria-hidden="true"
className="nav-arrow-left"
onClick={() => this.scrollLeft(100)}
>
&#171;
</a>}
<div className="nav-scroll">
<ul className="nav-inner">
{tabs.map((object, i) => {
// if the window pathname is the same or similar to the tab objects name, set the active toggle
const activeOrNot = location.pathname === object.link ||
location.pathname.substring(1) === object.link
? 'active'
: '';
return (
<li
className={`nav-item ${activeOrNot}`}
key={i}
onClick={this.tabClick(i)}
>
<Link
to={object.link}
aria-label={`nav item: ${translate(object.name)}`}
>
{translate(object.name)}
</Link>
</li>
);
})}
</ul>
</div>
{this.state.showRightArrow &&
<a
aria-hidden="true"
className="nav-arrow-right"
onClick={() => this.scrollRight(100)}
>
&#187;
</a>}
</nav>
</div>
);
}
}

View File

@ -3,84 +3,91 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types';
import TabsOptions from './components/TabsOptions';
import { Link } from 'react-router';
import Dropdown from '../ui/Dropdown';
import { Dropdown } from 'components/ui';
import { languages, nodeList } from '../../config/data';
export default class Header extends Component {
static propTypes = {
location: PropTypes.object,
static propTypes = {
location: PropTypes.object,
// Language DropDown
changeLanguage: PropTypes.func,
languageSelection: PropTypes.object,
// Language DropDown
changeLanguage: PropTypes.func,
languageSelection: PropTypes.object,
// Node Dropdown
changeNode: PropTypes.func,
nodeSelection: PropTypes.object
};
// Node Dropdown
changeNode: PropTypes.func,
nodeSelection: PropTypes.object
};
render() {
const { languageSelection, changeLanguage, changeNode, nodeSelection } = this.props;
return (
<div>
<section className="bg-gradient header-branding">
<section className="container">
<Link to={'/'} className="brand" aria-label="Go to homepage">
{/* TODO - don't hardcode image path*/}
<img
src={'https://www.myetherwallet.com/images/logo-myetherwallet.svg'}
height="64px"
width="245px"
alt="MyEtherWallet"
/>
</Link>
<div className="tagline">
<span style={{ maxWidth: '395px' }}>
Open-Source & Client-Side Ether Wallet · v3.6.0
</span>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change language. current language ${languageSelection.name}`}
options={languages}
formatTitle={o => o.name}
value={languageSelection}
extra={[
<li key={'separator'} role="separator" className="divider" />,
<li key={'disclaimer'}>
<a data-toggle="modal" data-target="#disclaimerModal">
Disclaimer
</a>
</li>
]}
onChange={changeLanguage}
/>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change node. current node ${nodeSelection.name} node by ${nodeSelection.service}`}
options={nodeList}
formatTitle={o => [
o.name,
' ',
<small key="service">({o.service})</small>
]}
value={nodeSelection}
extra={
<li>
<a onClick={() => {}}>
Add Custom Node
</a>
</li>
}
onChange={changeNode}
/>
</div>
</section>
</section>
<TabsOptions {...this.props} />
render() {
const {
languageSelection,
changeLanguage,
changeNode,
nodeSelection
} = this.props;
return (
<div>
<section className="bg-gradient header-branding">
<section className="container">
<Link to={'/'} className="brand" aria-label="Go to homepage">
{/* TODO - don't hardcode image path*/}
<img
src={
'https://www.myetherwallet.com/images/logo-myetherwallet.svg'
}
height="64px"
width="245px"
alt="MyEtherWallet"
/>
</Link>
<div className="tagline">
<span style={{ maxWidth: '395px' }}>
Open-Source & Client-Side Ether Wallet · v3.6.0
</span>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change language. current language ${languageSelection.name}`}
options={languages}
formatTitle={o => o.name}
value={languageSelection}
extra={[
<li key={'separator'} role="separator" className="divider" />,
<li key={'disclaimer'}>
<a data-toggle="modal" data-target="#disclaimerModal">
Disclaimer
</a>
</li>
]}
onChange={changeLanguage}
/>
&nbsp;&nbsp;&nbsp;
<Dropdown
ariaLabel={`change node. current node ${nodeSelection.name} node by ${nodeSelection.service}`}
options={nodeList}
formatTitle={o => [
o.name,
' ',
<small key="service">({o.service})</small>
]}
value={nodeSelection}
extra={
<li>
<a onClick={() => {}}>
Add Custom Node
</a>
</li>
}
onChange={changeNode}
/>
</div>
);
}
</section>
</section>
<TabsOptions {...this.props} />
</div>
);
}
}

View File

@ -3,79 +3,79 @@ 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
};
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
};
// FIXME
props: {
value: any,
options: any[],
ariaLabel: string,
formatTitle: (option: any) => any,
extra?: any,
onChange: () => void
};
// FIXME
props: {
value: any,
options: any[],
ariaLabel: string,
formatTitle: (option: any) => any,
extra?: any,
onChange: (value: any) => void
};
state = {
expanded: false
};
state = {
expanded: false
};
render() {
const { options, value, ariaLabel, extra } = this.props;
render() {
const { options, value, ariaLabel, extra } = this.props;
return (
<span className="dropdown">
<a
tabIndex="0"
aria-haspopup="true"
aria-expanded="false"
aria-label={ariaLabel}
className="dropdown-toggle"
onClick={this.toggleExpanded}
>
{this.formatTitle(value)}
<i className="caret" />
</a>
{this.state.expanded &&
<ul className="dropdown-menu">
{options.map((option, i) => {
return (
<li key={i}>
<a
className={option === value ? 'active' : ''}
onClick={this.onChange.bind(null, option)}
>
{this.formatTitle(option)}
</a>
</li>
);
})}
{extra}
</ul>}
</span>
);
}
return (
<span className="dropdown">
<a
tabIndex="0"
aria-haspopup="true"
aria-expanded="false"
aria-label={ariaLabel}
className="dropdown-toggle"
onClick={this.toggleExpanded}
>
{this.formatTitle(value)}
<i className="caret" />
</a>
{this.state.expanded &&
<ul className="dropdown-menu">
{options.map((option, i) => {
return (
<li key={i}>
<a
className={option === value ? 'active' : ''}
onClick={this.onChange.bind(null, option)}
>
{this.formatTitle(option)}
</a>
</li>
);
})}
{extra}
</ul>}
</span>
);
}
formatTitle(option: any) {
return this.props.formatTitle(option);
}
formatTitle(option: any) {
return this.props.formatTitle(option);
}
toggleExpanded = () => {
this.setState(state => {
return {
expanded: !state.expanded
};
});
};
toggleExpanded = () => {
this.setState(state => {
return {
expanded: !state.expanded
};
});
};
onChange = (value: any) => {
this.props.onChange(value);
this.setState({ expanded: false });
};
onChange = (value: any) => {
this.props.onChange(value);
this.setState({ expanded: false });
};
}

View File

@ -0,0 +1,23 @@
// @flow
import React from 'react';
import { toDataUrl } from 'ethereum-blockies';
import { isValidETHAddress } from 'libs/validators';
type Props = {
address: string
};
export default function Identicon(props: Props) {
// FIXME breaks on failed checksums
const style = !isValidETHAddress(props.address)
? {}
: { backgroundImage: `url(${toDataUrl(props.address.toLowerCase())})` };
return (
<div
className="addressIdenticon"
style={style}
title="Address Indenticon"
/>
);
}

View File

@ -0,0 +1,45 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import translate from 'translations';
export default class UnlockHeader extends React.Component {
props: {
title: string
};
static propTypes = {
title: PropTypes.string.isRequired
};
state: {
expanded: boolean
} = {
expanded: true
};
render() {
return (
<article className="collapse-container">
<div onClick={this.toggleExpanded}>
<a className="collapse-button">
<span>{this.state.expanded ? '-' : '+'}</span>
</a>
<h1>{translate(this.props.title)}</h1>
</div>
{this.state.expanded &&
<div>
{/* @@if (site === 'cx' ) { <cx-wallet-decrypt-drtv></cx-wallet-decrypt-drtv> }
@@if (site === 'mew' ) { <wallet-decrypt-drtv></wallet-decrypt-drtv> } */}
</div>}
{this.state.expanded && <hr />}
</article>
);
}
toggleExpanded = () => {
this.setState(state => {
return { expanded: !state.expanded };
});
};
}

View File

@ -0,0 +1,5 @@
// @flow
export { default as Dropdown } from './Dropdown';
export { default as UnlockHeader } from './UnlockHeader';
export { default as Identicon } from './Identicon';

View File

@ -0,0 +1,73 @@
// @flow
import React from 'react';
import { Identicon } from 'components/ui';
import { getEnsAddress } from 'selectors/ens';
import { connect } from 'react-redux';
import type { State } from 'reducers';
import { isValidENSorEtherAddress, isValidENSAddress } from 'libs/validators';
import { resolveEnsName } from 'actions/ens';
type PublicProps = {
placeholder: string,
value: string,
onChange?: (value: string) => void
};
export class AddressField extends React.Component {
props: PublicProps & {
ensAddress: ?string,
resolveEnsName: typeof resolveEnsName
};
render() {
const { placeholder, value, ensAddress } = this.props;
const isReadonly = !this.props.onChange;
return (
<div className="row form-group">
<div className="col-xs-11">
<label translate="SEND_addr"> To Address: </label>
<input
className={`form-control ${isValidENSorEtherAddress(value)
? 'is-valid'
: 'is-invalid'}`}
type="text"
placeholder={placeholder}
onChange={this.onChange}
disabled={isReadonly}
/>
{!!ensAddress &&
<p className="ens-response">
<span className="mono">
{ensAddress}
</span>
</p>}
</div>
<div className="col-xs-1 address-identicon-container">
<Identicon address={ensAddress || value} />
</div>
</div>
);
}
onChange = (e: SyntheticInputEvent) => {
const newValue = e.target.value;
const { onChange } = this.props;
if (!onChange) {
return;
}
// FIXME debounce?
if (isValidENSAddress(newValue)) {
this.props.resolveEnsName(newValue);
}
onChange(newValue);
};
}
function mapStateToProps(state: State, props: PublicProps) {
return {
ensAddress: getEnsAddress(state, props.value)
};
}
export default connect(mapStateToProps, { resolveEnsName })(AddressField);

View File

@ -0,0 +1,68 @@
// @flow
import React from 'react';
import translate from 'translations';
import UnitDropdown from './UnitDropdown';
type Props = {
value: string,
unit: string,
onChange?: (value: string, unit: string) => void
};
export default class AmountField extends React.Component {
props: Props;
render() {
const { value, unit, onChange } = this.props;
const isReadonly = !onChange;
return (
<div>
<label>{translate('SEND_amount')}</label>
<div className="input-group col-sm-11">
<input
className={`form-control ${isFinite(Number(value)) &&
Number(value) > 0
? 'is-valid'
: 'is-invalid'}`}
type="text"
placeholder={translate('SEND_amount_short')}
value={value}
disabled={isReadonly}
onChange={isReadonly ? void 0 : this.onValueChange}
/>
<UnitDropdown
value={unit}
options={['ether']}
onChange={isReadonly ? void 0 : this.onUnitChange}
/>
</div>
{!isReadonly &&
<p>
<a onClick={this.onSendEverything}>
<span className="strong">
{translate('SEND_TransferTotal')}
</span>
</a>
</p>}
</div>
);
}
onUnitChange = (unit: string) => {
if (this.props.onChange) {
this.props.onChange(this.props.value, unit);
}
};
onValueChange = (e: SyntheticInputEvent) => {
if (this.props.onChange) {
this.props.onChange(e.target.value, this.props.unit);
}
};
onSendEverything = () => {
if (this.props.onChange) {
this.props.onChange('everything', this.props.unit);
}
};
}

View File

@ -0,0 +1,21 @@
// @flow
import React from 'react';
type Props = {
message?: {
to: string,
msg: string
}
};
export default function CustomMessage(props: Props) {
return (
<div className="clearfix form-group">
{!!props.message &&
<div className="alert alert-info col-xs-12 clearfix">
<p><small>A message from {props.message.to}</small></p>
<p><strong>{props.message.msg}</strong></p>
</div>}
</div>
);
}

View File

@ -0,0 +1,65 @@
// @flow
import React from 'react';
import translate from 'translations';
import { isValidHex } from 'libs/validators';
export default class DataField extends React.Component {
props: {
value: string,
onChange?: (e: string) => void
};
state = {
expanded: false
};
render() {
const { value } = this.props;
const { expanded } = this.state;
const valid = isValidHex(value || '');
const readOnly = !this.props.onChange;
return (
<div className="row form-group">
<div className="col-sm-11 clearfix">
{!expanded &&
<a onClick={this.expand}>
<p className="strong">
{translate('TRANS_advanced')}
</p>
</a>}
{expanded &&
<section>
<div className="form-group">
<label>
{translate('TRANS_data')}
</label>
<input
className={`form-control ${valid
? 'is-valid'
: 'is-invalid'}`}
type="text"
placeholder={
readOnly
? ''
: '0x6d79657468657277616c6c65742e636f6d20697320746865206265737421'
}
value={value || ''}
disabled={readOnly}
onChange={this.onChange}
/>
</div>
</section>}
</div>
</div>
);
}
expand = () => {
this.setState({ expanded: true });
};
onChange = (e: SyntheticInputEvent) => {
if (this.props.onChange) {
this.props.onChange(e.target.value);
}
};
}

View File

@ -0,0 +1,42 @@
// @flow
import React from 'react';
import translate from 'translations';
export default class Donate extends React.Component {
props: {
onDonate: (address: string, amount: string, unit: string) => void
};
state: {
clicked: boolean
} = {
clicked: false
};
render() {
return (
<div className="well">
<p>
{translate('sidebar_donation')}
</p>
<a className="btn btn-primary btn-block" onClick={this.onClick}>
{translate('sidebar_donate')}
</a>
{this.state.clicked &&
<div className="text-success text-center marg-v-sm">
{translate('sidebar_thanks')}
</div>}
</div>
);
}
onClick = () => {
// FIXME move to config
this.props.onDonate(
'0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
'1',
'ETH'
);
this.setState({ clicked: true });
};
}

View File

@ -0,0 +1,41 @@
// @flow
import React from 'react';
import translate from 'translations';
export default class GasField extends React.Component {
props: {
value: string,
onChange?: (value: string) => void | null
};
render() {
const { value, onChange } = this.props;
const isReadonly = !onChange;
return (
<div className="row form-group">
<div className="col-sm-11 clearfix">
<label>{translate('TRANS_gas')}{' '}
</label>
<input
className={`form-control ${isFinite(parseFloat(value)) &&
parseFloat(value) > 0
? 'is-valid'
: 'is-invalid'}`}
type="text"
placeholder="21000"
disabled={isReadonly}
value={value}
onChange={this.onChange}
/>
</div>
</div>
);
}
onChange = (e: SyntheticInputEvent) => {
if (this.props.onChange) {
this.props.onChange(e.target.value);
}
};
}

View File

@ -0,0 +1,56 @@
// @flow
import React from 'react';
export default class UnitDropdown extends React.Component {
props: {
value: string,
options: string[],
onChange?: (value: string) => void
};
state: {
expanded: boolean
} = {
expanded: false
};
render() {
const { value, options, onChange } = this.props;
const isReadonly = !onChange;
return (
<div className="input-group-btn">
<a
style={{ minWidth: 170 }}
className="btn btn-default dropdown-toggle"
onClick={this.onToggleExpand}
>
<strong>
{value}<i className="caret" />
</strong>
</a>
{this.state.expanded &&
!isReadonly &&
<ul className="dropdown-menu dropdown-menu-right">
{options.map(o =>
<li>
<a
className={value === o ? 'active' : ''}
onClick={this.props.onChange}
>
{o}
</a>
</li>
)}
</ul>}
</div>
);
}
onToggleExpand = () => {
this.setState(state => {
return {
expanded: !state.expanded
};
});
};
}

View File

@ -0,0 +1,7 @@
// @flow
export { default as Donate } from './Donate';
export { default as DataField } from './DataField';
export { default as GasField } from './GasField';
export { default as CustomMessage } from './CustomMessage';
export { default as AmountField } from './AmountField';
export { default as AddressField } from './AddressField';

View File

@ -0,0 +1,278 @@
// @flow
import React from 'react';
import PropTypes from 'prop-types';
import translate from 'translations';
import { UnlockHeader } from 'components/ui';
import {
Donate,
DataField,
CustomMessage,
GasField,
AmountField,
AddressField
} from './components';
import pickBy from 'lodash/pickBy';
// import type { Transaction } from './types';
import customMessages from './messages';
type State = {
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;
}
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
};
componentDidMount() {
const queryPresets = pickBy(this.parseQuery());
if (Object.keys(queryPresets).length) {
this.setState({ ...queryPresets, hasQueryString: true });
}
this.setState(pickBy(queryPresets));
}
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'"
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>}
<UnlockHeader title={'NAV_SendEther'} />
{unlocked &&
<article className="row">
{'' /* <!-- Sidebar --> */}
<section className="col-sm-4">
<div style={{ maxWidth: 350 }}>
{'' /* <wallet-balance-drtv /> */}
<hr />
<Donate onDonate={this.onNewTx} />
</div>
</section>
<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">
<h4 className="col-xs-12">
{translate('SEND_trans')}
</h4>
</div>
<AddressField
placeholder="0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8"
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">
<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" } ) }
@@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');
}
const readOnly = getParam(query, 'readOnly') == null ? false : true;
return { to, data, value, unit, gasLimit, readOnly };
}
// 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;
}
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;

View File

@ -0,0 +1,32 @@
// @flow
export default [
{
// donation address example
to: '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
gasLimit: 21000,
data: '',
msg: 'Thank you for donating to MyEtherWallet. TO THE MOON!'
},
{
// BAT
to: '0x0D8775F648430679A709E98d2b0Cb6250d2887EF',
gasLimit: 200000,
data: '0xb4427263',
msg: 'BAT. THE SALE IS OVER. STOP CLOGGING THE BLOCKCHAIN PLEASE'
},
{
// BANCOR
to: '0x00000',
gasLimit: 200000,
data: '',
msg: 'Bancor. Starts June XX, 2017.'
},
{
// Moeda
to: '0x4870E705a3def9DDa6da7A953D1cd3CCEDD08573',
gasLimit: 200000,
data: '',
msg: 'Moeda. Ends at block 4,111,557.'
}
];

View File

@ -0,0 +1,355 @@
'use strict';
var sendTxCtrl = function($scope, $sce, walletService) {
$scope.ajaxReq = ajaxReq;
$scope.unitReadable = ajaxReq.type;
$scope.sendTxModal = new Modal(document.getElementById('sendTransaction'));
walletService.wallet = null;
walletService.password = '';
$scope.showAdvance = $scope.showRaw = false;
$scope.dropdownEnabled = true;
$scope.Validator = Validator;
$scope.gasLimitChanged = false;
// Tokens
$scope.tokenVisibility = 'hidden';
$scope.tokenTx = {
to: '',
value: 0,
id: -1
};
$scope.customGasMsg = '';
// For token sale holders:
// 1. Add the address users are sending to
// 2. Add the gas limit users should use to send successfully (this avoids OOG errors)
// 3. Add any data if applicable
// 4. Add a message if you want.
$scope.tx = {
// if there is no gasLimit or gas key in the URI, use the default value. Otherwise use value of gas or gasLimit. gasLimit wins over gas if both present
gasLimit: globalFuncs.urlGet('gaslimit') != null ||
globalFuncs.urlGet('gas') != null
? globalFuncs.urlGet('gaslimit') != null
? globalFuncs.urlGet('gaslimit')
: globalFuncs.urlGet('gas')
: globalFuncs.defaultTxGasLimit,
data: globalFuncs.urlGet('data') == null ? '' : globalFuncs.urlGet('data'),
to: globalFuncs.urlGet('to') == null ? '' : globalFuncs.urlGet('to'),
unit: 'ether',
value: globalFuncs.urlGet('value') == null
? ''
: globalFuncs.urlGet('value'),
nonce: null,
gasPrice: null,
donate: false,
tokenSymbol: globalFuncs.urlGet('tokenSymbol') == null
? false
: globalFuncs.urlGet('tokenSymbol'),
readOnly: globalFuncs.urlGet('readOnly') == null ? false : true
};
$scope.setSendMode = function(sendMode, tokenId = '', tokenSymbol = '') {
$scope.tx.sendMode = sendMode;
$scope.unitReadable = '';
if (sendMode == 'ether') {
$scope.unitReadable = ajaxReq.type;
} else {
$scope.unitReadable = tokenSymbol;
$scope.tokenTx.id = tokenId;
}
$scope.dropdownAmount = false;
};
$scope.setTokenSendMode = function() {
if ($scope.tx.sendMode == 'token' && !$scope.tx.tokenSymbol) {
$scope.tx.tokenSymbol = $scope.wallet.tokenObjs[0].symbol;
$scope.wallet.tokenObjs[0].type = 'custom';
$scope.setSendMode($scope.tx.sendMode, 0, $scope.tx.tokenSymbol);
} else if ($scope.tx.tokenSymbol) {
for (var i = 0; i < $scope.wallet.tokenObjs.length; i++) {
if (
$scope.wallet.tokenObjs[i].symbol
.toLowerCase()
.indexOf($scope.tx.tokenSymbol.toLowerCase()) !== -1
) {
$scope.wallet.tokenObjs[i].type = 'custom';
$scope.setSendMode('token', i, $scope.wallet.tokenObjs[i].symbol);
break;
} else $scope.tokenTx.id = -1;
}
}
if ($scope.tx.sendMode != 'token') $scope.tokenTx.id = -1;
};
var applyScope = function() {
if (!$scope.$$phase) $scope.$apply();
};
var defaultInit = function() {
globalFuncs.urlGet('sendMode') == null
? $scope.setSendMode('ether')
: $scope.setSendMode(globalFuncs.urlGet('sendMode'));
$scope.showAdvance =
globalFuncs.urlGet('gaslimit') != null ||
globalFuncs.urlGet('gas') != null ||
globalFuncs.urlGet('data') != null;
if (
globalFuncs.urlGet('data') ||
globalFuncs.urlGet('value') ||
globalFuncs.urlGet('to') ||
globalFuncs.urlGet('gaslimit') ||
globalFuncs.urlGet('sendMode') ||
globalFuncs.urlGet('gas') ||
globalFuncs.urlGet('tokenSymbol')
)
$scope.hasQueryString = true; // if there is a query string, show an warning at top of page
};
$scope.$watch(
function() {
if (walletService.wallet == null) return null;
return walletService.wallet.getAddressString();
},
function() {
if (walletService.wallet == null) return;
$scope.wallet = walletService.wallet;
$scope.wd = true;
$scope.wallet.setBalance(applyScope);
$scope.wallet.setTokens();
if ($scope.parentTxConfig) {
var setTxObj = function() {
$scope.tx.to = $scope.parentTxConfig.to;
$scope.tx.value = $scope.parentTxConfig.value;
$scope.tx.sendMode = $scope.parentTxConfig.sendMode
? $scope.parentTxConfig.sendMode
: 'ether';
$scope.tx.tokenSymbol = $scope.parentTxConfig.tokenSymbol
? $scope.parentTxConfig.tokenSymbol
: '';
$scope.tx.readOnly = $scope.parentTxConfig.readOnly
? $scope.parentTxConfig.readOnly
: false;
};
$scope.$watch(
'parentTxConfig',
function() {
setTxObj();
},
true
);
}
$scope.setTokenSendMode();
defaultInit();
}
);
$scope.$watch('ajaxReq.key', function() {
if ($scope.wallet) {
$scope.setSendMode('ether');
$scope.wallet.setBalance(applyScope);
$scope.wallet.setTokens();
}
});
$scope.$watch(
'tokenTx',
function() {
if (
$scope.wallet &&
$scope.wallet.tokenObjs !== undefined &&
$scope.wallet.tokenObjs[$scope.tokenTx.id] !== undefined &&
$scope.Validator.isValidAddress($scope.tokenTx.to) &&
$scope.Validator.isPositiveNumber($scope.tokenTx.value)
) {
if ($scope.estimateTimer) clearTimeout($scope.estimateTimer);
$scope.estimateTimer = setTimeout(function() {
$scope.estimateGasLimit();
}, 500);
}
},
true
);
$scope.$watch(
'tx',
function(newValue, oldValue) {
$scope.showRaw = false;
if (
oldValue.sendMode != newValue.sendMode &&
newValue.sendMode == 'ether'
) {
$scope.tx.data = '';
$scope.tx.gasLimit = globalFuncs.defaultTxGasLimit;
}
if (
newValue.gasLimit == oldValue.gasLimit &&
$scope.wallet &&
$scope.Validator.isValidAddress($scope.tx.to) &&
$scope.Validator.isPositiveNumber($scope.tx.value) &&
$scope.Validator.isValidHex($scope.tx.data) &&
$scope.tx.sendMode != 'token'
) {
if ($scope.estimateTimer) clearTimeout($scope.estimateTimer);
$scope.estimateTimer = setTimeout(function() {
$scope.estimateGasLimit();
}, 500);
}
if ($scope.tx.sendMode == 'token') {
$scope.tokenTx.to = $scope.tx.to;
$scope.tokenTx.value = $scope.tx.value;
}
},
true
);
$scope.estimateGasLimit = function() {
$scope.customGasMsg = '';
if ($scope.gasLimitChanged) return;
for (var i in $scope.customGas) {
if ($scope.tx.to.toLowerCase() == $scope.customGas[i].to.toLowerCase()) {
$scope.showAdvance = $scope.customGas[i].data != '' ? true : false;
$scope.tx.gasLimit = $scope.customGas[i].gasLimit;
$scope.tx.data = $scope.customGas[i].data;
$scope.customGasMsg = $scope.customGas[i].msg != ''
? $scope.customGas[i].msg
: '';
return;
}
}
if (globalFuncs.lightMode) {
$scope.tx.gasLimit = globalFuncs.defaultTokenGasLimit;
return;
}
var estObj = {
to: $scope.tx.to,
from: $scope.wallet.getAddressString(),
value: ethFuncs.sanitizeHex(
ethFuncs.decimalToHex(etherUnits.toWei($scope.tx.value, $scope.tx.unit))
)
};
if ($scope.tx.data != '')
estObj.data = ethFuncs.sanitizeHex($scope.tx.data);
if ($scope.tx.sendMode == 'token') {
estObj.to = $scope.wallet.tokenObjs[
$scope.tokenTx.id
].getContractAddress();
estObj.data = $scope.wallet.tokenObjs[$scope.tokenTx.id].getData(
$scope.tokenTx.to,
$scope.tokenTx.value
).data;
estObj.value = '0x00';
}
ethFuncs.estimateGas(estObj, function(data) {
uiFuncs.notifier.close();
if (!data.error) {
if (data.data == '-1')
$scope.notifier.danger(globalFuncs.errorMsgs[21]);
$scope.tx.gasLimit = data.data;
} else $scope.notifier.danger(data.msg);
});
};
var isEnough = function(valA, valB) {
return new BigNumber(valA).lte(new BigNumber(valB));
};
$scope.hasEnoughBalance = function() {
if ($scope.wallet.balance == 'loading') return false;
return isEnough($scope.tx.value, $scope.wallet.balance);
};
$scope.generateTx = function() {
if (!$scope.Validator.isValidAddress($scope.tx.to)) {
$scope.notifier.danger(globalFuncs.errorMsgs[5]);
return;
}
var txData = uiFuncs.getTxData($scope);
if ($scope.tx.sendMode == 'token') {
// if the amount of tokens you are trying to send > tokens you have, throw error
if (
!isEnough(
$scope.tx.value,
$scope.wallet.tokenObjs[$scope.tokenTx.id].balance
)
) {
$scope.notifier.danger(globalFuncs.errorMsgs[0]);
return;
}
txData.to = $scope.wallet.tokenObjs[
$scope.tokenTx.id
].getContractAddress();
txData.data = $scope.wallet.tokenObjs[$scope.tokenTx.id].getData(
$scope.tokenTx.to,
$scope.tokenTx.value
).data;
txData.value = '0x00';
}
uiFuncs.generateTx(txData, function(rawTx) {
if (!rawTx.isError) {
$scope.rawTx = rawTx.rawTx;
$scope.signedTx = rawTx.signedTx;
$scope.showRaw = true;
} else {
$scope.showRaw = false;
$scope.notifier.danger(rawTx.error);
}
if (!$scope.$$phase) $scope.$apply();
});
};
$scope.sendTx = function() {
$scope.sendTxModal.close();
uiFuncs.sendTx($scope.signedTx, function(resp) {
if (!resp.isError) {
var bExStr = $scope.ajaxReq.type != nodes.nodeTypes.Custom
? "<a class='strong' href='" +
$scope.ajaxReq.blockExplorerTX.replace('[[txHash]]', resp.data) +
"' class='strong' target='_blank'>View TX</a><br />"
: '';
var emailLink =
'<a class="strong" href="mailto:support@myetherwallet.com?Subject=Issue%20regarding%20my%20TX%20&Body=Hi%20Taylor%2C%20%0A%0AI%20have%20a%20question%20concerning%20my%20transaction.%20%0A%0AI%20was%20attempting%20to%3A%0A-%20Send%20ETH%0A-%20Send%20Tokens%0A-%20Send%20via%20my%20Ledger%0A-%20Send%20via%20my%20TREZOR%0A-%20Send%20via%20the%20offline%20tab%0A%0AFrom%20address%3A%20%0A%0ATo%20address%3A%20%0A%0AUnfortunately%20it%3A%0A-%20Never%20showed%20on%20the%20blockchain%0A-%20Failed%20due%20to%20out%20of%20gas%0A-%20Failed%20for%20another%20reason%0A-%20Never%20showed%20up%20in%20the%20account%20I%20was%20sending%20to%0A%0A%5B%20INSERT%20MORE%20INFORMATION%20HERE%20%5D%0A%0AThank%20you%0A%0A' +
'%0A%20TO%20' +
$scope.tx.to +
'%0A%20FROM%20' +
$scope.wallet.getAddressString() +
'%0A%20AMT%20' +
$scope.tx.value +
'%0A%20CUR%20' +
$scope.unitReadable +
'%0A%20NODE%20TYPE%20' +
$scope.ajaxReq.type +
'%0A%20TOKEN%20' +
$scope.tx.tokenSymbol +
'%0A%20TOKEN%20TO%20' +
$scope.tokenTx.to +
'%0A%20TOKEN%20AMT%20' +
$scope.tokenTx.value +
'%0A%20TOKEN%20CUR%20' +
$scope.unitReadable +
'%0A%20TX%20' +
resp.data +
'" target="_blank">Confused? Email Us.</a>';
$scope.notifier.success(
globalFuncs.successMsgs[2] +
resp.data +
'<p>' +
bExStr +
'</p><p>' +
emailLink +
'</p>'
);
$scope.wallet.setBalance(applyScope);
if ($scope.tx.sendMode == 'token')
$scope.wallet.tokenObjs[$scope.tokenTx.id].setBalance();
} else {
$scope.notifier.danger(resp.error);
}
});
};
$scope.transferAllBalance = function() {
if ($scope.tx.sendMode != 'token') {
uiFuncs.transferAllBalance(
$scope.wallet.getAddressString(),
$scope.tx.gasLimit,
function(resp) {
if (!resp.isError) {
$scope.tx.unit = resp.unit;
$scope.tx.value = resp.value;
} else {
$scope.showRaw = false;
$scope.notifier.danger(resp.error);
}
}
);
} else {
$scope.tx.value = $scope.wallet.tokenObjs[$scope.tokenTx.id].getBalance();
}
};
};
module.exports = sendTxCtrl;

View File

@ -0,0 +1,9 @@
// @flow
export type Transaction = {
to: string,
value: number,
unit: string, // 'ether' or token symbol
gasLimit: number,
data?: string // supported only in case of eth transfers, union type?
};

View File

@ -1,18 +1,11 @@
// @flow
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { DONATION_ADDRESSES_MAP } from 'config/data';
import Validator from 'libs/validator';
import { isValidBTCAddress, isValidETHAddress } from 'libs/validators';
import translate from 'translations';
export default class ReceivingAddress extends Component {
constructor(props) {
super(props);
this.validator = new Validator();
this.state = {
validAddress: false
};
}
static propTypes = {
destinationKind: PropTypes.string.isRequired,
destinationAddressSwap: PropTypes.func.isRequired,
@ -20,17 +13,9 @@ export default class ReceivingAddress extends Component {
partTwoCompleteSwap: PropTypes.func
};
onChangeDestinationAddress = event => {
onChangeDestinationAddress = (event: SyntheticInputEvent) => {
const value = event.target.value;
this.props.destinationAddressSwap(value);
let validAddress;
// TODO - find better pattern here once currencies move beyond BTC, ETH, REP
if (this.props.destinationKind === 'BTC') {
validAddress = this.validator.isValidBTCAddress(value);
} else {
validAddress = this.validator.isValidETHAddress(value);
}
this.setState({ validAddress });
};
onClickPartTwoComplete = () => {
@ -39,7 +24,14 @@ export default class ReceivingAddress extends Component {
render() {
const { destinationKind, destinationAddress } = this.props;
const { validAddress } = this.state;
let validAddress;
// TODO - find better pattern here once currencies move beyond BTC, ETH, REP
if (this.props.destinationKind === 'BTC') {
validAddress = isValidBTCAddress(destinationAddress);
} else {
validAddress = isValidETHAddress(destinationAddress);
}
return (
<article className="swap-start">
<section className="swap-address block">

View File

@ -10,6 +10,7 @@ import { Routing, history } from './routing';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import notificationsSaga from './sagas/notifications';
import ensSaga from './sagas/ens';
// application styles
import 'assets/styles/etherwallet-master.less';
@ -34,6 +35,7 @@ const configureStore = () => {
store = createStore(RootReducer, sagaApplied, middleware);
sagaMiddleware.run(notificationsSaga);
sagaMiddleware.run(ensSaga);
return store;
};

10
common/libs/ens.js Normal file
View File

@ -0,0 +1,10 @@
// @flow
import uts46 from 'idna-uts46';
export function normalise(name: string): string {
try {
return uts46.toUnicode(name, { useStd3ASCII: true, transitional: false });
} catch (e) {
throw e;
}
}

View File

@ -1,16 +0,0 @@
import WalletAddressValidator from 'wallet-address-validator';
import ethUtil from 'ethereumjs-util';
export default class Validator {
isValidETHAddress = function(address) {
if (address && address === '0x0000000000000000000000000000000000000000')
return false;
if (address) {
return ethUtil.isValidAddress(address);
}
return false;
};
isValidBTCAddress = function(address) {
return WalletAddressValidator.validate(address, 'BTC');
};
}

74
common/libs/validators.js Normal file
View File

@ -0,0 +1,74 @@
// @flow
import WalletAddressValidator from 'wallet-address-validator';
import { normalise } from './ens';
import { toChecksumAddress } from 'ethereumjs-util';
export function isValidETHAddress(address: string): boolean {
if (!address) {
return false;
}
if (address == '0x0000000000000000000000000000000000000000') return false;
return validateEtherAddress(address);
}
export function isValidBTCAddress(address: string): boolean {
return WalletAddressValidator.validate(address, 'BTC');
}
export function isValidHex(str: string): boolean {
if (typeof str !== 'string') {
return false;
}
if (str === '') return true;
str = str.substring(0, 2) == '0x'
? str.substring(2).toUpperCase()
: str.toUpperCase();
var re = /^[0-9A-F]+$/g;
return re.test(str);
}
export function isValidENSorEtherAddress(address: string): boolean {
return isValidETHAddress(address) || isValidENSAddress(address);
}
export function isValidENSName(str: string) {
try {
return (
str.length > 6 && normalise(str) != '' && str.substring(0, 2) != '0x'
);
} catch (e) {
return false;
}
}
export function isValidENSAddress(address: string): boolean {
try {
const normalized = normalise(address);
var tld = normalized.substr(normalized.lastIndexOf('.') + 1);
var validTLDs = {
eth: true,
test: true,
reverse: true
};
if (validTLDs[tld]) return true;
} catch (e) {
return false;
}
return false;
}
function isChecksumAddress(address: string): boolean {
return address == toChecksumAddress(address);
}
// FIXME we probably want to do checksum checks sideways
function validateEtherAddress(address: string): boolean {
if (address.substring(0, 2) != '0x') return false;
else if (!/^(0x)?[0-9a-f]{40}$/i.test(address)) return false;
else if (
/^(0x)?[0-9a-f]{40}$/.test(address) ||
/^(0x)?[0-9A-F]{40}$/.test(address)
)
return true;
else return isChecksumAddress(address);
}

View File

@ -1,29 +1,33 @@
import {
CONFIG_LANGUAGE_CHANGE,
CONFIG_NODE_CHANGE
} from 'actions/config';
import {languages, nodeList} from '../config/data';
// @flow
import { CONFIG_LANGUAGE_CHANGE, CONFIG_NODE_CHANGE } from 'actions/config';
import { languages, nodeList } from '../config/data';
export type State = {
// FIXME
languageSelection: string,
nodeSelection: string
};
const initialState = {
languageSelection: languages[0],
nodeSelection: nodeList[0]
}
languageSelection: languages[0],
nodeSelection: nodeList[0]
};
export function config(state = initialState, action) {
switch (action.type) {
case CONFIG_LANGUAGE_CHANGE: {
return {
...state,
languageSelection: action.value
}
}
case CONFIG_NODE_CHANGE: {
return {
...state,
nodeSelection: action.value
}
}
default:
return state
export function config(state: State = initialState, action): State {
switch (action.type) {
case CONFIG_LANGUAGE_CHANGE: {
return {
...state,
languageSelection: action.value
};
}
case CONFIG_NODE_CHANGE: {
return {
...state,
nodeSelection: action.value
};
}
default:
return state;
}
}

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

@ -0,0 +1,20 @@
// @flow
import type { EnsAction, CacheEnsAddressAction } from 'actions/ens';
export type State = { [string]: string };
const initialState: State = {};
function cacheEnsAddress(state: State, action: CacheEnsAddressAction): State {
const { ensName, address } = action.payload;
return { ...state, [ensName]: address };
}
export function ens(state: State = initialState, action: EnsAction): State {
switch (action.type) {
case 'ENS_CACHE':
return cacheEnsAddress(state, action);
default:
return state;
}
}

View File

@ -1,3 +1,4 @@
// @flow
import {
GENERATE_WALLET_SHOW_PASSWORD,
GENERATE_WALLET_FILE,
@ -5,14 +6,21 @@ import {
GENERATE_WALLET_CONFIRM_CONTINUE_TO_PAPER
} from 'actions/generateWalletConstants';
const initialState = {
export type State = {
showPassword: boolean,
generateWalletFile: boolean,
hasDownloadedWalletFile: boolean,
canProceedToPaper: boolean
};
const initialState: State = {
showPassword: false,
generateWalletFile: false,
hasDownloadedWalletFile: false,
canProceedToPaper: false
};
export function generateWallet(state = initialState, action) {
export function generateWallet(state: State = initialState, action): State {
switch (action.type) {
case GENERATE_WALLET_SHOW_PASSWORD: {
return {

View File

@ -1,18 +1,35 @@
// @flow
import * as generateWallet from './generateWallet'
import * as config from './config'
import * as swap from './swap'
import * as notifications from './notifications'
import * as generateWallet from './generateWallet';
import type { State as GenerateWalletState } from './generateWallet';
import { reducer as formReducer } from 'redux-form'
import {combineReducers} from 'redux';
import {routerReducer} from 'react-router-redux'
import * as config from './config';
import type { State as ConfigState } from './config';
import * as swap from './swap';
import * as notifications from './notifications';
import type { State as NotificationsState } from './notifications';
import * as ens from './ens';
import type { State as EnsState } from './ens';
import { reducer as formReducer } from 'redux-form';
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
export type State = {
generateWallet: GenerateWalletState,
conig: ConfigState,
notifications: NotificationsState,
ens: EnsState
};
export default combineReducers({
...generateWallet,
...config,
...swap,
...notifications,
form: formReducer,
routing: routerReducer
})
...generateWallet,
...config,
...swap,
...notifications,
...ens,
form: formReducer,
routing: routerReducer
});

View File

@ -1,32 +1,35 @@
// @flow
import type {
NotificationsAction,
Notification,
ShowNotificationAction,
CloseNotificationAction
NotificationsAction,
Notification,
ShowNotificationAction,
CloseNotificationAction
} from 'actions/notifications';
type State = Notification[];
export type State = Notification[];
const initialState: State = [];
function showNotification(state: State, action: ShowNotificationAction): State {
return state.concat(action.payload);
return state.concat(action.payload);
}
function closeNotification(state, action: CloseNotificationAction): State {
state = [...state];
state.splice(state.indexOf(action.payload), 1);
return state;
state = [...state];
state.splice(state.indexOf(action.payload), 1);
return state;
}
export function notifications(state: State = initialState, action: NotificationsAction): State {
switch (action.type) {
case 'SHOW_NOTIFICATION':
return showNotification(state, action);
case 'CLOSE_NOTIFICATION':
return closeNotification(state, action);
default:
return state;
}
export function notifications(
state: State = initialState,
action: NotificationsAction
): State {
switch (action.type) {
case 'SHOW_NOTIFICATION':
return showNotification(state, action);
case 'CLOSE_NOTIFICATION':
return closeNotification(state, action);
default:
return state;
}
}

View File

@ -1,27 +1,26 @@
import React from 'react';
import {browserHistory, Redirect, Route} from 'react-router';
import {useBasename} from 'history';
import {App} from 'containers';
import GenerateWallet from 'containers/Tabs/GenerateWallet'
import ViewWallet from 'containers/Tabs/ViewWallet'
import Help from 'containers/Tabs/Help'
import Swap from 'containers/Tabs/Swap'
import { browserHistory, Redirect, Route } from 'react-router';
import { useBasename } from 'history';
import { App } from 'containers';
import GenerateWallet from 'containers/Tabs/GenerateWallet';
import ViewWallet from 'containers/Tabs/ViewWallet';
import Help from 'containers/Tabs/Help';
import Swap from 'containers/Tabs/Swap';
import SendTransaction from 'containers/Tabs/SendTransaction';
export const history = getHistory();
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={ViewWallet}/>
<Route name="Help" path="/help" component={Help}/>
<Route name="Swap" path="/swap" component={Swap}/>
<Redirect from="/*" to="/"/>
</Route>
)
export const Routing = () =>
<Route name="App" path="" component={App}>
<Route name="GenerateWallet" path="/" component={GenerateWallet} />
<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} />
<Redirect from="/*" to="/" />
</Route>;
function getHistory() {
const basename = ''
return useBasename(() => browserHistory)({basename})
const basename = '';
return useBasename(() => browserHistory)({ basename });
}

35
common/sagas/ens.js Normal file
View File

@ -0,0 +1,35 @@
// @flow
import { takeEvery, call, put, select } from 'redux-saga/effects';
import { delay } from 'redux-saga';
import { cacheEnsAddress } from 'actions/ens';
import type { ResolveEnsNameAction } from 'actions/ens';
import { getEnsAddress } from 'selectors/ens';
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;
const cachedEnsAddress = yield select(getEnsAddress, ensName);
if (cachedEnsAddress) {
return;
}
yield call(delay, 1000);
yield put(
cacheEnsAddress(ensName, '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8')
);
}
export default function* notificationsSaga() {
yield takeEvery('ENS_RESOLVE', resolveEns);
}

6
common/selectors/ens.js Normal file
View File

@ -0,0 +1,6 @@
// @flow
import type { State } from 'reducers';
export function getEnsAddress(state: State, ensName: string): ?string {
return state.ens[ensName];
}

View File

@ -5,7 +5,9 @@
"description": "MyEtherWallet v4",
"dependencies": {
"axios": "^0.16.2",
"ethereum-blockies": "https://github.com/MyEtherWallet/blockies.git",
"ethereumjs-util": "^5.1.2",
"idna-uts46": "^1.1.0",
"lodash": "^4.17.4",
"prop-types": "^15.5.8",
"react": "^15.4.2",

View File

@ -1,34 +0,0 @@
import Validator from '../../common/libs/validator';
import { DONATION_ADDRESSES_MAP } from '../../common/config/data';
describe('Validator', () => {
it('should validate correct BTC address as true', () => {
const validator = new Validator();
expect(
validator.isValidBTCAddress(DONATION_ADDRESSES_MAP.BTC)
).toBeTruthy();
});
it('should validate incorrect BTC address as false', () => {
const validator = new Validator();
expect(
validator.isValidBTCAddress(
'nonsense' + DONATION_ADDRESSES_MAP.BTC + 'nonsense'
)
).toBeFalsy();
});
it('should validate correct ETH address as true', () => {
const validator = new Validator();
expect(
validator.isValidETHAddress(DONATION_ADDRESSES_MAP.ETH)
).toBeTruthy();
});
it('should validate incorrect ETH address as false', () => {
const validator = new Validator();
expect(
validator.isValidETHAddress(
'nonsense' + DONATION_ADDRESSES_MAP.ETH + 'nonsense'
)
).toBeFalsy();
});
});

View File

@ -0,0 +1,25 @@
import {
isValidBTCAddress,
isValidETHAddress
} from '../../common/libs/validators';
import { DONATION_ADDRESSES_MAP } from '../../common/config/data';
describe('Validator', () => {
it('should validate correct BTC address as true', () => {
expect(isValidBTCAddress(DONATION_ADDRESSES_MAP.BTC)).toBeTruthy();
});
it('should validate incorrect BTC address as false', () => {
expect(
isValidBTCAddress('nonsense' + DONATION_ADDRESSES_MAP.BTC + 'nonsense')
).toBeFalsy();
});
it('should validate correct ETH address as true', () => {
expect(isValidETHAddress(DONATION_ADDRESSES_MAP.ETH)).toBeTruthy();
});
it('should validate incorrect ETH address as false', () => {
expect(
isValidETHAddress('nonsense' + DONATION_ADDRESSES_MAP.ETH + 'nonsense')
).toBeFalsy();
});
});

View File

@ -1,85 +1,82 @@
'use strict'
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const config = require('./config')
const _ = require('./utils')
'use strict';
const path = require('path');
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const config = require('./config');
const _ = require('./utils');
module.exports = {
entry: {
client: './common/index.jsx'
entry: {
client: './common/index.jsx'
},
output: {
path: _.outputPath,
filename: '[name].js',
publicPath: config.publicPath
},
performance: {
hints: process.env.NODE_ENV === 'production' ? 'warning' : false
},
resolve: {
extensions: ['.js', '.jsx', '.css', '.json', '.scss', '.less'],
alias: {
actions: `${config.srcPath}/actions/`,
api: `${config.srcPath}/api/`,
reducers: `${config.srcPath}/reducers/`,
components: `${config.srcPath}/components/`,
containers: `${config.srcPath}/containers/`,
styles: `${config.srcPath}/styles/`,
less_vars: `${config.srcPath}/styles/etherwallet-variables.less`
},
output: {
path: _.outputPath,
filename: '[name].js',
publicPath: config.publicPath
},
performance: {
hints: process.env.NODE_ENV === 'production'
? 'warning'
: false
},
resolve: {
extensions: [
'.js', '.jsx', '.css', '.json', '.scss', '.less'
],
alias: {
actions: `${config.srcPath}/actions/`,
api: `${config.srcPath}/api/`,
reducers: `${config.srcPath}/reducers/`,
components: `${config.srcPath}/components/`,
containers: `${config.srcPath}/containers/`,
styles: `${config.srcPath}/styles/`,
less_vars: `${config.srcPath}/styles/etherwallet-variables.less`
},
// FIXME why aliases then?
modules: [
// places where to search for required modules
_.cwd('common'),
_.cwd('node_modules'),
_.cwd('./')
]
},
module: {
loaders: [
{
test: /\.(js|jsx)$/,
enforce: 'pre',
loaders: ['eslint-loader'],
exclude: [/node_modules/]
},
{
test: /\.(js|jsx)$/,
loaders: ['babel-loader'],
exclude: [/node_modules/]
},
{
test: /\.(ico|jpg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
loader: 'file-loader?limit=100000'
}, {
test: /\.svg$/,
loader: 'file-loader'
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.BUILD_GH_PAGES': JSON.stringify(!!process.env.BUILD_GH_PAGES)
}),
new HtmlWebpackPlugin({
title: config.title,
template: path.resolve(__dirname, '../common/index.html'),
filename: _.outputIndexPath
}),
new webpack.LoaderOptionsPlugin(_.loadersOptions()),
new CopyWebpackPlugin([
{
from: _.cwd('./static'),
// to the root of dist path
to: './'
}
])
],
target: _.target
}
// FIXME why aliases then?
modules: [
// places where to search for required modules
_.cwd('common'),
_.cwd('node_modules'),
_.cwd('./')
]
},
module: {
loaders: [
{
test: /\.(js|jsx)$/,
enforce: 'pre',
loaders: ['eslint-loader'],
exclude: [/node_modules/]
},
{
test: /\.(js|jsx)$/,
loaders: ['babel-loader'],
exclude: [/node_modules\/(?!ethereum-blockies)/]
},
{
test: /\.(ico|jpg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
loader: 'file-loader?limit=100000'
},
{
test: /\.svg$/,
loader: 'file-loader'
}
]
},
plugins: [
new webpack.DefinePlugin({
'process.env.BUILD_GH_PAGES': JSON.stringify(!!process.env.BUILD_GH_PAGES)
}),
new HtmlWebpackPlugin({
title: config.title,
template: path.resolve(__dirname, '../common/index.html'),
filename: _.outputIndexPath
}),
new webpack.LoaderOptionsPlugin(_.loadersOptions()),
new CopyWebpackPlugin([
{
from: _.cwd('./static'),
// to the root of dist path
to: './'
}
])
],
target: _.target
};