Merge pull request #4350 from MetaMask/i3914-fix-newui-send-gas-estimation

NewUI gas estimation produces same results as old-ui (exception: contract addresses)
feature/default_network_editable
Dan J Miller 7 years ago committed by GitHub
commit 139f930185
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 14
      app/scripts/metamask-controller.js
  2. 2
      test/integration/lib/send-new-ui.js
  3. 79
      ui/app/actions.js
  4. 18
      ui/app/components/customize-gas-modal/index.js
  5. 11
      ui/app/components/send/currency-display.js
  6. 7
      ui/app/components/send_/send-content/send-content.component.js
  7. 10
      ui/app/components/send_/send-content/send-to-row/send-to-row.component.js
  8. 5
      ui/app/components/send_/send-content/send-to-row/send-to-row.container.js
  9. 4
      ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js
  10. 25
      ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js
  11. 5
      ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js
  12. 1
      ui/app/components/send_/send-footer/send-footer.utils.js
  13. 16
      ui/app/components/send_/send.component.js
  14. 11
      ui/app/components/send_/send.constants.js
  15. 19
      ui/app/components/send_/send.container.js
  16. 18
      ui/app/components/send_/send.selectors.js
  17. 97
      ui/app/components/send_/send.utils.js
  18. 14
      ui/app/components/send_/tests/send-component.test.js
  19. 23
      ui/app/components/send_/tests/send-container.test.js
  20. 2
      ui/app/components/send_/tests/send-selectors-test-data.js
  21. 20
      ui/app/components/send_/tests/send-selectors.test.js
  22. 220
      ui/app/components/send_/tests/send-utils.test.js
  23. 5
      ui/app/conversion-util.js

@ -384,6 +384,8 @@ module.exports = class MetamaskController extends EventEmitter {
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
retryTransaction: nodeify(this.retryTransaction, this), retryTransaction: nodeify(this.retryTransaction, this),
getFilteredTxList: nodeify(txController.getFilteredTxList, txController), getFilteredTxList: nodeify(txController.getFilteredTxList, txController),
isNonceTaken: nodeify(txController.isNonceTaken, txController),
estimateGas: nodeify(this.estimateGas, this),
// messageManager // messageManager
signMessage: nodeify(this.signMessage, this), signMessage: nodeify(this.signMessage, this),
@ -922,6 +924,18 @@ module.exports = class MetamaskController extends EventEmitter {
return state return state
} }
estimateGas (estimateGasParams) {
return new Promise((resolve, reject) => {
return this.txController.txGasUtil.query.estimateGas(estimateGasParams, (err, res) => {
if (err) {
return reject(err)
}
return resolve(res)
})
})
}
//============================================================================= //=============================================================================
// PASSWORD MANAGEMENT // PASSWORD MANAGEMENT
//============================================================================= //=============================================================================

@ -117,7 +117,7 @@ async function runSendFlowTest(assert, done) {
const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') const sendGasField = await queryAsync($, '.send-v2__gas-fee-display')
assert.equal( assert.equal(
sendGasField.find('.currency-display__input-wrapper > input').val(), sendGasField.find('.currency-display__input-wrapper > input').val(),
'0.000198', '0.000198264',
'send gas field should show estimated gas total' 'send gas field should show estimated gas total'
) )
assert.equal( assert.equal(

@ -4,8 +4,9 @@ const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url')
const { getTokenAddressFromTokenObject } = require('./util') const { getTokenAddressFromTokenObject } = require('./util')
const { const {
calcGasTotal, calcGasTotal,
getParamsForGasEstimate,
calcTokenBalance, calcTokenBalance,
estimateGas,
estimateGasPriceFromRecentBlocks,
} = require('./components/send_/send.utils') } = require('./components/send_/send.utils')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const { fetchLocale } = require('../i18n-helper') const { fetchLocale } = require('../i18n-helper')
@ -160,8 +161,6 @@ var actions = {
updateTransactionParams, updateTransactionParams,
UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS',
// send screen // send screen
estimateGas,
getGasPrice,
UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT',
UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE',
UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL',
@ -176,9 +175,9 @@ var actions = {
CLEAR_SEND: 'CLEAR_SEND', CLEAR_SEND: 'CLEAR_SEND',
OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN',
CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN',
updateGasLimit, setGasLimit,
updateGasPrice, setGasPrice,
updateGasTotal, updateGasData,
setGasTotal, setGasTotal,
setSendTokenBalance, setSendTokenBalance,
updateSendTokenBalance, updateSendTokenBalance,
@ -710,46 +709,14 @@ function signTx (txData) {
} }
} }
function estimateGas (params = {}) { function setGasLimit (gasLimit) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.estimateGas(params, (err, data) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideWarning())
dispatch(actions.updateGasLimit(data))
return resolve(data)
})
})
}
}
function updateGasLimit (gasLimit) {
return { return {
type: actions.UPDATE_GAS_LIMIT, type: actions.UPDATE_GAS_LIMIT,
value: gasLimit, value: gasLimit,
} }
} }
function getGasPrice () { function setGasPrice (gasPrice) {
return (dispatch) => {
return new Promise((resolve, reject) => {
global.ethQuery.gasPrice((err, data) => {
if (err) {
dispatch(actions.displayWarning(err.message))
return reject(err)
}
dispatch(actions.hideWarning())
dispatch(actions.updateGasPrice(data))
return resolve(data)
})
})
}
}
function updateGasPrice (gasPrice) {
return { return {
type: actions.UPDATE_GAS_PRICE, type: actions.UPDATE_GAS_PRICE,
value: gasPrice, value: gasPrice,
@ -763,17 +730,35 @@ function setGasTotal (gasTotal) {
} }
} }
function updateGasTotal ({ selectedAddress, selectedToken, data }) { function updateGasData ({
blockGasLimit,
recentBlocks,
selectedAddress,
selectedToken,
to,
value,
}) {
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return (dispatch) => { return (dispatch) => {
const { symbol } = selectedToken || {}
const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data)
return Promise.all([ return Promise.all([
dispatch(actions.getGasPrice()), Promise.resolve(estimatedGasPrice),
dispatch(actions.estimateGas(estimateGasParams)), estimateGas({
estimateGasMethod: background.estimateGas,
blockGasLimit,
selectedAddress,
selectedToken,
to,
value,
gasPrice: estimatedGasPrice,
}),
]) ])
.then(([gasPrice, gas]) => { .then(([gasPrice, gas]) => {
const newGasTotal = calcGasTotal(gas, gasPrice) dispatch(actions.setGasPrice(gasPrice))
dispatch(actions.setGasTotal(newGasTotal)) dispatch(actions.setGasLimit(gas))
return calcGasTotal(gas, gasPrice)
})
.then((gasEstimate) => {
dispatch(actions.setGasTotal(gasEstimate))
dispatch(updateSendErrors({ gasLoadingError: null })) dispatch(updateSendErrors({ gasLoadingError: null }))
}) })
.catch(err => { .catch(err => {

@ -65,9 +65,9 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
hideModal: () => dispatch(actions.hideModal()), hideModal: () => dispatch(actions.hideModal()),
updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)),
updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)),
updateGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)), setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)),
updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)),
updateSendErrors: error => dispatch(updateSendErrors(error)), updateSendErrors: error => dispatch(updateSendErrors(error)),
} }
@ -109,10 +109,10 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal)
CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
const { const {
updateGasPrice, setGasPrice,
updateGasLimit, setGasLimit,
hideModal, hideModal,
updateGasTotal, setGasTotal,
maxModeOn, maxModeOn,
selectedToken, selectedToken,
balance, balance,
@ -129,9 +129,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateSendAmount(maxAmount) updateSendAmount(maxAmount)
} }
updateGasPrice(ethUtil.addHexPrefix(gasPrice)) setGasPrice(ethUtil.addHexPrefix(gasPrice))
updateGasLimit(ethUtil.addHexPrefix(gasLimit)) setGasLimit(ethUtil.addHexPrefix(gasLimit))
updateGasTotal(ethUtil.addHexPrefix(gasTotal)) setGasTotal(ethUtil.addHexPrefix(gasTotal))
updateSendErrors({ insufficientFunds: false }) updateSendErrors({ insufficientFunds: false })
hideModal() hideModal()
} }

@ -4,6 +4,7 @@ const inherits = require('util').inherits
const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const { conversionUtil, multiplyCurrencies } = require('../../conversion-util')
const currencyFormatter = require('currency-formatter') const currencyFormatter = require('currency-formatter')
const currencies = require('currency-formatter/currencies') const currencies = require('currency-formatter/currencies')
const ethUtil = require('ethereumjs-util')
module.exports = CurrencyDisplay module.exports = CurrencyDisplay
@ -48,23 +49,23 @@ CurrencyDisplay.prototype.getAmount = function (value) {
: toHexWei(value) : toHexWei(value)
} }
CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value }) { CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) {
if (value === '0x0') return '0' if (value === '0x0') return readOnly ? '0' : ''
const { decimals, symbol } = selectedToken || {} const { decimals, symbol } = selectedToken || {}
const multiplier = Math.pow(10, Number(decimals || 0)) const multiplier = Math.pow(10, Number(decimals || 0))
return selectedToken return selectedToken
? conversionUtil(value, { ? conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex', fromNumericBase: 'hex',
toCurrency: symbol, toCurrency: symbol,
conversionRate: multiplier, conversionRate: multiplier,
invertConversionRate: true, invertConversionRate: true,
}) })
: conversionUtil(value, { : conversionUtil(ethUtil.addHexPrefix(value), {
fromNumericBase: 'hex', fromNumericBase: 'hex',
toNumericBase: 'dec', toNumericBase: 'dec',
fromDenomination: 'WEI', fromDenomination: 'WEI',
numberOfDecimals: 6, numberOfDecimals: 9,
conversionRate, conversionRate,
}) })
} }

@ -1,4 +1,5 @@
import React, { Component } from 'react' import React, { Component } from 'react'
import PropTypes from 'prop-types'
import PageContainerContent from '../../page-container/page-container-content.component' import PageContainerContent from '../../page-container/page-container-content.component'
import SendAmountRow from './send-amount-row/' import SendAmountRow from './send-amount-row/'
import SendFromRow from './send-from-row/' import SendFromRow from './send-from-row/'
@ -7,12 +8,16 @@ import SendToRow from './send-to-row/'
export default class SendContent extends Component { export default class SendContent extends Component {
static propTypes = {
updateGas: PropTypes.func,
};
render () { render () {
return ( return (
<PageContainerContent> <PageContainerContent>
<div className="send-v2__form"> <div className="send-v2__form">
<SendFromRow /> <SendFromRow />
<SendToRow /> <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} />
<SendAmountRow /> <SendAmountRow />
<SendGasRow /> <SendGasRow />
</div> </div>

@ -2,6 +2,7 @@ import React, { Component } from 'react'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
import SendRowWrapper from '../send-row-wrapper/' import SendRowWrapper from '../send-row-wrapper/'
import EnsInput from '../../../ens-input' import EnsInput from '../../../ens-input'
import { getToErrorObject } from './send-to-row.utils.js'
export default class SendToRow extends Component { export default class SendToRow extends Component {
@ -13,14 +14,19 @@ export default class SendToRow extends Component {
to: PropTypes.string, to: PropTypes.string,
toAccounts: PropTypes.array, toAccounts: PropTypes.array,
toDropdownOpen: PropTypes.bool, toDropdownOpen: PropTypes.bool,
updateGas: PropTypes.func,
updateSendTo: PropTypes.func, updateSendTo: PropTypes.func,
updateSendToError: PropTypes.func, updateSendToError: PropTypes.func,
}; };
handleToChange (to, nickname = '') { handleToChange (to, nickname = '') {
const { updateSendTo, updateSendToError } = this.props const { updateSendTo, updateSendToError, updateGas } = this.props
const toErrorObject = getToErrorObject(to)
updateSendTo(to, nickname) updateSendTo(to, nickname)
updateSendToError(to) updateSendToError(toErrorObject)
if (toErrorObject.to === null) {
updateGas({ to })
}
} }
render () { render () {

@ -8,7 +8,6 @@ import {
getToDropdownOpen, getToDropdownOpen,
sendToIsInError, sendToIsInError,
} from './send-to-row.selectors.js' } from './send-to-row.selectors.js'
import { getToErrorObject } from './send-to-row.utils.js'
import { import {
updateSendTo, updateSendTo,
} from '../../../../actions' } from '../../../../actions'
@ -36,8 +35,8 @@ function mapDispatchToProps (dispatch) {
closeToDropdown: () => dispatch(closeToDropdown()), closeToDropdown: () => dispatch(closeToDropdown()),
openToDropdown: () => dispatch(openToDropdown()), openToDropdown: () => dispatch(openToDropdown()),
updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)),
updateSendToError: (to) => { updateSendToError: (toErrorObject) => {
dispatch(updateSendErrors(getToErrorObject(to))) dispatch(updateSendErrors(toErrorObject))
}, },
} }
} }

@ -8,9 +8,9 @@ function getToErrorObject (to) {
let toError = null let toError = null
if (!to) { if (!to) {
toError = REQUIRED_ERROR toError = REQUIRED_ERROR
} else if (!isValidAddress(to)) { } else if (!isValidAddress(to)) {
toError = INVALID_RECIPIENT_ADDRESS_ERROR toError = INVALID_RECIPIENT_ADDRESS_ERROR
} }
return { to: toError } return { to: toError }

@ -2,7 +2,15 @@ import React from 'react'
import assert from 'assert' import assert from 'assert'
import { shallow } from 'enzyme' import { shallow } from 'enzyme'
import sinon from 'sinon' import sinon from 'sinon'
import SendToRow from '../send-to-row.component.js' 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}`,
}),
},
}).default
import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component' import SendRowWrapper from '../../send-row-wrapper/send-row-wrapper.component'
import EnsInput from '../../../../ens-input' import EnsInput from '../../../../ens-input'
@ -10,6 +18,7 @@ import EnsInput from '../../../../ens-input'
const propsMethodSpies = { const propsMethodSpies = {
closeToDropdown: sinon.spy(), closeToDropdown: sinon.spy(),
openToDropdown: sinon.spy(), openToDropdown: sinon.spy(),
updateGas: sinon.spy(),
updateSendTo: sinon.spy(), updateSendTo: sinon.spy(),
updateSendToError: sinon.spy(), updateSendToError: sinon.spy(),
} }
@ -29,6 +38,7 @@ describe('SendToRow Component', function () {
to={'mockTo'} to={'mockTo'}
toAccounts={['mockAccount']} toAccounts={['mockAccount']}
toDropdownOpen={false} toDropdownOpen={false}
updateGas={propsMethodSpies.updateGas}
updateSendTo={propsMethodSpies.updateSendTo} updateSendTo={propsMethodSpies.updateSendTo}
updateSendToError={propsMethodSpies.updateSendToError} updateSendToError={propsMethodSpies.updateSendToError}
/>, { context: { t: str => str + '_t' } }) />, { context: { t: str => str + '_t' } })
@ -61,10 +71,21 @@ describe('SendToRow Component', function () {
assert.equal(propsMethodSpies.updateSendToError.callCount, 1) assert.equal(propsMethodSpies.updateSendToError.callCount, 1)
assert.deepEqual( assert.deepEqual(
propsMethodSpies.updateSendToError.getCall(0).args, propsMethodSpies.updateSendToError.getCall(0).args,
['mockTo2'] [{ to: 'mockToErrorObject:mockTo2' }]
) )
}) })
it('should not call updateGas if there is a to error', () => {
assert.equal(propsMethodSpies.updateGas.callCount, 0)
instance.handleToChange('mockTo2')
assert.equal(propsMethodSpies.updateGas.callCount, 0)
})
it('should call updateGas if there is no to error', () => {
assert.equal(propsMethodSpies.updateGas.callCount, 0)
instance.handleToChange(false)
assert.equal(propsMethodSpies.updateGas.callCount, 1)
})
}) })
describe('render', () => { describe('render', () => {

@ -31,7 +31,6 @@ proxyquire('../send-to-row.container.js', {
getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`,
sendToIsInError: (s) => `mockInError:${s}`, sendToIsInError: (s) => `mockInError:${s}`,
}, },
'./send-to-row.utils.js': { getToErrorObject: (t) => `mockError:${t}` },
'../../../../actions': actionSpies, '../../../../actions': actionSpies,
'../../../../ducks/send.duck': duckActionSpies, '../../../../ducks/send.duck': duckActionSpies,
}) })
@ -99,12 +98,12 @@ describe('send-to-row container', () => {
describe('updateSendToError()', () => { describe('updateSendToError()', () => {
it('should dispatch an action', () => { it('should dispatch an action', () => {
mapDispatchToPropsObject.updateSendToError('mockTo') mapDispatchToPropsObject.updateSendToError('mockToErrorObject')
assert(dispatchSpy.calledOnce) assert(dispatchSpy.calledOnce)
assert(duckActionSpies.updateSendErrors.calledOnce) assert(duckActionSpies.updateSendErrors.calledOnce)
assert.equal( assert.equal(
duckActionSpies.updateSendErrors.getCall(0).args[0], duckActionSpies.updateSendErrors.getCall(0).args[0],
'mockError:mockTo' 'mockToErrorObject'
) )
}) })
}) })

@ -42,7 +42,6 @@ function constructUpdatedTx ({
} }
if (selectedToken) { if (selectedToken) {
console.log(`ethAbi.rawEncode`, ethAbi.rawEncode)
const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]), ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),
x => ('00' + x.toString(16)).slice(-2) x => ('00' + x.toString(16)).slice(-2)

@ -18,8 +18,8 @@ export default class SendTransactionScreen extends PersistentForm {
PropTypes.string, PropTypes.string,
PropTypes.number, PropTypes.number,
]), ]),
blockGasLimit: PropTypes.string,
conversionRate: PropTypes.number, conversionRate: PropTypes.number,
data: PropTypes.string,
editingTransactionId: PropTypes.string, editingTransactionId: PropTypes.string,
from: PropTypes.object, from: PropTypes.object,
gasLimit: PropTypes.string, gasLimit: PropTypes.string,
@ -28,6 +28,7 @@ export default class SendTransactionScreen extends PersistentForm {
history: PropTypes.object, history: PropTypes.object,
network: PropTypes.string, network: PropTypes.string,
primaryCurrency: PropTypes.string, primaryCurrency: PropTypes.string,
recentBlocks: PropTypes.array,
selectedAddress: PropTypes.string, selectedAddress: PropTypes.string,
selectedToken: PropTypes.object, selectedToken: PropTypes.object,
tokenBalance: PropTypes.string, tokenBalance: PropTypes.string,
@ -37,24 +38,29 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func, updateSendTokenBalance: PropTypes.func,
}; };
updateGas () { updateGas ({ to } = {}) {
const { const {
data, amount,
blockGasLimit,
editingTransactionId, editingTransactionId,
gasLimit, gasLimit,
gasPrice, gasPrice,
recentBlocks,
selectedAddress, selectedAddress,
selectedToken = {}, selectedToken = {},
updateAndSetGasTotal, updateAndSetGasTotal,
} = this.props } = this.props
updateAndSetGasTotal({ updateAndSetGasTotal({
data, blockGasLimit,
editingTransactionId, editingTransactionId,
gasLimit, gasLimit,
gasPrice, gasPrice,
recentBlocks,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to: to && to.toLowerCase(),
value: amount,
}) })
} }
@ -141,7 +147,7 @@ export default class SendTransactionScreen extends PersistentForm {
return ( return (
<div className="page-container"> <div className="page-container">
<SendHeader history={history}/> <SendHeader history={history}/>
<SendContent/> <SendContent updateGas={(updateData) => this.updateGas(updateData)}/>
<SendFooter history={history}/> <SendFooter history={history}/>
</div> </div>
) )

@ -28,6 +28,15 @@ const NEGATIVE_ETH_ERROR = 'negativeETH'
const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient' const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient'
const REQUIRED_ERROR = 'required' const REQUIRED_ERROR = 'required'
const ONE_GWEI_IN_WEI_HEX = ethUtil.addHexPrefix(conversionUtil('0x1', {
fromDenomination: 'GWEI',
toDenomination: 'WEI',
fromNumericBase: 'hex',
toNumericBase: 'hex',
}))
const SIMPLE_GAS_COST = '0x5208' // Hex for 21000, cost of a simple send.
module.exports = { module.exports = {
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
@ -39,6 +48,8 @@ module.exports = {
MIN_GAS_PRICE_HEX, MIN_GAS_PRICE_HEX,
MIN_GAS_TOTAL, MIN_GAS_TOTAL,
NEGATIVE_ETH_ERROR, NEGATIVE_ETH_ERROR,
ONE_GWEI_IN_WEI_HEX,
REQUIRED_ERROR, REQUIRED_ERROR,
SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE, TOKEN_TRANSFER_FUNCTION_SIGNATURE,
} }

@ -4,12 +4,14 @@ import { withRouter } from 'react-router-dom'
import { compose } from 'recompose' import { compose } from 'recompose'
import { import {
getAmountConversionRate, getAmountConversionRate,
getBlockGasLimit,
getConversionRate, getConversionRate,
getCurrentNetwork, getCurrentNetwork,
getGasLimit, getGasLimit,
getGasPrice, getGasPrice,
getGasTotal, getGasTotal,
getPrimaryCurrency, getPrimaryCurrency,
getRecentBlocks,
getSelectedAddress, getSelectedAddress,
getSelectedToken, getSelectedToken,
getSelectedTokenContract, getSelectedTokenContract,
@ -21,7 +23,7 @@ import {
} from './send.selectors' } from './send.selectors'
import { import {
updateSendTokenBalance, updateSendTokenBalance,
updateGasTotal, updateGasData,
setGasTotal, setGasTotal,
} from '../../actions' } from '../../actions'
import { import {
@ -29,7 +31,6 @@ import {
} from '../../ducks/send.duck' } from '../../ducks/send.duck'
import { import {
calcGasTotal, calcGasTotal,
generateTokenTransferData,
} from './send.utils.js' } from './send.utils.js'
module.exports = compose( module.exports = compose(
@ -38,14 +39,11 @@ module.exports = compose(
)(SendEther) )(SendEther)
function mapStateToProps (state) { function mapStateToProps (state) {
const selectedAddress = getSelectedAddress(state)
const selectedToken = getSelectedToken(state)
return { return {
amount: getSendAmount(state), amount: getSendAmount(state),
amountConversionRate: getAmountConversionRate(state), amountConversionRate: getAmountConversionRate(state),
blockGasLimit: getBlockGasLimit(state),
conversionRate: getConversionRate(state), conversionRate: getConversionRate(state),
data: generateTokenTransferData(selectedAddress, selectedToken),
editingTransactionId: getSendEditingTransactionId(state), editingTransactionId: getSendEditingTransactionId(state),
from: getSendFromObject(state), from: getSendFromObject(state),
gasLimit: getGasLimit(state), gasLimit: getGasLimit(state),
@ -53,6 +51,7 @@ function mapStateToProps (state) {
gasTotal: getGasTotal(state), gasTotal: getGasTotal(state),
network: getCurrentNetwork(state), network: getCurrentNetwork(state),
primaryCurrency: getPrimaryCurrency(state), primaryCurrency: getPrimaryCurrency(state),
recentBlocks: getRecentBlocks(state),
selectedAddress: getSelectedAddress(state), selectedAddress: getSelectedAddress(state),
selectedToken: getSelectedToken(state), selectedToken: getSelectedToken(state),
tokenBalance: getTokenBalance(state), tokenBalance: getTokenBalance(state),
@ -64,16 +63,18 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return { return {
updateAndSetGasTotal: ({ updateAndSetGasTotal: ({
data, blockGasLimit,
editingTransactionId, editingTransactionId,
gasLimit, gasLimit,
gasPrice, gasPrice,
recentBlocks,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to,
value,
}) => { }) => {
console.log(`editingTransactionId`, editingTransactionId)
!editingTransactionId !editingTransactionId
? dispatch(updateGasTotal({ selectedAddress, selectedToken, data })) ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value }))
: dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
}, },
updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {

@ -3,12 +3,16 @@ const abi = require('human-standard-token-abi')
const { const {
multiplyCurrencies, multiplyCurrencies,
} = require('../../conversion-util') } = require('../../conversion-util')
const {
estimateGasPriceFromRecentBlocks,
} = require('./send.utils')
const selectors = { const selectors = {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
// autoAddToBetaUI, // autoAddToBetaUI,
getAddressBook, getAddressBook,
getAmountConversionRate, getAmountConversionRate,
getBlockGasLimit,
getConversionRate, getConversionRate,
getConvertedCurrency, getConvertedCurrency,
getCurrentAccountWithSendEtherInfo, getCurrentAccountWithSendEtherInfo,
@ -18,8 +22,10 @@ const selectors = {
getForceGasMin, getForceGasMin,
getGasLimit, getGasLimit,
getGasPrice, getGasPrice,
getGasPriceFromRecentBlocks,
getGasTotal, getGasTotal,
getPrimaryCurrency, getPrimaryCurrency,
getRecentBlocks,
getSelectedAccount, getSelectedAccount,
getSelectedAddress, getSelectedAddress,
getSelectedIdentity, getSelectedIdentity,
@ -84,6 +90,10 @@ function getAmountConversionRate (state) {
: getConversionRate(state) : getConversionRate(state)
} }
function getBlockGasLimit (state) {
return state.metamask.currentBlockGasLimit
}
function getConversionRate (state) { function getConversionRate (state) {
return state.metamask.conversionRate return state.metamask.conversionRate
} }
@ -124,6 +134,10 @@ function getGasPrice (state) {
return state.metamask.send.gasPrice return state.metamask.send.gasPrice
} }
function getGasPriceFromRecentBlocks (state) {
return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks)
}
function getGasTotal (state) { function getGasTotal (state) {
return state.metamask.send.gasTotal return state.metamask.send.gasTotal
} }
@ -133,6 +147,10 @@ function getPrimaryCurrency (state) {
return selectedToken && selectedToken.symbol return selectedToken && selectedToken.symbol
} }
function getRecentBlocks (state) {
return state.metamask.recentBlocks
}
function getSelectedAccount (state) { function getSelectedAccount (state) {
const accounts = state.metamask.accounts const accounts = state.metamask.accounts
const selectedAddress = getSelectedAddress(state) const selectedAddress = getSelectedAddress(state)

@ -12,16 +12,21 @@ const {
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
INSUFFICIENT_TOKENS_ERROR, INSUFFICIENT_TOKENS_ERROR,
NEGATIVE_ETH_ERROR, NEGATIVE_ETH_ERROR,
ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST,
TOKEN_TRANSFER_FUNCTION_SIGNATURE,
} = require('./send.constants') } = require('./send.constants')
const abi = require('ethereumjs-abi') const abi = require('ethereumjs-abi')
const ethUtil = require('ethereumjs-util')
module.exports = { module.exports = {
calcGasTotal, calcGasTotal,
calcTokenBalance,
doesAmountErrorRequireUpdate, doesAmountErrorRequireUpdate,
estimateGas,
estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getParamsForGasEstimate,
calcTokenBalance,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
} }
@ -139,23 +144,6 @@ function getAmountErrorObject ({
return { amount: amountError } 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 }) { function calcTokenBalance ({ selectedToken, usersToken }) {
const { decimals } = selectedToken || {} const { decimals } = selectedToken || {}
return calcTokenAmount(usersToken.balance.toString(), decimals) + '' return calcTokenAmount(usersToken.balance.toString(), decimals) + ''
@ -178,11 +166,74 @@ function doesAmountErrorRequireUpdate ({
return amountErrorRequiresUpdate return amountErrorRequiresUpdate
} }
function generateTokenTransferData (selectedAddress, selectedToken) { async function estimateGas ({ selectedAddress, selectedToken, blockGasLimit, to, value, gasPrice, estimateGasMethod }) {
const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }
if (selectedToken) {
paramsForGasEstimate.value = '0x0'
paramsForGasEstimate.data = generateTokenTransferData({ toAddress: to, amount: value, selectedToken })
}
// if recipient has no code, gas is 21k max:
const hasRecipient = Boolean(to)
if (hasRecipient && !selectedToken) {
const code = await global.eth.getCode(to)
if (!code || code === '0x') {
return SIMPLE_GAS_COST
}
}
paramsForGasEstimate.to = selectedToken ? selectedToken.address : to
// if not, fall back to block gasLimit
paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, {
multiplicandBase: 16,
multiplierBase: 10,
roundDown: '0',
toNumericBase: 'hex',
}))
// run tx
return new Promise((resolve, reject) => {
return estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => {
if (err) {
const simulationFailed = (
err.message.includes('Transaction execution error.') ||
err.message.includes('gas required exceeds allowance or always failing transaction')
)
if (simulationFailed) {
return resolve(paramsForGasEstimate.gas)
} else {
return reject(err)
}
}
return resolve(estimatedGas.toString(16))
})
})
}
function generateTokenTransferData ({ toAddress = '0x0', amount = '0x0', selectedToken }) {
if (!selectedToken) return if (!selectedToken) return
console.log(`abi.rawEncode`, abi.rawEncode) return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call(
return Array.prototype.map.call( abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]),
abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']),
x => ('00' + x.toString(16)).slice(-2) x => ('00' + x.toString(16)).slice(-2)
).join('') ).join('')
} }
function estimateGasPriceFromRecentBlocks (recentBlocks) {
// Return 1 gwei if no blocks have been observed:
if (!recentBlocks || recentBlocks.length === 0) {
return ONE_GWEI_IN_WEI_HEX
}
const lowestPrices = recentBlocks.map((block) => {
if (!block.gasPrices || block.gasPrices.length < 1) {
return ONE_GWEI_IN_WEI_HEX
}
return block.gasPrices.reduce((currentLowest, next) => {
return parseInt(next, 16) < parseInt(currentLowest, 16) ? next : currentLowest
})
})
.sort((a, b) => parseInt(a, 16) > parseInt(b, 16) ? 1 : -1)
return lowestPrices[Math.floor(lowestPrices.length / 2)]
}

@ -32,8 +32,8 @@ describe('Send Component', function () {
wrapper = shallow(<SendTransactionScreen wrapper = shallow(<SendTransactionScreen
amount={'mockAmount'} amount={'mockAmount'}
amountConversionRate={'mockAmountConversionRate'} amountConversionRate={'mockAmountConversionRate'}
blockGasLimit={'mockBlockGasLimit'}
conversionRate={10} conversionRate={10}
data={'mockData'}
editingTransactionId={'mockEditingTransactionId'} editingTransactionId={'mockEditingTransactionId'}
from={ { address: 'mockAddress', balance: 'mockBalance' } } from={ { address: 'mockAddress', balance: 'mockBalance' } }
gasLimit={'mockGasLimit'} gasLimit={'mockGasLimit'}
@ -42,6 +42,7 @@ describe('Send Component', function () {
history={{ mockProp: 'history-abc'}} history={{ mockProp: 'history-abc'}}
network={'3'} network={'3'}
primaryCurrency={'mockPrimaryCurrency'} primaryCurrency={'mockPrimaryCurrency'}
recentBlocks={['mockBlock']}
selectedAddress={'mockSelectedAddress'} selectedAddress={'mockSelectedAddress'}
selectedToken={'mockSelectedToken'} selectedToken={'mockSelectedToken'}
tokenBalance={'mockTokenBalance'} tokenBalance={'mockTokenBalance'}
@ -207,15 +208,24 @@ describe('Send Component', function () {
assert.deepEqual( assert.deepEqual(
propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0],
{ {
data: 'mockData', blockGasLimit: 'mockBlockGasLimit',
editingTransactionId: 'mockEditingTransactionId', editingTransactionId: 'mockEditingTransactionId',
gasLimit: 'mockGasLimit', gasLimit: 'mockGasLimit',
gasPrice: 'mockGasPrice', gasPrice: 'mockGasPrice',
recentBlocks: ['mockBlock'],
selectedAddress: 'mockSelectedAddress', selectedAddress: 'mockSelectedAddress',
selectedToken: 'mockSelectedToken', selectedToken: 'mockSelectedToken',
to: undefined,
value: 'mockAmount',
} }
) )
}) })
it('should call updateAndSetGasTotal with to set to lowercase if passed', () => {
propsMethodSpies.updateAndSetGasTotal.resetHistory()
wrapper.instance().updateGas({ to: '0xABC' })
assert.equal(propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0].to, '0xabc')
})
}) })
describe('render', () => { describe('render', () => {

@ -7,7 +7,7 @@ let mapDispatchToProps
const actionSpies = { const actionSpies = {
updateSendTokenBalance: sinon.spy(), updateSendTokenBalance: sinon.spy(),
updateGasTotal: sinon.spy(), updateGasData: sinon.spy(),
setGasTotal: sinon.spy(), setGasTotal: sinon.spy(),
} }
const duckActionSpies = { const duckActionSpies = {
@ -26,12 +26,14 @@ proxyquire('../send.container.js', {
'recompose': { compose: (arg1, arg2) => () => arg2() }, 'recompose': { compose: (arg1, arg2) => () => arg2() },
'./send.selectors': { './send.selectors': {
getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`,
getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`,
getConversionRate: (s) => `mockConversionRate:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`,
getCurrentNetwork: (s) => `mockNetwork:${s}`, getCurrentNetwork: (s) => `mockNetwork:${s}`,
getGasLimit: (s) => `mockGasLimit:${s}`, getGasLimit: (s) => `mockGasLimit:${s}`,
getGasPrice: (s) => `mockGasPrice:${s}`, getGasPrice: (s) => `mockGasPrice:${s}`,
getGasTotal: (s) => `mockGasTotal:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`,
getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`,
getRecentBlocks: (s) => `mockRecentBlocks:${s}`,
getSelectedAddress: (s) => `mockSelectedAddress:${s}`, getSelectedAddress: (s) => `mockSelectedAddress:${s}`,
getSelectedToken: (s) => `mockSelectedToken:${s}`, getSelectedToken: (s) => `mockSelectedToken:${s}`,
getSelectedTokenContract: (s) => `mockTokenContract:${s}`, getSelectedTokenContract: (s) => `mockTokenContract:${s}`,
@ -45,7 +47,6 @@ proxyquire('../send.container.js', {
'../../ducks/send.duck': duckActionSpies, '../../ducks/send.duck': duckActionSpies,
'./send.utils.js': { './send.utils.js': {
calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice,
generateTokenTransferData: (a, b) => `mockData:${a + b}`,
}, },
}) })
@ -57,8 +58,8 @@ describe('send container', () => {
assert.deepEqual(mapStateToProps('mockState'), { assert.deepEqual(mapStateToProps('mockState'), {
amount: 'mockAmount:mockState', amount: 'mockAmount:mockState',
amountConversionRate: 'mockAmountConversionRate:mockState', amountConversionRate: 'mockAmountConversionRate:mockState',
blockGasLimit: 'mockBlockGasLimit:mockState',
conversionRate: 'mockConversionRate:mockState', conversionRate: 'mockConversionRate:mockState',
data: 'mockData:mockSelectedAddress:mockStatemockSelectedToken:mockState',
editingTransactionId: 'mockEditingTransactionId:mockState', editingTransactionId: 'mockEditingTransactionId:mockState',
from: 'mockFrom:mockState', from: 'mockFrom:mockState',
gasLimit: 'mockGasLimit:mockState', gasLimit: 'mockGasLimit:mockState',
@ -66,6 +67,7 @@ describe('send container', () => {
gasTotal: 'mockGasTotal:mockState', gasTotal: 'mockGasTotal:mockState',
network: 'mockNetwork:mockState', network: 'mockNetwork:mockState',
primaryCurrency: 'mockPrimaryCurrency:mockState', primaryCurrency: 'mockPrimaryCurrency:mockState',
recentBlocks: 'mockRecentBlocks:mockState',
selectedAddress: 'mockSelectedAddress:mockState', selectedAddress: 'mockSelectedAddress:mockState',
selectedToken: 'mockSelectedToken:mockState', selectedToken: 'mockSelectedToken:mockState',
tokenBalance: 'mockTokenBalance:mockState', tokenBalance: 'mockTokenBalance:mockState',
@ -87,12 +89,15 @@ describe('send container', () => {
describe('updateAndSetGasTotal()', () => { describe('updateAndSetGasTotal()', () => {
const mockProps = { const mockProps = {
data: '0x1', blockGasLimit: 'mockBlockGasLimit',
editingTransactionId: '0x2', editingTransactionId: '0x2',
gasLimit: '0x3', gasLimit: '0x3',
gasPrice: '0x4', gasPrice: '0x4',
recentBlocks: ['mockBlock'],
selectedAddress: '0x4', selectedAddress: '0x4',
selectedToken: { address: '0x1' }, selectedToken: { address: '0x1' },
to: 'mockTo',
value: 'mockValue',
} }
it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => { it('should dispatch a setGasTotal action when editingTransactionId is truthy', () => {
@ -104,15 +109,15 @@ describe('send container', () => {
) )
}) })
it('should dispatch an updateGasTotal action when editingTransactionId is falsy', () => { it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
const { selectedAddress, selectedToken, data } = mockProps const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps
mapDispatchToPropsObject.updateAndSetGasTotal( mapDispatchToPropsObject.updateAndSetGasTotal(
Object.assign(mockProps, {editingTransactionId: false}) Object.assign({}, mockProps, {editingTransactionId: false})
) )
assert(dispatchSpy.calledOnce) assert(dispatchSpy.calledOnce)
assert.deepEqual( assert.deepEqual(
actionSpies.updateGasTotal.getCall(0).args[0], actionSpies.updateGasData.getCall(0).args[0],
{ selectedAddress, selectedToken, data } { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value }
) )
}) })
}) })

@ -22,6 +22,7 @@ module.exports = {
'name': 'Send Account 4', 'name': 'Send Account 4',
}, },
}, },
'currentBlockGasLimit': '0x4c1878',
'currentCurrency': 'USD', 'currentCurrency': 'USD',
'conversionRate': 1200.88200327, 'conversionRate': 1200.88200327,
'conversionDate': 1489013762, 'conversionDate': 1489013762,
@ -198,6 +199,7 @@ module.exports = {
}, },
}, },
'currentLocale': 'en', 'currentLocale': 'en',
recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'],
}, },
'appState': { 'appState': {
'menuOpen': false, 'menuOpen': false,

@ -5,6 +5,7 @@ const {
accountsWithSendEtherInfoSelector, accountsWithSendEtherInfoSelector,
// autoAddToBetaUI, // autoAddToBetaUI,
getAddressBook, getAddressBook,
getBlockGasLimit,
getAmountConversionRate, getAmountConversionRate,
getConversionRate, getConversionRate,
getConvertedCurrency, getConvertedCurrency,
@ -17,6 +18,7 @@ const {
getGasPrice, getGasPrice,
getGasTotal, getGasTotal,
getPrimaryCurrency, getPrimaryCurrency,
getRecentBlocks,
getSelectedAccount, getSelectedAccount,
getSelectedAddress, getSelectedAddress,
getSelectedIdentity, getSelectedIdentity,
@ -134,6 +136,15 @@ describe('send selectors', () => {
}) })
}) })
describe('getBlockGasLimit', () => {
it('should return the current block gas limit', () => {
assert.deepEqual(
getBlockGasLimit(mockState),
'0x4c1878'
)
})
})
describe('getConversionRate()', () => { describe('getConversionRate()', () => {
it('should return the eth conversion rate', () => { it('should return the eth conversion rate', () => {
assert.deepEqual( assert.deepEqual(
@ -239,6 +250,15 @@ describe('send selectors', () => {
}) })
}) })
describe('getRecentBlocks()', () => {
it('should return the recent blocks', () => {
assert.deepEqual(
getRecentBlocks(mockState),
['mockBlock1', 'mockBlock2', 'mockBlock3']
)
})
})
describe('getSelectedAccount()', () => { describe('getSelectedAccount()', () => {
it('should return the currently selected account', () => { it('should return the currently selected account', () => {
assert.deepEqual( assert.deepEqual(

@ -1,6 +1,14 @@
import assert from 'assert' import assert from 'assert'
import sinon from 'sinon' import sinon from 'sinon'
import proxyquire from 'proxyquire' import proxyquire from 'proxyquire'
import {
ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST,
} from '../send.constants'
const {
addCurrencies,
subtractCurrencies,
} = require('../../../conversion-util')
const { const {
INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_FUNDS_ERROR,
@ -11,7 +19,7 @@ const stubs = {
addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b),
conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), 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 * b), multiplyCurrencies: sinon.stub().callsFake((a, b) => `${a}x${b}`),
calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d), calcTokenAmount: sinon.stub().callsFake((a, d) => 'calc:' + a + d),
rawEncode: sinon.stub().returns([16, 1100]), rawEncode: sinon.stub().returns([16, 1100]),
} }
@ -31,10 +39,11 @@ const sendUtils = proxyquire('../send.utils.js', {
const { const {
calcGasTotal, calcGasTotal,
estimateGas,
doesAmountErrorRequireUpdate, doesAmountErrorRequireUpdate,
estimateGasPriceFromRecentBlocks,
generateTokenTransferData, generateTokenTransferData,
getAmountErrorObject, getAmountErrorObject,
getParamsForGasEstimate,
calcTokenBalance, calcTokenBalance,
isBalanceSufficient, isBalanceSufficient,
isTokenBalanceSufficient, isTokenBalanceSufficient,
@ -45,7 +54,7 @@ describe('send utils', () => {
describe('calcGasTotal()', () => { describe('calcGasTotal()', () => {
it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => { it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => {
const result = calcGasTotal(12, 15) const result = calcGasTotal(12, 15)
assert.equal(result, 180) assert.equal(result, '12x15')
const call_ = stubs.multiplyCurrencies.getCall(0).args const call_ = stubs.multiplyCurrencies.getCall(0).args
assert.deepEqual( assert.deepEqual(
call_, call_,
@ -97,11 +106,23 @@ describe('send utils', () => {
describe('generateTokenTransferData()', () => { describe('generateTokenTransferData()', () => {
it('should return undefined if not passed a selected token', () => { it('should return undefined if not passed a selected token', () => {
assert.equal(generateTokenTransferData('mockAddress', false), undefined) assert.equal(generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: false}), undefined)
})
it('should call abi.rawEncode with the correct params', () => {
stubs.rawEncode.resetHistory()
generateTokenTransferData({ toAddress: 'mockAddress', amount: 'ab', selectedToken: true})
assert.deepEqual(
stubs.rawEncode.getCall(0).args,
[['address', 'uint256'], ['mockAddress', '0xab']]
)
}) })
it('should return encoded token transfer data', () => { it('should return encoded token transfer data', () => {
assert.equal(generateTokenTransferData('mockAddress', true), '104c') assert.equal(
generateTokenTransferData({ toAddress: 'mockAddress', amount: '0xa', selectedToken: true}),
'0xa9059cbb104c'
)
}) })
}) })
@ -136,41 +157,6 @@ describe('send utils', () => {
}) })
}) })
describe('getParamsForGasEstimate()', () => {
it('should return from and gas properties if no symbol or data', () => {
assert.deepEqual(
getParamsForGasEstimate('mockAddress'),
{
from: 'mockAddress',
gas: '746a528800',
}
)
})
it('should return value property if symbol provided', () => {
assert.deepEqual(
getParamsForGasEstimate('mockAddress', 'ABC'),
{
from: 'mockAddress',
gas: '746a528800',
value: '0x0',
}
)
})
it('should return data property if data provided', () => {
assert.deepEqual(
getParamsForGasEstimate('mockAddress', 'ABC', 'somedata'),
{
from: 'mockAddress',
gas: '746a528800',
value: '0x0',
data: 'somedata',
}
)
})
})
describe('calcTokenBalance()', () => { describe('calcTokenBalance()', () => {
it('should return the calculated token blance', () => { it('should return the calculated token blance', () => {
assert.equal(calcTokenBalance({ assert.equal(calcTokenBalance({
@ -261,4 +247,158 @@ describe('send utils', () => {
}) })
}) })
describe('estimateGas', () => {
const baseMockParams = {
blockGasLimit: '0x64',
selectedAddress: 'mockAddress',
to: '0xisContract',
estimateGasMethod: sinon.stub().callsFake(
(data, cb) => cb(
data.to.match(/willFailBecauseOf:/) ? { message: data.to.match(/:(.+)$/)[1] } : null,
{ toString: (n) => `mockToString:${n}` }
)
),
}
const baseExpectedCall = {
from: 'mockAddress',
gas: '0x64x0.95',
to: '0xisContract',
}
beforeEach(() => {
global.eth = {
getCode: sinon.stub().callsFake(
(address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
),
}
})
afterEach(() => {
baseMockParams.estimateGasMethod.resetHistory()
global.eth.getCode.resetHistory()
})
it('should call ethQuery.estimateGas with the expected params', async () => {
const result = await 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')
})
it('should call ethQuery.estimateGas with a value of 0x0 and the expected data and to if passed a selectedToken', async () => {
const result = await estimateGas(Object.assign({ data: 'mockData', selectedToken: { address: 'mockAddress' } }, baseMockParams))
assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual(
baseMockParams.estimateGasMethod.getCall(0).args[0],
Object.assign({}, baseExpectedCall, {
gasPrice: undefined,
value: '0x0',
data: '0xa9059cbb104c',
to: 'mockAddress',
})
)
assert.equal(result, 'mockToString:16')
})
it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' }))
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 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.',
}))
assert.equal(result, '0x64x0.95')
})
it(`should return the adjusted blockGasLimit if it fails with a 'gas required exceeds allowance or always failing transaction.'`, async () => {
const result = await estimateGas(Object.assign({}, baseMockParams, {
to: 'isContract willFailBecauseOf:gas required exceeds allowance or always failing transaction.',
}))
assert.equal(result, '0x64x0.95')
})
it(`should reject other errors`, async () => {
try {
await estimateGas(Object.assign({}, baseMockParams, {
to: 'isContract willFailBecauseOf:some other error',
}))
} catch (err) {
assert.deepEqual(err, { message: 'some other error' })
}
})
})
describe('estimateGasPriceFromRecentBlocks', () => {
const ONE_GWEI_IN_WEI_HEX_PLUS_ONE = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
aBase: 16,
bBase: 16,
toNumericBase: 'hex',
})
const ONE_GWEI_IN_WEI_HEX_PLUS_TWO = addCurrencies(ONE_GWEI_IN_WEI_HEX, '0x2', {
aBase: 16,
bBase: 16,
toNumericBase: 'hex',
})
const ONE_GWEI_IN_WEI_HEX_MINUS_ONE = subtractCurrencies(ONE_GWEI_IN_WEI_HEX, '0x1', {
aBase: 16,
bBase: 16,
toNumericBase: 'hex',
})
it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is falsy`, () => {
assert.equal(estimateGasPriceFromRecentBlocks(), ONE_GWEI_IN_WEI_HEX)
})
it(`should return ${ONE_GWEI_IN_WEI_HEX} if recentBlocks is empty`, () => {
assert.equal(estimateGasPriceFromRecentBlocks([]), ONE_GWEI_IN_WEI_HEX)
})
it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has no gas prices`, () => {
const mockRecentBlocks = [
{ gasPrices: null },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
]
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
})
it(`should estimate a block's gasPrice as ${ONE_GWEI_IN_WEI_HEX} if it has empty gas prices`, () => {
const mockRecentBlocks = [
{ gasPrices: [] },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
]
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX)
})
it(`should return the middle value of all blocks lowest prices`, () => {
const mockRecentBlocks = [
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_TWO ] },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_MINUS_ONE ] },
{ gasPrices: [ ONE_GWEI_IN_WEI_HEX_PLUS_ONE ] },
]
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), ONE_GWEI_IN_WEI_HEX_PLUS_ONE)
})
it(`should work if a block has multiple gas prices`, () => {
const mockRecentBlocks = [
{ gasPrices: [ '0x1', '0x2', '0x3', '0x4', '0x5' ] },
{ gasPrices: [ '0x101', '0x100', '0x103', '0x104', '0x102' ] },
{ gasPrices: [ '0x150', '0x50', '0x100', '0x200', '0x5' ] },
]
assert.equal(estimateGasPriceFromRecentBlocks(mockRecentBlocks), '0x5')
})
})
}) })

@ -11,7 +11,8 @@
* @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value. * @param {string} [options.fromNumericBase = 'hex' | 'dec' | 'BN'] The numeric basic of the passed value.
* @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result. * @param {string} [options.toNumericBase = 'hex' | 'dec' | 'BN'] The desired numeric basic of the result.
* @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value * @param {string} [options.fromDenomination = 'WEI'] The denomination of the passed value
* @param {number} [options.numberOfDecimals] The desired number of in the result * @param {string} [options.numberOfDecimals] The desired number of decimals in the result
* @param {string} [options.roundDown] The desired number of decimals to round down to
* @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion * @param {number} [options.conversionRate] The rate to use to make the fromCurrency -> toCurrency conversion
* @returns {(number | string | BN)} * @returns {(number | string | BN)}
* *
@ -38,6 +39,7 @@ const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000')
// Individual Setters // Individual Setters
const convert = R.invoker(1, 'times') const convert = R.invoker(1, 'times')
const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN) const round = R.invoker(2, 'round')(R.__, BigNumber.ROUND_HALF_DOWN)
const roundDown = R.invoker(2, 'round')(R.__, BigNumber.ROUND_DOWN)
const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate) const invertConversionRate = conversionRate => () => new BigNumber(1.0).div(conversionRate)
const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) const decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec'])
@ -104,6 +106,7 @@ const converter = R.pipe(
whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert),
whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), whenPropApplySetterMap('toDenomination', toSpecifiedDenomination),
whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round), whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round),
whenPredSetWithPropAndSetter(R.prop('roundDown'), 'roundDown', roundDown),
whenPropApplySetterMap('toNumericBase', baseChange), whenPropApplySetterMap('toNumericBase', baseChange),
R.view(R.lensProp('value')) R.view(R.lensProp('value'))
) )

Loading…
Cancel
Save