diff options
author | Dan J Miller <danjm.com@gmail.com> | 2018-06-05 03:08:01 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-06-05 03:08:01 +0800 |
commit | 139f930185f134ef50d547ca07a580e11b5cf731 (patch) | |
tree | 4e184b7dc5f97d9d4da28e6b72b256e955b0a937 | |
parent | 2ca084b0557242b34733107701a14ba0724363b3 (diff) | |
parent | ada59054c9d102cc99b950f1377256cac5545649 (diff) | |
download | tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar.gz tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar.bz2 tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar.lz tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar.xz tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.tar.zst tangerine-wallet-browser-139f930185f134ef50d547ca07a580e11b5cf731.zip |
Merge pull request #4350 from MetaMask/i3914-fix-newui-send-gas-estimation
NewUI gas estimation produces same results as old-ui (exception: contract addresses)
23 files changed, 461 insertions, 165 deletions
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index a570f2567..9db7e186a 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -384,6 +384,8 @@ module.exports = class MetamaskController extends EventEmitter { updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController), retryTransaction: nodeify(this.retryTransaction, this), getFilteredTxList: nodeify(txController.getFilteredTxList, txController), + isNonceTaken: nodeify(txController.isNonceTaken, txController), + estimateGas: nodeify(this.estimateGas, this), // messageManager signMessage: nodeify(this.signMessage, this), @@ -922,6 +924,18 @@ module.exports = class MetamaskController extends EventEmitter { 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 //============================================================================= diff --git a/test/integration/lib/send-new-ui.js b/test/integration/lib/send-new-ui.js index f787eafe8..4d2ea2ea4 100644 --- a/test/integration/lib/send-new-ui.js +++ b/test/integration/lib/send-new-ui.js @@ -117,7 +117,7 @@ async function runSendFlowTest(assert, done) { const sendGasField = await queryAsync($, '.send-v2__gas-fee-display') assert.equal( sendGasField.find('.currency-display__input-wrapper > input').val(), - '0.000198', + '0.000198264', 'send gas field should show estimated gas total' ) assert.equal( diff --git a/ui/app/actions.js b/ui/app/actions.js index ae6b9637d..465b3d5fe 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -4,8 +4,9 @@ const getBuyEthUrl = require('../../app/scripts/lib/buy-eth-url') const { getTokenAddressFromTokenObject } = require('./util') const { calcGasTotal, - getParamsForGasEstimate, calcTokenBalance, + estimateGas, + estimateGasPriceFromRecentBlocks, } = require('./components/send_/send.utils') const ethUtil = require('ethereumjs-util') const { fetchLocale } = require('../i18n-helper') @@ -160,8 +161,6 @@ var actions = { updateTransactionParams, UPDATE_TRANSACTION_PARAMS: 'UPDATE_TRANSACTION_PARAMS', // send screen - estimateGas, - getGasPrice, UPDATE_GAS_LIMIT: 'UPDATE_GAS_LIMIT', UPDATE_GAS_PRICE: 'UPDATE_GAS_PRICE', UPDATE_GAS_TOTAL: 'UPDATE_GAS_TOTAL', @@ -176,9 +175,9 @@ var actions = { CLEAR_SEND: 'CLEAR_SEND', OPEN_FROM_DROPDOWN: 'OPEN_FROM_DROPDOWN', CLOSE_FROM_DROPDOWN: 'CLOSE_FROM_DROPDOWN', - updateGasLimit, - updateGasPrice, - updateGasTotal, + setGasLimit, + setGasPrice, + updateGasData, setGasTotal, setSendTokenBalance, updateSendTokenBalance, @@ -710,46 +709,14 @@ function signTx (txData) { } } -function estimateGas (params = {}) { - 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) { +function setGasLimit (gasLimit) { return { type: actions.UPDATE_GAS_LIMIT, value: gasLimit, } } -function getGasPrice () { - 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) { +function setGasPrice (gasPrice) { return { type: actions.UPDATE_GAS_PRICE, 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) => { - const { symbol } = selectedToken || {} - const estimateGasParams = getParamsForGasEstimate(selectedAddress, symbol, data) return Promise.all([ - dispatch(actions.getGasPrice()), - dispatch(actions.estimateGas(estimateGasParams)), + Promise.resolve(estimatedGasPrice), + estimateGas({ + estimateGasMethod: background.estimateGas, + blockGasLimit, + selectedAddress, + selectedToken, + to, + value, + gasPrice: estimatedGasPrice, + }), ]) .then(([gasPrice, gas]) => { - const newGasTotal = calcGasTotal(gas, gasPrice) - dispatch(actions.setGasTotal(newGasTotal)) + dispatch(actions.setGasPrice(gasPrice)) + dispatch(actions.setGasLimit(gas)) + return calcGasTotal(gas, gasPrice) + }) + .then((gasEstimate) => { + dispatch(actions.setGasTotal(gasEstimate)) dispatch(updateSendErrors({ gasLoadingError: null })) }) .catch(err => { diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 2dd0cbb0e..c8522a3c7 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -65,9 +65,9 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { hideModal: () => dispatch(actions.hideModal()), - updateGasPrice: newGasPrice => dispatch(actions.updateGasPrice(newGasPrice)), - updateGasLimit: newGasLimit => dispatch(actions.updateGasLimit(newGasLimit)), - updateGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)), + setGasPrice: newGasPrice => dispatch(actions.setGasPrice(newGasPrice)), + setGasLimit: newGasLimit => dispatch(actions.setGasLimit(newGasLimit)), + setGasTotal: newGasTotal => dispatch(actions.setGasTotal(newGasTotal)), updateSendAmount: newAmount => dispatch(actions.updateSendAmount(newAmount)), updateSendErrors: error => dispatch(updateSendErrors(error)), } @@ -109,10 +109,10 @@ module.exports = connect(mapStateToProps, mapDispatchToProps)(CustomizeGasModal) CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { const { - updateGasPrice, - updateGasLimit, + setGasPrice, + setGasLimit, hideModal, - updateGasTotal, + setGasTotal, maxModeOn, selectedToken, balance, @@ -129,9 +129,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) { updateSendAmount(maxAmount) } - updateGasPrice(ethUtil.addHexPrefix(gasPrice)) - updateGasLimit(ethUtil.addHexPrefix(gasLimit)) - updateGasTotal(ethUtil.addHexPrefix(gasTotal)) + setGasPrice(ethUtil.addHexPrefix(gasPrice)) + setGasLimit(ethUtil.addHexPrefix(gasLimit)) + setGasTotal(ethUtil.addHexPrefix(gasTotal)) updateSendErrors({ insufficientFunds: false }) hideModal() } diff --git a/ui/app/components/send/currency-display.js b/ui/app/components/send/currency-display.js index 360dd15d4..3bc9ad226 100644 --- a/ui/app/components/send/currency-display.js +++ b/ui/app/components/send/currency-display.js @@ -4,6 +4,7 @@ const inherits = require('util').inherits const { conversionUtil, multiplyCurrencies } = require('../../conversion-util') const currencyFormatter = require('currency-formatter') const currencies = require('currency-formatter/currencies') +const ethUtil = require('ethereumjs-util') module.exports = CurrencyDisplay @@ -48,23 +49,23 @@ CurrencyDisplay.prototype.getAmount = function (value) { : toHexWei(value) } -CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value }) { - if (value === '0x0') return '0' +CurrencyDisplay.prototype.getValueToRender = function ({ selectedToken, conversionRate, value, readOnly }) { + if (value === '0x0') return readOnly ? '0' : '' const { decimals, symbol } = selectedToken || {} const multiplier = Math.pow(10, Number(decimals || 0)) return selectedToken - ? conversionUtil(value, { + ? conversionUtil(ethUtil.addHexPrefix(value), { fromNumericBase: 'hex', toCurrency: symbol, conversionRate: multiplier, invertConversionRate: true, }) - : conversionUtil(value, { + : conversionUtil(ethUtil.addHexPrefix(value), { fromNumericBase: 'hex', toNumericBase: 'dec', fromDenomination: 'WEI', - numberOfDecimals: 6, + numberOfDecimals: 9, conversionRate, }) } diff --git a/ui/app/components/send_/send-content/send-content.component.js b/ui/app/components/send_/send-content/send-content.component.js index d610c2a3f..3a14054eb 100644 --- a/ui/app/components/send_/send-content/send-content.component.js +++ b/ui/app/components/send_/send-content/send-content.component.js @@ -1,4 +1,5 @@ import React, { Component } from 'react' +import PropTypes from 'prop-types' import PageContainerContent from '../../page-container/page-container-content.component' import SendAmountRow from './send-amount-row/' import SendFromRow from './send-from-row/' @@ -7,12 +8,16 @@ import SendToRow from './send-to-row/' export default class SendContent extends Component { + static propTypes = { + updateGas: PropTypes.func, + }; + render () { return ( <PageContainerContent> <div className="send-v2__form"> <SendFromRow /> - <SendToRow /> + <SendToRow updateGas={(updateData) => this.props.updateGas(updateData)} /> <SendAmountRow /> <SendGasRow /> </div> diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js index 901ae97e9..0a83186a5 100644 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.component.js @@ -2,6 +2,7 @@ import React, { Component } from 'react' import PropTypes from 'prop-types' import SendRowWrapper from '../send-row-wrapper/' import EnsInput from '../../../ens-input' +import { getToErrorObject } from './send-to-row.utils.js' export default class SendToRow extends Component { @@ -13,14 +14,19 @@ export default class SendToRow extends Component { to: PropTypes.string, toAccounts: PropTypes.array, toDropdownOpen: PropTypes.bool, + updateGas: PropTypes.func, updateSendTo: PropTypes.func, updateSendToError: PropTypes.func, }; handleToChange (to, nickname = '') { - const { updateSendTo, updateSendToError } = this.props + const { updateSendTo, updateSendToError, updateGas } = this.props + const toErrorObject = getToErrorObject(to) updateSendTo(to, nickname) - updateSendToError(to) + updateSendToError(toErrorObject) + if (toErrorObject.to === null) { + updateGas({ to }) + } } render () { 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 a10da505a..1c9c9d518 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 @@ -8,7 +8,6 @@ import { getToDropdownOpen, sendToIsInError, } from './send-to-row.selectors.js' -import { getToErrorObject } from './send-to-row.utils.js' import { updateSendTo, } from '../../../../actions' @@ -36,8 +35,8 @@ function mapDispatchToProps (dispatch) { closeToDropdown: () => dispatch(closeToDropdown()), openToDropdown: () => dispatch(openToDropdown()), updateSendTo: (to, nickname) => dispatch(updateSendTo(to, nickname)), - updateSendToError: (to) => { - dispatch(updateSendErrors(getToErrorObject(to))) + updateSendToError: (toErrorObject) => { + dispatch(updateSendErrors(toErrorObject)) }, } } diff --git a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js index 22e2e1f34..cea51ee20 100644 --- a/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js +++ b/ui/app/components/send_/send-content/send-to-row/send-to-row.utils.js @@ -8,9 +8,9 @@ function getToErrorObject (to) { let toError = null if (!to) { - toError = REQUIRED_ERROR + toError = REQUIRED_ERROR } else if (!isValidAddress(to)) { - toError = INVALID_RECIPIENT_ADDRESS_ERROR + toError = INVALID_RECIPIENT_ADDRESS_ERROR } return { to: toError } diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js index e58695210..58fe51dcf 100644 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-component.test.js @@ -2,7 +2,15 @@ import React from 'react' import assert from 'assert' import { shallow } from 'enzyme' 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 EnsInput from '../../../../ens-input' @@ -10,6 +18,7 @@ import EnsInput from '../../../../ens-input' const propsMethodSpies = { closeToDropdown: sinon.spy(), openToDropdown: sinon.spy(), + updateGas: sinon.spy(), updateSendTo: sinon.spy(), updateSendToError: sinon.spy(), } @@ -29,6 +38,7 @@ describe('SendToRow Component', function () { to={'mockTo'} toAccounts={['mockAccount']} toDropdownOpen={false} + updateGas={propsMethodSpies.updateGas} updateSendTo={propsMethodSpies.updateSendTo} updateSendToError={propsMethodSpies.updateSendToError} />, { context: { t: str => str + '_t' } }) @@ -61,10 +71,21 @@ describe('SendToRow Component', function () { assert.equal(propsMethodSpies.updateSendToError.callCount, 1) assert.deepEqual( 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', () => { diff --git a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js index 433b242b2..92355c00a 100644 --- a/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js +++ b/ui/app/components/send_/send-content/send-to-row/tests/send-to-row-container.test.js @@ -31,7 +31,6 @@ proxyquire('../send-to-row.container.js', { getToDropdownOpen: (s) => `mockToDropdownOpen:${s}`, sendToIsInError: (s) => `mockInError:${s}`, }, - './send-to-row.utils.js': { getToErrorObject: (t) => `mockError:${t}` }, '../../../../actions': actionSpies, '../../../../ducks/send.duck': duckActionSpies, }) @@ -99,12 +98,12 @@ describe('send-to-row container', () => { describe('updateSendToError()', () => { it('should dispatch an action', () => { - mapDispatchToPropsObject.updateSendToError('mockTo') + mapDispatchToPropsObject.updateSendToError('mockToErrorObject') assert(dispatchSpy.calledOnce) assert(duckActionSpies.updateSendErrors.calledOnce) assert.equal( duckActionSpies.updateSendErrors.getCall(0).args[0], - 'mockError:mockTo' + 'mockToErrorObject' ) }) }) diff --git a/ui/app/components/send_/send-footer/send-footer.utils.js b/ui/app/components/send_/send-footer/send-footer.utils.js index d5639629d..875e7d948 100644 --- a/ui/app/components/send_/send-footer/send-footer.utils.js +++ b/ui/app/components/send_/send-footer/send-footer.utils.js @@ -42,7 +42,6 @@ function constructUpdatedTx ({ } if (selectedToken) { - console.log(`ethAbi.rawEncode`, ethAbi.rawEncode) const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]), x => ('00' + x.toString(16)).slice(-2) diff --git a/ui/app/components/send_/send.component.js b/ui/app/components/send_/send.component.js index 21e1de09b..516251e22 100644 --- a/ui/app/components/send_/send.component.js +++ b/ui/app/components/send_/send.component.js @@ -18,8 +18,8 @@ export default class SendTransactionScreen extends PersistentForm { PropTypes.string, PropTypes.number, ]), + blockGasLimit: PropTypes.string, conversionRate: PropTypes.number, - data: PropTypes.string, editingTransactionId: PropTypes.string, from: PropTypes.object, gasLimit: PropTypes.string, @@ -28,6 +28,7 @@ export default class SendTransactionScreen extends PersistentForm { history: PropTypes.object, network: PropTypes.string, primaryCurrency: PropTypes.string, + recentBlocks: PropTypes.array, selectedAddress: PropTypes.string, selectedToken: PropTypes.object, tokenBalance: PropTypes.string, @@ -37,24 +38,29 @@ export default class SendTransactionScreen extends PersistentForm { updateSendTokenBalance: PropTypes.func, }; - updateGas () { + updateGas ({ to } = {}) { const { - data, + amount, + blockGasLimit, editingTransactionId, gasLimit, gasPrice, + recentBlocks, selectedAddress, selectedToken = {}, updateAndSetGasTotal, } = this.props updateAndSetGasTotal({ - data, + blockGasLimit, editingTransactionId, gasLimit, gasPrice, + recentBlocks, selectedAddress, selectedToken, + to: to && to.toLowerCase(), + value: amount, }) } @@ -141,7 +147,7 @@ export default class SendTransactionScreen extends PersistentForm { return ( <div className="page-container"> <SendHeader history={history}/> - <SendContent/> + <SendContent updateGas={(updateData) => this.updateGas(updateData)}/> <SendFooter history={history}/> </div> ) diff --git a/ui/app/components/send_/send.constants.js b/ui/app/components/send_/send.constants.js index b59fcaaf0..df5dee371 100644 --- a/ui/app/components/send_/send.constants.js +++ b/ui/app/components/send_/send.constants.js @@ -28,6 +28,15 @@ const NEGATIVE_ETH_ERROR = 'negativeETH' const INVALID_RECIPIENT_ADDRESS_ERROR = 'invalidAddressRecipient' 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 = { INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, @@ -39,6 +48,8 @@ module.exports = { MIN_GAS_PRICE_HEX, MIN_GAS_TOTAL, NEGATIVE_ETH_ERROR, + ONE_GWEI_IN_WEI_HEX, REQUIRED_ERROR, + SIMPLE_GAS_COST, TOKEN_TRANSFER_FUNCTION_SIGNATURE, } diff --git a/ui/app/components/send_/send.container.js b/ui/app/components/send_/send.container.js index 8efaf5aaf..1fd96d61f 100644 --- a/ui/app/components/send_/send.container.js +++ b/ui/app/components/send_/send.container.js @@ -4,12 +4,14 @@ import { withRouter } from 'react-router-dom' import { compose } from 'recompose' import { getAmountConversionRate, + getBlockGasLimit, getConversionRate, getCurrentNetwork, getGasLimit, getGasPrice, getGasTotal, getPrimaryCurrency, + getRecentBlocks, getSelectedAddress, getSelectedToken, getSelectedTokenContract, @@ -21,7 +23,7 @@ import { } from './send.selectors' import { updateSendTokenBalance, - updateGasTotal, + updateGasData, setGasTotal, } from '../../actions' import { @@ -29,7 +31,6 @@ import { } from '../../ducks/send.duck' import { calcGasTotal, - generateTokenTransferData, } from './send.utils.js' module.exports = compose( @@ -38,14 +39,11 @@ module.exports = compose( )(SendEther) function mapStateToProps (state) { - const selectedAddress = getSelectedAddress(state) - const selectedToken = getSelectedToken(state) - return { amount: getSendAmount(state), amountConversionRate: getAmountConversionRate(state), + blockGasLimit: getBlockGasLimit(state), conversionRate: getConversionRate(state), - data: generateTokenTransferData(selectedAddress, selectedToken), editingTransactionId: getSendEditingTransactionId(state), from: getSendFromObject(state), gasLimit: getGasLimit(state), @@ -53,6 +51,7 @@ function mapStateToProps (state) { gasTotal: getGasTotal(state), network: getCurrentNetwork(state), primaryCurrency: getPrimaryCurrency(state), + recentBlocks: getRecentBlocks(state), selectedAddress: getSelectedAddress(state), selectedToken: getSelectedToken(state), tokenBalance: getTokenBalance(state), @@ -64,16 +63,18 @@ function mapStateToProps (state) { function mapDispatchToProps (dispatch) { return { updateAndSetGasTotal: ({ - data, + blockGasLimit, editingTransactionId, gasLimit, gasPrice, + recentBlocks, selectedAddress, selectedToken, + to, + value, }) => { - console.log(`editingTransactionId`, editingTransactionId) !editingTransactionId - ? dispatch(updateGasTotal({ selectedAddress, selectedToken, data })) + ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, blockGasLimit, to, value })) : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) }, updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { diff --git a/ui/app/components/send_/send.selectors.js b/ui/app/components/send_/send.selectors.js index c5ae1ab7f..7e7cfe2e9 100644 --- a/ui/app/components/send_/send.selectors.js +++ b/ui/app/components/send_/send.selectors.js @@ -3,12 +3,16 @@ const abi = require('human-standard-token-abi') const { multiplyCurrencies, } = require('../../conversion-util') +const { + estimateGasPriceFromRecentBlocks, +} = require('./send.utils') const selectors = { accountsWithSendEtherInfoSelector, // autoAddToBetaUI, getAddressBook, getAmountConversionRate, + getBlockGasLimit, getConversionRate, getConvertedCurrency, getCurrentAccountWithSendEtherInfo, @@ -18,8 +22,10 @@ const selectors = { getForceGasMin, getGasLimit, getGasPrice, + getGasPriceFromRecentBlocks, getGasTotal, getPrimaryCurrency, + getRecentBlocks, getSelectedAccount, getSelectedAddress, getSelectedIdentity, @@ -84,6 +90,10 @@ function getAmountConversionRate (state) { : getConversionRate(state) } +function getBlockGasLimit (state) { + return state.metamask.currentBlockGasLimit +} + function getConversionRate (state) { return state.metamask.conversionRate } @@ -124,6 +134,10 @@ function getGasPrice (state) { return state.metamask.send.gasPrice } +function getGasPriceFromRecentBlocks (state) { + return estimateGasPriceFromRecentBlocks(state.metamask.recentBlocks) +} + function getGasTotal (state) { return state.metamask.send.gasTotal } @@ -133,6 +147,10 @@ function getPrimaryCurrency (state) { return selectedToken && selectedToken.symbol } +function getRecentBlocks (state) { + return state.metamask.recentBlocks +} + function getSelectedAccount (state) { const accounts = state.metamask.accounts const selectedAddress = getSelectedAddress(state) diff --git a/ui/app/components/send_/send.utils.js b/ui/app/components/send_/send.utils.js index a35a55bf8..67699be77 100644 --- a/ui/app/components/send_/send.utils.js +++ b/ui/app/components/send_/send.utils.js @@ -12,16 +12,21 @@ const { INSUFFICIENT_FUNDS_ERROR, INSUFFICIENT_TOKENS_ERROR, NEGATIVE_ETH_ERROR, + ONE_GWEI_IN_WEI_HEX, + SIMPLE_GAS_COST, + TOKEN_TRANSFER_FUNCTION_SIGNATURE, } = require('./send.constants') const abi = require('ethereumjs-abi') +const ethUtil = require('ethereumjs-util') module.exports = { calcGasTotal, + calcTokenBalance, doesAmountErrorRequireUpdate, + estimateGas, + estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, - getParamsForGasEstimate, - calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, } @@ -139,23 +144,6 @@ function getAmountErrorObject ({ 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) + '' @@ -178,11 +166,74 @@ function doesAmountErrorRequireUpdate ({ 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 - console.log(`abi.rawEncode`, abi.rawEncode) - return Array.prototype.map.call( - abi.rawEncode(['address', 'uint256'], [selectedAddress, '0x0']), + return TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( + abi.rawEncode(['address', 'uint256'], [toAddress, ethUtil.addHexPrefix(amount)]), x => ('00' + x.toString(16)).slice(-2) ).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)] +} diff --git a/ui/app/components/send_/tests/send-component.test.js b/ui/app/components/send_/tests/send-component.test.js index c82edd971..4e33d8f63 100644 --- a/ui/app/components/send_/tests/send-component.test.js +++ b/ui/app/components/send_/tests/send-component.test.js @@ -32,8 +32,8 @@ describe('Send Component', function () { wrapper = shallow(<SendTransactionScreen amount={'mockAmount'} amountConversionRate={'mockAmountConversionRate'} + blockGasLimit={'mockBlockGasLimit'} conversionRate={10} - data={'mockData'} editingTransactionId={'mockEditingTransactionId'} from={ { address: 'mockAddress', balance: 'mockBalance' } } gasLimit={'mockGasLimit'} @@ -42,6 +42,7 @@ describe('Send Component', function () { history={{ mockProp: 'history-abc'}} network={'3'} primaryCurrency={'mockPrimaryCurrency'} + recentBlocks={['mockBlock']} selectedAddress={'mockSelectedAddress'} selectedToken={'mockSelectedToken'} tokenBalance={'mockTokenBalance'} @@ -207,15 +208,24 @@ describe('Send Component', function () { assert.deepEqual( propsMethodSpies.updateAndSetGasTotal.getCall(0).args[0], { - data: 'mockData', + blockGasLimit: 'mockBlockGasLimit', editingTransactionId: 'mockEditingTransactionId', gasLimit: 'mockGasLimit', gasPrice: 'mockGasPrice', + recentBlocks: ['mockBlock'], selectedAddress: 'mockSelectedAddress', 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', () => { diff --git a/ui/app/components/send_/tests/send-container.test.js b/ui/app/components/send_/tests/send-container.test.js index 7b6ca1f7b..056aad148 100644 --- a/ui/app/components/send_/tests/send-container.test.js +++ b/ui/app/components/send_/tests/send-container.test.js @@ -7,7 +7,7 @@ let mapDispatchToProps const actionSpies = { updateSendTokenBalance: sinon.spy(), - updateGasTotal: sinon.spy(), + updateGasData: sinon.spy(), setGasTotal: sinon.spy(), } const duckActionSpies = { @@ -26,12 +26,14 @@ proxyquire('../send.container.js', { 'recompose': { compose: (arg1, arg2) => () => arg2() }, './send.selectors': { getAmountConversionRate: (s) => `mockAmountConversionRate:${s}`, + getBlockGasLimit: (s) => `mockBlockGasLimit:${s}`, getConversionRate: (s) => `mockConversionRate:${s}`, getCurrentNetwork: (s) => `mockNetwork:${s}`, getGasLimit: (s) => `mockGasLimit:${s}`, getGasPrice: (s) => `mockGasPrice:${s}`, getGasTotal: (s) => `mockGasTotal:${s}`, getPrimaryCurrency: (s) => `mockPrimaryCurrency:${s}`, + getRecentBlocks: (s) => `mockRecentBlocks:${s}`, getSelectedAddress: (s) => `mockSelectedAddress:${s}`, getSelectedToken: (s) => `mockSelectedToken:${s}`, getSelectedTokenContract: (s) => `mockTokenContract:${s}`, @@ -45,7 +47,6 @@ proxyquire('../send.container.js', { '../../ducks/send.duck': duckActionSpies, './send.utils.js': { calcGasTotal: (gasLimit, gasPrice) => gasLimit + gasPrice, - generateTokenTransferData: (a, b) => `mockData:${a + b}`, }, }) @@ -57,8 +58,8 @@ describe('send container', () => { assert.deepEqual(mapStateToProps('mockState'), { amount: 'mockAmount:mockState', amountConversionRate: 'mockAmountConversionRate:mockState', + blockGasLimit: 'mockBlockGasLimit:mockState', conversionRate: 'mockConversionRate:mockState', - data: 'mockData:mockSelectedAddress:mockStatemockSelectedToken:mockState', editingTransactionId: 'mockEditingTransactionId:mockState', from: 'mockFrom:mockState', gasLimit: 'mockGasLimit:mockState', @@ -66,6 +67,7 @@ describe('send container', () => { gasTotal: 'mockGasTotal:mockState', network: 'mockNetwork:mockState', primaryCurrency: 'mockPrimaryCurrency:mockState', + recentBlocks: 'mockRecentBlocks:mockState', selectedAddress: 'mockSelectedAddress:mockState', selectedToken: 'mockSelectedToken:mockState', tokenBalance: 'mockTokenBalance:mockState', @@ -87,12 +89,15 @@ describe('send container', () => { describe('updateAndSetGasTotal()', () => { const mockProps = { - data: '0x1', + blockGasLimit: 'mockBlockGasLimit', editingTransactionId: '0x2', gasLimit: '0x3', gasPrice: '0x4', + recentBlocks: ['mockBlock'], selectedAddress: '0x4', selectedToken: { address: '0x1' }, + to: 'mockTo', + value: 'mockValue', } 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', () => { - const { selectedAddress, selectedToken, data } = mockProps + it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { + const { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } = mockProps mapDispatchToPropsObject.updateAndSetGasTotal( - Object.assign(mockProps, {editingTransactionId: false}) + Object.assign({}, mockProps, {editingTransactionId: false}) ) assert(dispatchSpy.calledOnce) assert.deepEqual( - actionSpies.updateGasTotal.getCall(0).args[0], - { selectedAddress, selectedToken, data } + actionSpies.updateGasData.getCall(0).args[0], + { selectedAddress, selectedToken, recentBlocks, blockGasLimit, to, value } ) }) }) diff --git a/ui/app/components/send_/tests/send-selectors-test-data.js b/ui/app/components/send_/tests/send-selectors-test-data.js index ecfe9022f..8f9c19314 100644 --- a/ui/app/components/send_/tests/send-selectors-test-data.js +++ b/ui/app/components/send_/tests/send-selectors-test-data.js @@ -22,6 +22,7 @@ module.exports = { 'name': 'Send Account 4', }, }, + 'currentBlockGasLimit': '0x4c1878', 'currentCurrency': 'USD', 'conversionRate': 1200.88200327, 'conversionDate': 1489013762, @@ -198,6 +199,7 @@ module.exports = { }, }, 'currentLocale': 'en', + recentBlocks: ['mockBlock1', 'mockBlock2', 'mockBlock3'], }, 'appState': { 'menuOpen': false, diff --git a/ui/app/components/send_/tests/send-selectors.test.js b/ui/app/components/send_/tests/send-selectors.test.js index 977fe2a47..152af8059 100644 --- a/ui/app/components/send_/tests/send-selectors.test.js +++ b/ui/app/components/send_/tests/send-selectors.test.js @@ -5,6 +5,7 @@ const { accountsWithSendEtherInfoSelector, // autoAddToBetaUI, getAddressBook, + getBlockGasLimit, getAmountConversionRate, getConversionRate, getConvertedCurrency, @@ -17,6 +18,7 @@ const { getGasPrice, getGasTotal, getPrimaryCurrency, + getRecentBlocks, getSelectedAccount, getSelectedAddress, 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()', () => { it('should return the eth conversion rate', () => { 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()', () => { it('should return the currently selected account', () => { assert.deepEqual( diff --git a/ui/app/components/send_/tests/send-utils.test.js b/ui/app/components/send_/tests/send-utils.test.js index 4d471bcc1..b3f6372ef 100644 --- a/ui/app/components/send_/tests/send-utils.test.js +++ b/ui/app/components/send_/tests/send-utils.test.js @@ -1,6 +1,14 @@ import assert from 'assert' import sinon from 'sinon' import proxyquire from 'proxyquire' +import { + ONE_GWEI_IN_WEI_HEX, + SIMPLE_GAS_COST, +} from '../send.constants' +const { + addCurrencies, + subtractCurrencies, +} = require('../../../conversion-util') const { INSUFFICIENT_FUNDS_ERROR, @@ -11,7 +19,7 @@ const stubs = { addCurrencies: sinon.stub().callsFake((a, b, obj) => a + b), conversionUtil: sinon.stub().callsFake((val, obj) => parseInt(val, 16)), conversionGTE: sinon.stub().callsFake((obj1, obj2) => obj1.value > obj2.value), - 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), rawEncode: sinon.stub().returns([16, 1100]), } @@ -31,10 +39,11 @@ const sendUtils = proxyquire('../send.utils.js', { const { calcGasTotal, + estimateGas, doesAmountErrorRequireUpdate, + estimateGasPriceFromRecentBlocks, generateTokenTransferData, getAmountErrorObject, - getParamsForGasEstimate, calcTokenBalance, isBalanceSufficient, isTokenBalanceSufficient, @@ -45,7 +54,7 @@ describe('send utils', () => { describe('calcGasTotal()', () => { it('should call multiplyCurrencies with the correct params and return the multiplyCurrencies return', () => { const result = calcGasTotal(12, 15) - assert.equal(result, 180) + assert.equal(result, '12x15') const call_ = stubs.multiplyCurrencies.getCall(0).args assert.deepEqual( call_, @@ -97,11 +106,23 @@ describe('send utils', () => { describe('generateTokenTransferData()', () => { 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', () => { - 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()', () => { it('should return the calculated token blance', () => { 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') + }) + }) }) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index d484ed16d..100402d95 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -11,7 +11,8 @@ * @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.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 * @returns {(number | string | BN)} * @@ -38,6 +39,7 @@ const BIG_NUMBER_GWEI_MULTIPLIER = new BigNumber('1000000000') // Individual Setters const convert = R.invoker(1, 'times') 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 decToBigNumberViaString = n => R.pipe(String, toBigNumber['dec']) @@ -104,6 +106,7 @@ const converter = R.pipe( whenPredSetWithPropAndSetter(fromAndToCurrencyPropsNotEqual, 'conversionRate', convert), whenPropApplySetterMap('toDenomination', toSpecifiedDenomination), whenPredSetWithPropAndSetter(R.prop('numberOfDecimals'), 'numberOfDecimals', round), + whenPredSetWithPropAndSetter(R.prop('roundDown'), 'roundDown', roundDown), whenPropApplySetterMap('toNumericBase', baseChange), R.view(R.lensProp('value')) ) |