1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

Auto update gas estimate when to changes.

This commit is contained in:
Dan 2018-05-23 14:13:25 -02:30
parent 3d597cd1d2
commit 0f20fce9b7
15 changed files with 123 additions and 64 deletions

View File

@ -383,6 +383,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),
@ -921,6 +923,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
//============================================================================= //=============================================================================

View File

@ -731,16 +731,21 @@ function updateGasData ({
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to, to,
value,
}) { }) {
const estimatedGasPrice = estimateGasPriceFromRecentBlocks(recentBlocks)
return (dispatch) => { return (dispatch) => {
return Promise.all([ return Promise.all([
Promise.resolve(estimateGasPriceFromRecentBlocks(recentBlocks)), Promise.resolve(estimatedGasPrice),
estimateGas({ estimateGas({
estimateGasMethod: background.estimateGas,
blockGasLimit, blockGasLimit,
data, data,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to, to,
value,
gasPrice: estimatedGasPrice,
}), }),
]) ])
.then(([gasPrice, gas]) => { .then(([gasPrice, gas]) => {

View File

@ -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>

View File

@ -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 () {

View File

@ -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))
}, },
} }
} }

View File

@ -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 }

View File

@ -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', () => {

View File

@ -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'
) )
}) })
}) })

View File

@ -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)

View File

@ -39,8 +39,9 @@ export default class SendTransactionScreen extends PersistentForm {
updateSendTokenBalance: PropTypes.func, updateSendTokenBalance: PropTypes.func,
}; };
updateGas () { updateGas ({ to } = {}) {
const { const {
amount,
blockGasLimit, blockGasLimit,
data, data,
editingTransactionId, editingTransactionId,
@ -61,6 +62,8 @@ export default class SendTransactionScreen extends PersistentForm {
recentBlocks, recentBlocks,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to: to && to.toLowerCase(),
value: amount,
}) })
} }
@ -147,7 +150,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>
) )

View File

@ -76,9 +76,11 @@ function mapDispatchToProps (dispatch) {
recentBlocks, recentBlocks,
selectedAddress, selectedAddress,
selectedToken, selectedToken,
to,
value,
}) => { }) => {
!editingTransactionId !editingTransactionId
? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, data, blockGasLimit })) ? dispatch(updateGasData({ recentBlocks, selectedAddress, selectedToken, data, blockGasLimit, to, value }))
: dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice))) : dispatch(setGasTotal(calcGasTotal(gasLimit, gasPrice)))
}, },
updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => { updateSendTokenBalance: ({ selectedToken, tokenContract, address }) => {

View File

@ -15,8 +15,8 @@ const {
ONE_GWEI_IN_WEI_HEX, ONE_GWEI_IN_WEI_HEX,
SIMPLE_GAS_COST, SIMPLE_GAS_COST,
} = require('./send.constants') } = require('./send.constants')
const EthQuery = require('ethjs-query')
const abi = require('ethereumjs-abi') const abi = require('ethereumjs-abi')
const ethUtil = require('ethereumjs-util')
module.exports = { module.exports = {
calcGasTotal, calcGasTotal,
@ -165,40 +165,44 @@ function doesAmountErrorRequireUpdate ({
return amountErrorRequiresUpdate return amountErrorRequiresUpdate
} }
async function estimateGas ({ selectedAddress, selectedToken, data, blockGasLimit, to }) { async function estimateGas ({ selectedAddress, selectedToken, data, blockGasLimit, to, value, gasPrice, estimateGasMethod }) {
const ethQuery = new EthQuery(global.ethereumProvider)
const { symbol } = selectedToken || {} const { symbol } = selectedToken || {}
const estimatedGasParams = { from: selectedAddress } const paramsForGasEstimate = { from: selectedAddress, value, gasPrice }
if (symbol) { if (symbol) {
Object.assign(estimatedGasParams, { value: '0x0' }) Object.assign(paramsForGasEstimate, { value: '0x0' })
} }
if (data) { if (data) {
Object.assign(estimatedGasParams, { data }) Object.assign(paramsForGasEstimate, { data })
} }
// if recipient has no code, gas is 21k max: // if recipient has no code, gas is 21k max:
const hasRecipient = Boolean(to) const hasRecipient = Boolean(to)
let code let code
if (hasRecipient) code = await ethQuery.getCode(to) if (hasRecipient) code = await global.eth.getCode(to)
if (hasRecipient && (!code || code === '0x')) { if (hasRecipient && (!code || code === '0x')) {
return SIMPLE_GAS_COST return SIMPLE_GAS_COST
} }
estimatedGasParams.to = to paramsForGasEstimate.to = to
// if not, fall back to block gasLimit // if not, fall back to block gasLimit
estimatedGasParams.gas = multiplyCurrencies(blockGasLimit, 0.95, { paramsForGasEstimate.gas = ethUtil.addHexPrefix(multiplyCurrencies(blockGasLimit, 0.95, {
multiplicandBase: 16, multiplicandBase: 16,
multiplierBase: 10, multiplierBase: 10,
roundDown: '0', roundDown: '0',
toNumericBase: 'hex', toNumericBase: 'hex',
}) }))
// run tx // run tx
const estimatedGas = await ethQuery.estimateGas(estimatedGasParams) return new Promise((resolve, reject) => {
return estimatedGas.toString(16) estimateGasMethod(paramsForGasEstimate, (err, estimatedGas) => {
if (err) {
reject(err)
}
resolve(estimatedGas.toString(16))
})
})
} }
function generateTokenTransferData (selectedAddress, selectedToken) { function generateTokenTransferData (selectedAddress, selectedToken) {

View File

@ -217,9 +217,17 @@ describe.only('Send Component', function () {
recentBlocks: ['mockBlock'], 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', () => {

View File

@ -99,6 +99,8 @@ describe('send container', () => {
recentBlocks: ['mockBlock'], 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', () => {
@ -111,14 +113,14 @@ describe('send container', () => {
}) })
it('should dispatch an updateGasData action when editingTransactionId is falsy', () => { it('should dispatch an updateGasData action when editingTransactionId is falsy', () => {
const { selectedAddress, selectedToken, data, recentBlocks, blockGasLimit } = mockProps const { selectedAddress, selectedToken, data, 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.updateGasData.getCall(0).args[0], actionSpies.updateGasData.getCall(0).args[0],
{ selectedAddress, selectedToken, data, recentBlocks, blockGasLimit } { selectedAddress, selectedToken, data, recentBlocks, blockGasLimit, to, value }
) )
}) })
}) })

View File

@ -24,14 +24,6 @@ const stubs = {
rawEncode: sinon.stub().returns([16, 1100]), rawEncode: sinon.stub().returns([16, 1100]),
} }
const EthQuery = function () {}
EthQuery.prototype.estimateGas = sinon.stub().callsFake(
(data) => Promise.resolve({ toString: (n) => `mockToString:${n}` })
)
EthQuery.prototype.getCode = sinon.stub().callsFake(
(address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
)
const sendUtils = proxyquire('../send.utils.js', { const sendUtils = proxyquire('../send.utils.js', {
'../../conversion-util': { '../../conversion-util': {
addCurrencies: stubs.addCurrencies, addCurrencies: stubs.addCurrencies,
@ -43,7 +35,6 @@ const sendUtils = proxyquire('../send.utils.js', {
'ethereumjs-abi': { 'ethereumjs-abi': {
rawEncode: stubs.rawEncode, rawEncode: stubs.rawEncode,
}, },
'ethjs-query': EthQuery,
}) })
const { const {
@ -249,6 +240,9 @@ describe('send utils', () => {
blockGasLimit: '0x64', blockGasLimit: '0x64',
selectedAddress: 'mockAddress', selectedAddress: 'mockAddress',
to: '0xisContract', to: '0xisContract',
estimateGasMethod: sinon.stub().callsFake(
(data, cb) => cb(null, { toString: (n) => `mockToString:${n}` })
),
} }
const baseExpectedCall = { const baseExpectedCall = {
from: 'mockAddress', from: 'mockAddress',
@ -256,53 +250,51 @@ describe('send utils', () => {
to: '0xisContract', to: '0xisContract',
} }
beforeEach(() => {
global.eth = {
getCode: sinon.stub().callsFake(
(address) => Promise.resolve(address.match(/isContract/) ? 'not-0x' : '0x')
),
}
})
afterEach(() => { afterEach(() => {
EthQuery.prototype.estimateGas.resetHistory() baseMockParams.estimateGasMethod.resetHistory()
EthQuery.prototype.getCode.resetHistory() global.eth.getCode.resetHistory()
}) })
it('should call ethQuery.estimateGas with the expected params', async () => { it('should call ethQuery.estimateGas with the expected params', async () => {
const result = await estimateGas(baseMockParams) const result = await estimateGas(baseMockParams)
assert.equal(EthQuery.prototype.estimateGas.callCount, 1) assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual( assert.deepEqual(
EthQuery.prototype.estimateGas.getCall(0).args[0], baseMockParams.estimateGasMethod.getCall(0).args[0],
baseExpectedCall Object.assign({ gasPrice: undefined, value: undefined }, baseExpectedCall)
) )
assert.equal(result, 'mockToString:16') assert.equal(result, 'mockToString:16')
}) })
it('should call ethQuery.estimateGas with a value of 0x0 if the passed selectedToken has a symbol', async () => { it('should call ethQuery.estimateGas with a value of 0x0 if the passed selectedToken has a symbol', async () => {
const result = await estimateGas(Object.assign({ selectedToken: { symbol: true } }, baseMockParams)) const result = await estimateGas(Object.assign({ selectedToken: { symbol: true } }, baseMockParams))
assert.equal(EthQuery.prototype.estimateGas.callCount, 1) assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual( assert.deepEqual(
EthQuery.prototype.estimateGas.getCall(0).args[0], baseMockParams.estimateGasMethod.getCall(0).args[0],
Object.assign({ value: '0x0' }, baseExpectedCall) Object.assign({ gasPrice: undefined, value: '0x0' }, baseExpectedCall)
) )
assert.equal(result, 'mockToString:16') assert.equal(result, 'mockToString:16')
}) })
it('should call ethQuery.estimateGas with data if data is passed', async () => { it('should call ethQuery.estimateGas with data if data is passed', async () => {
const result = await estimateGas(Object.assign({ data: 'mockData' }, baseMockParams)) const result = await estimateGas(Object.assign({ data: 'mockData' }, baseMockParams))
assert.equal(EthQuery.prototype.estimateGas.callCount, 1) assert.equal(baseMockParams.estimateGasMethod.callCount, 1)
assert.deepEqual( assert.deepEqual(
EthQuery.prototype.estimateGas.getCall(0).args[0], baseMockParams.estimateGasMethod.getCall(0).args[0],
Object.assign({ data: 'mockData' }, baseExpectedCall) Object.assign({ gasPrice: undefined, value: undefined, data: 'mockData' }, baseExpectedCall)
)
assert.equal(result, 'mockToString:16')
})
it('should call ethQuery.estimateGas with data if data is passed', async () => {
const result = await estimateGas(Object.assign({ data: 'mockData' }, baseMockParams))
assert.equal(EthQuery.prototype.estimateGas.callCount, 1)
assert.deepEqual(
EthQuery.prototype.estimateGas.getCall(0).args[0],
Object.assign({ data: 'mockData' }, baseExpectedCall)
) )
assert.equal(result, 'mockToString:16') assert.equal(result, 'mockToString:16')
}) })
it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => { it(`should return ${SIMPLE_GAS_COST} if ethQuery.getCode does not return '0x'`, async () => {
assert.equal(EthQuery.prototype.estimateGas.callCount, 0) assert.equal(baseMockParams.estimateGasMethod.callCount, 0)
const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' })) const result = await estimateGas(Object.assign({}, baseMockParams, { to: '0x123' }))
assert.equal(result, SIMPLE_GAS_COST) assert.equal(result, SIMPLE_GAS_COST)
}) })