send wip
This commit is contained in:
parent
a277134bc4
commit
1fb4a004ac
|
@ -30,5 +30,8 @@
|
|||
"no-unreachable": 1,
|
||||
"no-alert": 0,
|
||||
"react/jsx-uses-react": 1
|
||||
},
|
||||
"globals": {
|
||||
"SyntheticInputEvent": false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// @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
|
||||
}
|
||||
};
|
||||
}
|
|
@ -9,7 +9,8 @@ const tabs = [
|
|||
link: '/'
|
||||
},
|
||||
{
|
||||
name: 'NAV_SendEther'
|
||||
name: 'NAV_SendEther',
|
||||
link: 'send-transaction'
|
||||
},
|
||||
{
|
||||
name: 'NAV_Swap',
|
||||
|
|
|
@ -3,7 +3,7 @@ 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 {
|
||||
|
|
|
@ -19,7 +19,7 @@ export default class DropdownComponent extends Component {
|
|||
ariaLabel: string,
|
||||
formatTitle: (option: any) => any,
|
||||
extra?: any,
|
||||
onChange: () => void
|
||||
onChange: (value: any) => void
|
||||
};
|
||||
|
||||
state = {
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
import React from 'react';
|
||||
import { toDataUrl } from 'ethereum-blockies';
|
||||
import { isValidAddress } from 'eth/validators';
|
||||
|
||||
type Props = {
|
||||
address: string
|
||||
};
|
||||
|
||||
export default function Identicon(props: Props) {
|
||||
// FIXME breaks on failed checksums
|
||||
const style = !isValidAddress(props.address)
|
||||
? {}
|
||||
: { backgroundImage: `url(${toDataUrl(props.address.toLowerCase())})` };
|
||||
return <div className="addressIdenticon" style={style} title="Address Indenticon" />;
|
||||
}
|
|
@ -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 };
|
||||
});
|
||||
};
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
// @flow
|
||||
|
||||
export { default as Dropdown } from './Dropdown';
|
||||
export { default as UnlockHeader } from './UnlockHeader';
|
||||
export { default as Identicon } from './Identicon';
|
|
@ -0,0 +1,74 @@
|
|||
// @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 'eth/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;
|
||||
// FIXME identicon is passed address only if valid
|
||||
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);
|
|
@ -0,0 +1,67 @@
|
|||
// @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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
// @flow
|
||||
import React from 'react';
|
||||
import translate from 'translations';
|
||||
import { isValidHex } from 'eth/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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
// @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 });
|
||||
};
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
// @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);
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
|
@ -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';
|
|
@ -0,0 +1,270 @@
|
|||
// @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 = {
|
||||
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 = (e: SyntheticInputEvent) => {
|
||||
const value = e.target.value;
|
||||
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;
|
|
@ -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.'
|
||||
}
|
||||
];
|
|
@ -0,0 +1,336 @@
|
|||
'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;
|
|
@ -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?
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
// @flow
|
||||
import { normalise } from './ens';
|
||||
import { toChecksumAddress } from 'ethereumjs-util';
|
||||
|
||||
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 isValidAddress(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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// FIXME already in swap PR somewhere
|
||||
export function isValidAddress(address: string): boolean {
|
||||
if (!address) {
|
||||
return false;
|
||||
}
|
||||
if (address == '0x0000000000000000000000000000000000000000') return false;
|
||||
return validateEtherAddress(address);
|
||||
}
|
|
@ -1,20 +1,21 @@
|
|||
import React from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {syncHistoryWithStore, routerMiddleware} from 'react-router-redux'
|
||||
import {composeWithDevTools} from 'redux-devtools-extension'
|
||||
import Perf from 'react-addons-perf'
|
||||
import {createStore, applyMiddleware} from 'redux'
|
||||
import RootReducer from './reducers'
|
||||
import {Root} from 'components'
|
||||
import {Routing, history} from './routing'
|
||||
import {createLogger} from 'redux-logger'
|
||||
import createSagaMiddleware from 'redux-saga'
|
||||
import notificationsSaga from './sagas/notifications'
|
||||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { syncHistoryWithStore, routerMiddleware } from 'react-router-redux';
|
||||
import { composeWithDevTools } from 'redux-devtools-extension';
|
||||
import Perf from 'react-addons-perf';
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
import RootReducer from './reducers';
|
||||
import { Root } from 'components';
|
||||
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'
|
||||
import 'assets/styles/etherwallet-master.less';
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware()
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
const configureStore = () => {
|
||||
let sagaApplied = applyMiddleware(sagaMiddleware);
|
||||
|
@ -33,22 +34,22 @@ const configureStore = () => {
|
|||
}
|
||||
|
||||
store = createStore(RootReducer, sagaApplied, middleware);
|
||||
sagaMiddleware.run(notificationsSaga)
|
||||
return store
|
||||
sagaMiddleware.run(notificationsSaga);
|
||||
sagaMiddleware.run(ensSaga);
|
||||
return store;
|
||||
};
|
||||
|
||||
const renderRoot = (Root) => {
|
||||
const renderRoot = Root => {
|
||||
let store = configureStore();
|
||||
let syncedHistory = syncHistoryWithStore(history, store);
|
||||
render(
|
||||
<Root key={Math.random()}
|
||||
routes={Routing}
|
||||
history={syncedHistory}
|
||||
store={store}/>, document.getElementById('app'))
|
||||
<Root key={Math.random()} routes={Routing} history={syncedHistory} store={store} />,
|
||||
document.getElementById('app')
|
||||
);
|
||||
};
|
||||
|
||||
renderRoot(Root);
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept()
|
||||
module.hot.accept();
|
||||
}
|
||||
|
|
|
@ -1,15 +1,22 @@
|
|||
// @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]
|
||||
}
|
||||
|
||||
export function config(state = initialState, action) {
|
||||
export function config(state: State = initialState, action): State {
|
||||
switch (action.type) {
|
||||
case CONFIG_LANGUAGE_CHANGE: {
|
||||
return {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
...ens,
|
||||
form: formReducer,
|
||||
routing: routerReducer
|
||||
})
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ import type {
|
|||
CloseNotificationAction
|
||||
} from 'actions/notifications';
|
||||
|
||||
type State = Notification[];
|
||||
export type State = Notification[];
|
||||
|
||||
const initialState: State = [];
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
// @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);
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// @flow
|
||||
import type { State } from 'reducers';
|
||||
|
||||
export function getEnsAddress(state: State, ensName: string): ?string {
|
||||
return state.ens[ensName];
|
||||
}
|
|
@ -5,6 +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",
|
||||
|
|
|
@ -52,7 +52,7 @@ module.exports = {
|
|||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
loaders: ['babel-loader'],
|
||||
exclude: [/node_modules/]
|
||||
exclude: [/node_modules\/(?!ethereum-blockies)/]
|
||||
},
|
||||
{
|
||||
test: /\.(ico|jpg|png|gif|eot|otf|webp|ttf|woff|woff2)(\?.*)?$/,
|
||||
|
|
Loading…
Reference in New Issue