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 ( -
- -
+ scrollRight() {} - ) - } + render() { + const { location } = this.props; + return ( +
+ +
+ ); + } } diff --git a/common/components/Header/index.jsx b/common/components/Header/index.jsx index 4d8417da..4866e1d7 100644 --- a/common/components/Header/index.jsx +++ b/common/components/Header/index.jsx @@ -3,84 +3,91 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import TabsOptions from './components/TabsOptions'; import { Link } from 'react-router'; -import Dropdown from '../ui/Dropdown'; +import { Dropdown } from 'components/ui'; import { languages, nodeList } from '../../config/data'; export default class Header extends Component { - static propTypes = { - location: PropTypes.object, + static propTypes = { + location: PropTypes.object, - // Language DropDown - changeLanguage: PropTypes.func, - languageSelection: PropTypes.object, + // Language DropDown + changeLanguage: PropTypes.func, + languageSelection: PropTypes.object, - // Node Dropdown - changeNode: PropTypes.func, - nodeSelection: PropTypes.object - }; + // Node Dropdown + changeNode: PropTypes.func, + nodeSelection: PropTypes.object + }; - render() { - const { languageSelection, changeLanguage, changeNode, nodeSelection } = this.props; - - return ( -
-
-
- - {/* TODO - don't hardcode image path*/} - MyEtherWallet - -
- - 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*/} + MyEtherWallet + +
    + + 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.state.expanded && - } - - ); - } + return ( + + + {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 ? '-' : '+'} + +

    {translate(this.props.title)}

    +
    + {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 ( +
    +
    + {!expanded && + +

    + {translate('TRANS_advanced')} +

    +
    } + {expanded && +
    +
    + + +
    +
    } +
    +
    + ); + } + + 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 ( +
    +

    + {translate('sidebar_donation')} +

    + + {translate('sidebar_donate')} + + {this.state.clicked && +
    + {translate('sidebar_thanks')} +
    } +
    + ); + } + + 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 ( +
    + + + {value} + + + {this.state.expanded && + !isReadonly && + } +
    + ); + } + + 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 +};