diff --git a/ui/app/actions.js b/ui/app/actions.js index 4f902a6a2..3e050c38c 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -1228,8 +1228,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)) }) } } diff --git a/ui/app/components/customize-gas-modal/index.js b/ui/app/components/customize-gas-modal/index.js index 826d2cd4b..34a93d6c6 100644 --- a/ui/app/components/customize-gas-modal/index.js +++ b/ui/app/components/customize-gas-modal/index.js @@ -21,12 +21,14 @@ const { conversionUtil, multiplyCurrencies, conversionGreaterThan, + conversionMax, subtractCurrencies, } = require('../../conversion-util') const { getGasPrice, getGasLimit, + getForceGasMin, conversionRateSelector, getSendAmount, getSelectedToken, @@ -44,6 +46,7 @@ function mapStateToProps (state) { return { gasPrice: getGasPrice(state), gasLimit: getGasLimit(state), + forceGasMin: getForceGasMin(state), conversionRate, amount: getSendAmount(state), maxModeOn: getSendMaxModeState(state), @@ -217,7 +220,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, { @@ -229,6 +232,17 @@ CustomizeGasModal.prototype.render = function () { convertedGasPrice += convertedGasPrice.match(/[.]/) ? priceSigZeros : `${priceSigDec}${priceSigZeros}` + if (forceGasMin) { + const convertedMinPrice = conversionUtil(forceGasMin, { + fromNumericBase: 'hex', + toNumericBase: 'dec', + }) + convertedGasPrice = conversionMax( + { value: convertedMinPrice, fromNumericBase: 'dec' }, + { value: convertedGasPrice, fromNumericBase: 'dec' } + ) + } + const convertedGasLimit = conversionUtil(gasLimit, { fromNumericBase: 'hex', toNumericBase: 'dec', @@ -251,7 +265,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), diff --git a/ui/app/components/pending-tx/confirm-send-ether.js b/ui/app/components/pending-tx/confirm-send-ether.js index 3f8d9c28f..a6e00d922 100644 --- a/ui/app/components/pending-tx/confirm-send-ether.js +++ b/ui/app/components/pending-tx/confirm-send-ether.js @@ -36,13 +36,29 @@ function mapDispatchToProps (dispatch) { return { clearSend: () => dispatch(actions.clearSend()), editTransaction: txMeta => { - const { id, txParams } = txMeta + const { id, txParams, lastGasPrice } = txMeta const { gas: gasLimit, gasPrice, to, value: amount, } = txParams + + let forceGasMin + let nonce + if (lastGasPrice) { + const stripped = ethUtil.stripHexPrefix(lastGasPrice) + forceGasMin = ethUtil.addHexPrefix(addCurrencies(stripped, MIN_GAS_PRICE_HEX, { + aBase: 16, + bBase: 16, + toNumericBase: 'hex', + fromDenomination: 'WEI', + toDenomination: 'GWEI', + })) + + nonce = txParams.nonce + } + dispatch(actions.updateSend({ gasLimit, gasPrice, @@ -51,6 +67,8 @@ function mapDispatchToProps (dispatch) { amount, errors: { to: null, amount: null }, editingTransactionId: id, + forceGasMin, + nonce, })) dispatch(actions.showSendPage()) }, diff --git a/ui/app/components/tx-list-item.js b/ui/app/components/tx-list-item.js index 574d10e14..123f723be 100644 --- a/ui/app/components/tx-list-item.js +++ b/ui/app/components/tx-list-item.js @@ -9,18 +9,26 @@ 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') -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 { + retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)), } } @@ -167,12 +175,32 @@ TxListItem.prototype.getSendTokenTotal = async function () { } } +TxListItem.prototype.showRetryButton = function () { + const { + transactionStatus, + transactionTime, + selectedAddressTxList, + transactionId, + txParams, + } = this.props + const currentNonce = txParams.nonce + const currentNonceTxs = selectedAddressTxList.filter(tx => tx.txParams.nonce === currentNonce) + const isLastWithNonce = currentNonceTxs[currentNonceTxs.length - 1].id === transactionId + + return transactionStatus === 'submitted' && isLastWithNonce && Date.now() - transactionTime > 5000 +} + +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, @@ -181,8 +209,8 @@ TxListItem.prototype.render = function () { 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`, {}, [ @@ -241,12 +269,17 @@ TxListItem.prototype.render = function () { ]), ]), - h('div.tx-list-item-retry-container', [ + 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', 'Increase the gas on your transaction'), - + h('span.tx-list-item-retry-link', { + onClick: (event) => { + event.stopPropagation() + this.resubmit() + }, + }, 'Increase the gas on your transaction'), + ]), ]), // holding on icon from design diff --git a/ui/app/components/tx-list.js b/ui/app/components/tx-list.js index 1729e6a6f..30f816d58 100644 --- a/ui/app/components/tx-list.js +++ b/ui/app/components/tx-list.js @@ -74,9 +74,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, + transactionTime: transaction.time, } const { @@ -84,29 +85,31 @@ TxList.prototype.renderTransactionListItem = function (transaction, conversionRa transactionStatus, transactionAmount, dateString, - transActionId, + transactionId, transactionHash, transactionNetworkId, + transactionTime, } = 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, + transactionTime, } const isUnapproved = transactionStatus === 'unapproved' if (isUnapproved) { - opts.onClick = () => showConfTxPage({id: transActionId}) + opts.onClick = () => showConfTxPage({id: transactionId}) opts.transactionStatus = 'Not Started' } else if (transactionHash) { opts.onClick = () => this.view(transactionHash, transactionNetworkId) diff --git a/ui/app/conversion-util.js b/ui/app/conversion-util.js index ee42ebea1..d484ed16d 100644 --- a/ui/app/conversion-util.js +++ b/ui/app/conversion-util.js @@ -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, } diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index cddcd0c1f..999939b89 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -38,6 +38,8 @@ function reduceMetamask (state, action) { errors: {}, maxModeOn: false, editingTransactionId: null, + forceGasMin: null, + nonce: null, }, coinOptions: {}, useBlockie: false, @@ -298,6 +300,8 @@ function reduceMetamask (state, action) { memo: '', errors: {}, editingTransactionId: null, + forceGasMin: null, + nonce: null, }, }) diff --git a/ui/app/selectors.js b/ui/app/selectors.js index 5d2635775..d37c26f7e 100644 --- a/ui/app/selectors.js +++ b/ui/app/selectors.js @@ -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 } diff --git a/ui/app/send-v2.js b/ui/app/send-v2.js index 1d67150e3..edb6bc41d 100644 --- a/ui/app/send-v2.js +++ b/ui/app/send-v2.js @@ -543,6 +543,7 @@ SendTransactionScreen.prototype.getEditedTx = function () { selectedToken, editingTransactionId, unapprovedTxs, + nonce, } = this.props const editingTx = { @@ -554,6 +555,10 @@ SendTransactionScreen.prototype.getEditedTx = function () { }, } + if (nonce) { + editingTx.txParams.nonce = ethUtil.addHexPrefix(nonce) + } + if (selectedToken) { const data = TOKEN_TRANSFER_FUNCTION_SIGNATURE + Array.prototype.map.call( ethAbi.rawEncode(['address', 'uint256'], [to, ethUtil.addHexPrefix(amount)]),