diff --git a/.eslintrc.json b/.eslintrc.json
index 54110f3d..9f0c853c 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -30,5 +30,8 @@
"no-unreachable": 1,
"no-alert": 0,
"react/jsx-uses-react": 1
+ },
+ "globals": {
+ "SyntheticInputEvent": false
}
}
diff --git a/common/actions/ens.js b/common/actions/ens.js
new file mode 100644
index 00000000..45f2d3b1
--- /dev/null
+++ b/common/actions/ens.js
@@ -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
+ }
+ };
+}
diff --git a/common/components/Header/components/TabsOptions.jsx b/common/components/Header/components/TabsOptions.jsx
index dada34d9..fd7f8bbd 100644
--- a/common/components/Header/components/TabsOptions.jsx
+++ b/common/components/Header/components/TabsOptions.jsx
@@ -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 (
-
-
-
-
- {/* TODO - don't hardcode image path*/}
-
-
-
-
- Open-Source & Client-Side Ether Wallet · v3.6.0
-
-
-
o.name}
- value={languageSelection}
- extra={[
- ,
-
-
- Disclaimer
-
-
- ]}
- onChange={changeLanguage}
- />
-
- [
- o.name,
- ' ',
- ({o.service})
- ]}
- value={nodeSelection}
- extra={
-
- {}}>
- Add Custom Node
-
-
- }
- onChange={changeNode}
- />
-
-
-
-
-
+ render() {
+ const {
+ languageSelection,
+ changeLanguage,
+ changeNode,
+ nodeSelection
+ } = this.props;
+ return (
+
+
+
+
+ {/* TODO - don't hardcode image path*/}
+
+
+
+
+ Open-Source & Client-Side Ether Wallet · v3.6.0
+
+
+
o.name}
+ value={languageSelection}
+ extra={[
+ ,
+
+
+ Disclaimer
+
+
+ ]}
+ onChange={changeLanguage}
+ />
+
+ [
+ o.name,
+ ' ',
+ ({o.service})
+ ]}
+ value={nodeSelection}
+ extra={
+
+ {}}>
+ Add Custom Node
+
+
+ }
+ onChange={changeNode}
+ />
- );
- }
+
+
+
+
+
+
+ );
+ }
}
diff --git a/common/components/ui/Dropdown.jsx b/common/components/ui/Dropdown.jsx
index 4c70ed0f..e9f5f6a2 100644
--- a/common/components/ui/Dropdown.jsx
+++ b/common/components/ui/Dropdown.jsx
@@ -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 (
-
-
- {this.formatTitle(value)}
-
-
- {this.state.expanded &&
- }
-
- );
- }
+ return (
+
+
+ {this.formatTitle(value)}
+
+
+ {this.state.expanded &&
+ }
+
+ );
+ }
- 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 });
+ };
}
diff --git a/common/components/ui/Identicon.jsx b/common/components/ui/Identicon.jsx
new file mode 100644
index 00000000..e7f25d5f
--- /dev/null
+++ b/common/components/ui/Identicon.jsx
@@ -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 (
+
+ );
+}
diff --git a/common/components/ui/UnlockHeader.jsx b/common/components/ui/UnlockHeader.jsx
new file mode 100644
index 00000000..38a1bb90
--- /dev/null
+++ b/common/components/ui/UnlockHeader.jsx
@@ -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 (
+
+
+ {this.state.expanded &&
+
+ {/* @@if (site === 'cx' ) { }
+ @@if (site === 'mew' ) { } */}
+
}
+
+ {this.state.expanded &&
}
+
+ );
+ }
+
+ toggleExpanded = () => {
+ this.setState(state => {
+ return { expanded: !state.expanded };
+ });
+ };
+}
diff --git a/common/components/ui/index.js b/common/components/ui/index.js
new file mode 100644
index 00000000..471d34e3
--- /dev/null
+++ b/common/components/ui/index.js
@@ -0,0 +1,5 @@
+// @flow
+
+export { default as Dropdown } from './Dropdown';
+export { default as UnlockHeader } from './UnlockHeader';
+export { default as Identicon } from './Identicon';
diff --git a/common/containers/Tabs/SendTransaction/components/AddressField.jsx b/common/containers/Tabs/SendTransaction/components/AddressField.jsx
new file mode 100644
index 00000000..473884ee
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/AddressField.jsx
@@ -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 (
+
+
+
+
+ {!!ensAddress &&
+
+ ↳
+
+ {ensAddress}
+
+
}
+
+
+
+
+
+ );
+ }
+
+ 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);
diff --git a/common/containers/Tabs/SendTransaction/components/AmountField.jsx b/common/containers/Tabs/SendTransaction/components/AmountField.jsx
new file mode 100644
index 00000000..b6091432
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/AmountField.jsx
@@ -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 (
+
+
+
+ 0
+ ? 'is-valid'
+ : 'is-invalid'}`}
+ type="text"
+ placeholder={translate('SEND_amount_short')}
+ value={value}
+ disabled={isReadonly}
+ onChange={isReadonly ? void 0 : this.onValueChange}
+ />
+
+
+ {!isReadonly &&
+
+
+
+ {translate('SEND_TransferTotal')}
+
+
+
}
+
+ );
+ }
+
+ 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);
+ }
+ };
+}
diff --git a/common/containers/Tabs/SendTransaction/components/CustomMessage.jsx b/common/containers/Tabs/SendTransaction/components/CustomMessage.jsx
new file mode 100644
index 00000000..db1483b4
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/CustomMessage.jsx
@@ -0,0 +1,21 @@
+// @flow
+import React from 'react';
+
+type Props = {
+ message?: {
+ to: string,
+ msg: string
+ }
+};
+
+export default function CustomMessage(props: Props) {
+ return (
+
+ {!!props.message &&
+
+
A message from {props.message.to}
+
{props.message.msg}
+
}
+
+ );
+}
diff --git a/common/containers/Tabs/SendTransaction/components/DataField.jsx b/common/containers/Tabs/SendTransaction/components/DataField.jsx
new file mode 100644
index 00000000..c8e4c28c
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/DataField.jsx
@@ -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 (
+
+ );
+ }
+
+ expand = () => {
+ this.setState({ expanded: true });
+ };
+
+ onChange = (e: SyntheticInputEvent) => {
+ if (this.props.onChange) {
+ this.props.onChange(e.target.value);
+ }
+ };
+}
diff --git a/common/containers/Tabs/SendTransaction/components/Donate.jsx b/common/containers/Tabs/SendTransaction/components/Donate.jsx
new file mode 100644
index 00000000..5a59d30e
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/Donate.jsx
@@ -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 (
+
+ );
+ }
+
+ onClick = () => {
+ // FIXME move to config
+ this.props.onDonate(
+ '0x7cB57B5A97eAbe94205C07890BE4c1aD31E486A8',
+ '1',
+ 'ETH'
+ );
+
+ this.setState({ clicked: true });
+ };
+}
diff --git a/common/containers/Tabs/SendTransaction/components/GasField.jsx b/common/containers/Tabs/SendTransaction/components/GasField.jsx
new file mode 100644
index 00000000..ca541807
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/GasField.jsx
@@ -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 (
+
+
+
+ 0
+ ? 'is-valid'
+ : 'is-invalid'}`}
+ type="text"
+ placeholder="21000"
+ disabled={isReadonly}
+ value={value}
+ onChange={this.onChange}
+ />
+
+
+ );
+ }
+
+ onChange = (e: SyntheticInputEvent) => {
+ if (this.props.onChange) {
+ this.props.onChange(e.target.value);
+ }
+ };
+}
diff --git a/common/containers/Tabs/SendTransaction/components/UnitDropdown.jsx b/common/containers/Tabs/SendTransaction/components/UnitDropdown.jsx
new file mode 100644
index 00000000..0f20632a
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/UnitDropdown.jsx
@@ -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 (
+
+ );
+ }
+
+ onToggleExpand = () => {
+ this.setState(state => {
+ return {
+ expanded: !state.expanded
+ };
+ });
+ };
+}
diff --git a/common/containers/Tabs/SendTransaction/components/index.js b/common/containers/Tabs/SendTransaction/components/index.js
new file mode 100644
index 00000000..b2370061
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/index.js
@@ -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';
diff --git a/common/containers/Tabs/SendTransaction/index.jsx b/common/containers/Tabs/SendTransaction/index.jsx
new file mode 100644
index 00000000..d54a283b
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/index.jsx
@@ -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 (
+
+
+
+
+ {hasQueryString &&
+
+
+ {translate('WARN_Send_Link')}
+
+
}
+
+
+
+ {unlocked &&
+
+ {'' /* */}
+
+
+
+ {readOnly &&
+ !hasEnoughBalance &&
+
+
+
+ Warning! You do not have enough funds to
+ complete this swap.
+
+ {' '}
+
+ Please add more funds or access a different wallet.
+
+
}
+
+
+
+ {translate('SEND_trans')}
+
+
+
+
+
+ {unit === 'ether' &&
+ }
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'' /* */}
+ {
+ '' /* @@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" } ) } */
+ }
+ }
+
+
+
+ );
+ }
+
+ 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;
diff --git a/common/containers/Tabs/SendTransaction/messages.js b/common/containers/Tabs/SendTransaction/messages.js
new file mode 100644
index 00000000..6067fecc
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/messages.js
@@ -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.'
+ }
+];
diff --git a/common/containers/Tabs/SendTransaction/ref.js b/common/containers/Tabs/SendTransaction/ref.js
new file mode 100644
index 00000000..50775dd4
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/ref.js
@@ -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
+ ? "
View TX"
+ : '';
+ var emailLink =
+ '
Confused? Email Us.';
+ $scope.notifier.success(
+ globalFuncs.successMsgs[2] +
+ resp.data +
+ '
' +
+ bExStr +
+ '
' +
+ emailLink +
+ '
'
+ );
+ $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;
diff --git a/common/containers/Tabs/SendTransaction/types.js b/common/containers/Tabs/SendTransaction/types.js
new file mode 100644
index 00000000..331ae490
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/types.js
@@ -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?
+};
diff --git a/common/containers/Tabs/Swap/components/receivingAddress.js b/common/containers/Tabs/Swap/components/receivingAddress.js
index 485e50c0..39c8aa8b 100644
--- a/common/containers/Tabs/Swap/components/receivingAddress.js
+++ b/common/containers/Tabs/Swap/components/receivingAddress.js
@@ -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 (
diff --git a/common/index.jsx b/common/index.jsx
index 3f4d9555..91ed1076 100644
--- a/common/index.jsx
+++ b/common/index.jsx
@@ -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;
};
diff --git a/common/libs/ens.js b/common/libs/ens.js
new file mode 100644
index 00000000..39b96cba
--- /dev/null
+++ b/common/libs/ens.js
@@ -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;
+ }
+}
diff --git a/common/libs/validator.js b/common/libs/validator.js
deleted file mode 100644
index c930f47a..00000000
--- a/common/libs/validator.js
+++ /dev/null
@@ -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');
- };
-}
diff --git a/common/libs/validators.js b/common/libs/validators.js
new file mode 100644
index 00000000..c658c17d
--- /dev/null
+++ b/common/libs/validators.js
@@ -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);
+}
diff --git a/common/reducers/config.js b/common/reducers/config.js
index aaf5b38a..59c0fd9d 100644
--- a/common/reducers/config.js
+++ b/common/reducers/config.js
@@ -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;
+ }
}
diff --git a/common/reducers/ens.js b/common/reducers/ens.js
new file mode 100644
index 00000000..9c5d0871
--- /dev/null
+++ b/common/reducers/ens.js
@@ -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;
+ }
+}
diff --git a/common/reducers/generateWallet.js b/common/reducers/generateWallet.js
index 33df5af4..7159206a 100644
--- a/common/reducers/generateWallet.js
+++ b/common/reducers/generateWallet.js
@@ -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 {
diff --git a/common/reducers/index.js b/common/reducers/index.js
index 3161d505..263d5635 100644
--- a/common/reducers/index.js
+++ b/common/reducers/index.js
@@ -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
+});
diff --git a/common/reducers/notifications.js b/common/reducers/notifications.js
index 28be68e6..bdc6b5ca 100644
--- a/common/reducers/notifications.js
+++ b/common/reducers/notifications.js
@@ -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;
+ }
}
diff --git a/common/routing/index.jsx b/common/routing/index.jsx
index 17d500f7..9c296629 100644
--- a/common/routing/index.jsx
+++ b/common/routing/index.jsx
@@ -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 = () => (
-
-
-
-
-
-
-
-
-)
+export const Routing = () =>
+
+
+
+
+
+
+
+ ;
function getHistory() {
- const basename = ''
- return useBasename(() => browserHistory)({basename})
+ const basename = '';
+ return useBasename(() => browserHistory)({ basename });
}
diff --git a/common/sagas/ens.js b/common/sagas/ens.js
new file mode 100644
index 00000000..240dd855
--- /dev/null
+++ b/common/sagas/ens.js
@@ -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);
+}
diff --git a/common/selectors/ens.js b/common/selectors/ens.js
new file mode 100644
index 00000000..f1c03d7d
--- /dev/null
+++ b/common/selectors/ens.js
@@ -0,0 +1,6 @@
+// @flow
+import type { State } from 'reducers';
+
+export function getEnsAddress(state: State, ensName: string): ?string {
+ return state.ens[ensName];
+}
diff --git a/package.json b/package.json
index c18c6355..69a85824 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/spec/libs/validator.spec.js b/spec/libs/validator.spec.js
deleted file mode 100644
index a29b0bc8..00000000
--- a/spec/libs/validator.spec.js
+++ /dev/null
@@ -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();
- });
-});
diff --git a/spec/libs/validators.spec.js b/spec/libs/validators.spec.js
new file mode 100644
index 00000000..4ca3a81f
--- /dev/null
+++ b/spec/libs/validators.spec.js
@@ -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();
+ });
+});
diff --git a/webpack_config/webpack.base.js b/webpack_config/webpack.base.js
index 8056b374..2ed58c86 100644
--- a/webpack_config/webpack.base.js
+++ b/webpack_config/webpack.base.js
@@ -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
+};