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 ## Current Master
- MetaMask will no longer allow nonces to be specified by the dapp
- Add ability for internationalization. - Add ability for internationalization.
- Will now throw an error if the `to` field in txParams is not valid. - Will now throw an error if the `to` field in txParams is not valid.
- Will strip null values from the `to` field. - 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 TransactionStateManager = require('../lib/tx-state-manager')
const TxGasUtil = require('../lib/tx-gas-utils') const TxGasUtil = require('../lib/tx-gas-utils')
const PendingTransactionTracker = require('../lib/pending-tx-tracker') const PendingTransactionTracker = require('../lib/pending-tx-tracker')
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker') const NonceTracker = require('../lib/nonce-tracker')
/* /*
@ -92,8 +91,8 @@ module.exports = class TransactionController extends EventEmitter {
this.pendingTxTracker.on('tx:warning', (txMeta) => { this.pendingTxTracker.on('tx:warning', (txMeta) => {
this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:warning') 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: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) => { this.pendingTxTracker.on('tx:block-update', (txMeta, latestBlockNumber) => {
if (!txMeta.firstRetryBlockNumber) { if (!txMeta.firstRetryBlockNumber) {
txMeta.firstRetryBlockNumber = latestBlockNumber txMeta.firstRetryBlockNumber = latestBlockNumber
@ -186,14 +185,7 @@ module.exports = class TransactionController extends EventEmitter {
// validate // validate
await this.txGasUtil.validateTxParams(txParams) await this.txGasUtil.validateTxParams(txParams)
// construct txMeta // construct txMeta
const txMeta = { const txMeta = this.txStateManager.generateTxMeta({txParams})
id: createId(),
time: (new Date()).getTime(),
status: 'unapproved',
metamaskNetworkId: this.getNetwork(),
txParams: txParams,
loadingDefaults: true,
}
this.addTx(txMeta) this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta) this.emit('newUnapprovedTx', txMeta)
// add default tx params // add default tx params
@ -215,7 +207,6 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams const txParams = txMeta.txParams
// ensure value // ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice) txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
txMeta.nonceSpecified = Boolean(txParams.nonce)
let gasPrice = txParams.gasPrice let gasPrice = txParams.gasPrice
if (!gasPrice) { if (!gasPrice) {
gasPrice = this.getGasPrice ? this.getGasPrice() : await this.query.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) return await this.txGasUtil.analyzeGasUsage(txMeta)
} }
async retryTransaction (txId) { async retryTransaction (originalTxId) {
this.txStateManager.setTxStatusUnapproved(txId) const originalTxMeta = this.txStateManager.getTx(originalTxId)
const txMeta = this.txStateManager.getTx(txId) const lastGasPrice = originalTxMeta.txParams.gasPrice
txMeta.lastGasPrice = txMeta.txParams.gasPrice const txMeta = this.txStateManager.generateTxMeta({
this.txStateManager.updateTx(txMeta, 'retryTransaction: manual retry') txParams: originalTxMeta.txParams,
lastGasPrice,
loadingDefaults: false,
})
this.addTx(txMeta)
this.emit('newUnapprovedTx', txMeta)
return txMeta
} }
async updateTransaction (txMeta) { async updateTransaction (txMeta) {
@ -253,11 +250,9 @@ module.exports = class TransactionController extends EventEmitter {
// wait for a nonce // wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress) nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams // add nonce to txParams
const nonce = txMeta.nonceSpecified ? txMeta.txParams.nonce : nonceLock.nextNonce // if txMeta has lastGasPrice then it is a retry at same nonce with higher
if (nonce > nonceLock.nextNonce) { // gas price transaction and their for the nonce should not be calculated
const message = `Specified nonce may not be larger than account's next valid nonce.` const nonce = txMeta.lastGasPrice ? txMeta.txParams.nonce : nonceLock.nextNonce
throw new Error(message)
}
txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16)) txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16))
// add nonce debugging information to txMeta // add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails txMeta.nonceDetails = nonceLock.nonceDetails
@ -314,6 +309,22 @@ module.exports = class TransactionController extends EventEmitter {
// PRIVATE METHODS // 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 () { _updateMemstore () {
const unapprovedTxs = this.txStateManager.getUnapprovedTxList() const unapprovedTxs = this.txStateManager.getUnapprovedTxList()
const selectedAddressTxList = this.txStateManager.getFilteredTxList({ const selectedAddressTxList = this.txStateManager.getFilteredTxList({

@ -1,9 +1,21 @@
const extend = require('xtend') const extend = require('xtend')
const EventEmitter = require('events') const EventEmitter = require('events')
const ObservableStore = require('obs-store') const ObservableStore = require('obs-store')
const createId = require('./random-id')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const txStateHistoryHelper = require('./tx-state-history-helper') 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 { module.exports = class TransactionStateManager extends EventEmitter {
constructor ({ initState, txHistoryLimit, getNetwork }) { constructor ({ initState, txHistoryLimit, getNetwork }) {
super() super()
@ -16,6 +28,16 @@ module.exports = class TransactionStateManager extends EventEmitter {
this.getNetwork = getNetwork 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. // Returns the number of txs for the current network.
getTxCount () { getTxCount () {
return this.getTxList().length 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 // get::set status
// should return the status of the tx. // 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'. // should update the status of the tx to 'submitted'.
// and add a time stamp for when it was called
setTxStatusSubmitted (txId) { setTxStatusSubmitted (txId) {
const txMeta = this.getTx(txId)
txMeta.submittedTime = (new Date()).getTime()
this.updateTx(txMeta, 'txStateManager - add submitted time stamp')
this._setTxStatus(txId, 'submitted') this._setTxStatus(txId, 'submitted')
} }
@ -211,6 +227,12 @@ module.exports = class TransactionStateManager extends EventEmitter {
this._setTxStatus(txId, 'confirmed') this._setTxStatus(txId, 'confirmed')
} }
// should update the status dropped
setTxStatusDropped (txId) {
this._setTxStatus(txId, 'dropped')
}
setTxStatusFailed (txId, err) { setTxStatusFailed (txId, err) {
const txMeta = this.getTx(txId) const txMeta = this.getTx(txId)
txMeta.err = { txMeta.err = {

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

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

@ -29,9 +29,16 @@ function TransactionListItem () {
} }
TransactionListItem.prototype.showRetryButton = function () { TransactionListItem.prototype.showRetryButton = function () {
const { transaction = {} } = this.props const { transaction = {}, transactions } = this.props
const { status, time } = transaction const { status, submittedTime, txParams } = transaction
return status === 'submitted' && Date.now() - time > 30000 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 () { TransactionListItem.prototype.render = function () {
@ -201,6 +208,11 @@ function formatDate (date) {
function renderErrorOrWarning (transaction) { function renderErrorOrWarning (transaction) {
const { status, err, warning } = transaction const { status, err, warning } = transaction
// show dropped
if (status === 'dropped') {
return h('span.dropped', ' (Dropped)')
}
// show rejected // show rejected
if (status === 'rejected') { if (status === 'rejected') {
return h('span.error', ' (Rejected)') return h('span.error', ' (Rejected)')

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

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

@ -93,7 +93,7 @@ async function runSendFlowTest(assert, done) {
'send gas field should show estimated gas total converted to USD' '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() sendGasOpenCustomizeModalButton[0].click()
const customizeGasModal = await queryAsync($, '.send-v2__customize-gas') 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') assert.equal(confirmToName[0].textContent, 'Send Account 3', 'confirm screen should show correct to name')
const confirmScreenRows = await queryAsync($, '.confirm-screen-rows') const confirmScreenRows = await queryAsync($, '.confirm-screen-rows')
const confirmScreenGas = confirmScreenRows.find('.confirm-screen-row-info')[2] const confirmScreenGas = confirmScreenRows.find('.currency-display__converted-value')[0]
assert.equal(confirmScreenGas.textContent, '3.6 USD', 'confirm screen should show correct gas') assert.equal(confirmScreenGas.textContent, '3.60 USD', 'confirm screen should show correct gas')
const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[3] const confirmScreenTotal = confirmScreenRows.find('.confirm-screen-row-info')[2]
assert.equal(confirmScreenTotal.textContent, '2405.36 USD', 'confirm screen should show correct total') assert.equal(confirmScreenTotal.textContent, '2405.36 USD', 'confirm screen should show correct total')
const confirmScreenBackButton = await queryAsync($, '.confirm-screen-back-button') 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 () { describe('#getPendingTransactions', function () {
beforeEach(function () { beforeEach(function () {
@ -401,7 +444,7 @@ describe('Transaction Controller', function () {
{ id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 3, status: 'approved', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} }, { id: 4, status: 'signed', metamaskNetworkId: currentNetworkId, txParams: {} },
{ id: 5, status: 'submitted', 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: {} }, { id: 7, status: 'failed', metamaskNetworkId: currentNetworkId, txParams: {} },
]) ])
}) })

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

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

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

@ -9,6 +9,7 @@ const actions = require('../../actions')
const t = require('../../../i18n') const t = require('../../../i18n')
const clone = require('clone') const clone = require('clone')
const Identicon = require('../identicon') const Identicon = require('../identicon')
const GasFeeDisplay = require('../send/gas-fee-display-v2.js')
const ethUtil = require('ethereumjs-util') const ethUtil = require('ethereumjs-util')
const BN = ethUtil.BN const BN = ethUtil.BN
const { const {
@ -89,6 +90,39 @@ function mapDispatchToProps (dispatch, ownProps) {
})) }))
dispatch(actions.showSendTokenPage()) 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 token: tokenExchangeRate
? tokenGas ? tokenGas
: null, : null,
gasFeeInHex: gasTotal.toString(16),
} }
} }
@ -240,19 +275,25 @@ ConfirmSendToken.prototype.renderHeroAmount = function () {
} }
ConfirmSendToken.prototype.renderGasFee = function () { ConfirmSendToken.prototype.renderGasFee = function () {
const { token: { symbol }, currentCurrency } = this.props const {
const { fiat: fiatGas, token: tokenGas, eth: ethGas } = this.getGasFee() currentCurrency: convertedCurrency,
conversionRate,
send: { gasTotal, gasLimit: sendGasLimit, gasPrice: sendGasPrice },
showCustomizeGasModal,
} = this.props
const txMeta = this.gatherTxMeta()
const { gasFeeInHex } = this.getGasFee()
return ( return (
h('section.flex-row.flex-center.confirm-screen-row', [ h('section.flex-row.flex-center.confirm-screen-row', [
h('span.confirm-screen-label.confirm-screen-section-column', [ t('gasFee') ]), h('span.confirm-screen-label.confirm-screen-section-column', [ t('gasFee') ]),
h('div.confirm-screen-section-column', [ h('div.confirm-screen-section-column', [
h('div.confirm-screen-row-info', `${fiatGas} ${currentCurrency}`), h(GasFeeDisplay, {
gasTotal: gasTotal || gasFeeInHex,
h( conversionRate,
'div.confirm-screen-row-detail', convertedCurrency,
tokenGas ? `${tokenGas} ${symbol}` : `${ethGas} ETH` onClick: () => showCustomizeGasModal(txMeta, sendGasLimit, sendGasPrice, gasTotal),
), }),
]), ]),
]) ])
) )
@ -308,16 +349,21 @@ ConfirmSendToken.prototype.render = function () {
this.inputs = [] 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 ( return (
h('div.confirm-screen-container.confirm-send-token', [ h('div.confirm-screen-container.confirm-send-token', [
// Main Send token Card // Main Send token Card
h('div.page-container', [ h('div.page-container', [
h('div.page-container__header', [ h('div.page-container__header', [
h('button.confirm-screen-back-button', { !txMeta.lastGasPrice && h('button.confirm-screen-back-button', {
onClick: () => editTransaction(txMeta), onClick: () => editTransaction(txMeta),
}, t('edit')), }, t('edit')),
h('div.page-container__title', t('confirm')), h('div.page-container__title', title),
h('div.page-container__subtitle', t('pleaseReviewTransaction')), h('div.page-container__subtitle', subtitle),
]), ]),
h('.page-container__content', [ h('.page-container__content', [
h('div.flex-row.flex-center.confirm-screen-identicons', [ h('div.flex-row.flex-center.confirm-screen-identicons', [
@ -441,6 +487,27 @@ ConfirmSendToken.prototype.gatherTxMeta = function () {
const state = this.state const state = this.state
const txData = clone(state.txData) || clone(props.txData) 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)}`) // log.debug(`UI has defaulted to tx meta ${JSON.stringify(txData)}`)
return 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.currency-display--message', 'Set with the gas price customizer.')
: h('div.currency-display', t('loading')), : h('div.currency-display', t('loading')),
h('button.send-v2__sliders-icon-container', { h('button.sliders-icon-container', {
onClick, onClick,
disabled: !gasTotal && !gasLoadingError, 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 Identicon = require('./identicon')
const contractMap = require('eth-contract-metadata') const contractMap = require('eth-contract-metadata')
const actions = require('../actions')
const { conversionUtil, multiplyCurrencies } = require('../conversion-util') const { conversionUtil, multiplyCurrencies } = require('../conversion-util')
const { calcTokenAmount } = require('../token-util') const { calcTokenAmount } = require('../token-util')
const { getCurrentCurrency } = require('../selectors') const { getCurrentCurrency } = require('../selectors')
const t = require('../../i18n') const t = require('../../i18n')
module.exports = connect(mapStateToProps)(TxListItem) module.exports = connect(mapStateToProps, mapDispatchToProps)(TxListItem)
function mapStateToProps (state) { function mapStateToProps (state) {
return { return {
tokens: state.metamask.tokens, tokens: state.metamask.tokens,
currentCurrency: getCurrentCurrency(state), currentCurrency: getCurrentCurrency(state),
tokenExchangeRates: state.metamask.tokenExchangeRates, 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 = { this.state = {
total: null, total: null,
fiatTotal: null, fiatTotal: null,
isTokenTx: null,
} }
} }
@ -40,12 +50,13 @@ TxListItem.prototype.componentDidMount = async function () {
const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data) const decodedData = txParams.data && abiDecoder.decodeMethod(txParams.data)
const { name: txDataName } = decodedData || {} const { name: txDataName } = decodedData || {}
const isTokenTx = txDataName === 'transfer'
const { total, fiatTotal } = txDataName === 'transfer' const { total, fiatTotal } = isTokenTx
? await this.getSendTokenTotal() ? await this.getSendTokenTotal()
: this.getSendEtherTotal() : this.getSendEtherTotal()
this.setState({ total, fiatTotal }) this.setState({ total, fiatTotal, isTokenTx })
} }
TxListItem.prototype.getAddressText = function () { 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 () { TxListItem.prototype.render = function () {
const { const {
transactionStatus, transactionStatus,
transactionAmount, transactionAmount,
onClick, onClick,
transActionId, transactionId,
dateString, dateString,
address, address,
className, className,
txParams,
} = this.props } = this.props
const { total, fiatTotal } = this.state const { total, fiatTotal, isTokenTx } = this.state
const showFiatTotal = transactionAmount !== '0x0' && fiatTotal const showFiatTotal = transactionAmount !== '0x0' && fiatTotal
return h(`div${className || ''}`, { return h(`div${className || ''}`, {
key: transActionId, key: transactionId,
onClick: () => onClick && onClick(transActionId), onClick: () => onClick && onClick(transactionId),
}, [ }, [
h(`div.flex-column.tx-list-item-wrapper`, {}, [ h(`div.flex-column.tx-list-item-wrapper`, {}, [
@ -224,6 +262,7 @@ TxListItem.prototype.render = function () {
className: classnames('tx-list-status', { className: classnames('tx-list-status', {
'tx-list-status--rejected': transactionStatus === 'rejected', 'tx-list-status--rejected': transactionStatus === 'rejected',
'tx-list-status--failed': transactionStatus === 'failed', 'tx-list-status--failed': transactionStatus === 'failed',
'tx-list-status--dropped': transactionStatus === 'dropped',
}), }),
}, },
transactionStatus, 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 ]), // holding on icon from design
]) ])
} }

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

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

@ -660,6 +660,7 @@
&__gas-fee-display { &__gas-fee-display {
width: 100%; width: 100%;
position: relative;
.currency-display--message { .currency-display--message {
padding: 8px 38px 8px 10px; 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 { .tx-list-date {
color: $dusty-gray; color: $dusty-gray;
font-size: 12px; font-size: 12px;
@ -190,6 +237,10 @@
.tx-list-status--failed { .tx-list-status--failed {
color: $monzo; color: $monzo;
} }
.tx-list-status--dropped {
opacity: 0.5;
}
} }
.tx-list-item { .tx-list-item {

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

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

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

Loading…
Cancel
Save