From 67bdfe87e31e695f8c4beab1659a3a4b764ccf24 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 30 Oct 2017 15:18:50 -0230 Subject: [PATCH] Token balance in send state; validating sufficient tokens, validation updates on 'from' switching. --- ui/app/actions.js | 9 ++ .../pending-tx/confirm-send-token.js | 6 +- ui/app/components/send/send-utils.js | 43 +++++- ui/app/components/send/send-v2-container.js | 3 + ui/app/components/tx-list-item.js | 4 +- ui/app/conversion-util.js | 1 + ui/app/reducers/metamask.js | 9 ++ ui/app/selectors.js | 9 ++ ui/app/send-v2.js | 131 +++++++++++++++--- ui/app/token-tracker.js | 0 ui/app/token-util.js | 9 ++ 11 files changed, 191 insertions(+), 33 deletions(-) create mode 100644 ui/app/token-tracker.js diff --git a/ui/app/actions.js b/ui/app/actions.js index 5d3befa63..93cd40ed6 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -140,6 +140,7 @@ var actions = { UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', UPDATE_SEND_FROM: 'UPDATE_SEND_FROM', + UPDATE_SEND_TOKEN_BALANCE: 'UPDATE_SEND_TOKEN_BALANCE', UPDATE_SEND_TO: 'UPDATE_SEND_TO', UPDATE_SEND_AMOUNT: 'UPDATE_SEND_AMOUNT', UPDATE_SEND_MEMO: 'UPDATE_SEND_MEMO', @@ -148,6 +149,7 @@ var actions = { updateGasLimit, updateGasPrice, updateGasTotal, + updateSendTokenBalance, updateSendFrom, updateSendTo, updateSendAmount, @@ -584,6 +586,13 @@ function updateGasTotal (gasTotal) { } } +function updateSendTokenBalance (tokenBalance) { + return { + type: actions.UPDATE_SEND_TOKEN_BALANCE, + value: tokenBalance, + } +} + function updateSendFrom (from) { return { type: actions.UPDATE_SEND_FROM, diff --git a/ui/app/components/pending-tx/confirm-send-token.js b/ui/app/components/pending-tx/confirm-send-token.js index 3b8ae7f7f..f14da38ef 100644 --- a/ui/app/components/pending-tx/confirm-send-token.js +++ b/ui/app/components/pending-tx/confirm-send-token.js @@ -15,6 +15,9 @@ const { multiplyCurrencies, addCurrencies, } = require('../../conversion-util') +const { + calcTokenAmount, +} = require('../../token-util') const { MIN_GAS_PRICE_HEX } = require('../send/send-constants') @@ -73,8 +76,7 @@ ConfirmSendToken.prototype.getAmount = function () { const { params = [] } = tokenData const { value } = params[1] || {} const { decimals } = token - const multiplier = Math.pow(10, Number(decimals || 0)) - const sendTokenAmount = Number(value / multiplier) + const sendTokenAmount = calcTokenAmount(value, decimals) return { fiat: tokenExchangeRate diff --git a/ui/app/components/send/send-utils.js b/ui/app/components/send/send-utils.js index 6ec04a223..4eb010173 100644 --- a/ui/app/components/send/send-utils.js +++ b/ui/app/components/send/send-utils.js @@ -1,11 +1,18 @@ -const { addCurrencies, conversionGreaterThan } = require('../../conversion-util') +const { + addCurrencies, + conversionGreaterThan, + conversionUtil, + conversionGTE, +} = require('../../conversion-util') +const { + calcTokenAmount, +} = require('../../token-util') -function isBalanceSufficient ({ - amount, - gasTotal, +function isBalanceSufficient({ + amount = '0x0', + gasTotal = '0x0', balance, primaryCurrency, - selectedToken, amountConversionRate, conversionRate, }) { @@ -26,13 +33,37 @@ function isBalanceSufficient ({ value: totalAmount, fromNumericBase: 'hex', conversionRate: amountConversionRate, - fromCurrency: selectedToken || primaryCurrency, + fromCurrency: primaryCurrency, }, ) return balanceIsSufficient } +function isTokenBalanceSufficient({ + amount = '0x0', + tokenBalance, + decimals, +}) { + const amountInDec = conversionUtil(amount, { + fromNumericBase: 'hex', + }) + + const tokenBalanceIsSufficient = conversionGTE( + { + value: tokenBalance, + fromNumericBase: 'dec', + }, + { + value: calcTokenAmount(amountInDec, decimals), + fromNumericBase: 'dec', + }, + ) + + return tokenBalanceIsSufficient +} + module.exports = { isBalanceSufficient, + isTokenBalanceSufficient, } diff --git a/ui/app/components/send/send-v2-container.js b/ui/app/components/send/send-v2-container.js index 5a6e83ae6..ee18d0b4b 100644 --- a/ui/app/components/send/send-v2-container.js +++ b/ui/app/components/send/send-v2-container.js @@ -13,6 +13,7 @@ const { getSendFrom, getCurrentCurrency, getSelectedTokenToFiatRate, + getSelectedTokenContract, } = require('../../selectors') module.exports = connect(mapStateToProps, mapDispatchToProps)(SendEther) @@ -48,6 +49,7 @@ function mapStateToProps (state) { convertedCurrency: getCurrentCurrency(state), data, amountConversionRate: selectedToken ? tokenToFiatRate : conversionRate, + tokenContract: getSelectedTokenContract(state), } } @@ -64,6 +66,7 @@ function mapDispatchToProps (dispatch) { setSelectedAddress: address => dispatch(actions.setSelectedAddress(address)), addToAddressBook: address => dispatch(actions.addToAddressBook(address)), updateGasTotal: newTotal => dispatch(actions.updateGasTotal(newTotal)), + updateSendTokenBalance: tokenBalance => dispatch(actions.updateSendTokenBalance(tokenBalance)), updateSendFrom: newFrom => dispatch(actions.updateSendFrom(newFrom)), updateSendTo: newTo => dispatch(actions.updateSendTo(newTo)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 26de19f15..4e76147a1 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -10,6 +10,7 @@ const Identicon = require('./identicon') const contractMap = require('eth-contract-metadata') const { conversionUtil, multiplyCurrencies } = require('../conversion-util') +const { calcTokenAmount } = require('../token-util') const { getCurrentCurrency } = require('../selectors') @@ -135,8 +136,7 @@ TxListItem.prototype.getSendTokenTotal = async function () { const { params = [] } = decodedData || {} const { value } = params[1] || {} const { decimals, symbol } = await this.getTokenInfo() - const multiplier = Math.pow(10, Number(decimals || 0)) - const total = Number(value / multiplier) + const total = calcTokenAmount(value, decimals) const pair = symbol && `${symbol.toLowerCase()}_eth` diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index 9359d7c90..5eadbdb99 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -169,6 +169,7 @@ const conversionGreaterThan = ( ) => { const firstValue = converter({ ...firstProps }) const secondValue = converter({ ...secondProps }) + return firstValue.gt(secondValue) } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 50c9712ff..3b93a1625 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -27,6 +27,7 @@ function reduceMetamask (state, action) { gasLimit: null, gasPrice: null, gasTotal: null, + tokenBalance: null, from: '', to: '', amount: '0x0', @@ -196,6 +197,14 @@ function reduceMetamask (state, action) { }, }) + case actions.UPDATE_SEND_TOKEN_BALANCE: + return extend(metamaskState, { + send: { + ...metamaskState.send, + tokenBalance: action.value, + }, + }) + case actions.UPDATE_SEND_FROM: return extend(metamaskState, { send: { diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 3a15cef4c..a5f9a75d8 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -1,4 +1,5 @@ const valuesFor = require('./util').valuesFor +const abi = require('human-standard-token-abi') const { multiplyCurrencies, @@ -22,6 +23,7 @@ const selectors = { getCurrentCurrency, getSendAmount, getSelectedTokenToFiatRate, + getSelectedTokenContract, } module.exports = selectors @@ -149,3 +151,10 @@ function getSelectedTokenToFiatRate (state) { return tokenToFiatRate } + +function getSelectedTokenContract (state) { + const selectedToken = getSelectedToken(state) + return selectedToken + ? global.eth.contract(abi).at(selectedToken.address) + : null +} diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 6ce3e1b70..8ad2b912c 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -10,14 +10,19 @@ const MemoTextArea = require('./components/send/memo-textarea') const GasFeeDisplay = require('./components/send/gas-fee-display-v2') const { MIN_GAS_TOTAL } = require('./components/send/send-constants') +const abi = require('human-standard-token-abi') const { multiplyCurrencies, conversionGreaterThan, } = require('./conversion-util') +const { + calcTokenAmount, +} = require('./token-util') const { isBalanceSufficient, -} = require('./components/send/send-utils.js') + isTokenBalanceSufficient, +} = require('./components/send/send-utils') const { isValidAddress } = require('./util') module.exports = SendTransactionScreen @@ -40,6 +45,37 @@ function SendTransactionScreen () { this.validateAmount = this.validateAmount.bind(this) } +const getParamsForGasEstimate = function (selectedAddress, symbol, data) { + const estimatedGasParams = { + from: selectedAddress, + gas: '746a528800', + } + + if (symbol) { + Object.assign(estimatedGasParams, { value: '0x0' }) + } + + if (data) { + Object.assign(estimatedGasParams, { data }) + } + + return estimatedGasParams +} + +SendTransactionScreen.prototype.updateSendTokenBalance = function (usersToken) { + if (!usersToken) return + + const { + selectedToken = {}, + updateSendTokenBalance, + } = this.props + const { decimals } = selectedToken || {} + + const tokenBalance = calcTokenAmount(usersToken.balance.toString(), decimals) + + updateSendTokenBalance(tokenBalance) +} + SendTransactionScreen.prototype.componentWillMount = function () { const { updateTokenExchangeRate, @@ -49,32 +85,25 @@ SendTransactionScreen.prototype.componentWillMount = function () { selectedAddress, data, updateGasTotal, + updateSendTokenBalance, + from, + tokenContract, } = this.props - const { symbol } = selectedToken || {} - - const estimateGasParams = { - from: selectedAddress, - gas: '746a528800', - } + const { symbol, decimals } = selectedToken || {} if (symbol) { updateTokenExchangeRate(symbol) - Object.assign(estimateGasParams, { value: '0x0' }) } - if (data) { - Object.assign(estimateGasParams, { data }) - } + const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data) Promise .all([ getGasPrice(), - estimateGas({ - from: selectedAddress, - gas: '746a528800', - }), + estimateGas(estimateGasParams), + tokenContract && tokenContract.balanceOf(from.address) ]) - .then(([gasPrice, gas]) => { + .then(([gasPrice, gas, usersToken]) => { const newGasTotal = multiplyCurrencies(gas, gasPrice, { toNumericBase: 'hex', @@ -82,9 +111,36 @@ SendTransactionScreen.prototype.componentWillMount = function () { multiplierBase: 16, }) updateGasTotal(newGasTotal) + this.updateSendTokenBalance(usersToken) }) } +SendTransactionScreen.prototype.componentDidUpdate = function (prevProps) { + const { + from: { balance }, + gasTotal, + tokenBalance, + amount, + selectedToken, + } = this.props + const { + from: { balance: prevBalance }, + gasTotal: prevGasTotal, + tokenBalance: prevTokenBalance, + } = prevProps + + const notFirstRender = [prevBalance, prevGasTotal].every(n => n !== null) + + const balanceHasChanged = balance !== prevBalance + const gasTotalHasChange = gasTotal !== prevGasTotal + const tokenBalanceHasChanged = selectedToken && tokenBalance !== prevTokenBalance + const amountValidationChange = balanceHasChanged || gasTotalHasChange || tokenBalanceHasChanged + + if (notFirstRender && amountValidationChange) { + this.validateAmount(amount) + } +} + SendTransactionScreen.prototype.renderHeaderIcon = function () { const { selectedToken } = this.props @@ -144,12 +200,31 @@ SendTransactionScreen.prototype.renderErrorMessage = function (errorType) { : null } +SendTransactionScreen.prototype.handleFromChange = async function (newFrom) { + const { + from, + updateSendFrom, + updateSendTokenBalance, + tokenContract, + selectedToken, + } = this.props + const { decimals } = selectedToken || {} + + if (tokenContract) { + const usersToken = await tokenContract.balanceOf(newFrom.address) + this.updateSendTokenBalance(usersToken) + } + updateSendFrom(newFrom) +} + SendTransactionScreen.prototype.renderFromRow = function () { const { from, fromAccounts, conversionRate, updateSendFrom, + updateSendTokenBalance, + tokenContract, } = this.props const { fromDropdownOpen } = this.state @@ -163,7 +238,7 @@ SendTransactionScreen.prototype.renderFromRow = function () { dropdownOpen: fromDropdownOpen, accounts: fromAccounts, selectedAccount: from, - onSelect: updateSendFrom, + onSelect: newFrom => this.handleFromChange(newFrom), openDropdown: () => this.setState({ fromDropdownOpen: true }), closeDropdown: () => this.setState({ fromDropdownOpen: false }), conversionRate, @@ -239,21 +314,31 @@ SendTransactionScreen.prototype.validateAmount = function (value) { primaryCurrency, selectedToken, gasTotal, + tokenBalance, } = this.props + const { decimals } = selectedToken || {} const amount = value let amountError = null const sufficientBalance = isBalanceSufficient({ - amount, + amount: selectedToken ? '0x0' : amount, gasTotal, balance, primaryCurrency, - selectedToken, amountConversionRate, conversionRate, }) + let sufficientTokens + if (selectedToken) { + sufficientTokens = isTokenBalanceSufficient({ + tokenBalance, + amount, + decimals, + }) + } + const amountLessThanZero = conversionGreaterThan( { value: 0, fromNumericBase: 'dec' }, { value: amount, fromNumericBase: 'hex' }, @@ -261,6 +346,8 @@ SendTransactionScreen.prototype.validateAmount = function (value) { if (!sufficientBalance) { amountError = 'Insufficient funds.' + } else if (selectedToken && !sufficientTokens) { + amountError = 'Insufficient tokens.' } else if (amountLessThanZero) { amountError = 'Can not send negative amounts of ETH.' } @@ -275,10 +362,9 @@ SendTransactionScreen.prototype.renderAmountRow = function () { convertedCurrency, amountConversionRate, errors, + amount, } = this.props - const { amount } = this.state - return h('div.send-v2__form-row', [ h('div.send-v2__form-label', [ @@ -335,8 +421,7 @@ SendTransactionScreen.prototype.renderGasRow = function () { } SendTransactionScreen.prototype.renderMemoRow = function () { - const { updateSendMemo } = this.props - const { memo } = this.state + const { updateSendMemo, memo } = this.props return h('div.send-v2__form-row', [ diff --git a/ui/app/token-tracker.js b/ui/app/token-tracker.js new file mode 100644 index 000000000..e69de29bb diff --git a/ui/app/token-util.js b/ui/app/token-util.js index eec518556..f84051ef5 100644 --- a/ui/app/token-util.js +++ b/ui/app/token-util.js @@ -31,6 +31,15 @@ const tokenInfoGetter = function () { } } +function calcTokenAmount (value, decimals) { + const multiplier = Math.pow(10, Number(decimals || 0)) + const amount = Number(value / multiplier) + + return amount +} + + module.exports = { tokenInfoGetter, + calcTokenAmount, }