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

Retry tx refractor
feature/default_network_editable
Thomas Huang 7 years ago committed by GitHub
commit 1079d57f8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 53
      app/scripts/controllers/transactions.js
  3. 42
      app/scripts/lib/tx-state-manager.js
  4. 2
      development/states/confirm-new-ui.json
  5. 2
      development/states/send-edit.json
  6. 18
      old-ui/app/components/transaction-list-item.js
  7. 2
      old-ui/app/components/transaction-list.js
  8. 4
      old-ui/app/css/index.css
  9. 8
      test/integration/lib/send-new-ui.js
  10. 45
      test/unit/tx-controller-test.js
  11. 4
      ui/app/actions.js
  12. 31
      ui/app/components/customize-gas-modal/index.js
  13. 90
      ui/app/components/pending-tx/confirm-send-ether.js
  14. 89
      ui/app/components/pending-tx/confirm-send-token.js
  15. 4
      ui/app/components/send/gas-fee-display-v2.js
  16. 70
      ui/app/components/tx-list-item.js
  17. 13
      ui/app/components/tx-list.js
  18. 13
      ui/app/conversion-util.js
  19. 21
      ui/app/css/itcss/components/send.scss
  20. 51
      ui/app/css/itcss/components/transaction-list.scss
  21. 1
      ui/app/css/itcss/settings/variables.scss
  22. 2
      ui/app/reducers/metamask.js
  23. 5
      ui/app/selectors.js

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

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

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

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

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

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

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

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

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

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

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

@ -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')]),
]),

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

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

@ -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'),
]),
])

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

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

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

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

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

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

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

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

Loading…
Cancel
Save