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

Merge pull request #3489 from MetaMask/retry-tx-refractor

Retry tx refractor
This commit is contained in:
Thomas Huang 2018-03-20 09:59:21 -07:00 committed by GitHub
commit 1079d57f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 485 additions and 86 deletions

View File

@ -2,6 +2,7 @@
## Current Master
- MetaMask will no longer allow nonces to be specified by the dapp
- Add ability for internationalization.
- Will now throw an error if the `to` field in txParams is not valid.
- Will strip null values from the `to` field.

View File

@ -6,7 +6,6 @@ const EthQuery = require('ethjs-query')
const TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker')
/*
@ -92,8 +91,8 @@ module.exports = class TransactionController extends EventEmitter {
this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning')
})
this.pendingTxTracker.on('tx:confirmed', (txId) => this._markNonceDuplicatesDropped(txId))
this.pendingTxTracker.on('tx:failed', this.txStateManager.setTxStatusFailed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:confirmed', this.txStateManager.setTxStatusConfirmed.bind(this.txStateManager))
this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber
@ -186,14 +185,7 @@ module.exports = class TransactionController extends EventEmitter {
// validate
await this.txGasUtil.validateTxParams(txParams)
// construct txMeta
const txMeta = {
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
txParams: txParams,
loadingDefaults: true,
}
const txMeta = this.txStateManager.generateTxMeta({txParams})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
// add default tx params
@ -215,7 +207,6 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams
// ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
txMeta.nonceSpecified = Boolean(txParams.nonce)
let gasPrice = txParams.gasPrice
if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.gasPrice()
@ -226,11 +217,17 @@ module.exports = class TransactionController extends EventEmitter {
return await this.txGasUtil.analyzeGasUsage(txMeta)
}
async retryTransaction (txId) {
this.txStateManager.setTxStatusUnapproved(txId)
const txMeta = this.txStateManager.getTx(txId)
txMeta.lastGasPrice = txMeta.txParams.gasPrice
this.txStateManager.updateTx(txMeta, 'retryTransaction: manual retry')
async retryTransaction (originalTxId) {
const originalTxMeta = this.txStateManager.getTx(originalTxId)
const lastGasPrice = originalTxMeta.txParams.gasPrice
const txMeta = this.txStateManager.generateTxMeta({
txParams: originalTxMeta.txParams,
lastGasPrice,
loadingDefaults: false,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
return txMeta
}
async updateTransaction (txMeta) {
@ -253,11 +250,9 @@ module.exports = class TransactionController extends EventEmitter {
// wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams
const nonce = txMeta.nonceSpecified ? txMeta.txParams.nonce : nonceLock.nextNonce
if (nonce > nonceLock.nextNonce) {
const message = `Specified nonce may not be larger than account's next valid nonce.`
throw new Error(message)
}
// if txMeta has lastGasPrice then it is a retry at same nonce with higher
// gas price transaction and their for the nonce should not be calculated
const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16))
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
@ -314,6 +309,22 @@ module.exports = class TransactionController extends EventEmitter {
// PRIVATE METHODS
//
_markNonceDuplicatesDropped (txId) {
this.txStateManager.setTxStatusConfirmed(txId)
// get the confirmed transactions nonce and from address
const txMeta = this.txStateManager.getTx(txId)
const { nonce, from } = txMeta.txParams
const sameNonceTxs = this.txStateManager.getFilteredTxList({nonce, from})
if (!sameNonceTxs.length) return
// mark all same nonce transactions as dropped and give i a replacedBy hash
sameNonceTxs.forEach((otherTxMeta) => {
if (otherTxMeta.id === txId) return
otherTxMeta.replacedBy = txMeta.hash
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:confirmed reference to confirmed txHash with same nonce')
this.txStateManager.setTxStatusDropped(otherTxMeta.id)
})
}
_updateMemstore () {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({

View File

@ -1,9 +1,21 @@
const extend = require('xtend')
const EventEmitter = require('events')
const ObservableStore = require('obs-store')
const createId = require('./random-id')
const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper')
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// - `'dropped'` the tx nonce was already used
module.exports = class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) {
super()
@ -16,6 +28,16 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork
}
generateTxMeta (opts) {
return extend({
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
loadingDefaults: true,
}, opts)
}
// Returns the number of txs for the current network.
getTxCount () {
return this.getTxList().length
@ -164,16 +186,6 @@ module.exports = class TransactionStateManager extends EventEmitter {
})
}
// STATUS METHODS
// statuses:
// - `'unapproved'` the user has not responded
// - `'rejected'` the user has responded no!
// - `'approved'` the user has approved the tx
// - `'signed'` the tx is signed
// - `'submitted'` the tx is sent to a server
// - `'confirmed'` the tx has been included in a block.
// - `'failed'` the tx failed for some reason, included on tx data.
// get::set status
// should return the status of the tx.
@ -202,7 +214,11 @@ module.exports = class TransactionStateManager extends EventEmitter {
}
// should update the status of the tx to 'submitted'.
// and add a time stamp for when it was called
setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
this._setTxStatus(txId, 'submitted')
}
@ -211,6 +227,12 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'confirmed')
}
// should update the status dropped
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
}
setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId)
txMeta.err = {

View File

@ -116,7 +116,7 @@
"send": {
"gasLimit": "0xea60",
"gasPrice": "0xba43b7400",
"gasTotal": "0xb451dc41b578",
"gasTotal": "0xaa87bee538000",
"tokenBalance": null,
"from": {
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",

View File

@ -116,7 +116,7 @@
"send": {
"gasLimit": "0xea60",
"gasPrice": "0xba43b7400",
"gasTotal": "0xb451dc41b578",
"gasTotal": "0xaa87bee538000",
"tokenBalance": null,
"from": {
"address": "0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb",

View File

@ -29,9 +29,16 @@ function TransactionListItem () {
}
TransactionListItem.prototype.showRetryButton = function () {
const { transaction = {} } = this.props
const { status, time } = transaction
return status === 'submitted' && Date.now() - time > 30000
const { transaction = {}, transactions } = this.props
const { status, submittedTime, txParams } = transaction
const currentNonce = txParams.nonce
const currentNonceTxs = transactions.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[0]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce
&& lastSubmittedTxWithCurrentNonce.id === transaction.id
return currentTxIsLatestWithNonce && Date.now() - submittedTime > 30000
}
TransactionListItem.prototype.render = function () {
@ -201,6 +208,11 @@ function formatDate (date) {
function renderErrorOrWarning (transaction) {
const { status, err, warning } = transaction
// show dropped
if (status === 'dropped') {
return h('span.dropped', ' (Dropped)')
}
// show rejected
if (status === 'rejected') {
return h('span.error', ' (Rejected)')

View File

@ -62,7 +62,7 @@ TransactionList.prototype.render = function () {
}
return h(TransactionListItem, {
transaction, i, network, key,
conversionRate,
conversionRate, transactions,
showTx: (txId) => {
this.props.viewPendingTx(txId)
},

View File

@ -247,6 +247,10 @@ app sections
color: #FFAE00;
}
.dropped {
color: #6195ED;
}
.lock {
width: 50px;
height: 50px;

View File

@ -93,7 +93,7 @@ async function runSendFlowTest(assert, done) {
'send gas field should show estimated gas total converted to USD'
)
const sendGasOpenCustomizeModalButton = await queryAsync($, '.send-v2__sliders-icon-container')
const sendGasOpenCustomizeModalButton = await queryAsync($, '.sliders-icon-container')
sendGasOpenCustomizeModalButton[0].click()
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas')
@ -135,9 +135,9 @@ async function runSendFlowTest(assert, done) {
assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRows = await queryAsync($, '.confirm-screen-rows')
const confirmScreenGas = confirmScreenRows.find('.confirm-screen-row-info')[2]
assert.equal(confirmScreenGas.textContent, '3.6 USD', 'confirm screen should show correct gas')
const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[3]
const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0]
assert.equal(confirmScreenGas.textContent, '3.60 USD', 'confirm screen should show correct gas')
const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2]
assert.equal(confirmScreenTotal.textContent, '2405.36 USD', 'confirm screen should show correct total')
const confirmScreenBackButton = await queryAsync($, '.confirm-screen-back-button')

View File

@ -392,6 +392,49 @@ describe('Transaction Controller', function () {
})
})
describe('#retryTransaction', function () {
it('should create a new txMeta with the same txParams as the original one', function (done) {
let txParams = {
nonce: '0x00',
from: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
to: '0xB09d8505E1F4EF1CeA089D47094f5DD3464083d4',
data: '0x0',
}
txController.txStateManager._saveTxList([
{ id: 1, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams },
])
txController.retryTransaction(1)
.then((txMeta) => {
assert.equal(txMeta.txParams.nonce, txParams.nonce, 'nonce should be the same')
assert.equal(txMeta.txParams.from, txParams.from, 'from should be the same')
assert.equal(txMeta.txParams.to, txParams.to, 'to should be the same')
assert.equal(txMeta.txParams.data, txParams.data, 'data should be the same')
assert.ok(('lastGasPrice' in txMeta), 'should have the key `lastGasPrice`')
assert.equal(txController.txStateManager.getTxList().length, 2)
done()
}).catch(done)
})
})
describe('#_markNonceDuplicatesDropped', function () {
it('should mark all nonce duplicates as dropped without marking the confirmed transaction as dropped', function () {
txController.txStateManager._saveTxList([
{ id: 1, status: 'confirmed', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 2, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 3, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 4, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 6, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
{ id: 7, status: 'submitted', metamaskNetworkId: currentNetworkId, history: [{}], txParams: { nonce: '0x01' } },
])
txController._markNonceDuplicatesDropped(1)
const confirmedTx = txController.txStateManager.getTx(1)
const droppedTxs = txController.txStateManager.getFilteredTxList({ nonce: '0x01', status: 'dropped' })
assert.equal(confirmedTx.status, 'confirmed', 'the confirmedTx should remain confirmed')
assert.equal(droppedTxs.length, 6, 'their should be 6 dropped txs')
})
})
describe('#getPendingTransactions', function () {
beforeEach(function () {
@ -401,7 +444,7 @@ describe('Transaction Controller', function () {
{ id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 5, status: 'submitted', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 6, status: 'confimed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 6, status: 'confirmed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} },
])
})

View File

@ -1278,8 +1278,10 @@ function retryTransaction (txId) {
if (err) {
return dispatch(actions.displayWarning(err.message))
}
const { selectedAddressTxList } = newState
const { id: newTxId } = selectedAddressTxList[selectedAddressTxList.length - 1]
dispatch(actions.updateMetamaskState(newState))
dispatch(actions.viewPendingTx(txId))
dispatch(actions.viewPendingTx(newTxId))
})
}
}

View File

@ -22,12 +22,14 @@ const {
conversionUtil,
multiplyCurrencies,
conversionGreaterThan,
conversionMax,
subtractCurrencies,
} = require('../../conversion-util')
const {
getGasPrice,
getGasLimit,
getForceGasMin,
conversionRateSelector,
getSendAmount,
getSelectedToken,
@ -45,6 +47,7 @@ function mapStateToProps (state) {
return {
gasPrice: getGasPrice(state),
gasLimit: getGasLimit(state),
forceGasMin: getForceGasMin(state),
conversionRate,
amount: getSendAmount(state),
maxModeOn: getSendMaxModeState(state),
@ -115,9 +118,9 @@ CustomizeGasModal.prototype.save = function (gasPrice, gasLimit, gasTotal) {
updateSendAmount(maxAmount)
}
updateGasPrice(gasPrice)
updateGasLimit(gasLimit)
updateGasTotal(gasTotal)
updateGasPrice(ethUtil.addHexPrefix(gasPrice))
updateGasLimit(ethUtil.addHexPrefix(gasLimit))
updateGasTotal(ethUtil.addHexPrefix(gasTotal))
hideModal()
}
@ -218,7 +221,7 @@ CustomizeGasModal.prototype.convertAndSetGasPrice = function (newGasPrice) {
}
CustomizeGasModal.prototype.render = function () {
const { hideModal } = this.props
const { hideModal, forceGasMin } = this.props
const { gasPrice, gasLimit, gasTotal, error, priceSigZeros, priceSigDec } = this.state
let convertedGasPrice = conversionUtil(gasPrice, {
@ -230,6 +233,22 @@ CustomizeGasModal.prototype.render = function () {
convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}`
let newGasPrice = gasPrice
if (forceGasMin) {
const convertedMinPrice = conversionUtil(forceGasMin, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
})
convertedGasPrice = conversionMax(
{ value: convertedMinPrice, fromNumericBase: 'dec' },
{ value: convertedGasPrice, fromNumericBase: 'dec' }
)
newGasPrice = conversionMax(
{ value: gasPrice, fromNumericBase: 'hex' },
{ value: forceGasMin, fromNumericBase: 'hex' }
)
}
const convertedGasLimit = conversionUtil(gasLimit, {
fromNumericBase: 'hex',
toNumericBase: 'dec',
@ -252,7 +271,7 @@ CustomizeGasModal.prototype.render = function () {
h(GasModalCard, {
value: convertedGasPrice,
min: MIN_GAS_PRICE_GWEI,
min: forceGasMin || MIN_GAS_PRICE_GWEI,
// max: 1000,
step: multiplyCurrencies(MIN_GAS_PRICE_GWEI, 10),
onChange: value => this.convertAndSetGasPrice(value),
@ -288,7 +307,7 @@ CustomizeGasModal.prototype.render = function () {
}, [t('cancel')]),
h(`div.send-v2__customize-gas__save${error ? '__error' : ''}.allcaps`, {
onClick: () => !error && this.save(gasPrice, gasLimit, gasTotal),
onClick: () => !error && this.save(newGasPrice, gasLimit, gasTotal),
}, [t('save')]),
]),

View File

@ -8,7 +8,12 @@ const Identicon = require('../identicon')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const hexToBn = require('../../../../app/scripts/lib/hex-to-bn')
const { conversionUtil, addCurrencies } = require('../../conversion-util')
const {
conversionUtil,
addCurrencies,
multiplyCurrencies,
} = require('../../conversion-util')
const GasFeeDisplay = require('../send/gas-fee-display-v2')
const t = require('../../../i18n')
const { MIN_GAS_PRICE_HEX } = require('../send/send-constants')
@ -44,6 +49,7 @@ function mapDispatchToProps (dispatch) {
to,
value: amount,
} = txParams
dispatch(actions.updateSend({
gasLimit,
gasPrice,
@ -56,6 +62,29 @@ function mapDispatchToProps (dispatch) {
dispatch(actions.showSendPage())
},
cancelTransaction: ({ id }) => dispatch(actions.cancelTx({ id })),
showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
const { id, txParams, lastGasPrice } = txMeta
const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
fromDenomination: 'WEI',
}))
}
dispatch(actions.updateSend({
gasLimit: sendGasLimit || txGasLimit,
gasPrice: sendGasPrice || txGasPrice,
editingTransactionId: id,
gasTotal: sendGasTotal,
forceGasMin,
}))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
},
}
}
@ -140,6 +169,7 @@ ConfirmSendEther.prototype.getGasFee = function () {
return {
FIAT,
ETH,
gasFeeInHex: txFeeBn.toString(16),
}
}
@ -147,7 +177,7 @@ ConfirmSendEther.prototype.getData = function () {
const { identities } = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH } = this.getGasFee()
const { FIAT: gasFeeInFIAT, ETH: gasFeeInETH, gasFeeInHex } = this.getGasFee()
const { FIAT: amountInFIAT, ETH: amountInETH } = this.getAmount()
const totalInFIAT = addCurrencies(gasFeeInFIAT, amountInFIAT, {
@ -175,11 +205,20 @@ ConfirmSendEther.prototype.getData = function () {
amountInETH,
totalInFIAT,
totalInETH,
gasFeeInHex,
}
}
ConfirmSendEther.prototype.render = function () {
const { editTransaction, currentCurrency, clearSend } = this.props
const {
editTransaction,
currentCurrency,
clearSend,
conversionRate,
currentCurrency: convertedCurrency,
showCustomizeGasModal,
send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
} = this.props
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
@ -193,13 +232,17 @@ ConfirmSendEther.prototype.render = function () {
name: toName,
},
memo,
gasFeeInFIAT,
gasFeeInETH,
gasFeeInHex,
amountInFIAT,
totalInFIAT,
totalInETH,
} = this.getData()
const title = txMeta.lastGasPrice ? 'Reprice Transaction' : 'Confirm'
const subtitle = txMeta.lastGasPrice
? 'Increase your gas fee to attempt to overwrite and speed up your transaction'
: 'Please review your transaction.'
// This is from the latest master
// It handles some of the errors that we are not currently handling
// Leaving as comments fo reference
@ -218,11 +261,11 @@ ConfirmSendEther.prototype.render = function () {
// Main Send token Card
h('div.page-container', [
h('div.page-container__header', [
h('button.confirm-screen-back-button', {
!txMeta.lastGasPrice && h('button.confirm-screen-back-button', {
onClick: () => editTransaction(txMeta),
}, 'Edit'),
h('div.page-container__title', 'Confirm'),
h('div.page-container__subtitle', `Please review your transaction.`),
h('div.page-container__title', title),
h('div.page-container__subtitle', subtitle),
]),
h('.page-container__content', [
h('div.flex-row.flex-center.confirm-screen-identicons', [
@ -286,13 +329,15 @@ ConfirmSendEther.prototype.render = function () {
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ t('gasFee') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${gasFeeInFIAT} ${currentCurrency.toUpperCase()}`),
h('div.confirm-screen-row-detail', `${gasFeeInETH} ETH`),
h(GasFeeDisplay, {
gasTotal: gasTotal || gasFeeInHex,
conversionRate,
convertedCurrency,
onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
}),
]),
]),
h('section.flex-row.flex-center.confirm-screen-row.confirm-screen-total-box ', [
h('div.confirm-screen-section-column', [
h('span.confirm-screen-label', [ t('total') + ' ' ]),
@ -450,6 +495,27 @@ ConfirmSendEther.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {
gasPrice: txGasPrice,
gas: txGasLimit,
},
} = txData
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
}))
}
txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
txData.txParams.gas = sendGasLimit || txGasLimit
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}

View File

@ -9,6 +9,7 @@ const actions = require('../../actions')
const t = require('../../../i18n')
const clone = require('clone')
const Identicon = require('../identicon')
const GasFeeDisplay = require('../send/gas-fee-display-v2.js')
const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN
const {
@ -89,6 +90,39 @@ function mapDispatchToProps (dispatch, ownProps) {
}))
dispatch(actions.showSendTokenPage())
},
showCustomizeGasModal: (txMeta, sendGasLimit, sendGasPrice, sendGasTotal) => {
const { id, txParams, lastGasPrice } = txMeta
const { gas: txGasLimit, gasPrice: txGasPrice } = txParams
const tokenData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { params = [] } = tokenData
const { value: to } = params[0] || {}
const { value: tokenAmountInDec } = params[1] || {}
const tokenAmountInHex = conversionUtil(tokenAmountInDec, {
fromNumericBase: 'dec',
toNumericBase: 'hex',
})
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
fromDenomination: 'WEI',
}))
}
dispatch(actions.updateSend({
gasLimit: sendGasLimit || txGasLimit,
gasPrice: sendGasPrice || txGasPrice,
editingTransactionId: id,
gasTotal: sendGasTotal,
to,
amount: tokenAmountInHex,
forceGasMin,
}))
dispatch(actions.showModal({ name: 'CUSTOMIZE_GAS' }))
},
}
}
@ -188,6 +222,7 @@ ConfirmSendToken.prototype.getGasFee = function () {
token: tokenExchangeRate
? tokenGas
: null,
gasFeeInHex: gasTotal.toString(16),
}
}
@ -240,19 +275,25 @@ ConfirmSendToken.prototype.renderHeroAmount = function () {
}
ConfirmSendToken.prototype.renderGasFee = function () {
const { token: { symbol }, currentCurrency } = this.props
const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee()
const {
currentCurrency: convertedCurrency,
conversionRate,
send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
showCustomizeGasModal,
} = this.props
const txMeta = this.gatherTxMeta()
const { gasFeeInHex } = this.getGasFee()
return (
h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ t('gasFee') ]),
h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency}`),
h(
'div.confirm-screen-row-detail',
tokenGas ? `${tokenGas} ${symbol}` : `${ethGas} ETH`
),
h(GasFeeDisplay, {
gasTotal: gasTotal || gasFeeInHex,
conversionRate,
convertedCurrency,
onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
}),
]),
])
)
@ -308,16 +349,21 @@ ConfirmSendToken.prototype.render = function () {
this.inputs = []
const title = txMeta.lastGasPrice ? 'Reprice Transaction' : t('confirm')
const subtitle = txMeta.lastGasPrice
? 'Increase your gas fee to attempt to overwrite and speed up your transaction'
: t('pleaseReviewTransaction')
return (
h('div.confirm-screen-container.confirm-send-token', [
// Main Send token Card
h('div.page-container', [
h('div.page-container__header', [
h('button.confirm-screen-back-button', {
!txMeta.lastGasPrice && h('button.confirm-screen-back-button', {
onClick: () => editTransaction(txMeta),
}, t('edit')),
h('div.page-container__title', t('confirm')),
h('div.page-container__subtitle', t('pleaseReviewTransaction')),
h('div.page-container__title', title),
h('div.page-container__subtitle', subtitle),
]),
h('.page-container__content', [
h('div.flex-row.flex-center.confirm-screen-identicons', [
@ -441,6 +487,27 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
const state = this.state
const txData = clone(state.txData) || clone(props.txData)
const { gasPrice: sendGasPrice, gas: sendGasLimit } = props.send
const {
lastGasPrice,
txParams: {
gasPrice: txGasPrice,
gas: txGasLimit,
},
} = txData
let forceGasMin
if (lastGasPrice) {
forceGasMin = ethUtil.addHexPrefix(multiplyCurrencies(lastGasPrice, 1.1, {
multiplicandBase: 16,
multiplierBase: 10,
toNumericBase: 'hex',
}))
}
txData.txParams.gasPrice = sendGasPrice || forceGasMin || txGasPrice
txData.txParams.gas = sendGasLimit || txGasLimit
// log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return txData
}

View File

@ -36,11 +36,11 @@ GasFeeDisplay.prototype.render = function () {
? h('div..currency-display.currency-display--message', 'Set with the gas price customizer.')
: h('div.currency-display', t('loading')),
h('button.send-v2__sliders-icon-container', {
h('button.sliders-icon-container', {
onClick,
disabled: !gasTotal && !gasLoadingError,
}, [
h('i.fa.fa-sliders.send-v2__sliders-icon'),
h('i.fa.fa-sliders.sliders-icon'),
]),
])

View File

@ -9,19 +9,28 @@ abiDecoder.addABI(abi)
const Identicon = require('./identicon')
const contractMap = require('eth-contract-metadata')
const actions = require('../actions')
const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
const { calcTokenAmount } = require('../token-util')
const { getCurrentCurrency } = require('../selectors')
const t = require('../../i18n')
module.exports = connect(mapStateToProps)(TxListItem)
module.exports = connect(mapStateToProps, mapDispatchToProps)(TxListItem)
function mapStateToProps (state) {
return {
tokens: state.metamask.tokens,
currentCurrency: getCurrentCurrency(state),
tokenExchangeRates: state.metamask.tokenExchangeRates,
selectedAddressTxList: state.metamask.selectedAddressTxList,
}
}
function mapDispatchToProps (dispatch) {
return {
setSelectedToken: tokenAddress => dispatch(actions.setSelectedToken(tokenAddress)),
retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
}
}
@ -32,6 +41,7 @@ function TxListItem () {
this.state = {
total: null,
fiatTotal: null,
isTokenTx: null,
}
}
@ -40,12 +50,13 @@ TxListItem.prototype.componentDidMount = async function () {
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { name: txDataName } = decodedData || {}
const isTokenTx = txDataName === 'transfer'
const { total, fiatTotal } = txDataName === 'transfer'
const { total, fiatTotal } = isTokenTx
? await this.getSendTokenTotal()
: this.getSendEtherTotal()
this.setState({ total, fiatTotal })
this.setState({ total, fiatTotal, isTokenTx })
}
TxListItem.prototype.getAddressText = function () {
@ -168,22 +179,49 @@ TxListItem.prototype.getSendTokenTotal = async function () {
}
}
TxListItem.prototype.showRetryButton = function () {
const {
transactionSubmittedTime,
selectedAddressTxList,
transactionId,
txParams,
} = this.props
const currentNonce = txParams.nonce
const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce)
const currentNonceSubmittedTxs = currentNonceTxs.filter(tx => tx.status === 'submitted')
const lastSubmittedTxWithCurrentNonce = currentNonceSubmittedTxs[currentNonceSubmittedTxs.length - 1]
const currentTxIsLatestWithNonce = lastSubmittedTxWithCurrentNonce
&& lastSubmittedTxWithCurrentNonce.id === transactionId
return currentTxIsLatestWithNonce && Date.now() - transactionSubmittedTime > 30000
}
TxListItem.prototype.setSelectedToken = function (tokenAddress) {
this.props.setSelectedToken(tokenAddress)
}
TxListItem.prototype.resubmit = function () {
const { transactionId } = this.props
this.props.retryTransaction(transactionId)
}
TxListItem.prototype.render = function () {
const {
transactionStatus,
transactionAmount,
onClick,
transActionId,
transactionId,
dateString,
address,
className,
txParams,
} = this.props
const { total, fiatTotal } = this.state
const { total, fiatTotal, isTokenTx } = this.state
const showFiatTotal = transactionAmount !== '0x0' && fiatTotal
return h(`div${className || ''}`, {
key: transActionId,
onClick: () => onClick && onClick(transActionId),
key: transactionId,
onClick: () => onClick && onClick(transactionId),
}, [
h(`div.flex-column.tx-list-item-wrapper`, {}, [
@ -224,6 +262,7 @@ TxListItem.prototype.render = function () {
className: classnames('tx-list-status', {
'tx-list-status--rejected': transactionStatus === 'rejected',
'tx-list-status--failed': transactionStatus === 'failed',
'tx-list-status--dropped': transactionStatus === 'dropped',
}),
},
transactionStatus,
@ -241,6 +280,23 @@ TxListItem.prototype.render = function () {
]),
]),
this.showRetryButton() && h('div.tx-list-item-retry-container', [
h('span.tx-list-item-retry-copy', 'Taking too long?'),
h('span.tx-list-item-retry-link', {
onClick: (event) => {
event.stopPropagation()
if (isTokenTx) {
this.setSelectedToken(txParams.to)
}
this.resubmit()
},
}, 'Increase the gas price on your transaction'),
]),
]), // holding on icon from design
])
}

View File

@ -75,9 +75,10 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
address: transaction.txParams.to,
transactionStatus: transaction.status,
transactionAmount: transaction.txParams.value,
transActionId: transaction.id,
transactionId: transaction.id,
transactionHash: transaction.hash,
transactionNetworkId: transaction.metamaskNetworkId,
transactionSubmittedTime: transaction.submittedTime,
}
const {
@ -85,29 +86,31 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa
transactionStatus,
transactionAmount,
dateString,
transActionId,
transactionId,
transactionHash,
transactionNetworkId,
transactionSubmittedTime,
} = props
const { showConfTxPage } = this.props
const opts = {
key: transActionId || transactionHash,
key: transactionId || transactionHash,
txParams: transaction.txParams,
transactionStatus,
transActionId,
transactionId,
dateString,
address,
transactionAmount,
transactionHash,
conversionRate,
tokenInfoGetter: this.tokenInfoGetter,
transactionSubmittedTime,
}
const isUnapproved = transactionStatus === 'unapproved'
if (isUnapproved) {
opts.onClick = () => showConfTxPage({id: transActionId})
opts.onClick = () => showConfTxPage({id: transactionId})
opts.transactionStatus = t('Not Started')
} else if (transactionHash) {
opts.onClick = () => this.view(transactionHash, transactionNetworkId)

View File

@ -187,6 +187,18 @@ const conversionGreaterThan = (
return firstValue.gt(secondValue)
}
const conversionMax = (
{ ...firstProps },
{ ...secondProps },
) => {
const firstIsGreater = conversionGreaterThan(
{ ...firstProps },
{ ...secondProps }
)
return firstIsGreater ? firstProps.value : secondProps.value
}
const conversionGTE = (
{ ...firstProps },
{ ...secondProps },
@ -216,6 +228,7 @@ module.exports = {
conversionGreaterThan,
conversionGTE,
conversionLTE,
conversionMax,
toNegative,
subtractCurrencies,
}

View File

@ -660,6 +660,7 @@
&__gas-fee-display {
width: 100%;
position: relative;
.currency-display--message {
padding: 8px 38px 8px 10px;
@ -891,3 +892,23 @@
}
}
}
.sliders-icon-container {
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 24px;
border: 1px solid $curious-blue;
border-radius: 4px;
background-color: $white;
position: absolute;
right: 15px;
top: 14px;
cursor: pointer;
font-size: 1em;
}
.sliders-icon {
color: $curious-blue;
}

View File

@ -126,6 +126,53 @@
}
}
.tx-list-item-retry-container {
background: #d1edff;
width: 100%;
border-radius: 4px;
font-size: 0.8em;
display: flex;
justify-content: center;
margin-left: 44px;
width: calc(100% - 44px);
@media screen and (min-width: 576px) and (max-width: 679px) {
flex-flow: column;
align-items: center;
}
@media screen and (min-width: 380px) and (max-width: 575px) {
flex-flow: row;
}
@media screen and (max-width: 379px) {
flex-flow: column;
align-items: center;
}
}
.tx-list-item-retry-copy {
font-family: Roboto;
}
.tx-list-item-retry-link {
text-decoration: underline;
margin-left: 6px;
cursor: pointer;
@media screen and (min-width: 576px) and (max-width: 679px) {
margin-left: 0px;
}
@media screen and (min-width: 380px) and (max-width: 575px) {
margin-left: 6px;
}
@media screen and (max-width: 379px) {
margin-left: 0px;
}
}
.tx-list-date {
color: $dusty-gray;
font-size: 12px;
@ -190,6 +237,10 @@
.tx-list-status--failed {
color: $monzo;
}
.tx-list-status--dropped {
opacity: 0.5;
}
}
.tx-list-item {

View File

@ -46,6 +46,7 @@ $manatee: #93949d;
$spindle: #c7ddec;
$mid-gray: #5b5d67;
$cape-cod: #38393a;
$onahau: #d1edff;
$java: #29b6af;
$wild-strawberry: #ff4a8d;
$cornflower-blue: #7057ff;

View File

@ -38,6 +38,7 @@ function reduceMetamask (state, action) {
errors: {},
maxModeOn: false,
editingTransactionId: null,
forceGasMin: null,
},
coinOptions: {},
useBlockie: false,
@ -297,6 +298,7 @@ function reduceMetamask (state, action) {
memo: '',
errors: {},
editingTransactionId: null,
forceGasMin: null,
},
})

View File

@ -18,6 +18,7 @@ const selectors = {
getCurrentAccountWithSendEtherInfo,
getGasPrice,
getGasLimit,
getForceGasMin,
getAddressBook,
getSendFrom,
getCurrentCurrency,
@ -130,6 +131,10 @@ function getGasLimit (state) {
return state.metamask.send.gasLimit
}
function getForceGasMin (state) {
return state.metamask.send.forceGasMin
}
function getSendFrom (state) {
return state.metamask.send.from
}