diff options
Diffstat (limited to 'ui')
54 files changed, 569 insertions, 203 deletions
diff --git a/ui/app/actions.js b/ui/app/actions.js index 758696203..c822ef3ee 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -177,6 +177,8 @@ var actions = { CLEAR_SEND: 'CLEAR_SEND', OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', + GAS_LOADING_STARTED: 'GAS_LOADING_STARTED', + GAS_LOADING_FINISHED: 'GAS_LOADING_FINISHED', setGasLimit, setGasPrice, updateGasData, @@ -192,6 +194,8 @@ var actions = { updateSendErrors, clearSend, setSelectedAddress, + gasLoadingStarted, + gasLoadingFinished, // app messages confirmSeedWords: confirmSeedWords, showAccountDetail: showAccountDetail, @@ -782,8 +786,9 @@ function updateGasData ({ to, value, }) { - const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks) return (dispatch) => { + dispatch(actions.gasLoadingStarted()) + const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks) return Promise.all([ Promise.resolve(estimatedGasPrice), estimateGas({ @@ -804,14 +809,28 @@ function updateGasData ({ .then((gasEstimate) => { dispatch(actions.setGasTotal(gasEstimate)) dispatch(updateSendErrors({ gasLoadingError: null })) + dispatch(actions.gasLoadingFinished()) }) .catch(err => { log.error(err) dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })) + dispatch(actions.gasLoadingFinished()) }) } } +function gasLoadingStarted () { + return { + type: actions.GAS_LOADING_STARTED, + } +} + +function gasLoadingFinished () { + return { + type: actions.GAS_LOADING_FINISHED, + } +} + function updateSendTokenBalance ({ selectedToken, tokenContract, diff --git a/ui/app/app.js b/ui/app/app.js index ec2329463..d0e48a368 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -314,7 +314,7 @@ function mapStateToProps (state) { noActiveNotices, seedWords, unapprovedTxs, - lastUnreadNotice, + nextUnreadNotice, lostAccounts, unapprovedMsgCount, unapprovedPersonalMsgCount, @@ -348,7 +348,7 @@ function mapStateToProps (state) { network: state.metamask.network, provider: state.metamask.provider, forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice, + nextUnreadNotice, lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], currentCurrency: state.metamask.currentCurrency, diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index c8522a3c7..cd8f76ed5 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -33,6 +33,7 @@ const { const { getGasPrice, getGasLimit, + getGasIsLoading, getForceGasMin, conversionRateSelector, getSendAmount, @@ -51,6 +52,7 @@ function mapStateToProps (state) { return { gasPrice: getGasPrice(state), gasLimit: getGasLimit(state), + gasIsLoading: getGasIsLoading(state), forceGasMin: getForceGasMin(state), conversionRate, amount: getSendAmount(state), @@ -73,7 +75,7 @@ function mapDispatchToProps (dispatch) { } } -function getOriginalState (props) { +function getFreshState (props) { const gasPrice = props.gasPrice || MIN_GAS_PRICE_DEC const gasLimit = props.gasLimit || MIN_GAS_LIMIT_DEC @@ -97,7 +99,11 @@ inherits(CustomizeGasModal, Component) function CustomizeGasModal (props) { Component.call(this) - this.state = getOriginalState(props) + const originalState = getFreshState(props) + this.state = { + ...originalState, + originalState, + } } CustomizeGasModal.contextTypes = { @@ -106,6 +112,36 @@ CustomizeGasModal.contextTypes = { module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) +CustomizeGasModal.prototype.componentWillReceiveProps = function (nextProps) { + const currentState = getFreshState(this.props) + const { + gasPrice: currentGasPrice, + gasLimit: currentGasLimit, + } = currentState + const newState = getFreshState(nextProps) + const { + gasPrice: newGasPrice, + gasLimit: newGasLimit, + gasTotal: newGasTotal, + } = newState + const gasPriceChanged = currentGasPrice !== newGasPrice + const gasLimitChanged = currentGasLimit !== newGasLimit + + if (gasPriceChanged) { + this.setState({ + gasPrice: newGasPrice, + gasTotal: newGasTotal, + priceSigZeros: '', + priceSigDec: '', + }) + } + if (gasLimitChanged) { + this.setState({ gasLimit: newGasLimit, gasTotal: newGasTotal }) + } + if (gasLimitChanged || gasPriceChanged) { + this.validate({ gasLimit: newGasLimit, gasTotal: newGasTotal }) + } +} CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { const { @@ -137,7 +173,7 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { } CustomizeGasModal.prototype.revert = function () { - this.setState(getOriginalState(this.props)) + this.setState(this.state.originalState) } CustomizeGasModal.prototype.validate = function ({ gasTotal, gasLimit }) { @@ -233,7 +269,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) { } CustomizeGasModal.prototype.render = function () { - const { hideModal, forceGasMin } = this.props + const { hideModal, forceGasMin, gasIsLoading } = this.props const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state let convertedGasPrice = conversionUtil(gasPrice, { @@ -266,7 +302,7 @@ CustomizeGasModal.prototype.render = function () { toNumericBase: 'dec', }) - return h('div.send-v2__customize-gas', {}, [ + return !gasIsLoading && h('div.send-v2__customize-gas', {}, [ h('div.send-v2__customize-gas__content', { }, [ h('div.send-v2__customize-gas__header', {}, [ @@ -288,6 +324,7 @@ CustomizeGasModal.prototype.render = function () { onChange: value => this.convertAndSetGasPrice(value), title: this.context.t('gasPrice'), copy: this.context.t('gasPriceCalculation'), + gasIsLoading, }), h(GasModalCard, { @@ -297,6 +334,7 @@ CustomizeGasModal.prototype.render = function () { onChange: value => this.convertAndSetGasLimit(value), title: this.context.t('gasLimit'), copy: this.context.t('gasLimitCalculation'), + gasIsLoading, }), ]), diff --git a/ui/app/components/dropdowns/token-menu-dropdown.js b/ui/app/components/dropdowns/token-menu-dropdown.js index b70d0b893..fac7c451b 100644 --- a/ui/app/components/dropdowns/token-menu-dropdown.js +++ b/ui/app/components/dropdowns/token-menu-dropdown.js @@ -4,14 +4,21 @@ const h = require('react-hyperscript') const inherits = require('util').inherits const connect = require('react-redux').connect const actions = require('../../actions') - +const genAccountLink = require('etherscan-link').createAccountLink +const copyToClipboard = require('copy-to-clipboard') +const { Menu, Item, CloseArea } = require('./components/menu') TokenMenuDropdown.contextTypes = { t: PropTypes.func, } -module.exports = connect(null, mapDispatchToProps)(TokenMenuDropdown) +module.exports = connect(mapStateToProps, mapDispatchToProps)(TokenMenuDropdown) +function mapStateToProps (state) { + return { + network: state.metamask.network, + } +} function mapDispatchToProps (dispatch) { return { @@ -37,22 +44,34 @@ TokenMenuDropdown.prototype.onClose = function (e) { TokenMenuDropdown.prototype.render = function () { const { showHideTokenConfirmationModal } = this.props - return h('div.token-menu-dropdown', {}, [ - h('div.token-menu-dropdown__close-area', { + return h(Menu, { className: 'token-menu-dropdown', isShowing: true }, [ + h(CloseArea, { onClick: this.onClose, }), - h('div.token-menu-dropdown__container', {}, [ - h('div.token-menu-dropdown__options', {}, [ - - h('div.token-menu-dropdown__option', { - onClick: (e) => { - e.stopPropagation() - showHideTokenConfirmationModal(this.props.token) - this.props.onClose() - }, - }, this.context.t('hideToken')), - - ]), - ]), + h(Item, { + onClick: (e) => { + e.stopPropagation() + showHideTokenConfirmationModal(this.props.token) + this.props.onClose() + }, + text: this.context.t('hideToken'), + }), + h(Item, { + onClick: (e) => { + e.stopPropagation() + copyToClipboard(this.props.token.address) + this.props.onClose() + }, + text: this.context.t('copyContractAddress'), + }), + h(Item, { + onClick: (e) => { + e.stopPropagation() + const url = genAccountLink(this.props.token.address, this.props.network) + global.platform.openWindow({ url }) + this.props.onClose() + }, + text: this.context.t('viewOnEtherscan'), + }), ]) } diff --git a/ui/app/components/ens-input.js b/ui/app/components/ens-input.js index aff4b6ef6..292dcdde6 100644 --- a/ui/app/components/ens-input.js +++ b/ui/app/components/ens-input.js @@ -12,6 +12,7 @@ const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' const connect = require('react-redux').connect const ToAutoComplete = require('./send/to-autocomplete') const log = require('loglevel') +const { isValidENSAddress } = require('../util') EnsInput.contextTypes = { t: PropTypes.func, @@ -25,31 +26,34 @@ function EnsInput () { Component.call(this) } -EnsInput.prototype.render = function () { - const props = this.props - const opts = extend(props, { - list: 'addresses', - onChange: (recipient) => { - const network = this.props.network - const networkHasEnsSupport = getNetworkEnsSupport(network) +EnsInput.prototype.onChange = function (recipient) { + const network = this.props.network + const networkHasEnsSupport = getNetworkEnsSupport(network) - props.onChange(recipient) + this.props.onChange({ toAddress: recipient }) - if (!networkHasEnsSupport) return + if (!networkHasEnsSupport) return - if (recipient.match(ensRE) === null) { - return this.setState({ - loadingEns: false, - ensResolution: null, - ensFailure: null, - }) - } + if (recipient.match(ensRE) === null) { + return this.setState({ + loadingEns: false, + ensResolution: null, + ensFailure: null, + toError: null, + }) + } - this.setState({ - loadingEns: true, - }) - this.checkName(recipient) - }, + this.setState({ + loadingEns: true, + }) + this.checkName(recipient) +} + +EnsInput.prototype.render = function () { + const props = this.props + const opts = extend(props, { + list: 'addresses', + onChange: this.onChange.bind(this), }) return h('div', { style: { width: '100%', position: 'relative' }, @@ -85,17 +89,27 @@ EnsInput.prototype.lookupEnsName = function (recipient) { nickname: recipient.trim(), hoverText: address + '\n' + this.context.t('clickCopy'), ensFailure: false, + toError: null, }) } }) .catch((reason) => { - log.error(reason) - return this.setState({ + const setStateObj = { loadingEns: false, - ensResolution: ZERO_ADDRESS, + ensResolution: recipient, ensFailure: true, - hoverText: reason.message, - }) + toError: null, + } + if (isValidENSAddress(recipient) && reason.message === 'ENS name not defined.') { + setStateObj.hoverText = this.context.t('ensNameNotFound') + setStateObj.toError = 'ensNameNotFound' + setStateObj.ensFailure = false + } else { + log.error(reason) + setStateObj.hoverText = reason.message + } + + return this.setState(setStateObj) }) } @@ -105,9 +119,14 @@ EnsInput.prototype.componentDidUpdate = function (prevProps, prevState) { // If an address is sent without a nickname, meaning not from ENS or from // the user's own accounts, a default of a one-space string is used. const nickname = state.nickname || ' ' + if (prevProps.network !== this.props.network) { + const provider = global.ethereumProvider + this.ens = new ENS({ provider, network: this.props.network }) + this.onChange(ensResolution) + } if (prevState && ensResolution && this.props.onChange && ensResolution !== prevState.ensResolution) { - this.props.onChange(ensResolution, nickname) + this.props.onChange({ toAddress: ensResolution, nickname, toError: state.toError }) } } @@ -124,7 +143,9 @@ EnsInput.prototype.ensIcon = function (recipient) { } EnsInput.prototype.ensIconContents = function (recipient) { - const { loadingEns, ensFailure, ensResolution } = this.state || { ensResolution: ZERO_ADDRESS} + const { loadingEns, ensFailure, ensResolution, toError } = this.state || { ensResolution: ZERO_ADDRESS } + + if (toError) return if (loadingEns) { return h('img', { diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index dce9b0449..424048745 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -36,6 +36,7 @@ IdenticonComponent.prototype.render = function () { key: 'identicon-' + address, style: { display: 'flex', + flexShrink: 0, alignItems: 'center', justifyContent: 'center', height: diameter, diff --git a/ui/app/components/input-number.js b/ui/app/components/input-number.js index de5fcca54..59c6842ef 100644 --- a/ui/app/components/input-number.js +++ b/ui/app/components/input-number.js @@ -22,12 +22,16 @@ function isValidInput (text) { return re.test(text) } +function removeLeadingZeroes (str) { + return str.replace(/^0*(?=\d)/, '') +} + InputNumber.prototype.setValue = function (newValue) { + newValue = removeLeadingZeroes(newValue) if (newValue && !isValidInput(newValue)) return const { fixed, min = -1, max = Infinity, onChange } = this.props newValue = fixed ? newValue.toFixed(4) : newValue - const newValueGreaterThanMin = conversionGTE( { value: newValue || '0', fromNumericBase: 'dec' }, { value: min, fromNumericBase: 'hex' }, @@ -47,7 +51,7 @@ InputNumber.prototype.setValue = function (newValue) { } InputNumber.prototype.render = function () { - const { unitLabel, step = 1, placeholder, value = 0 } = this.props + const { unitLabel, step = 1, placeholder, value } = this.props return h('div.customize-gas-input-wrapper', {}, [ h('input', { @@ -63,11 +67,11 @@ InputNumber.prototype.render = function () { h('span.gas-tooltip-input-detail', {}, [unitLabel]), h('div.gas-tooltip-input-arrows', {}, [ h('i.fa.fa-angle-up', { - onClick: () => this.setValue(addCurrencies(value, step)), + onClick: () => this.setValue(addCurrencies(value, step, { toNumericBase: 'dec' })), }), h('i.fa.fa-angle-down', { style: { cursor: 'pointer' }, - onClick: () => this.setValue(subtractCurrencies(value, step)), + onClick: () => this.setValue(subtractCurrencies(value, step, { toNumericBase: 'dec' })), }), ]), ]) diff --git a/ui/app/components/pages/home.js b/ui/app/components/pages/home.js index 9110f8202..c53413d3b 100644 --- a/ui/app/components/pages/home.js +++ b/ui/app/components/pages/home.js @@ -86,9 +86,9 @@ class Home extends Component { // if (!props.noActiveNotices) { // log.debug('rendering notice screen for unread notices.') // return h(NoticeScreen, { - // notice: props.lastUnreadNotice, + // notice: props.nextUnreadNotice, // key: 'NoticeScreen', - // onConfirm: () => props.dispatch(actions.markNoticeRead(props.lastUnreadNotice)), + // onConfirm: () => props.dispatch(actions.markNoticeRead(props.nextUnreadNotice)), // }) // } else if (props.lostAccounts && props.lostAccounts.length > 0) { // log.debug('rendering notice screen for lost accounts view.') @@ -279,7 +279,7 @@ function mapStateToProps (state) { noActiveNotices, seedWords, unapprovedTxs, - lastUnreadNotice, + nextUnreadNotice, lostAccounts, unapprovedMsgCount, unapprovedPersonalMsgCount, @@ -313,7 +313,7 @@ function mapStateToProps (state) { network: state.metamask.network, provider: state.metamask.provider, forgottenPassword: state.appState.forgottenPassword, - lastUnreadNotice, + nextUnreadNotice, lostAccounts, frequentRpcList: state.metamask.frequentRpcList || [], currentCurrency: state.metamask.currentCurrency, diff --git a/ui/app/components/pages/notice.js b/ui/app/components/pages/notice.js index 2329a9147..a9077b98b 100644 --- a/ui/app/components/pages/notice.js +++ b/ui/app/components/pages/notice.js @@ -154,11 +154,11 @@ class Notice extends Component { const mapStateToProps = state => { const { metamask } = state - const { noActiveNotices, lastUnreadNotice, lostAccounts } = metamask + const { noActiveNotices, nextUnreadNotice, lostAccounts } = metamask return { noActiveNotices, - lastUnreadNotice, + nextUnreadNotice, lostAccounts, } } @@ -171,21 +171,21 @@ Notice.propTypes = { const mapDispatchToProps = dispatch => { return { - markNoticeRead: lastUnreadNotice => dispatch(actions.markNoticeRead(lastUnreadNotice)), + markNoticeRead: nextUnreadNotice => dispatch(actions.markNoticeRead(nextUnreadNotice)), markAccountsFound: () => dispatch(actions.markAccountsFound()), } } const mergeProps = (stateProps, dispatchProps, ownProps) => { - const { noActiveNotices, lastUnreadNotice, lostAccounts } = stateProps + const { noActiveNotices, nextUnreadNotice, lostAccounts } = stateProps const { markNoticeRead, markAccountsFound } = dispatchProps let notice let onConfirm if (!noActiveNotices) { - notice = lastUnreadNotice - onConfirm = () => markNoticeRead(lastUnreadNotice) + notice = nextUnreadNotice + onConfirm = () => markNoticeRead(nextUnreadNotice) } else if (lostAccounts && lostAccounts.length > 0) { notice = generateLostAccountsNotice(lostAccounts) onConfirm = () => markAccountsFound() diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index bbf5683f0..22b2670d8 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -20,7 +20,7 @@ const { calcGasTotal, isBalanceSufficient, } = require('../send_/send.utils') -const GasFeeDisplay = require('../send/gas-fee-display-v2') +const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component').default const SenderToRecipient = require('../sender-to-recipient') const NetworkDisplay = require('../network-display') const currencyFormatter = require('currency-formatter') diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index ee066b8f4..535347cee 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -11,7 +11,7 @@ abiDecoder.addABI(tokenAbi) const actions = require('../../actions') const clone = require('clone') const Identicon = require('../identicon') -const GasFeeDisplay = require('../send/gas-fee-display-v2.js') +const GasFeeDisplay = require('../send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js').default const NetworkDisplay = require('../network-display') const ethUtil = require('ethereumjs-util') const BN = ethUtil.BN diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 3bc9ad226..5e2c5fdf6 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -57,6 +57,7 @@ CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversi return selectedToken ? conversionUtil(ethUtil.addHexPrefix(value), { fromNumericBase: 'hex', + toNumericBase: 'dec', toCurrency: symbol, conversionRate: multiplier, invertConversionRate: true, @@ -91,8 +92,12 @@ CurrencyDisplay.prototype.getConvertedValueToRender = function (nonFormattedValu : convertedValue } +function removeLeadingZeroes (str) { + return str.replace(/^0*(?=\d)/, '') +} + CurrencyDisplay.prototype.handleChange = function (newVal) { - this.setState({ valueToRender: newVal }) + this.setState({ valueToRender: removeLeadingZeroes(newVal) }) this.props.onChange(this.getAmount(newVal)) } @@ -113,6 +118,7 @@ CurrencyDisplay.prototype.render = function () { readOnly = false, inError = false, onBlur, + step, } = this.props const { valueToRender } = this.state @@ -147,6 +153,7 @@ CurrencyDisplay.prototype.render = function () { width: this.getInputWidth(valueToRender, readOnly), }, min: 0, + step, }), h('span.currency-display__currency-symbol', primaryCurrency), diff --git a/ui/app/components/send/gas-fee-display-v2.js b/ui/app/components/send/gas-fee-display-v2.js deleted file mode 100644 index 1423aa84d..000000000 --- a/ui/app/components/send/gas-fee-display-v2.js +++ /dev/null @@ -1,53 +0,0 @@ -const Component = require('react').Component -const PropTypes = require('prop-types') -const h = require('react-hyperscript') -const inherits = require('util').inherits -const CurrencyDisplay = require('./currency-display') -const connect = require('react-redux').connect - -GasFeeDisplay.contextTypes = { - t: PropTypes.func, -} - -module.exports = connect()(GasFeeDisplay) - - -inherits(GasFeeDisplay, Component) -function GasFeeDisplay () { - Component.call(this) -} - -GasFeeDisplay.prototype.render = function () { - const { - conversionRate, - gasTotal, - onClick, - primaryCurrency = 'ETH', - convertedCurrency, - gasLoadingError, - } = this.props - - return h('div.send-v2__gas-fee-display', [ - - gasTotal - ? h(CurrencyDisplay, { - primaryCurrency, - convertedCurrency, - value: gasTotal, - conversionRate, - convertedPrefix: '$', - readOnly: true, - }) - : gasLoadingError - ? h('div.currency-display.currency-display--message', this.context.t('setGasPrice')) - : h('div.currency-display', this.context.t('loading')), - - h('button.sliders-icon-container', { - onClick, - disabled: !gasTotal && !gasLoadingError, - }, [ - h('i.fa.fa-sliders.sliders-icon'), - ]), - - ]) -} diff --git a/ui/app/components/send_/account-list-item/account-list-item.container.js b/ui/app/components/send_/account-list-item/account-list-item.container.js index 3151b1f1d..4b4519288 100644 --- a/ui/app/components/send_/account-list-item/account-list-item.container.js +++ b/ui/app/components/send_/account-list-item/account-list-item.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { getConversionRate, - getConvertedCurrency, + getCurrentCurrency, } from '../send.selectors.js' import AccountListItem from './account-list-item.component' @@ -10,6 +10,6 @@ export default connect(mapStateToProps)(AccountListItem) function mapStateToProps (state) { return { conversionRate: getConversionRate(state), - currentCurrency: getConvertedCurrency(state), + currentCurrency: getCurrentCurrency(state), } } diff --git a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js b/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js index 49da920e6..af0859117 100644 --- a/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js +++ b/ui/app/components/send_/account-list-item/tests/account-list-item-container.test.js @@ -12,7 +12,7 @@ proxyquire('../account-list-item.container.js', { }, '../send.selectors.js': { getConversionRate: (s) => `mockConversionRate:${s}`, - getConvertedCurrency: (s) => `mockCurrentCurrency:${s}`, + getCurrentCurrency: (s) => `mockCurrentCurrency:${s}`, }, }) diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js index 8aefeed4a..196538c11 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.component.js @@ -23,6 +23,7 @@ export default class SendAmountRow extends Component { tokenBalance: PropTypes.string, updateSendAmount: PropTypes.func, updateSendAmountError: PropTypes.func, + updateGas: PropTypes.func, } validateAmount (amount) { @@ -56,6 +57,14 @@ export default class SendAmountRow extends Component { updateSendAmount(amount) } + updateGas (amount) { + const { selectedToken, updateGas } = this.props + + if (selectedToken) { + updateGas({ amount }) + } + } + render () { const { amount, @@ -77,12 +86,16 @@ export default class SendAmountRow extends Component { <CurrencyDisplay conversionRate={amountConversionRate} convertedCurrency={convertedCurrency} - onBlur={newAmount => this.updateAmount(newAmount)} + onBlur={newAmount => { + this.updateGas(newAmount) + this.updateAmount(newAmount) + }} onChange={newAmount => this.validateAmount(newAmount)} inError={inError} primaryCurrency={primaryCurrency || 'ETH'} selectedToken={selectedToken} - value={amount || '0x0'} + value={amount} + step="any" /> </SendRowWrapper> ) diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js index bbbf56971..b816d948f 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.container.js @@ -2,7 +2,7 @@ import { connect } from 'react-redux' import { getAmountConversionRate, getConversionRate, - getConvertedCurrency, + getCurrentCurrency, getGasTotal, getPrimaryCurrency, getSelectedToken, @@ -31,7 +31,7 @@ function mapStateToProps (state) { amountConversionRate: getAmountConversionRate(state), balance: getSendFromBalance(state), conversionRate: getConversionRate(state), - convertedCurrency: getConvertedCurrency(state), + convertedCurrency: getCurrentCurrency(state), gasTotal: getGasTotal(state), inError: sendAmountIsInError(state), primaryCurrency: getPrimaryCurrency(state), diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js index 2205579ca..579e18585 100644 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-component.test.js @@ -12,10 +12,12 @@ const propsMethodSpies = { setMaxModeTo: sinon.spy(), updateSendAmount: sinon.spy(), updateSendAmountError: sinon.spy(), + updateGas: sinon.spy(), } sinon.spy(SendAmountRow.prototype, 'updateAmount') sinon.spy(SendAmountRow.prototype, 'validateAmount') +sinon.spy(SendAmountRow.prototype, 'updateGas') describe('SendAmountRow Component', function () { let wrapper @@ -36,6 +38,7 @@ describe('SendAmountRow Component', function () { tokenBalance={'mockTokenBalance'} updateSendAmount={propsMethodSpies.updateSendAmount} updateSendAmountError={propsMethodSpies.updateSendAmountError} + updateGas={propsMethodSpies.updateGas} />, { context: { t: str => str + '_t' } }) instance = wrapper.instance() }) @@ -139,8 +142,14 @@ describe('SendAmountRow Component', function () { assert.equal(primaryCurrency, 'mockPrimaryCurrency') assert.deepEqual(selectedToken, { address: 'mockTokenAddress' }) assert.equal(value, 'mockAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 0) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 0) onBlur('mockNewAmount') + assert.equal(SendAmountRow.prototype.updateGas.callCount, 1) + assert.deepEqual( + SendAmountRow.prototype.updateGas.getCall(0).args, + ['mockNewAmount'] + ) assert.equal(SendAmountRow.prototype.updateAmount.callCount, 1) assert.deepEqual( SendAmountRow.prototype.updateAmount.getCall(0).args, diff --git a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js index e4c913c69..94d9918a7 100644 --- a/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js +++ b/ui/app/components/send_/send-content/send-amount-row/tests/send-amount-row-container.test.js @@ -24,7 +24,7 @@ proxyquire('../send-amount-row.container.js', { '../../send.selectors': { getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`, - getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`, getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, getSelectedToken: (s) => `mockSelectedToken:${s}`, diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js index 3a14054eb..adc114c0e 100644 --- a/ui/app/components/send_/send-content/send-content.component.js +++ b/ui/app/components/send_/send-content/send-content.component.js @@ -18,7 +18,7 @@ export default class SendContent extends Component { <div className="send-v2__form"> <SendFromRow /> <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> - <SendAmountRow /> + <SendAmountRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendGasRow /> </div> </PageContainerContent> diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js new file mode 100644 index 000000000..b1fd67412 --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/gas-fee-display.component.js @@ -0,0 +1,61 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import CurrencyDisplay from '../../../../send/currency-display' + + +export default class GasFeeDisplay extends Component { + + static propTypes = { + conversionRate: PropTypes.number, + primaryCurrency: PropTypes.string, + convertedCurrency: PropTypes.string, + gasLoadingError: PropTypes.bool, + gasTotal: PropTypes.string, + onClick: PropTypes.func, + }; + + render() { + const { + conversionRate, + gasTotal, + onClick, + primaryCurrency = 'ETH', + convertedCurrency, + gasLoadingError, + } = this.props + + return ( + <div className="send-v2__gas-fee-display"> + {gasTotal + ? <CurrencyDisplay + primaryCurrency={primaryCurrency} + convertedCurrency={convertedCurrency} + value={gasTotal} + conversionRate={conversionRate} + gasLoadingError={gasLoadingError} + convertedPrefix={'$'} + readOnly + /> + : gasLoadingError + ? <div className="currency-display.currency-display--message"> + {this.context.t('setGasPrice')} + </div> + : <div className="currency-display"> + {this.context.t('loading')} + </div> + } + <button + className="sliders-icon-container" + onClick={onClick} + disabled={!gasTotal && !gasLoadingError} + > + <i className="fa fa-sliders sliders-icon" /> + </button> + </div> + ) + } +} + +GasFeeDisplay.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js new file mode 100644 index 000000000..dba0edb7b --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/index.js @@ -0,0 +1 @@ +export { default } from './gas-fee-display.component' diff --git a/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js new file mode 100644 index 000000000..66f3a94df --- /dev/null +++ b/ui/app/components/send_/send-content/send-gas-row/gas-fee-display/test/gas-fee-display.component.test.js @@ -0,0 +1,55 @@ +import React from 'react' +import assert from 'assert' +import {shallow} from 'enzyme' +import GasFeeDisplay from '../gas-fee-display.component' +import CurrencyDisplay from '../../../../../send/currency-display' +import sinon from 'sinon' + + +const propsMethodSpies = { + showCustomizeGasModal: sinon.spy(), +} + +describe('SendGasRow Component', function() { + let wrapper + + beforeEach(() => { + wrapper = shallow(<GasFeeDisplay + conversionRate={20} + gasTotal={'mockGasTotal'} + onClick={propsMethodSpies.showCustomizeGasModal} + primaryCurrency={'mockPrimaryCurrency'} + convertedCurrency={'mockConvertedCurrency'} + />, {context: {t: str => str + '_t'}}) + }) + + afterEach(() => { + propsMethodSpies.showCustomizeGasModal.resetHistory() + }) + + describe('render', () => { + it('should render a CurrencyDisplay component', () => { + assert.equal(wrapper.find(CurrencyDisplay).length, 1) + }) + + it('should render the CurrencyDisplay with the correct props', () => { + const { + conversionRate, + convertedCurrency, + value, + } = wrapper.find(CurrencyDisplay).props() + assert.equal(conversionRate, 20) + assert.equal(convertedCurrency, 'mockConvertedCurrency') + assert.equal(value, 'mockGasTotal') + }) + + it('should render the Button with the correct props', () => { + const { + onClick, + } = wrapper.find('button').props() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 0) + onClick() + assert.equal(propsMethodSpies.showCustomizeGasModal.callCount, 1) + }) + }) +}) diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js index c80d8c0bb..17cea3d4e 100644 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.component.js @@ -1,7 +1,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' -import GasFeeDisplay from '../../../send/gas-fee-display-v2' +import GasFeeDisplay from './gas-fee-display/gas-fee-display.component' export default class SendGasRow extends Component { diff --git a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js index 20d3daa59..6e6fbc8a8 100644 --- a/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js +++ b/ui/app/components/send_/send-content/send-gas-row/send-gas-row.container.js @@ -1,7 +1,7 @@ import { connect } from 'react-redux' import { getConversionRate, - getConvertedCurrency, + getCurrentCurrency, getGasTotal, } from '../../send.selectors.js' import { sendGasIsInError } from './send-gas-row.selectors.js' @@ -13,7 +13,7 @@ export default connect(mapStateToProps, mapDispatchToProps)(SendGasRow) function mapStateToProps (state) { return { conversionRate: getConversionRate(state), - convertedCurrency: getConvertedCurrency(state), + convertedCurrency: getCurrentCurrency(state), gasTotal: getGasTotal(state), gasLoadingError: sendGasIsInError(state), } diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js index e4f05d708..db37f18be 100644 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-component.test.js @@ -5,7 +5,7 @@ import sinon from 'sinon' import SendGasRow from '../send-gas-row.component.js' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' -import GasFeeDisplay from '../../../../send/gas-fee-display-v2' +import GasFeeDisplay from '../gas-fee-display/gas-fee-display.component' const propsMethodSpies = { showCustomizeGasModal: sinon.spy(), diff --git a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js index 9135524d1..e928c8aba 100644 --- a/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js +++ b/ui/app/components/send_/send-content/send-gas-row/tests/send-gas-row-container.test.js @@ -19,7 +19,7 @@ proxyquire('../send-gas-row.container.js', { }, '../../send.selectors.js': { getConversionRate: (s) => `mockConversionRate:${s}`, - getConvertedCurrency: (s) => `mockConvertedCurrency:${s}`, + getCurrentCurrency: (s) => `mockConvertedCurrency:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`, }, './send-gas-row.selectors.js': { sendGasIsInError: (s) => `mockGasLoadingError:${s}` }, diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js index 0a83186a5..1c2ecdf9c 100644 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js @@ -19,9 +19,9 @@ export default class SendToRow extends Component { updateSendToError: PropTypes.func, }; - handleToChange (to, nickname = '') { + handleToChange (to, nickname = '', toError) { const { updateSendTo, updateSendToError, updateGas } = this.props - const toErrorObject = getToErrorObject(to) + const toErrorObject = getToErrorObject(to, toError) updateSendTo(to, nickname) updateSendToError(toErrorObject) if (toErrorObject.to === null) { @@ -53,7 +53,7 @@ export default class SendToRow extends Component { inError={inError} name={'address'} network={network} - onChange={(newTo, newNickname) => this.handleToChange(newTo, newNickname)} + onChange={({ toAddress, nickname, toError }) => this.handleToChange(toAddress, nickname, toError)} openDropdown={() => openToDropdown()} placeholder={this.context.t('recipientAddress')} to={to} diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js index cea51ee20..6b90a9f09 100644 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js @@ -4,12 +4,10 @@ const { } = require('../../send.constants') const { isValidAddress } = require('../../../../util') -function getToErrorObject (to) { - let toError = null - +function getToErrorObject (to, toError = null) { if (!to) { toError = REQUIRED_ERROR - } else if (!isValidAddress(to)) { + } else if (!isValidAddress(to) && !toError) { toError = INVALID_RECIPIENT_ADDRESS_ERROR } diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js index 58fe51dcf..781371004 100644 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js @@ -6,8 +6,8 @@ import proxyquire from 'proxyquire' const SendToRow = proxyquire('../send-to-row.component.js', { './send-to-row.utils.js': { - getToErrorObject: (to) => ({ - to: to === false ? null : `mockToErrorObject:${to}`, + getToErrorObject: (to, toError) => ({ + to: to === false ? null : `mockToErrorObject:${to}${toError}`, }), }, }).default @@ -67,11 +67,11 @@ describe('SendToRow Component', function () { it('should call updateSendToError', () => { assert.equal(propsMethodSpies.updateSendToError.callCount, 0) - instance.handleToChange('mockTo2') + instance.handleToChange('mockTo2', '', 'mockToError') assert.equal(propsMethodSpies.updateSendToError.callCount, 1) assert.deepEqual( propsMethodSpies.updateSendToError.getCall(0).args, - [{ to: 'mockToErrorObject:mockTo2' }] + [{ to: 'mockToErrorObject:mockTo2mockToError' }] ) }) @@ -138,11 +138,11 @@ describe('SendToRow Component', function () { openDropdown() assert.equal(propsMethodSpies.openToDropdown.callCount, 1) assert.equal(SendToRow.prototype.handleToChange.callCount, 0) - onChange('mockNewTo', 'mockNewNickname') + onChange({ toAddress: 'mockNewTo', nickname: 'mockNewNickname', toError: 'mockToError' }) assert.equal(SendToRow.prototype.handleToChange.callCount, 1) assert.deepEqual( SendToRow.prototype.handleToChange.getCall(0).args, - ['mockNewTo', 'mockNewNickname'] + ['mockNewTo', 'mockNewNickname', 'mockToError'] ) }) }) diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js index 615c9581b..4d2447c32 100644 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-utils.test.js @@ -40,6 +40,12 @@ describe('send-to-row utils', () => { to: null, }) }) + + it('should return the passed error if to is truthy but invalid if to is truthy and valid', () => { + assert.deepEqual(getToErrorObject('invalid #$ 345878', 'someExplicitError'), { + to: 'someExplicitError', + }) + }) }) }) diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js index 516251e22..219b362f2 100644 --- a/ui/app/components/send_/send.component.js +++ b/ui/app/components/send_/send.component.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types' import PersistentForm from '../../../lib/persistent-form' import { getAmountErrorObject, + getToAddressForGasUpdate, doesAmountErrorRequireUpdate, } from './send.utils' @@ -38,7 +39,7 @@ export default class SendTransactionScreen extends PersistentForm { updateSendTokenBalance: PropTypes.func, }; - updateGas ({ to } = {}) { + updateGas ({ to: updatedToAddress, amount: value } = {}) { const { amount, blockGasLimit, @@ -48,6 +49,7 @@ export default class SendTransactionScreen extends PersistentForm { recentBlocks, selectedAddress, selectedToken = {}, + to: currentToAddress, updateAndSetGasTotal, } = this.props @@ -59,8 +61,8 @@ export default class SendTransactionScreen extends PersistentForm { recentBlocks, selectedAddress, selectedToken, - to: to && to.toLowerCase(), - value: amount, + to: getToAddressForGasUpdate(updatedToAddress, currentToAddress), + value: value || amount, }) } diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send_/send.constants.js index df5dee371..8acdf0641 100644 --- a/ui/app/components/send_/send.constants.js +++ b/ui/app/components/send_/send.constants.js @@ -36,6 +36,7 @@ const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', { })) const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send. +const BASE_TOKEN_GAS_COST = '0x186a0' // Hex for 100000, a base estimate for token transfers. module.exports = { INSUFFICIENT_FUNDS_ERROR, @@ -52,4 +53,5 @@ module.exports = { REQUIRED_ERROR, SIMPLE_GAS_COST, TOKEN_TRANSFER_FUNCTION_SIGNATURE, + BASE_TOKEN_GAS_COST, } diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js index 1fd96d61f..185653c5f 100644 --- a/ui/app/components/send_/send.container.js +++ b/ui/app/components/send_/send.container.js @@ -19,6 +19,7 @@ import { getSendAmount, getSendEditingTransactionId, getSendFromObject, + getSendTo, getTokenBalance, } from './send.selectors' import { @@ -54,6 +55,7 @@ function mapStateToProps (state) { recentBlocks: getRecentBlocks(state), selectedAddress: getSelectedAddress(state), selectedToken: getSelectedToken(state), + to: getSendTo(state), tokenBalance: getTokenBalance(state), tokenContract: getSelectedTokenContract(state), tokenToFiatRate: getSelectedTokenToFiatRate(state), diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js index 7e7cfe2e9..f910f7caf 100644 --- a/ui/app/components/send_/send.selectors.js +++ b/ui/app/components/send_/send.selectors.js @@ -14,7 +14,6 @@ const selectors = { getAmountConversionRate, getBlockGasLimit, getConversionRate, - getConvertedCurrency, getCurrentAccountWithSendEtherInfo, getCurrentCurrency, getCurrentNetwork, @@ -98,10 +97,6 @@ function getConversionRate (state) { return state.metamask.conversionRate } -function getConvertedCurrency (state) { - return state.metamask.currentCurrency -} - function getCurrentAccountWithSendEtherInfo (state) { const currentAddress = getSelectedAddress(state) const accounts = accountsWithSendEtherInfoSelector(state) diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js index 67699be77..872df1d2f 100644 --- a/ui/app/components/send_/send.utils.js +++ b/ui/app/components/send_/send.utils.js @@ -4,11 +4,13 @@ const { conversionGTE, multiplyCurrencies, conversionGreaterThan, + conversionLessThan, } = require('../../conversion-util') const { calcTokenAmount, } = require('../../token-util') const { + BASE_TOKEN_GAS_COST, INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, NEGATIVE_ETH_ERROR, @@ -20,6 +22,7 @@ const abi = require('ethereumjs-abi') const ethUtil = require('ethereumjs-util') module.exports = { + addGasBuffer, calcGasTotal, calcTokenBalance, doesAmountErrorRequireUpdate, @@ -27,6 +30,7 @@ module.exports = { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getToAddressForGasUpdate, isBalanceSufficient, isTokenBalanceSufficient, } @@ -175,12 +179,13 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, } // if recipient has no code, gas is 21k max: - const hasRecipient = Boolean(to) - if (hasRecipient && !selectedToken) { - const code = await global.eth.getCode(to) + if (!selectedToken) { + const code = Boolean(to) && await global.eth.getCode(to) if (!code || code === '0x') { return SIMPLE_GAS_COST } + } else if (selectedToken && !to) { + return BASE_TOKEN_GAS_COST } paramsForGasEstimate.to = selectedToken ? selectedToken.address : to @@ -201,16 +206,46 @@ async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, err.message.includes('gas required exceeds allowance or always failing transaction') ) if (simulationFailed) { - return resolve(paramsForGasEstimate.gas) + const estimateWithBuffer = addGasBuffer(paramsForGasEstimate.gas, blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) } else { return reject(err) } } - return resolve(estimatedGas.toString(16)) + const estimateWithBuffer = addGasBuffer(estimatedGas.toString(16), blockGasLimit, 1.5) + return resolve(ethUtil.addHexPrefix(estimateWithBuffer)) }) }) } +function addGasBuffer (initialGasLimitHex, blockGasLimitHex, bufferMultiplier = 1.5) { + const upperGasLimit = multiplyCurrencies(blockGasLimitHex, 0.9, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + const bufferedGasLimit = multiplyCurrencies(initialGasLimitHex, bufferMultiplier, { + toNumericBase: 'hex', + multiplicandBase: 16, + multiplierBase: 10, + numberOfDecimals: '0', + }) + + // if initialGasLimit is above blockGasLimit, dont modify it + if (conversionGreaterThan( + { value: initialGasLimitHex, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return initialGasLimitHex + // if bufferedGasLimit is below blockGasLimit, use bufferedGasLimit + if (conversionLessThan( + { value: bufferedGasLimit, fromNumericBase: 'hex' }, + { value: upperGasLimit, fromNumericBase: 'hex' }, + )) return bufferedGasLimit + // otherwise use blockGasLimit + return upperGasLimit +} + function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) { if (!selectedToken) return return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( @@ -237,3 +272,7 @@ function estimateGasPriceFromRecentBlocks (recentBlocks) { return lowestPrices[Math.floor(lowestPrices.length / 2)] } + +function getToAddressForGasUpdate (...addresses) { + return [...addresses, ''].find(str => str !== undefined && str !== null).toLowerCase() +} diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js index 4e33d8f63..4ba9b226d 100644 --- a/ui/app/components/send_/tests/send-component.test.js +++ b/ui/app/components/send_/tests/send-component.test.js @@ -201,7 +201,7 @@ describe('Send Component', function () { }) describe('updateGas', () => { - it('should call updateAndSetGasTotal with the correct params', () => { + it('should call updateAndSetGasTotal with the correct params if no to prop is passed', () => { propsMethodSpies.updateAndSetGasTotal.resetHistory() wrapper.instance().updateGas() assert.equal(propsMethodSpies.updateAndSetGasTotal.callCount, 1) @@ -215,12 +215,22 @@ describe('Send Component', function () { recentBlocks: ['mockBlock'], selectedAddress: 'mockSelectedAddress', selectedToken: 'mockSelectedToken', - to: undefined, + to: '', value: 'mockAmount', } ) }) + it('should call updateAndSetGasTotal with the correct params if a to prop is passed', () => { + propsMethodSpies.updateAndSetGasTotal.resetHistory() + wrapper.setProps({ to: 'someAddress' }) + wrapper.instance().updateGas() + assert.equal( + propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, + 'someaddress', + ) + }) + it('should call updateAndSetGasTotal with to set to lowercase if passed', () => { propsMethodSpies.updateAndSetGasTotal.resetHistory() wrapper.instance().updateGas({ to: '0xABC' }) diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js index 056aad148..91484f4d8 100644 --- a/ui/app/components/send_/tests/send-container.test.js +++ b/ui/app/components/send_/tests/send-container.test.js @@ -39,6 +39,7 @@ proxyquire('../send.container.js', { getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenToFiatRate: (s) => `mockTokenToFiatRate:${s}`, getSendAmount: (s) => `mockAmount:${s}`, + getSendTo: (s) => `mockTo:${s}`, getSendEditingTransactionId: (s) => `mockEditingTransactionId:${s}`, getSendFromObject: (s) => `mockFrom:${s}`, getTokenBalance: (s) => `mockTokenBalance:${s}`, @@ -70,6 +71,7 @@ describe('send container', () => { recentBlocks: 'mockRecentBlocks:mockState', selectedAddress: 'mockSelectedAddress:mockState', selectedToken: 'mockSelectedToken:mockState', + to: 'mockTo:mockState', tokenBalance: 'mockTokenBalance:mockState', tokenContract: 'mockTokenContract:mockState', tokenToFiatRate: 'mockTokenToFiatRate:mockState', diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send_/tests/send-selectors.test.js index 152af8059..218da656b 100644 --- a/ui/app/components/send_/tests/send-selectors.test.js +++ b/ui/app/components/send_/tests/send-selectors.test.js @@ -8,7 +8,6 @@ const { getBlockGasLimit, getAmountConversionRate, getConversionRate, - getConvertedCurrency, getCurrentAccountWithSendEtherInfo, getCurrentCurrency, getCurrentNetwork, @@ -154,15 +153,6 @@ describe('send selectors', () => { }) }) - describe('getConvertedCurrency()', () => { - it('should return the currently selected currency', () => { - assert.equal( - getConvertedCurrency(mockState), - 'USD' - ) - }) - }) - describe('getCurrentAccountWithSendEtherInfo()', () => { it('should return the currently selected account with identity info', () => { assert.deepEqual( diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js index b3f6372ef..a518a64e9 100644 --- a/ui/app/components/send_/tests/send-utils.test.js +++ b/ui/app/components/send_/tests/send-utils.test.js @@ -2,6 +2,7 @@ import assert from 'assert' import sinon from 'sinon' import proxyquire from 'proxyquire' import { + BASE_TOKEN_GAS_COST, ONE_GWEI_IN_WEI_HEX, SIMPLE_GAS_COST, } from '../send.constants' @@ -18,10 +19,12 @@ const { const stubs = { addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), - conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), + conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value >= obj2.value), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), rawEncode: sinon.stub().returns([16, 1100]), + conversionGreaterThan: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), + conversionLessThan: sinon.stub().callsFake((obj1, obj2) => obj1.value < obj2.value), } const sendUtils = proxyquire('../send.utils.js', { @@ -30,6 +33,8 @@ const sendUtils = proxyquire('../send.utils.js', { conversionUtil: stubs.conversionUtil, conversionGTE: stubs.conversionGTE, multiplyCurrencies: stubs.multiplyCurrencies, + conversionGreaterThan: stubs.conversionGreaterThan, + conversionLessThan: stubs.conversionLessThan, }, '../../token-util': { calcTokenAmount: stubs.calcTokenAmount }, 'ethereumjs-abi': { @@ -44,6 +49,7 @@ const { estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, + getToAddressForGasUpdate, calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, @@ -255,7 +261,7 @@ describe('send utils', () => { estimateGasMethod: sinon.stub().callsFake( (data, cb) => cb( data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null, - { toString: (n) => `mockToString:${n}` } + { toString: (n) => `0xabc${n}` } ) ), } @@ -279,13 +285,23 @@ describe('send utils', () => { }) it('should call ethQuery.estimateGas with the expected params', async () => { - const result = await estimateGas(baseMockParams) + const result = await sendUtils.estimateGas(baseMockParams) assert.equal(baseMockParams.estimateGasMethod.callCount, 1) assert.deepEqual( baseMockParams.estimateGasMethod.getCall(0).args[0], Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall) ) - assert.equal(result, 'mockToString:16') + assert.equal(result, '0xabc16') + }) + + it('should call ethQuery.estimateGas with the expected params when initialGasLimitHex is lower than the upperGasLimit', async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { blockGasLimit: '0xbcd' })) + assert.equal(baseMockParams.estimateGasMethod.callCount, 1) + assert.deepEqual( + baseMockParams.estimateGasMethod.getCall(0).args[0], + Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall, { gas: '0xbcdx0.95' }) + ) + assert.equal(result, '0xabc16x1.5') }) it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => { @@ -300,7 +316,7 @@ describe('send utils', () => { to: 'mockAddress', }) ) - assert.equal(result, 'mockToString:16') + assert.equal(result, '0xabc16') }) it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { @@ -309,12 +325,23 @@ describe('send utils', () => { assert.equal(result, SIMPLE_GAS_COST) }) + it(`should return ${SIMPLE_GAS_COST} if not passed a selectedToken or truthy to address`, async () => { + assert.equal(baseMockParams.estimateGasMethod.callCount, 0) + const result = await estimateGas(Object.assign({}, baseMockParams, { to: null })) + assert.equal(result, SIMPLE_GAS_COST) + }) + it(`should not return ${SIMPLE_GAS_COST} if passed a selectedToken`, async () => { assert.equal(baseMockParams.estimateGasMethod.callCount, 0) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123', selectedToken: { address: '' } })) assert.notEqual(result, SIMPLE_GAS_COST) }) + it(`should return ${BASE_TOKEN_GAS_COST} if passed a selectedToken but no to address`, async () => { + const result = await estimateGas(Object.assign({}, baseMockParams, { to: null, selectedToken: { address: '' } })) + assert.equal(result, BASE_TOKEN_GAS_COST) + }) + it(`should return the adjusted blockGasLimit if it fails with a 'Transaction execution error.'`, async () => { const result = await estimateGas(Object.assign({}, baseMockParams, { to: 'isContract willFailBecauseOf:Transaction execution error.', @@ -401,4 +428,15 @@ describe('send utils', () => { assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5') }) }) + + describe('getToAddressForGasUpdate()', () => { + it('should return empty string if all params are undefined or null', () => { + assert.equal(getToAddressForGasUpdate(undefined, null), '') + }) + + it('should return the first string that is not defined or null in lower case', () => { + assert.equal(getToAddressForGasUpdate('A', null), 'a') + assert.equal(getToAddressForGasUpdate(undefined, 'B'), 'b') + }) + }) }) diff --git a/ui/app/components/shapeshift-form.js b/ui/app/components/shapeshift-form.js index 93d2023b5..2c4ba40bf 100644 --- a/ui/app/components/shapeshift-form.js +++ b/ui/app/components/shapeshift-form.js @@ -181,7 +181,7 @@ ShapeshiftForm.prototype.render = function () { return h('div.shapeshift-form-wrapper', [ showQrCode ? this.renderQrCode() - : h('div.shapeshift-form', [ + : h('div.modal-shapeshift-form', [ h('div.shapeshift-form__selectors', [ h('div.shapeshift-form__selector', [ diff --git a/ui/app/components/signature-request.js b/ui/app/components/signature-request.js index ab780dcf4..bbb340fcf 100644 --- a/ui/app/components/signature-request.js +++ b/ui/app/components/signature-request.js @@ -178,7 +178,14 @@ SignatureRequest.prototype.renderBody = function () { rows = data } else if (type === 'eth_sign') { rows = [{ name: this.context.t('message'), value: data }] - notice = this.context.t('signNotice') + notice = [this.context.t('signNotice'), + h('span.request-signature__help-link', { + onClick: () => { + global.platform.openWindow({ + url: 'https://consensys.zendesk.com/hc/en-us/articles/360004427792', + }) + }, + }, this.context.t('learnMore'))] } return h('div.request-signature__body', {}, [ @@ -197,6 +204,9 @@ SignatureRequest.prototype.renderBody = function () { h('div.request-signature__rows', [ ...rows.map(({ name, value }) => { + if (typeof value === 'boolean') { + value = value.toString() + } return h('div.request-signature__row', [ h('div.request-signature__row-title', [`${name}:`]), h('div.request-signature__row-value', value), diff --git a/ui/app/components/token-balance.js b/ui/app/components/token-balance.js index 1900ccec7..df3bd59bb 100644 --- a/ui/app/components/token-balance.js +++ b/ui/app/components/token-balance.js @@ -34,7 +34,7 @@ TokenBalance.prototype.render = function () { return isLoading ? h('span', '') : h('span.token-balance', [ - h('span.token-balance__amount', string), + h('span.hide-text-overflow.token-balance__amount', string), !balanceOnly && h('span.token-balance__symbol', symbol), ]) } diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 100402d95..337763067 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -190,6 +190,16 @@ const conversionGreaterThan = ( return firstValue.gt(secondValue) } +const conversionLessThan = ( + { ...firstProps }, + { ...secondProps }, +) => { + const firstValue = converter({ ...firstProps }) + const secondValue = converter({ ...secondProps }) + + return firstValue.lt(secondValue) +} + const conversionMax = ( { ...firstProps }, { ...secondProps }, @@ -229,6 +239,7 @@ module.exports = { addCurrencies, multiplyCurrencies, conversionGreaterThan, + conversionLessThan, conversionGTE, conversionLTE, conversionMax, diff --git a/ui/app/css/itcss/components/currency-display.scss b/ui/app/css/itcss/components/currency-display.scss index 3560b0b0c..b1a74dce2 100644 --- a/ui/app/css/itcss/components/currency-display.scss +++ b/ui/app/css/itcss/components/currency-display.scss @@ -1,6 +1,5 @@ .currency-display { height: 54px; - width: 100%ß; border: 1px solid $alto; border-radius: 4px; background-color: $white; @@ -21,7 +20,7 @@ line-height: 22px; border: none; outline: 0 !important; - max-width: 100%; + max-width: 22ch; } &__primary-currency { @@ -47,14 +46,22 @@ &__input-wrapper { position: relative; display: flex; + flex: 1; + max-width: 100%; + + input[type="number"] { + -moz-appearance: textfield; + } input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; + -moz-appearance: none; display: none; } input[type="number"]:hover::-webkit-inner-spin-button { -webkit-appearance: none; + -moz-appearance: none; display: none; } } @@ -67,12 +74,14 @@ .react-numeric-input { input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; + -moz-appearance: none; display: none; } input[type="number"]:hover::-webkit-inner-spin-button { -webkit-appearance: none; + -moz-appearance: none; display: none; } } -}
\ No newline at end of file +} diff --git a/ui/app/css/itcss/components/hero-balance.scss b/ui/app/css/itcss/components/hero-balance.scss index 09d66aedd..eba93ecb4 100644 --- a/ui/app/css/itcss/components/hero-balance.scss +++ b/ui/app/css/itcss/components/hero-balance.scss @@ -27,25 +27,37 @@ @media screen and (max-width: $break-small) { flex-direction: column; flex: 0 0 auto; + max-width: 100%; } @media screen and (min-width: $break-large) { flex-direction: row; flex-grow: 3; + min-width: 0; } } .balance-display { .token-amount { color: $black; + max-width: 100%; + + .token-balance { + display: flex; + } } @media screen and (max-width: $break-small) { + max-width: 100%; text-align: center; .token-amount { font-size: 1.75rem; margin-top: 1rem; + + .token-balance { + flex-direction: column; + } } .fiat-amount { @@ -56,9 +68,10 @@ } @media screen and (min-width: $break-large) { - margin-left: .8em; + margin: 0 .8em; justify-content: flex-start; align-items: flex-start; + min-width: 0; .token-amount { font-size: 1.5rem; diff --git a/ui/app/css/itcss/components/modal.scss b/ui/app/css/itcss/components/modal.scss index 74658f656..42ef7ae0a 100644 --- a/ui/app/css/itcss/components/modal.scss +++ b/ui/app/css/itcss/components/modal.scss @@ -642,10 +642,31 @@ display: flex; flex-flow: column nowrap; flex: 1; + align-items: center; @media screen and (max-width: 575px) { height: 0; } + + .shapeshift-form-wrapper { + display: flex; + flex-flow: column; + justify-content: center; + align-items: center; + flex: 1 0 auto; + + .shapeshift-form, .modal-shapeshift-form { + border-radius: 8px; + background-color: rgba(0, 0, 0, .05); + padding: 17px 15px; + margin-bottom: 10px; + + &__caret { + width: auto; + flex: 1; + } + } + } } &__logo { @@ -773,17 +794,15 @@ margin-top: 28px; flex: 1 0 auto; - .shapeshift-form { - width: auto; + .shapeshift-form, .modal-shapeshift-form { + border-radius: 8px; + background-color: rgba(0, 0, 0, .05); + padding: 17px 15px; &__caret { width: auto; flex: 1; } - - @media screen and (max-width: 575px) { - width: auto; - } } } diff --git a/ui/app/css/itcss/components/newui-sections.scss b/ui/app/css/itcss/components/newui-sections.scss index bbe0ee661..667e45ba2 100644 --- a/ui/app/css/itcss/components/newui-sections.scss +++ b/ui/app/css/itcss/components/newui-sections.scss @@ -26,14 +26,16 @@ $wallet-view-bg: $alabaster; //Account and transaction details .account-and-transaction-details { display: flex; - flex: 1 0 auto; + flex: 1 1 auto; + min-width: 0; } // tx view .tx-view { - flex: 63.5 0 66.5%; + flex: 1 1 66.5%; background: $tx-view-bg; + min-width: 0; // No title on mobile @media screen and (max-width: 575px) { @@ -286,7 +288,7 @@ $wallet-view-bg: $alabaster; } .token-balance__amount { - padding-right: 6px; + padding: 0 6px; } // first time diff --git a/ui/app/css/itcss/components/request-signature.scss b/ui/app/css/itcss/components/request-signature.scss index e6916c418..4707ff60e 100644 --- a/ui/app/css/itcss/components/request-signature.scss +++ b/ui/app/css/itcss/components/request-signature.scss @@ -183,6 +183,12 @@ padding: 6px 18px 15px; } + &__help-link { + cursor: pointer; + text-decoration: underline; + color: $curious-blue; + } + &__footer { width: 100%; display: flex; diff --git a/ui/app/css/itcss/components/token-list.scss b/ui/app/css/itcss/components/token-list.scss index 72fda372f..4b706abce 100644 --- a/ui/app/css/itcss/components/token-list.scss +++ b/ui/app/css/itcss/components/token-list.scss @@ -81,13 +81,9 @@ $wallet-balance-breakpoint-range: "screen and (min-width: #{$break-large}) and ( } .token-menu-dropdown { - height: 55px; width: 80%; - border-radius: 4px; - background-color: rgba(0, 0, 0, .82); - box-shadow: 0 2px 4px 0 rgba(0, 0, 0, .5); position: absolute; - top: 60px; + top: 52px; right: 25px; z-index: 2000; diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 4e9d0848c..9cacf5fe7 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -62,6 +62,7 @@ function reduceApp (state, action) { warning: null, buyView: {}, isMouseUser: false, + gasIsLoading: false, }, state.appState) switch (action.type) { @@ -675,6 +676,16 @@ function reduceApp (state, action) { isMouseUser: action.value, }) + case actions.GAS_LOADING_STARTED: + return extend(appState, { + gasIsLoading: true, + }) + + case actions.GAS_LOADING_FINISHED: + return extend(appState, { + gasIsLoading: false, + }) + default: return appState } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index a4d1aece5..6c8ac9ed7 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -21,7 +21,7 @@ function reduceMetamask (state, action) { identities: {}, unapprovedTxs: {}, noActiveNotices: true, - lastUnreadNotice: undefined, + nextUnreadNotice: undefined, frequentRpcList: [], addressBook: [], selectedTokenAddress: null, @@ -65,7 +65,7 @@ function reduceMetamask (state, action) { case actions.SHOW_NOTICE: return extend(metamaskState, { noActiveNotices: false, - lastUnreadNotice: action.value, + nextUnreadNotice: action.value, }) case actions.CLEAR_NOTICES: diff --git a/ui/app/selectors.js b/ui/app/selectors.js index a29294b86..cf0affe9c 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -16,6 +16,7 @@ const selectors = { transactionsSelector, accountsWithSendEtherInfoSelector, getCurrentAccountWithSendEtherInfo, + getGasIsLoading, getGasPrice, getGasLimit, getForceGasMin, @@ -117,6 +118,10 @@ function transactionsSelector (state) { .sort((a, b) => b.time - a.time) } +function getGasIsLoading (state) { + return state.appState.gasIsLoading +} + function getGasPrice (state) { return state.metamask.send.gasPrice } diff --git a/ui/app/util.js b/ui/app/util.js index 1ccd17ba7..8c85c5926 100644 --- a/ui/app/util.js +++ b/ui/app/util.js @@ -36,6 +36,7 @@ module.exports = { miniAddressSummary: miniAddressSummary, isAllOneCase: isAllOneCase, isValidAddress: isValidAddress, + isValidENSAddress, numericBalance: numericBalance, parseBalance: parseBalance, formatBalance: formatBalance, @@ -87,6 +88,10 @@ function isValidAddress (address) { return (isAllOneCase(prefixed) && ethUtil.isValidAddress(prefixed)) || ethUtil.isValidChecksumAddress(prefixed) } +function isValidENSAddress (address) { + return address.match(/^.{7,}\.(eth|test)$/) +} + function isInvalidChecksumAddress (address) { var prefixed = ethUtil.addHexPrefix(address) if (address === '0x0000000000000000000000000000000000000000') return false |