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..0d3f7818
--- /dev/null
+++ b/common/actions/ens.js
@@ -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
+ }
+ };
+}
diff --git a/common/components/Header/components/TabsOptions.jsx b/common/components/Header/components/TabsOptions.jsx
index dada34d9..569fef3a 100644
--- a/common/components/Header/components/TabsOptions.jsx
+++ b/common/components/Header/components/TabsOptions.jsx
@@ -9,7 +9,8 @@ const tabs = [
link: '/'
},
{
- name: 'NAV_SendEther'
+ name: 'NAV_SendEther',
+ link: 'send-transaction'
},
{
name: 'NAV_Swap',
diff --git a/common/components/Header/index.jsx b/common/components/Header/index.jsx
index 4d8417da..727d4cac 100644
--- a/common/components/Header/index.jsx
+++ b/common/components/Header/index.jsx
@@ -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 {
diff --git a/common/components/ui/Dropdown.jsx b/common/components/ui/Dropdown.jsx
index 4c70ed0f..0ccc6c43 100644
--- a/common/components/ui/Dropdown.jsx
+++ b/common/components/ui/Dropdown.jsx
@@ -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 = {
diff --git a/common/components/ui/Identicon.jsx b/common/components/ui/Identicon.jsx
new file mode 100644
index 00000000..2422bedd
--- /dev/null
+++ b/common/components/ui/Identicon.jsx
@@ -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
;
+}
diff --git a/common/components/ui/UnlockHeader.jsx b/common/components/ui/UnlockHeader.jsx
new file mode 100644
index 00000000..e74d052f
--- /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..fd95da84
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/AddressField.jsx
@@ -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 (
+
+
+
+
+ {!!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..4234da8c
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/AmountField.jsx
@@ -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 (
+
+
+
+ 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..99a91ead
--- /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..1cc35fc3
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/DataField.jsx
@@ -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 (
+
+ );
+ }
+
+ 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..281e1879
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/Donate.jsx
@@ -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 (
+
+ );
+ }
+
+ 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..788c3373
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/components/GasField.jsx
@@ -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 (
+
+
+
+ 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..c412182f
--- /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..2247f098
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/index.jsx
@@ -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 (
+
+
+
+
+ {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 = (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;
diff --git a/common/containers/Tabs/SendTransaction/messages.js b/common/containers/Tabs/SendTransaction/messages.js
new file mode 100644
index 00000000..22808052
--- /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..e071f9ab
--- /dev/null
+++ b/common/containers/Tabs/SendTransaction/ref.js
@@ -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
+ ? "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..40e3040b
--- /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/eth/ens.js b/common/eth/ens.js
new file mode 100644
index 00000000..4110b482
--- /dev/null
+++ b/common/eth/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/eth/validators.js b/common/eth/validators.js
new file mode 100644
index 00000000..bff7f035
--- /dev/null
+++ b/common/eth/validators.js
@@ -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);
+}
diff --git a/common/index.jsx b/common/index.jsx
index be993df6..93cbaf9b 100644
--- a/common/index.jsx
+++ b/common/index.jsx
@@ -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(
- , document.getElementById('app'))
+ ,
+ document.getElementById('app')
+ );
};
renderRoot(Root);
if (module.hot) {
- module.hot.accept()
+ module.hot.accept();
}
diff --git a/common/reducers/config.js b/common/reducers/config.js
index aaf5b38a..984a08a5 100644
--- a/common/reducers/config.js
+++ b/common/reducers/config.js
@@ -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 {
diff --git a/common/reducers/ens.js b/common/reducers/ens.js
new file mode 100644
index 00000000..8937206f
--- /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..e9bdb6ef 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..73f2735f 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,
+ ...ens,
form: formReducer,
routing: routerReducer
-})
+});
diff --git a/common/reducers/notifications.js b/common/reducers/notifications.js
index 28be68e6..362bf6bf 100644
--- a/common/reducers/notifications.js
+++ b/common/reducers/notifications.js
@@ -6,7 +6,7 @@ import type {
CloseNotificationAction
} from 'actions/notifications';
-type State = Notification[];
+export type State = Notification[];
const initialState: State = [];
diff --git a/common/routing/index.jsx b/common/routing/index.jsx
index 17d500f7..491039e3 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..6c9fd571
--- /dev/null
+++ b/common/sagas/ens.js
@@ -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);
+}
diff --git a/common/selectors/ens.js b/common/selectors/ens.js
new file mode 100644
index 00000000..bd33384b
--- /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 ee4c86d0..67af8b8c 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/webpack_config/webpack.base.js b/webpack_config/webpack.base.js
index 8056b374..14142cd1 100644
--- a/webpack_config/webpack.base.js
+++ b/webpack_config/webpack.base.js
@@ -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)(\?.*)?$/,