aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md14
-rw-r--r--app/manifest.json2
-rw-r--r--app/scripts/controllers/network.js61
-rw-r--r--app/scripts/controllers/transactions.js35
-rw-r--r--app/scripts/lib/account-tracker.js2
-rw-r--r--app/scripts/lib/pending-tx-tracker.js18
-rw-r--r--app/scripts/lib/tx-state-manager.js6
-rw-r--r--app/scripts/metamask-controller.js9
-rw-r--r--development/states/pending-tx.json739
-rw-r--r--package.json3
-rw-r--r--test/unit/pending-tx-test.js87
-rw-r--r--ui/app/actions.js14
-rw-r--r--ui/app/components/pending-tx.js12
-rw-r--r--ui/app/components/token-list.js5
-rw-r--r--ui/app/components/transaction-list-item.js127
-rw-r--r--ui/app/config.js2
-rw-r--r--ui/app/css/index.css4
17 files changed, 1057 insertions, 83 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 009cd5f7c..106881ea5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,20 @@
## Current Master
+- Show tokens that are held that have no balance.
+
+## 3.13.2 2017-12-9
+
+- Reduce new block polling interval to 8000 ms, to ease server load.
+
+## 3.13.1 2017-12-7
+
+- Allow Dapps to specify a transaction nonce, allowing dapps to propose resubmit and force-cancel transactions.
+
+## 3.13.0 2017-12-7
+
+- Allow resubmitting transactions that are taking long to complete.
+
## 3.12.1 2017-11-29
- Fix bug where a user could be shown two different seed phrases.
diff --git a/app/manifest.json b/app/manifest.json
index 4219f3298..a5645bb7c 100644
--- a/app/manifest.json
+++ b/app/manifest.json
@@ -1,7 +1,7 @@
{
"name": "MetaMask",
"short_name": "Metamask",
- "version": "3.12.1",
+ "version": "3.13.2",
"manifest_version": 2,
"author": "https://metamask.io",
"description": "Ethereum Browser Extension",
diff --git a/app/scripts/controllers/network.js b/app/scripts/controllers/network.js
index 045bfcc5d..65d58008a 100644
--- a/app/scripts/controllers/network.js
+++ b/app/scripts/controllers/network.js
@@ -1,6 +1,7 @@
const assert = require('assert')
const EventEmitter = require('events')
const createMetamaskProvider = require('web3-provider-engine/zero.js')
+const createInfuraProvider = require('eth-json-rpc-infura/src/createProvider')
const ObservableStore = require('obs-store')
const ComposedStore = require('obs-store/lib/composed')
const extend = require('xtend')
@@ -8,6 +9,7 @@ const EthQuery = require('eth-query')
const createEventEmitterProxy = require('../lib/events-proxy.js')
const RPC_ADDRESS_LIST = require('../config.js').network
const DEFAULT_RPC = RPC_ADDRESS_LIST['rinkeby']
+const INFURA_PROVIDER_TYPES = ['ropsten', 'rinkeby', 'kovan', 'mainnet']
module.exports = class NetworkController extends EventEmitter {
@@ -24,8 +26,13 @@ module.exports = class NetworkController extends EventEmitter {
initializeProvider (_providerParams) {
this._baseProviderParams = _providerParams
- const rpcUrl = this.getCurrentRpcAddress()
- this._configureStandardProvider({ rpcUrl })
+ const { type, rpcTarget } = this.providerStore.getState()
+ // map rpcTarget to rpcUrl
+ const opts = {
+ type,
+ rpcUrl: rpcTarget,
+ }
+ this._configureProvider(opts)
this._proxy.on('block', this._logBlock.bind(this))
this._proxy.on('error', this.verifyNetwork.bind(this))
this.ethQuery = new EthQuery(this._proxy)
@@ -83,7 +90,7 @@ module.exports = class NetworkController extends EventEmitter {
const rpcTarget = this.getRpcAddressForType(type)
assert(rpcTarget, `NetworkController - unknown rpc address for type "${type}"`)
this.providerStore.updateState({ type, rpcTarget })
- this._switchNetwork({ rpcUrl: rpcTarget })
+ this._switchNetwork({ type })
}
getProviderConfig () {
@@ -99,14 +106,54 @@ module.exports = class NetworkController extends EventEmitter {
// Private
//
- _switchNetwork (providerParams) {
+ _switchNetwork (opts) {
this.setNetworkState('loading')
- this._configureStandardProvider(providerParams)
+ this._configureProvider(opts)
this.emit('networkDidChange')
}
- _configureStandardProvider (_providerParams) {
- const providerParams = extend(this._baseProviderParams, _providerParams)
+ _configureProvider (opts) {
+ // type-based rpc endpoints
+ const { type } = opts
+ if (type) {
+ // type-based infura rpc endpoints
+ const isInfura = INFURA_PROVIDER_TYPES.includes(type)
+ opts.rpcUrl = this.getRpcAddressForType(type)
+ if (isInfura) {
+ this._configureInfuraProvider(opts)
+ // other type-based rpc endpoints
+ } else {
+ this._configureStandardProvider(opts)
+ }
+ // url-based rpc endpoints
+ } else {
+ this._configureStandardProvider(opts)
+ }
+ }
+
+ _configureInfuraProvider (opts) {
+ console.log('_configureInfuraProvider', opts)
+ const blockTrackerProvider = createInfuraProvider({
+ network: opts.type,
+ })
+ const providerParams = extend(this._baseProviderParams, {
+ rpcUrl: opts.rpcUrl,
+ engineParams: {
+ pollingInterval: 8000,
+ blockTrackerProvider,
+ },
+ })
+ const provider = createMetamaskProvider(providerParams)
+ this._setProvider(provider)
+ }
+
+ _configureStandardProvider ({ rpcUrl }) {
+ const providerParams = extend(this._baseProviderParams, {
+ rpcUrl,
+ engineParams: {
+ pollingInterval: 8000,
+ },
+ })
const provider = createMetamaskProvider(providerParams)
this._setProvider(provider)
}
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index a861c0342..f95b5e39a 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -72,6 +72,12 @@ module.exports = class TransactionController extends EventEmitter {
})
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
+ this.txStateManager.updateTx(txMeta, 'transactions/pending-tx-tracker#event: tx:block-update')
+ }
+ })
this.pendingTxTracker.on('tx:retry', (txMeta) => {
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
txMeta.retryCount++
@@ -132,18 +138,20 @@ module.exports = class TransactionController extends EventEmitter {
async newUnapprovedTransaction (txParams) {
log.debug(`MetaMaskController newUnapprovedTransaction ${JSON.stringify(txParams)}`)
- const txMeta = await this.addUnapprovedTransaction(txParams)
- this.emit('newUnapprovedTx', txMeta)
+ const initialTxMeta = await this.addUnapprovedTransaction(txParams)
+ this.emit('newUnapprovedTx', initialTxMeta)
// listen for tx completion (success, fail)
return new Promise((resolve, reject) => {
- this.txStateManager.once(`${txMeta.id}:finished`, (completedTx) => {
- switch (completedTx.status) {
+ this.txStateManager.once(`${initialTxMeta.id}:finished`, (finishedTxMeta) => {
+ switch (finishedTxMeta.status) {
case 'submitted':
- return resolve(completedTx.hash)
+ return resolve(finishedTxMeta.hash)
case 'rejected':
return reject(new Error('MetaMask Tx Signature: User denied transaction signature.'))
+ case 'failed':
+ return reject(new Error(finishedTxMeta.err.message))
default:
- return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(completedTx.txParams)}`))
+ return reject(new Error(`MetaMask Tx Signature: Unknown problem: ${JSON.stringify(finishedTxMeta.txParams)}`))
}
})
})
@@ -171,6 +179,7 @@ module.exports = class TransactionController extends EventEmitter {
const txParams = txMeta.txParams
// ensure value
txMeta.gasPriceSpecified = Boolean(txParams.gasPrice)
+ txMeta.nonceSpecified = Boolean(txParams.nonce)
const gasPrice = txParams.gasPrice || await this.query.gasPrice()
txParams.gasPrice = ethUtil.addHexPrefix(gasPrice.toString(16))
txParams.value = txParams.value || '0x0'
@@ -178,6 +187,13 @@ 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 updateAndApproveTransaction (txMeta) {
this.txStateManager.updateTx(txMeta, 'confTx: user approved transaction')
await this.approveTransaction(txMeta.id)
@@ -194,7 +210,12 @@ module.exports = class TransactionController extends EventEmitter {
// wait for a nonce
nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
// add nonce to txParams
- txMeta.txParams.nonce = ethUtil.addHexPrefix(nonceLock.nextNonce.toString(16))
+ 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)
+ }
+ txMeta.txParams.nonce = ethUtil.addHexPrefix(nonce.toString(16))
// add nonce debugging information to txMeta
txMeta.nonceDetails = nonceLock.nonceDetails
this.txStateManager.updateTx(txMeta, 'transactions#approveTransaction')
diff --git a/app/scripts/lib/account-tracker.js b/app/scripts/lib/account-tracker.js
index ce6642150..8c3dd8c71 100644
--- a/app/scripts/lib/account-tracker.js
+++ b/app/scripts/lib/account-tracker.js
@@ -117,8 +117,6 @@ class AccountTracker extends EventEmitter {
const query = this._query
async.parallel({
balance: query.getBalance.bind(query, address),
- nonce: query.getTransactionCount.bind(query, address),
- code: query.getCode.bind(query, address),
}, cb)
}
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
index 0d7c6a92c..dc6e526fd 100644
--- a/app/scripts/lib/pending-tx-tracker.js
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -65,11 +65,11 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}
- resubmitPendingTxs () {
+ resubmitPendingTxs (block) {
const pending = this.getPendingTransactions()
// only try resubmitting if their are transactions to resubmit
if (!pending.length) return
- pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => {
+ pending.forEach((txMeta) => this._resubmitTx(txMeta, block.number).catch((err) => {
/*
Dont marked as failed if the error is a "known" transaction warning
"there is already a transaction with the same sender-nonce
@@ -101,13 +101,25 @@ module.exports = class PendingTransactionTracker extends EventEmitter {
}))
}
- async _resubmitTx (txMeta) {
+ async _resubmitTx (txMeta, latestBlockNumber) {
+ if (!txMeta.firstRetryBlockNumber) {
+ this.emit('tx:block-update', txMeta, latestBlockNumber)
+ }
+
if (Date.now() > txMeta.time + this.retryTimePeriod) {
const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1)
const err = new Error(`Gave up submitting after ${hours} hours.`)
return this.emit('tx:failed', txMeta.id, err)
}
+ const firstRetryBlockNumber = txMeta.firstRetryBlockNumber || latestBlockNumber
+ const txBlockDistance = Number.parseInt(latestBlockNumber, 16) - Number.parseInt(firstRetryBlockNumber, 16)
+
+ const retryCount = txMeta.retryCount || 0
+
+ // Exponential backoff to limit retries at publishing
+ if (txBlockDistance <= Math.pow(2, retryCount) - 1) return
+
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return
diff --git a/app/scripts/lib/tx-state-manager.js b/app/scripts/lib/tx-state-manager.js
index 0fd6bed4b..a8ef39891 100644
--- a/app/scripts/lib/tx-state-manager.js
+++ b/app/scripts/lib/tx-state-manager.js
@@ -187,6 +187,10 @@ module.exports = class TransactionStateManger extends EventEmitter {
this._setTxStatus(txId, 'rejected')
}
+ // should update the status of the tx to 'unapproved'.
+ setTxStatusUnapproved (txId) {
+ this._setTxStatus(txId, 'unapproved')
+ }
// should update the status of the tx to 'approved'.
setTxStatusApproved (txId) {
this._setTxStatus(txId, 'approved')
@@ -236,7 +240,7 @@ module.exports = class TransactionStateManger extends EventEmitter {
txMeta.status = status
this.emit(`${txMeta.id}:${status}`, txId)
this.emit(`tx:status-update`, txId, status)
- if (status === 'submitted' || status === 'rejected') {
+ if (['submitted', 'rejected', 'failed'].includes(status)) {
this.emit(`${txMeta.id}:finished`, txMeta)
}
this.updateTx(txMeta, `txStateManager: setting status to ${status}`)
diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js
index 130ad1471..9d126b416 100644
--- a/app/scripts/metamask-controller.js
+++ b/app/scripts/metamask-controller.js
@@ -363,6 +363,7 @@ module.exports = class MetamaskController extends EventEmitter {
// txController
cancelTransaction: nodeify(txController.cancelTransaction, txController),
updateAndApproveTransaction: nodeify(txController.updateAndApproveTransaction, txController),
+ retryTransaction: nodeify(this.retryTransaction, this),
// messageManager
signMessage: nodeify(this.signMessage, this),
@@ -573,6 +574,14 @@ module.exports = class MetamaskController extends EventEmitter {
//
// Identity Management
//
+ //
+
+ async retryTransaction (txId, cb) {
+ await this.txController.retryTransaction(txId)
+ const state = await this.getState()
+ return state
+ }
+
newUnsignedMessage (msgParams, cb) {
const msgId = this.messageManager.addUnapprovedMessage(msgParams)
diff --git a/development/states/pending-tx.json b/development/states/pending-tx.json
new file mode 100644
index 000000000..bfa93f7ae
--- /dev/null
+++ b/development/states/pending-tx.json
@@ -0,0 +1,739 @@
+{
+ "metamask": {
+ "isInitialized": true,
+ "isUnlocked": true,
+ "isMascara": false,
+ "rpcTarget": "https://rawtestrpc.metamask.io/",
+ "identities": {
+ "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
+ "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "name": "Account 1"
+ }
+ },
+ "unapprovedTxs": {},
+ "noActiveNotices": true,
+ "frequentRpcList": [
+ "http://192.168.1.34:7545/"
+ ],
+ "addressBook": [],
+ "tokenExchangeRates": {},
+ "coinOptions": {},
+ "provider": {
+ "type": "mainnet",
+ "rpcTarget": "https://mainnet.infura.io/metamask"
+ },
+ "network": "1",
+ "accounts": {
+ "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825": {
+ "code": "0x",
+ "balance": "0x1b3f641ed0c2f62",
+ "nonce": "0x35",
+ "address": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
+ }
+ },
+ "currentBlockGasLimit": "0x66df83",
+ "selectedAddressTxList": [
+ {
+ "id": 3516145537630216,
+ "time": 1512615655535,
+ "status": "submitted",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xc1b710800",
+ "gas": "0x7b0c",
+ "nonce": "0x35",
+ "chainId": "0x1"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208",
+ "history": [
+ {
+ "id": 3516145537630216,
+ "time": 1512615655535,
+ "status": "unapproved",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xe6f7cec00",
+ "gas": "0x7b0c"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208"
+ },
+ [
+ {
+ "op": "replace",
+ "path": "/txParams/gasPrice",
+ "value": "0xc1b710800",
+ "note": "confTx: user approved transaction"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "approved",
+ "note": "txStateManager: setting status to approved"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/nonce",
+ "value": "0x35",
+ "note": "transactions#approveTransaction"
+ },
+ {
+ "op": "add",
+ "path": "/nonceDetails",
+ "value": {
+ "params": {
+ "highestLocalNonce": 53,
+ "highestSuggested": 53,
+ "nextNetworkNonce": 53
+ },
+ "local": {
+ "name": "local",
+ "nonce": 53,
+ "details": {
+ "startPoint": 53,
+ "highest": 53
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 53,
+ "details": {
+ "baseCount": 53
+ }
+ }
+ }
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/chainId",
+ "value": "0x1",
+ "note": "txStateManager: setting status to signed"
+ },
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "signed"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/rawTx",
+ "value": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95",
+ "note": "transactions#publishTransaction"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/hash",
+ "value": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353",
+ "note": "transactions#setTxHash"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "submitted",
+ "note": "txStateManager: setting status to submitted"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/firstRetryBlockNumber",
+ "value": "0x478ab3",
+ "note": "transactions/pending-tx-tracker#event: tx:block-update"
+ }
+ ]
+ ],
+ "nonceDetails": {
+ "params": {
+ "highestLocalNonce": 53,
+ "highestSuggested": 53,
+ "nextNetworkNonce": 53
+ },
+ "local": {
+ "name": "local",
+ "nonce": 53,
+ "details": {
+ "startPoint": 53,
+ "highest": 53
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 53,
+ "details": {
+ "baseCount": 53
+ }
+ }
+ },
+ "rawTx": "0xf86c35850c1b710800827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0f5142ba79a13ca7ec65548953017edafb217803244bbf9821d9ad077d89921e9a03afcb614169c90be9905d5b469d06984825c76675d3a535937cdb8f2ad1c0a95",
+ "hash": "0x7ce19c0d128ca11293b44a4e6d3cc9063665c00ea8c8eb400f548e132c147353",
+ "firstRetryBlockNumber": "0x478ab3"
+ },
+ {
+ "id": 3516145537630211,
+ "time": 1512613432658,
+ "status": "confirmed",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xba43b7400",
+ "gas": "0x7b0c",
+ "nonce": "0x34",
+ "chainId": "0x1"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208",
+ "history": [
+ {
+ "id": 3516145537630211,
+ "time": 1512613432658,
+ "status": "unapproved",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xdf8475800",
+ "gas": "0x7b0c"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208"
+ },
+ [
+ {
+ "op": "replace",
+ "path": "/txParams/gasPrice",
+ "value": "0xba43b7400",
+ "note": "confTx: user approved transaction"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "approved",
+ "note": "txStateManager: setting status to approved"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/nonce",
+ "value": "0x34",
+ "note": "transactions#approveTransaction"
+ },
+ {
+ "op": "add",
+ "path": "/nonceDetails",
+ "value": {
+ "params": {
+ "highestLocalNonce": 52,
+ "highestSuggested": 52,
+ "nextNetworkNonce": 52
+ },
+ "local": {
+ "name": "local",
+ "nonce": 52,
+ "details": {
+ "startPoint": 52,
+ "highest": 52
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 52,
+ "details": {
+ "baseCount": 52
+ }
+ }
+ }
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/chainId",
+ "value": "0x1",
+ "note": "txStateManager: setting status to signed"
+ },
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "signed"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/rawTx",
+ "value": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e",
+ "note": "transactions#publishTransaction"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/hash",
+ "value": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d",
+ "note": "transactions#setTxHash"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "submitted",
+ "note": "txStateManager: setting status to submitted"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/firstRetryBlockNumber",
+ "value": "0x478a2c",
+ "note": "transactions/pending-tx-tracker#event: tx:block-update"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "confirmed",
+ "note": "txStateManager: setting status to confirmed"
+ }
+ ]
+ ],
+ "nonceDetails": {
+ "params": {
+ "highestLocalNonce": 52,
+ "highestSuggested": 52,
+ "nextNetworkNonce": 52
+ },
+ "local": {
+ "name": "local",
+ "nonce": 52,
+ "details": {
+ "startPoint": 52,
+ "highest": 52
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 52,
+ "details": {
+ "baseCount": 52
+ }
+ }
+ },
+ "rawTx": "0xf86c34850ba43b7400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a073a4afdb8e8ad32b0cf9039af56c66baffd60d30e75cee5c1b783208824eafb8a0021ca6c1714a2c71281333ab77f776d3514348ab77967280fca8a5b4be44285e",
+ "hash": "0x5c98409883fdfd3cd24058a83b91470da6c40ffae41a40eb90d7dee0b837d26d",
+ "firstRetryBlockNumber": "0x478a2c"
+ },
+ {
+ "id": 3516145537630210,
+ "time": 1512612826136,
+ "status": "confirmed",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xa7a358200",
+ "gas": "0x7b0c",
+ "nonce": "0x33",
+ "chainId": "0x1"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208",
+ "history": [
+ {
+ "id": 3516145537630210,
+ "time": 1512612826136,
+ "status": "unapproved",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xba43b7400",
+ "gas": "0x7b0c"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208"
+ },
+ [
+ {
+ "op": "replace",
+ "path": "/txParams/gasPrice",
+ "value": "0xa7a358200",
+ "note": "confTx: user approved transaction"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "approved",
+ "note": "txStateManager: setting status to approved"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/nonce",
+ "value": "0x33",
+ "note": "transactions#approveTransaction"
+ },
+ {
+ "op": "add",
+ "path": "/nonceDetails",
+ "value": {
+ "params": {
+ "highestLocalNonce": 0,
+ "highestSuggested": 51,
+ "nextNetworkNonce": 51
+ },
+ "local": {
+ "name": "local",
+ "nonce": 51,
+ "details": {
+ "startPoint": 51,
+ "highest": 51
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 51,
+ "details": {
+ "baseCount": 51
+ }
+ }
+ }
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/chainId",
+ "value": "0x1",
+ "note": "txStateManager: setting status to signed"
+ },
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "signed"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/rawTx",
+ "value": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8",
+ "note": "transactions#publishTransaction"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/hash",
+ "value": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044",
+ "note": "transactions#setTxHash"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "submitted",
+ "note": "txStateManager: setting status to submitted"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/firstRetryBlockNumber",
+ "value": "0x478a04",
+ "note": "transactions/pending-tx-tracker#event: tx:block-update"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "confirmed",
+ "note": "txStateManager: setting status to confirmed"
+ }
+ ]
+ ],
+ "nonceDetails": {
+ "params": {
+ "highestLocalNonce": 0,
+ "highestSuggested": 51,
+ "nextNetworkNonce": 51
+ },
+ "local": {
+ "name": "local",
+ "nonce": 51,
+ "details": {
+ "startPoint": 51,
+ "highest": 51
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 51,
+ "details": {
+ "baseCount": 51
+ }
+ }
+ },
+ "rawTx": "0xf86c33850a7a358200827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008026a0021a8cd6c10208cc593e22af53637e5d127cee5cc6f9443a3e758a02afff1d7ca025f7420e974d3f2c668c165040987c72543a8e709bfea3528a62836a6ced9ce8",
+ "hash": "0x289772800898bc9cd414530d8581c0da257a9055e4aaaa6d10d92d700bfbd044",
+ "firstRetryBlockNumber": "0x478a04"
+ },
+ {
+ "id": 3516145537630209,
+ "time": 1512612809252,
+ "status": "failed",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0x77359400",
+ "gas": "0x7b0c",
+ "nonce": "0x33",
+ "chainId": "0x1"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208",
+ "history": [
+ {
+ "id": 3516145537630209,
+ "time": 1512612809252,
+ "status": "unapproved",
+ "metamaskNetworkId": "1",
+ "txParams": {
+ "from": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "to": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "value": "0x16345785d8a0000",
+ "gasPrice": "0xba43b7400",
+ "gas": "0x7b0c"
+ },
+ "gasPriceSpecified": false,
+ "gasLimitSpecified": false,
+ "estimatedGas": "5208"
+ },
+ [
+ {
+ "op": "replace",
+ "path": "/txParams/gasPrice",
+ "value": "0x77359400",
+ "note": "confTx: user approved transaction"
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "approved",
+ "note": "txStateManager: setting status to approved"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/nonce",
+ "value": "0x33",
+ "note": "transactions#approveTransaction"
+ },
+ {
+ "op": "add",
+ "path": "/nonceDetails",
+ "value": {
+ "params": {
+ "highestLocalNonce": 0,
+ "highestSuggested": 51,
+ "nextNetworkNonce": 51
+ },
+ "local": {
+ "name": "local",
+ "nonce": 51,
+ "details": {
+ "startPoint": 51,
+ "highest": 51
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 51,
+ "details": {
+ "baseCount": 51
+ }
+ }
+ }
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/txParams/chainId",
+ "value": "0x1",
+ "note": "txStateManager: setting status to signed"
+ },
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "signed"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/rawTx",
+ "value": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7",
+ "note": "transactions#publishTransaction"
+ }
+ ],
+ [
+ {
+ "op": "add",
+ "path": "/err",
+ "value": {
+ "message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced",
+ "stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)"
+ }
+ }
+ ],
+ [
+ {
+ "op": "replace",
+ "path": "/status",
+ "value": "failed",
+ "note": "txStateManager: setting status to failed"
+ }
+ ]
+ ],
+ "nonceDetails": {
+ "params": {
+ "highestLocalNonce": 0,
+ "highestSuggested": 51,
+ "nextNetworkNonce": 51
+ },
+ "local": {
+ "name": "local",
+ "nonce": 51,
+ "details": {
+ "startPoint": 51,
+ "highest": 51
+ }
+ },
+ "network": {
+ "name": "network",
+ "nonce": 51,
+ "details": {
+ "baseCount": 51
+ }
+ }
+ },
+ "rawTx": "0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7",
+ "err": {
+ "message": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced",
+ "stack": "Error: [ethjs-rpc] rpc error with payload {\"id\":7801900228852,\"jsonrpc\":\"2.0\",\"params\":[\"0xf86b338477359400827b0c94fdea65c8e26263f6d9a1b5de9555d2931a33b82588016345785d8a00008025a098624a27ae79b2b1adc63b913850f266a920cb9d93e6588b8df9b8883eb1b323a00cc6fd855723a234f4f93b48caf7a7659366d09e5c5887f0a4c2e5fa68012cd7\"],\"method\":\"eth_sendRawTransaction\"} Error: transaction underpriced\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:60327:26\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88030:9\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16678:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16522:25)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16694:16\n at resultObj.id (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:88012:9)\n at chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16813:16\n at replenish (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16527:17)\n at iterateeCallback (chrome-extension://ebjbdknjcgcbchkagneicjfpneaghdhb/scripts/background.js:16512:17)"
+ }
+ }
+ ],
+ "unapprovedMsgs": {},
+ "unapprovedMsgCount": 0,
+ "unapprovedPersonalMsgs": {},
+ "unapprovedPersonalMsgCount": 0,
+ "unapprovedTypedMessages": {},
+ "unapprovedTypedMessagesCount": 0,
+ "keyringTypes": [
+ "Simple Key Pair",
+ "HD Key Tree"
+ ],
+ "keyrings": [
+ {
+ "type": "HD Key Tree",
+ "accounts": [
+ "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
+ ]
+ }
+ ],
+ "computedBalances": {},
+ "currentAccountTab": "history",
+ "tokens": [
+ {
+ "address": "0x0d8775f648430679a709e98d2b0cb6250d2887ef",
+ "symbol": "BAT",
+ "decimals": "18"
+ }
+ ],
+ "selectedAddress": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825",
+ "currentCurrency": "usd",
+ "conversionRate": 418.62,
+ "conversionDate": 1512615622,
+ "infuraNetworkStatus": {
+ "mainnet": "ok",
+ "ropsten": "ok",
+ "kovan": "ok",
+ "rinkeby": "ok"
+ },
+ "shapeShiftTxList": [],
+ "lostAccounts": []
+ },
+ "appState": {
+ "shouldClose": true,
+ "menuOpen": false,
+ "currentView": {
+ "name": "accountDetail",
+ "context": "0xfdea65c8e26263f6d9a1b5de9555d2931a33b825"
+ },
+ "accountDetail": {
+ "subview": "transactions",
+ "accountExport": "none",
+ "privateKey": ""
+ },
+ "transForward": false,
+ "isLoading": false,
+ "warning": null,
+ "forgottenPassword": false,
+ "scrollToBottom": false
+ },
+ "identities": {},
+ "version": "3.12.1",
+ "platform": {
+ "arch": "x86-64",
+ "nacl_arch": "x86-64",
+ "os": "mac"
+ },
+ "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36"
+} \ No newline at end of file
diff --git a/package.json b/package.json
index 5129dd3cb..fce548066 100644
--- a/package.json
+++ b/package.json
@@ -74,6 +74,7 @@
"eth-contract-metadata": "^1.1.4",
"eth-hd-keyring": "^1.2.1",
"eth-json-rpc-filters": "^1.2.4",
+ "eth-json-rpc-infura": "^1.0.2",
"eth-keyring-controller": "^2.1.2",
"eth-phishing-detect": "^1.1.4",
"eth-query": "^2.1.2",
@@ -152,7 +153,7 @@
"valid-url": "^1.0.9",
"vreme": "^3.0.2",
"web3": "^0.20.1",
- "web3-provider-engine": "^13.3.2",
+ "web3-provider-engine": "^13.4.0",
"web3-stream-provider": "^3.0.1",
"xtend": "^4.0.1"
},
diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js
index 4b5170dfe..393601a57 100644
--- a/test/unit/pending-tx-test.js
+++ b/test/unit/pending-tx-test.js
@@ -206,6 +206,7 @@ describe('PendingTransactionTracker', function () {
})
describe('#resubmitPendingTxs', function () {
+ const blockStub = { number: '0x0' };
beforeEach(function () {
const txMeta2 = txMeta3 = txMeta
txList = [txMeta, txMeta2, txMeta3].map((tx) => {
@@ -223,7 +224,7 @@ describe('PendingTransactionTracker', function () {
Promise.all(txList.map((tx) => tx.processed))
.then((txCompletedList) => done())
.catch(done)
- pendingTxTracker.resubmitPendingTxs()
+ pendingTxTracker.resubmitPendingTxs(blockStub)
})
it('should not emit \'tx:failed\' if the txMeta throws a known txError', function (done) {
knownErrors =[
@@ -250,7 +251,7 @@ describe('PendingTransactionTracker', function () {
.then((txCompletedList) => done())
.catch(done)
- pendingTxTracker.resubmitPendingTxs()
+ pendingTxTracker.resubmitPendingTxs(blockStub)
})
it('should emit \'tx:warning\' if it encountered a real error', function (done) {
pendingTxTracker.once('tx:warning', (txMeta, err) => {
@@ -268,28 +269,74 @@ describe('PendingTransactionTracker', function () {
.then((txCompletedList) => done())
.catch(done)
- pendingTxTracker.resubmitPendingTxs()
+ pendingTxTracker.resubmitPendingTxs(blockStub)
})
})
describe('#_resubmitTx', function () {
- it('should publishing the transaction', function (done) {
- const enoughBalance = '0x100000'
- pendingTxTracker.getBalance = (address) => {
- assert.equal(address, txMeta.txParams.from, 'Should pass the address')
- return enoughBalance
- }
- pendingTxTracker.publishTransaction = async (rawTx) => {
- assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx')
- }
+ const mockFirstRetryBlockNumber = '0x1'
+ let txMetaToTestExponentialBackoff
- // Stubbing out current account state:
- // Adding the fake tx:
- pendingTxTracker._resubmitTx(txMeta)
- .then(() => done())
- .catch((err) => {
- assert.ifError(err, 'should not throw an error')
- done(err)
+ beforeEach(() => {
+ pendingTxTracker.getBalance = (address) => {
+ assert.equal(address, txMeta.txParams.from, 'Should pass the address')
+ return enoughBalance
+ }
+ pendingTxTracker.publishTransaction = async (rawTx) => {
+ assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx')
+ }
+ sinon.spy(pendingTxTracker, 'publishTransaction')
+
+ txMetaToTestExponentialBackoff = Object.assign({}, txMeta, {
+ retryCount: 4,
+ firstRetryBlockNumber: mockFirstRetryBlockNumber,
+ })
+ })
+
+ afterEach(() => {
+ pendingTxTracker.publishTransaction.reset()
+ })
+
+ it('should publish the transaction', function (done) {
+ const enoughBalance = '0x100000'
+
+ // Stubbing out current account state:
+ // Adding the fake tx:
+ pendingTxTracker._resubmitTx(txMeta)
+ .then(() => done())
+ .catch((err) => {
+ assert.ifError(err, 'should not throw an error')
+ done(err)
+ })
+
+ assert.equal(pendingTxTracker.publishTransaction.callCount, 1, 'Should call publish transaction')
+ })
+
+ it('should not publish the transaction if the limit of retries has been exceeded', function (done) {
+ const enoughBalance = '0x100000'
+ const mockLatestBlockNumber = '0x5'
+
+ pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber)
+ .then(() => done())
+ .catch((err) => {
+ assert.ifError(err, 'should not throw an error')
+ done(err)
+ })
+
+ assert.equal(pendingTxTracker.publishTransaction.callCount, 0, 'Should NOT call publish transaction')
+ })
+
+ it('should publish the transaction if the number of blocks since last retry exceeds the last set limit', function (done) {
+ const enoughBalance = '0x100000'
+ const mockLatestBlockNumber = '0x11'
+
+ pendingTxTracker._resubmitTx(txMetaToTestExponentialBackoff, mockLatestBlockNumber)
+ .then(() => done())
+ .catch((err) => {
+ assert.ifError(err, 'should not throw an error')
+ done(err)
+ })
+
+ assert.equal(pendingTxTracker.publishTransaction.callCount, 1, 'Should call publish transaction')
})
- })
})
})
diff --git a/ui/app/actions.js b/ui/app/actions.js
index 04fd35b20..52ea899aa 100644
--- a/ui/app/actions.js
+++ b/ui/app/actions.js
@@ -168,6 +168,7 @@ var actions = {
callBackgroundThenUpdate,
forceUpdateMetamaskState,
+ retryTransaction,
}
module.exports = actions
@@ -759,6 +760,19 @@ function markAccountsFound () {
return callBackgroundThenUpdate(background.markAccountsFound)
}
+function retryTransaction (txId) {
+ log.debug(`background.retryTransaction`)
+ return (dispatch) => {
+ background.retryTransaction(txId, (err, newState) => {
+ if (err) {
+ return dispatch(actions.displayWarning(err.message))
+ }
+ dispatch(actions.updateMetamaskState(newState))
+ dispatch(actions.viewPendingTx(txId))
+ })
+ }
+}
+
//
// config
//
diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js
index 5b1b367c6..32d54902e 100644
--- a/ui/app/components/pending-tx.js
+++ b/ui/app/components/pending-tx.js
@@ -38,6 +38,16 @@ PendingTx.prototype.render = function () {
const txMeta = this.gatherTxMeta()
const txParams = txMeta.txParams || {}
+ // Allow retry txs
+ const { lastGasPrice } = txMeta
+ let forceGasMin
+ if (lastGasPrice) {
+ const stripped = ethUtil.stripHexPrefix(lastGasPrice)
+ const lastGas = new BN(stripped, 16)
+ const priceBump = lastGas.divn('10')
+ forceGasMin = lastGas.add(priceBump)
+ }
+
// Account Details
const address = txParams.from || props.selectedAddress
const identity = props.identities[address] || { address: address }
@@ -199,7 +209,7 @@ PendingTx.prototype.render = function () {
precision: 9,
scale: 9,
suffix: 'GWEI',
- min: MIN_GAS_PRICE_BN,
+ min: forceGasMin || MIN_GAS_PRICE_BN,
style: {
position: 'relative',
top: '5px',
diff --git a/ui/app/components/token-list.js b/ui/app/components/token-list.js
index 998ec901d..149733b89 100644
--- a/ui/app/components/token-list.js
+++ b/ui/app/components/token-list.js
@@ -194,10 +194,7 @@ TokenList.prototype.componentWillUpdate = function (nextProps) {
}
TokenList.prototype.updateBalances = function (tokens) {
- const heldTokens = tokens.filter(token => {
- return token.balance !== '0' && token.string !== '0.000'
- })
- this.setState({ tokens: heldTokens, isLoading: false })
+ this.setState({ tokens, isLoading: false })
}
TokenList.prototype.componentWillUnmount = function () {
diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js
index 891d5e227..42ef665b1 100644
--- a/ui/app/components/transaction-list-item.js
+++ b/ui/app/components/transaction-list-item.js
@@ -1,6 +1,7 @@
const Component = require('react').Component
const h = require('react-hyperscript')
const inherits = require('util').inherits
+const connect = require('react-redux').connect
const EthBalance = require('./eth-balance')
const addressSummary = require('../util').addressSummary
@@ -9,18 +10,33 @@ const CopyButton = require('./copyButton')
const vreme = new (require('vreme'))()
const Tooltip = require('./tooltip')
const numberToBN = require('number-to-bn')
+const actions = require('../actions')
const TransactionIcon = require('./transaction-list-item-icon')
const ShiftListItem = require('./shift-list-item')
-module.exports = TransactionListItem
+
+const mapDispatchToProps = dispatch => {
+ return {
+ retryTransaction: transactionId => dispatch(actions.retryTransaction(transactionId)),
+ }
+}
+
+module.exports = connect(null, mapDispatchToProps)(TransactionListItem)
inherits(TransactionListItem, Component)
function TransactionListItem () {
Component.call(this)
}
+TransactionListItem.prototype.showRetryButton = function () {
+ const { transaction = {} } = this.props
+ const { status, time } = transaction
+ return status === 'submitted' && Date.now() - time > 30000
+}
+
TransactionListItem.prototype.render = function () {
const { transaction, network, conversionRate, currentCurrency } = this.props
+ const { status } = transaction
if (transaction.key === 'shapeshift') {
if (network === '1') return h(ShiftListItem, transaction)
}
@@ -32,7 +48,7 @@ TransactionListItem.prototype.render = function () {
var isMsg = ('msgParams' in transaction)
var isTx = ('txParams' in transaction)
- var isPending = transaction.status === 'unapproved'
+ var isPending = status === 'unapproved'
let txParams
if (isTx) {
txParams = transaction.txParams
@@ -44,7 +60,7 @@ TransactionListItem.prototype.render = function () {
const isClickable = ('hash' in transaction && isLinkable) || isPending
return (
- h(`.transaction-list-item.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, {
+ h('.transaction-list-item.flex-column', {
onClick: (event) => {
if (isPending) {
this.props.showTx(transaction.id)
@@ -56,51 +72,92 @@ TransactionListItem.prototype.render = function () {
},
style: {
padding: '20px 0',
+ alignItems: 'center',
},
}, [
+ h(`.flex-row.flex-space-between${isClickable ? '.pointer' : ''}`, {
+ style: {
+ width: '100%',
+ },
+ }, [
+ h('.identicon-wrapper.flex-column.flex-center.select-none', [
+ h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
+ ]),
- h('.identicon-wrapper.flex-column.flex-center.select-none', [
- h(TransactionIcon, { txParams, transaction, isTx, isMsg }),
+ h(Tooltip, {
+ title: 'Transaction Number',
+ position: 'right',
+ }, [
+ h('span', {
+ style: {
+ display: 'flex',
+ cursor: 'normal',
+ flexDirection: 'column',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: '10px',
+ },
+ }, nonce),
+ ]),
+
+ h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [
+ domainField(txParams),
+ h('div', date),
+ recipientField(txParams, transaction, isTx, isMsg),
+ ]),
+
+ // Places a copy button if tx is successful, else places a placeholder empty div.
+ transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}),
+
+ isTx ? h(EthBalance, {
+ value: txParams.value,
+ conversionRate,
+ currentCurrency,
+ width: '55px',
+ shorten: true,
+ showFiat: false,
+ style: {fontSize: '15px'},
+ }) : h('.flex-column'),
]),
- h(Tooltip, {
- title: 'Transaction Number',
- position: 'right',
+ this.showRetryButton() && h('.transition-list-item__retry.grow-on-hover', {
+ onClick: event => {
+ event.stopPropagation()
+ this.resubmit()
+ },
+ style: {
+ height: '22px',
+ borderRadius: '22px',
+ color: '#F9881B',
+ padding: '0 20px',
+ backgroundColor: '#FFE3C9',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ fontSize: '8px',
+ cursor: 'pointer',
+ },
}, [
- h('span', {
+ h('div', {
style: {
- display: 'flex',
- cursor: 'normal',
- flexDirection: 'column',
- alignItems: 'center',
- justifyContent: 'center',
- padding: '10px',
+ paddingRight: '2px',
},
- }, nonce),
- ]),
-
- h('.flex-column', {style: {width: '200px', overflow: 'hidden'}}, [
- domainField(txParams),
- h('div', date),
- recipientField(txParams, transaction, isTx, isMsg),
+ }, 'Taking too long?'),
+ h('div', {
+ style: {
+ textDecoration: 'underline',
+ },
+ }, 'Retry with a higher gas price here'),
]),
-
- // Places a copy button if tx is successful, else places a placeholder empty div.
- transaction.hash ? h(CopyButton, { value: transaction.hash }) : h('div', {style: { display: 'flex', alignItems: 'center', width: '26px' }}),
-
- isTx ? h(EthBalance, {
- value: txParams.value,
- conversionRate,
- currentCurrency,
- width: '55px',
- shorten: true,
- showFiat: false,
- style: {fontSize: '15px'},
- }) : h('.flex-column'),
])
)
}
+TransactionListItem.prototype.resubmit = function () {
+ const { transaction } = this.props
+ this.props.retryTransaction(transaction.id)
+}
+
function domainField (txParams) {
return h('div', {
style: {
diff --git a/ui/app/config.js b/ui/app/config.js
index c14fa1d28..9cb2a0aad 100644
--- a/ui/app/config.js
+++ b/ui/app/config.js
@@ -117,7 +117,7 @@ ConfigScreen.prototype.render = function () {
if (err) {
state.dispatch(actions.displayWarning('Error in retrieving state logs.'))
} else {
- exportAsFile('MetaMask State Logs', result)
+ exportAsFile('MetaMask State Logs.json', result)
}
})
},
diff --git a/ui/app/css/index.css b/ui/app/css/index.css
index 0630c4c12..c0bf18c23 100644
--- a/ui/app/css/index.css
+++ b/ui/app/css/index.css
@@ -108,6 +108,10 @@ button:not([disabled]):active, input[type="submit"]:not([disabled]):active {
transform: scale(0.95);
}
+.grow-on-hover:hover {
+ transform: scale(1.05);
+}
+
a {
text-decoration: none;
color: inherit;