diff options
25 files changed, 489 insertions, 156 deletions
diff --git a/test/integration/index.js b/test/integration/index.js index b266ddf37..7789eb283 100644 --- a/test/integration/index.js +++ b/test/integration/index.js @@ -2,7 +2,8 @@ const fs = require('fs') const path = require('path') const pump = require('pump') const browserify = require('browserify') -const tests = fs.readdirSync(path.join(__dirname, 'lib')) +let tests = fs.readdirSync(path.join(__dirname, 'lib')) +tests = tests.filter(fln => fln.match(/send/)) const bundlePath = path.join(__dirname, 'bundle.js') const b = browserify() diff --git a/ui/app/actions.js b/ui/app/actions.js index 674669eed..7b838940a 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1,6 +1,11 @@ const abi = require('human-standard-token-abi') const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const { getTokenAddressFromTokenObject } = require('./util') +const { + calcGasTotal, + getParamsForGasEstimate, + calcTokenBalance, +} = require('./components/send_/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') const log = require('loglevel') @@ -173,14 +178,16 @@ var actions = { updateGasLimit, updateGasPrice, updateGasTotal, + setGasTotal, + setSendTokenBalance, updateSendTokenBalance, updateSendFrom, updateSendTo, updateSendAmount, updateSendMemo, - updateSendErrors, setMaxModeTo, updateSend, + updateSendErrors, clearSend, setSelectedAddress, // app messages @@ -716,14 +723,64 @@ function updateGasPrice (gasPrice) { } } -function updateGasTotal (gasTotal) { +function setGasTotal (gasTotal) { return { type: actions.UPDATE_GAS_TOTAL, value: gasTotal, } } -function updateSendTokenBalance (tokenBalance) { +function updateGasTotal ({ selectedAddress, selectedToken, data }) { + return (dispatch) => { + const { symbol } = selectedToken || {} + const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data) + return Promise.all([ + dispatch(actions.getGasPrice()), + dispatch(actions.estimateGas(estimateGasParams)), + ]) + .then(([gasPrice, gas]) => { + const newGasTotal = calcGasTotal(gas, gasPrice) + dispatch(actions.setGasTotal(newGasTotal)) + dispatch(updateSendErrors({ gasLoadingError: null })) + }) + .catch(err => { + log.error(err) + dispatch(updateSendErrors({ gasLoadingError: 'gasLoadingError' })) + }) + } +} + +function updateSendTokenBalance ({ + selectedToken, + tokenContract, + address, +}) { + return (dispatch) => { + const tokenBalancePromise = tokenContract + ? tokenContract.balanceOf(address) + : Promise.resolve() + return tokenBalancePromise + .then(usersToken => { + if (usersToken) { + const newTokenBalance = calcTokenBalance({ selectedToken, usersToken }) + dispatch(setSendTokenBalance(newTokenBalance)) + } + }) + .catch(err => { + log.error(err) + updateSendErrors({ tokenBalance: 'tokenBalanceError' }) + }) + } +} + +function updateSendErrors (errorObject) { + return { + type: actions.UPDATE_SEND_ERRORS, + value: errorObject, + } +} + +function setSendTokenBalance (tokenBalance) { return { type: actions.UPDATE_SEND_TOKEN_BALANCE, value: tokenBalance, @@ -758,13 +815,6 @@ function updateSendMemo (memo) { } } -function updateSendErrors (error) { - return { - type: actions.UPDATE_SEND_ERRORS, - value: error, - } -} - function setMaxModeTo (bool) { return { type: actions.UPDATE_MAX_MODE, diff --git a/ui/app/app.js b/ui/app/app.js index 0b38b1326..6511979b2 100644 --- a/ui/app/app.js +++ b/ui/app/app.js @@ -11,7 +11,7 @@ const log = require('loglevel') // init const InitializeScreen = require('../../mascara/src/app/first-time').default // accounts -const SendTransactionScreen2 = require('./components/send/send-v2-container') +const SendTransactionScreen = require('./components/send_/send.container') const ConfirmTxScreen = require('./conf-tx') // slideout menu @@ -84,7 +84,7 @@ class App extends Component { h(Initialized, { path: RESTORE_VAULT_ROUTE, exact, component: RestoreVaultPage }), h(Initialized, { path: NOTICE_ROUTE, exact, component: NoticeScreen }), h(Authenticated, { path: CONFIRM_TRANSACTION_ROUTE, component: ConfirmTxScreen }), - h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen2 }), + h(Authenticated, { path: SEND_ROUTE, exact, component: SendTransactionScreen }), h(Authenticated, { path: ADD_TOKEN_ROUTE, exact, component: AddTokenPage }), h(Authenticated, { path: NEW_ACCOUNT_ROUTE, component: CreateAccountPage }), h(Authenticated, { path: DEFAULT_ROUTE, exact, component: Home }), diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 4c693d1c3..0761faa99 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -8,6 +8,10 @@ const GasModalCard = require('./gas-modal-card') const ethUtil = require('ethereumjs-util') +import { + updateSendErrors, +} from '../../ducks/send' + const { MIN_GAS_PRICE_DEC, MIN_GAS_LIMIT_DEC, @@ -63,9 +67,9 @@ function mapDispatchToProps (dispatch) { hideModal: () => dispatch(actions.hideModal()), updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), - updateGasTotal: newGasTotal => dispatch(actions.updateGasTotal(newGasTotal)), + updateGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), - updateSendErrors: error => dispatch(actions.updateSendErrors(error)), + updateSendErrors: error => dispatch(updateSendErrors(error)), } } diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 936db3bcb..ac1b15a4f 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -29,6 +29,10 @@ const currencies = require('currency-formatter/currencies') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') +import { + updateSendErrors, +} from '../../ducks/send' + ConfirmSendEther.contextTypes = { t: PropTypes.func, } @@ -105,7 +109,7 @@ function mapDispatchToProps (dispatch) { })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) }, - updateSendErrors: error => dispatch(actions.updateSendErrors(error)), + updateSendErrors: error => dispatch(updateSendErrors(error)), } } diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 281c42824..e1abc6d3d 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -39,6 +39,10 @@ const { } = require('../../selectors') const { SEND_ROUTE, DEFAULT_ROUTE } = require('../../routes') +import { + updateSendErrors, +} from '../../ducks/send' + ConfirmSendToken.contextTypes = { t: PropTypes.func, } @@ -141,7 +145,7 @@ function mapDispatchToProps (dispatch, ownProps) { })) dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' })) }, - updateSendErrors: error => dispatch(actions.updateSendErrors(error)), + updateSendErrors: error => dispatch(updateSendErrors(error)), } } diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index adfc91240..6464439f2 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -18,6 +18,10 @@ const { getSelectedTokenContract, } = require('../../selectors') +import { + updateSendErrors, +} from '../../ducks/send' + module.exports = compose( withRouter, connect(mapStateToProps, mapDispatchToProps) @@ -74,7 +78,8 @@ function mapDispatchToProps (dispatch) { updateTx: txData => dispatch(actions.updateTransaction(txData)), setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)), addToAddressBook: (address, nickname) => dispatch(actions.addToAddressBook(address, nickname)), - updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)), + setGasTotal: newTotal => dispatch(actions.setGasTotal(newTotal)), + updateGasTotal: () => dispatch(actions.updateGasTotal()), updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)), @@ -82,7 +87,7 @@ function mapDispatchToProps (dispatch) { updateSendTo: (newTo, nickname) => dispatch(actions.updateSendTo(newTo, nickname)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendMemo: newMemo => dispatch(actions.updateSendMemo(newMemo)), - updateSendErrors: newError => dispatch(actions.updateSendErrors(newError)), + updateSendErrors: newError => dispatch(updateSendErrors(newError)), clearSend: () => dispatch(actions.clearSend()), setMaxModeTo: bool => dispatch(actions.setMaxModeTo(bool)), } 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 e1bc1dd79..3151b1f1d 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 @@ -12,4 +12,4 @@ function mapStateToProps (state) { conversionRate: getConversionRate(state), currentCurrency: getConvertedCurrency(state), } -}
\ No newline at end of file +} diff --git a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js index 5bdb67995..4211f16ab 100644 --- a/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js +++ b/ui/app/components/send_/send-content/send-amount-row/amount-max-button/amount-max-button.container.js @@ -9,10 +9,12 @@ import { getMaxModeOn } from '../send-amount-row.selectors.js' import { calcMaxAmount } from './amount-max-button.utils.js' import { updateSendAmount, - updateSendErrors, setMaxModeTo, -} from '../../../actions' +} from '../../../../../actions' import AmountMaxButton from './amount-max-button.component' +import { + updateSendErrors, +} from '../../../../../ducks/send' export default connect(mapStateToProps, mapDispatchToProps)(AmountMaxButton) 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 5c3084365..dbebc0bdf 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 @@ -8,7 +8,10 @@ export default class SendAmountRow extends Component { static propTypes = { amount: PropTypes.string, - amountConversionRate: PropTypes.number, + amountConversionRate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), balance: PropTypes.string, conversionRate: PropTypes.number, convertedCurrency: PropTypes.string, 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 16e88bede..13888ec53 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 @@ -1,24 +1,26 @@ import { connect } from 'react-redux' import { + getAmountConversionRate, getConversionRate, getConvertedCurrency, getGasTotal, + getPrimaryCurrency, getSelectedToken, getSendAmount, getSendFromBalance, getTokenBalance, -} from '../../send.selectors.js' +} from '../../send.selectors' import { - getAmountConversionRate, - getPrimaryCurrency, sendAmountIsInError, -} from './send-amount-row.selectors.js' -import { getAmountErrorObject } from './send-amount-row.utils.js' +} from './send-amount-row.selectors' +import { getAmountErrorObject } from '../../send.utils' import { setMaxModeTo, updateSendAmount, - updateSendErrors, } from '../../../../actions' +import { + updateSendErrors, +} from '../../../../ducks/send' import SendAmountRow from './send-amount-row.component' export default connect(mapStateToProps, mapDispatchToProps)(SendAmountRow) diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js index 88dee0dcb..62e4f6b32 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.selectors.js @@ -1,13 +1,5 @@ -import { - getSelectedToken, - getSelectedTokenToFiatRate, - getConversionRate, -} from '../../send.selectors.js' - const selectors = { - getAmountConversionRate, getMaxModeOn, - getPrimaryCurrency, sendAmountIsInError, } @@ -18,16 +10,5 @@ function getMaxModeOn (state) { } function sendAmountIsInError (state) { - return Boolean(state.metamask.send.errors.amount) -} - -function getPrimaryCurrency (state) { - const selectedToken = getSelectedToken(state) - return selectedToken && selectedToken.symbol -} - -function getAmountConversionRate (state) { - return getSelectedToken(state) - ? getSelectedTokenToFiatRate(state) - : getConversionRate(state) + return Boolean(state.send.errors.amount) } diff --git a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js index 6ec5463d3..e69de29bb 100644 --- a/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js +++ b/ui/app/components/send_/send-content/send-amount-row/send-amount-row.utils.js @@ -1,61 +0,0 @@ -const { - conversionGreaterThan, -} = require('../../../../conversion-util') -const { - isBalanceSufficient, - isTokenBalanceSufficient, -} = require('../../send.utils') - -function getAmountErrorObject ({ - amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - selectedToken, - tokenBalance, -}) { - let insufficientFunds = false - if (gasTotal && conversionRate) { - insufficientFunds = !isBalanceSufficient({ - amount: selectedToken ? '0x0' : amount, - amountConversionRate, - balance, - conversionRate, - gasTotal, - primaryCurrency, - }) - } - - let inSufficientTokens = false - if (selectedToken && tokenBalance !== null) { - const { decimals } = selectedToken - inSufficientTokens = !isTokenBalanceSufficient({ - tokenBalance, - amount, - decimals, - }) - } - - const amountLessThanZero = conversionGreaterThan( - { value: 0, fromNumericBase: 'dec' }, - { value: amount, fromNumericBase: 'hex' }, - ) - - let amountError = null - - if (insufficientFunds) { - amountError = 'insufficientFunds' - } else if (inSufficientTokens) { - amountError = 'insufficientTokens' - } else if (amountLessThanZero) { - amountError = 'negativeETH' - } - - return { amount: amountError } -} - -module.exports = { - getAmountErrorObject, -} diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js index 193262fa3..17e7f8e46 100644 --- a/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.component.js @@ -14,19 +14,19 @@ export default class SendFromRow extends Component { openFromDropdown: PropTypes.func, tokenContract: PropTypes.object, updateSendFrom: PropTypes.func, - updateSendTokenBalance: PropTypes.func, + setSendTokenBalance: PropTypes.func, }; async handleFromChange (newFrom) { const { updateSendFrom, tokenContract, - updateSendTokenBalance, + setSendTokenBalance, } = this.props if (tokenContract) { const usersToken = await tokenContract.balanceOf(newFrom.address) - updateSendTokenBalance(usersToken) + setSendTokenBalance(usersToken) } updateSendFrom(newFrom) } diff --git a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js index d62782e9f..9e366445d 100644 --- a/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js +++ b/ui/app/components/send_/send-content/send-from-row/send-from-row.container.js @@ -11,7 +11,7 @@ import { import { calcTokenUpdateAmount } from './send-from-row.utils.js' import { updateSendFrom, - updateSendTokenBalance, + setSendTokenBalance, } from '../../../../actions' import { closeFromDropdown, @@ -36,11 +36,11 @@ function mapDispatchToProps (dispatch) { closeFromDropdown: () => dispatch(closeFromDropdown()), openFromDropdown: () => dispatch(openFromDropdown()), updateSendFrom: newFrom => dispatch(updateSendFrom(newFrom)), - updateSendTokenBalance: (usersToken, selectedToken) => { + setSendTokenBalance: (usersToken, selectedToken) => { if (!usersToken) return const tokenBalance = calcTokenUpdateAmount(selectedToken, selectedToken) - dispatch(updateSendTokenBalance(tokenBalance)) + dispatch(setSendTokenBalance(tokenBalance)) }, } } 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 a8441c92f..c62c110e0 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 @@ -18,23 +18,8 @@ export default class SendGasRow extends Component { showCustomizeGasModal: PropTypes.func, tokenContract: PropTypes.object, updateSendFrom: PropTypes.func, - updateSendTokenBalance: PropTypes.func, }; - async handleFromChange (newFrom) { - const { - tokenContract, - updateSendFrom, - updateSendTokenBalance, - } = this.props - - if (tokenContract) { - const usersToken = await tokenContract.balanceOf(newFrom.address) - updateSendTokenBalance(usersToken) - } - updateSendFrom(newFrom) - } - render () { const { conversionRate, diff --git a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js index c031248c7..0d314208b 100644 --- a/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js +++ b/ui/app/components/send_/send-content/send-row-wrapper/send-row-error-message/send-row-error-message.component.js @@ -10,6 +10,7 @@ export default class SendRowErrorMessage extends Component { render () { const { errors, errorType } = this.props + const errorMessage = errors[errorType] return ( diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js index b55455f8b..bffdda49c 100644 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.container.js @@ -10,12 +10,12 @@ import { } from './send-to-row.selectors.js' import { getToErrorObject } from './send-to-row.utils.js' import { - updateSendErrors, updateSendTo, } from '../../../../actions' import { - openToDropdown, - closeToDropdown, + updateSendErrors, + openToDropdown, + closeToDropdown, } from '../../../../ducks/send' import SendToRow from './send-to-row.component' diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js index e69de29bb..e014e5bce 100644 --- a/ui/app/components/send_/send.component.js +++ b/ui/app/components/send_/send.component.js @@ -0,0 +1,143 @@ +import React from 'react' +import PropTypes from 'prop-types' +import PersistentForm from '../../../lib/persistent-form' +import { + getAmountErrorObject, + doesAmountErrorRequireUpdate, +} from './send.utils' + +import PageContainer from '..//page-container/page-container.component' +import SendHeader from './send-header/send-header.container' +import SendContent from './send-content/send-content.component' +import SendFooter from './send-footer/send-footer.container' + +export default class SendTransactionScreen extends PersistentForm { + + static propTypes = { + amount: PropTypes.string, + amountConversionRate: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + conversionRate: PropTypes.number, + data: PropTypes.string, + editingTransactionId: PropTypes.string, + from: PropTypes.object, + gasLimit: PropTypes.string, + gasPrice: PropTypes.string, + gasTotal: PropTypes.string, + history: PropTypes.object, + network: PropTypes.string, + primaryCurrency: PropTypes.string, + selectedAddress: PropTypes.string, + selectedToken: PropTypes.object, + tokenBalance: PropTypes.string, + tokenContract: PropTypes.object, + updateAndSetGasTotal: PropTypes.func, + updateSendErrors: PropTypes.func, + updateSendTokenBalance: PropTypes.func, + }; + + updateGas () { + const { + data, + editingTransactionId, + gasLimit, + gasPrice, + selectedAddress, + selectedToken = {}, + updateAndSetGasTotal, + } = this.props + + updateAndSetGasTotal({ + data, + editingTransactionId, + gasLimit, + gasPrice, + selectedAddress, + selectedToken, + }) + } + + componentDidUpdate (prevProps) { + const { + amount, + amountConversionRate, + conversionRate, + from: { address, balance }, + gasTotal, + network, + primaryCurrency, + selectedToken, + tokenBalance, + updateSendErrors, + updateSendTokenBalance, + tokenContract, + } = this.props + + const { + from: { balance: prevBalance }, + gasTotal: prevGasTotal, + tokenBalance: prevTokenBalance, + network: prevNetwork, + } = prevProps + + const uninitialized = [prevBalance, prevGasTotal].every(n => n === null) + + if (!uninitialized) { + const amountErrorRequiresUpdate = doesAmountErrorRequireUpdate({ + balance, + gasTotal, + prevBalance, + prevGasTotal, + prevTokenBalance, + selectedToken, + tokenBalance, + }) + + if (amountErrorRequiresUpdate) { + const amountErrorObject = getAmountErrorObject({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, + }) + updateSendErrors(amountErrorObject) + } + + if (network !== prevNetwork && network !== 'loading') { + updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + }) + this.updateGas() + } + } + } + + componentWillMount () { + this.updateGas() + } + + render () { + const { history } = this.props + + return ( + <PageContainer> + <SendHeader/> + <SendContent/> + <SendFooter history={history}/> + </PageContainer> + ) + } + +} + +SendTransactionScreen.contextTypes = { + t: PropTypes.func, +} diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js index e69de29bb..df605469a 100644 --- a/ui/app/components/send_/send.container.js +++ b/ui/app/components/send_/send.container.js @@ -0,0 +1,88 @@ +import { connect } from 'react-redux' +import abi from 'ethereumjs-abi' +import SendEther from './send.component' +import { withRouter } from 'react-router-dom' +import { compose } from 'recompose' +import { + getAmountConversionRate, + getConversionRate, + getCurrentNetwork, + getGasLimit, + getGasPrice, + getGasTotal, + getPrimaryCurrency, + getSelectedAddress, + getSelectedToken, + getSelectedTokenContract, + getSelectedTokenToFiatRate, + getSendAmount, + getSendEditingTransactionId, + getSendFromObject, + getTokenBalance, +} from './send.selectors' +import { + updateSendTokenBalance, + updateGasTotal, + setGasTotal, +} from '../../actions' +import { + updateSendErrors, +} from '../../ducks/send' +import { + calcGasTotal, + generateTokenTransferData, +} from './send.utils.js' + +module.exports = compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps) +)(SendEther) + +function mapStateToProps (state) { + const selectedAddress = getSelectedAddress(state) + const selectedToken = getSelectedToken(state) + + return { + amount: getSendAmount(state), + amountConversionRate: getAmountConversionRate(state), + conversionRate: getConversionRate(state), + data: generateTokenTransferData(abi, selectedAddress, selectedToken), + editingTransactionId: getSendEditingTransactionId(state), + from: getSendFromObject(state), + gasLimit: getGasLimit(state), + gasPrice: getGasPrice(state), + gasTotal: getGasTotal(state), + network: getCurrentNetwork(state), + primaryCurrency: getPrimaryCurrency(state), + selectedAddress: getSelectedAddress(state), + selectedToken: getSelectedToken(state), + tokenBalance: getTokenBalance(state), + tokenContract: getSelectedTokenContract(state), + tokenToFiatRate: getSelectedTokenToFiatRate(state), + } +} + +function mapDispatchToProps (dispatch) { + return { + updateAndSetGasTotal: ({ + data, + editingTransactionId, + gasLimit, + gasPrice, + selectedAddress, + selectedToken, + }) => { + !editingTransactionId + ? dispatch(updateGasTotal({ selectedAddress, selectedToken, data })) + : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) + }, + updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { + dispatch(updateSendTokenBalance({ + selectedToken, + tokenContract, + address, + })) + }, + updateSendErrors: newError => dispatch(updateSendErrors(newError)), + } +} diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js index acce4afb7..761b15182 100644 --- a/ui/app/components/send_/send.selectors.js +++ b/ui/app/components/send_/send.selectors.js @@ -8,6 +8,7 @@ const selectors = { accountsWithSendEtherInfoSelector, autoAddToBetaUI, getAddressBook, + getAmountConversionRate, getConversionRate, getConvertedCurrency, getCurrentAccountWithSendEtherInfo, @@ -18,6 +19,7 @@ const selectors = { getGasLimit, getGasPrice, getGasTotal, + getPrimaryCurrency, getSelectedAccount, getSelectedAddress, getSelectedIdentity, @@ -77,6 +79,12 @@ function getAddressBook (state) { return state.metamask.addressBook } +function getAmountConversionRate (state) { + return getSelectedToken(state) + ? getSelectedTokenToFiatRate(state) + : getConversionRate(state) +} + function getConversionRate (state) { return state.metamask.conversionRate } @@ -121,6 +129,11 @@ function getGasTotal (state) { return state.metamask.send.gasTotal } +function getPrimaryCurrency (state) { + const selectedToken = getSelectedToken(state) + return selectedToken && selectedToken.symbol +} + function getSelectedAccount (state) { const accounts = state.metamask.accounts const selectedAddress = getSelectedAddress(state) @@ -161,9 +174,8 @@ function getSelectedTokenExchangeRate (state) { const tokenExchangeRates = state.metamask.tokenExchangeRates const selectedToken = getSelectedToken(state) || {} const { symbol = '' } = selectedToken - const pair = `${symbol.toLowerCase()}_eth` - const { rate: tokenExchangeRate = 0 } = tokenExchangeRates[pair] || {} + const { rate: tokenExchangeRate = 0 } = tokenExchangeRates && tokenExchangeRates[pair] || {} return tokenExchangeRate } @@ -190,7 +202,7 @@ function getSendEditingTransactionId (state) { } function getSendErrors (state) { - return state.metamask.send.errors + return state.send.errors } function getSendFrom (state) { diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js index f289ea989..2e17b04a3 100644 --- a/ui/app/components/send_/send.utils.js +++ b/ui/app/components/send_/send.utils.js @@ -7,8 +7,22 @@ const { const { calcTokenAmount, } = require('../../token-util') +const { + conversionGreaterThan, +} = require('../../conversion-util') + +module.exports = { + calcGasTotal, + doesAmountErrorRequireUpdate, + generateTokenTransferData, + getAmountErrorObject, + getParamsForGasEstimate, + calcTokenBalance, + isBalanceSufficient, + isTokenBalanceSufficient, +} -function getGasTotal (gasLimit, gasPrice) { +function calcGasTotal (gasLimit, gasPrice) { return multiplyCurrencies(gasLimit, gasPrice, { toNumericBase: 'hex', multiplicandBase: 16, @@ -71,8 +85,99 @@ function isTokenBalanceSufficient ({ return tokenBalanceIsSufficient } -module.exports = { - getGasTotal, - isBalanceSufficient, - isTokenBalanceSufficient, +function getAmountErrorObject ({ + amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + selectedToken, + tokenBalance, +}) { + let insufficientFunds = false + if (gasTotal && conversionRate) { + insufficientFunds = !isBalanceSufficient({ + amount: selectedToken ? '0x0' : amount, + amountConversionRate, + balance, + conversionRate, + gasTotal, + primaryCurrency, + }) + } + + let inSufficientTokens = false + if (selectedToken && tokenBalance !== null) { + const { decimals } = selectedToken + inSufficientTokens = !isTokenBalanceSufficient({ + tokenBalance, + amount, + decimals, + }) + } + + const amountLessThanZero = conversionGreaterThan( + { value: 0, fromNumericBase: 'dec' }, + { value: amount, fromNumericBase: 'hex' }, + ) + + let amountError = null + + if (insufficientFunds) { + amountError = 'insufficientFunds' + } else if (inSufficientTokens) { + amountError = 'insufficientTokens' + } else if (amountLessThanZero) { + amountError = 'negativeETH' + } + + return { amount: amountError } +} + +function getParamsForGasEstimate (selectedAddress, symbol, data) { + const estimatedGasParams = { + from: selectedAddress, + gas: '746a528800', + } + + if (symbol) { + Object.assign(estimatedGasParams, { value: '0x0' }) + } + + if (data) { + Object.assign(estimatedGasParams, { data }) + } + + return estimatedGasParams +} + +function calcTokenBalance ({ selectedToken, usersToken }) { + const { decimals } = selectedToken || {} + return calcTokenAmount(usersToken.balance.toString(), decimals) +} + +function doesAmountErrorRequireUpdate ({ + balance, + gasTotal, + prevBalance, + prevGasTotal, + prevTokenBalance, + selectedToken, + tokenBalance, +}) { + const balanceHasChanged = balance !== prevBalance + const gasTotalHasChange = gasTotal !== prevGasTotal + const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance + const amountErrorRequiresUpdate = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged + + return amountErrorRequiresUpdate +} + +function generateTokenTransferData (abi, selectedAddress, selectedToken) { + if (!selectedToken) return + return Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), + x => ('00' + x.toString(16)).slice(-2) + ).join('') } diff --git a/ui/app/ducks/send.js b/ui/app/ducks/send.js index e0feb9f06..aef493ea0 100644 --- a/ui/app/ducks/send.js +++ b/ui/app/ducks/send.js @@ -5,6 +5,7 @@ const OPEN_FROM_DROPDOWN = 'metamask/send/OPEN_FROM_DROPDOWN' const CLOSE_FROM_DROPDOWN = 'metamask/send/CLOSE_FROM_DROPDOWN' const OPEN_TO_DROPDOWN = 'metamask/send/OPEN_TO_DROPDOWN' const CLOSE_TO_DROPDOWN = 'metamask/send/CLOSE_TO_DROPDOWN' +const UPDATE_SEND_ERRORS = 'metamask/send/UPDATE_SEND_ERRORS' // TODO: determine if this approach to initState is consistent with conventional ducks pattern const initState = { @@ -32,6 +33,13 @@ export default function reducer ({ send: sendState = initState }, action = {}) { return extend(sendState, { toDropdownOpen: false, }) + case UPDATE_SEND_ERRORS: + return extend(sendState, { + errors: { + ...sendState.errors, + ...action.value, + }, + }) default: return sendState } @@ -53,3 +61,10 @@ export function openToDropdown () { export function closeToDropdown () { return { type: CLOSE_TO_DROPDOWN } } + +export function updateSendErrors (errorObject) { + return { + type: UPDATE_SEND_ERRORS, + value: errorObject, + } +} diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index bb35cf990..5564c8080 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -254,17 +254,6 @@ function reduceMetamask (state, action) { }, }) - case actions.UPDATE_SEND_ERRORS: - return extend(metamaskState, { - send: { - ...metamaskState.send, - errors: { - ...metamaskState.send.errors, - ...action.value, - }, - }, - }) - case actions.UPDATE_MAX_MODE: return extend(metamaskState, { send: { diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index ef67ebf5e..16964b45d 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -84,7 +84,7 @@ SendTransactionScreen.prototype.updateGas = function () { estimateGas, selectedAddress, data, - updateGasTotal, + setGasTotal, from, tokenContract, editingTransactionId, @@ -110,7 +110,7 @@ SendTransactionScreen.prototype.updateGas = function () { ]) .then(([gasPrice, gas]) => { const newGasTotal = getGasTotal(gas, gasPrice) - updateGasTotal(newGasTotal) + setGasTotal(newGasTotal) this.setState({ gasLoadingError: false }) }) .catch(err => { @@ -118,7 +118,7 @@ SendTransactionScreen.prototype.updateGas = function () { }) } else { const newGasTotal = getGasTotal(gasLimit, gasPrice) - updateGasTotal(newGasTotal) + setGasTotal(newGasTotal) } } |