diff options
-rw-r--r-- | app/scripts/controllers/transactions.js | 178 | ||||
-rw-r--r-- | app/scripts/lib/pending-tx-watchers.js | 162 | ||||
-rw-r--r-- | app/scripts/lib/tx-utils.js | 41 | ||||
-rw-r--r-- | app/scripts/lib/util.js | 31 | ||||
-rw-r--r-- | package.json | 1 | ||||
-rw-r--r-- | test/unit/message-manager-test.js | 2 | ||||
-rw-r--r-- | test/unit/pending-tx-test.js | 271 | ||||
-rw-r--r-- | test/unit/tx-controller-test.js | 49 | ||||
-rw-r--r-- | test/unit/tx-utils-test.js | 58 | ||||
-rw-r--r-- | test/unit/util-test.js | 41 | ||||
-rw-r--r-- | ui/app/components/transaction-list-item.js | 15 |
11 files changed, 564 insertions, 285 deletions
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js index 308d43cb0..efc8d7117 100644 --- a/app/scripts/controllers/transactions.js +++ b/app/scripts/controllers/transactions.js @@ -4,7 +4,8 @@ const clone = require('clone') const ObservableStore = require('obs-store') const ethUtil = require('ethereumjs-util') const EthQuery = require('ethjs-query') -const TxProviderUtil = require('../lib/tx-utils') +const TxProviderUtils = require('../lib/tx-utils') +const PendingTransactionWatchers = require('../lib/pending-tx-watchers') const createId = require('../lib/random-id') const NonceTracker = require('../lib/nonce-tracker') @@ -20,6 +21,9 @@ module.exports = class TransactionController extends EventEmitter { this.txHistoryLimit = opts.txHistoryLimit this.provider = opts.provider this.blockTracker = opts.blockTracker + this.signEthTx = opts.signTransaction + this.ethStore = opts.ethStore + this.nonceTracker = new NonceTracker({ provider: this.provider, getPendingTransactions: (address) => { @@ -31,15 +35,31 @@ module.exports = class TransactionController extends EventEmitter { }, }) this.query = new EthQuery(this.provider) - this.txProviderUtils = new TxProviderUtil(this.query) - this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this)) + this.txProviderUtils = new TxProviderUtils(this.provider) + + this.pendingTxWatchers = new PendingTransactionWatchers({ + provider: this.provider, + nonceTracker: this.nonceTracker, + getBalance: (address) => this.ethStore.getState().accounts[address].balance, + publishTransaction: this.txProviderUtils.publishTransaction.bind(this.txProviderUtils), + getPendingTransactions: (address) => { + return this.getFilteredTxList({ + from: address, + status: 'submitted', + }) + }, + }) + + this.pendingTxWatchers.on('txWarning', this.updateTx.bind(this)) + this.pendingTxWatchers.on('txFailed', this.setTxStatusFailed.bind(this)) + this.pendingTxWatchers.on('txConfirmed', this.setTxStatusConfirmed.bind(this)) + + this.blockTracker.on('rawBlock', this.pendingTxWatchers.checkForTxInBlock.bind(this)) // this is a little messy but until ethstore has been either // removed or redone this is to guard against the race condition // where ethStore hasent been populated by the results yet - this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.resubmitPendingTxs.bind(this))) - this.blockTracker.on('sync', this.queryPendingTxs.bind(this)) - this.signEthTx = opts.signTransaction - this.ethStore = opts.ethStore + this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.pendingTxWatchers.resubmitPendingTxs.bind(this))) + this.blockTracker.on('sync', this.pendingTxWatchers.queryPendingTxs.bind(this)) // memstore is computed from a few different stores this._updateMemstore() this.store.subscribe(() => this._updateMemstore()) @@ -226,11 +246,7 @@ module.exports = class TransactionController extends EventEmitter { // must set transaction to submitted/failed before releasing lock nonceLock.releaseLock() } catch (err) { - this.setTxStatusFailed(txId, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message || 'Transaction failed during approval', - }) + this.setTxStatusFailed(txId, err) // must set transaction to submitted/failed before releasing lock if (nonceLock) nonceLock.releaseLock() // continue with error chain @@ -374,73 +390,6 @@ module.exports = class TransactionController extends EventEmitter { this.updateTx(txMeta) } - // checks if a signed tx is in a block and - // if included sets the tx status as 'confirmed' - checkForTxInBlock (block) { - const signedTxList = this.getFilteredTxList({status: 'submitted'}) - if (!signedTxList.length) return - signedTxList.forEach((txMeta) => { - const txHash = txMeta.hash - const txId = txMeta.id - - if (!txHash) { - const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') - noTxHashErr.name = 'NoTxHashError' - this.setTxStatusFailed(txId, noTxHashErr) - } - - - block.transactions.forEach((tx) => { - if (tx.hash === txHash) this.setTxStatusConfirmed(txId) - }) - }) - } - - queryPendingTxs ({oldBlock, newBlock}) { - // check pending transactions on start - if (!oldBlock) { - this._checkPendingTxs() - return - } - // if we synced by more than one block, check for missed pending transactions - const diff = Number.parseInt(newBlock.number) - Number.parseInt(oldBlock.number) - if (diff > 1) this._checkPendingTxs() - } - - resubmitPendingTxs () { - const pending = this.getTxsByMetaData('status', 'submitted') - // only try resubmitting if their are transactions to resubmit - if (!pending.length) return - pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { - /* - Dont marked as failed if the error is a "known" transaction warning - "there is already a transaction with the same sender-nonce - but higher/same gas price" - */ - const errorMessage = err.message.toLowerCase() - const isKnownTx = ( - // geth - errorMessage.includes('replacement transaction underpriced') - || errorMessage.includes('known transaction') - // parity - || errorMessage.includes('gas price too low to replace') - || errorMessage.includes('transaction with the same hash was already imported') - // other - || errorMessage.includes('gateway timeout') - || errorMessage.includes('nonce too low') - ) - // ignore resubmit warnings, return early - if (isKnownTx) return - // encountered real error - transition to error state - this.setTxStatusFailed(txMeta.id, { - stack: err.stack || err.message, - errCode: err.errCode || err, - message: err.message, - }) - })) - } - - /* _____________________________________ | | | PRIVATE METHODS | @@ -482,75 +431,4 @@ module.exports = class TransactionController extends EventEmitter { }) this.memStore.updateState({ unapprovedTxs, selectedAddressTxList }) } - - async _resubmitTx (txMeta) { - const address = txMeta.txParams.from - const balance = this.ethStore.getState().accounts[address].balance - if (!('retryCount' in txMeta)) txMeta.retryCount = 0 - - // if the value of the transaction is greater then the balance, fail. - if (!this.txProviderUtils.sufficientBalance(txMeta.txParams, balance)) { - const message = 'Insufficient balance.' - this.setTxStatusFailed(txMeta.id, { - stack: '_resubmitTx: custom tx-controller error', - message, - }) - log.error(message) - return - } - - // Only auto-submit already-signed txs: - if (!('rawTx' in txMeta)) return - - // Increment a try counter. - txMeta.retryCount++ - const rawTx = txMeta.rawTx - return await this.txProviderUtils.publishTransaction(rawTx) - } - - // checks the network for signed txs and - // if confirmed sets the tx status as 'confirmed' - async _checkPendingTxs () { - const signedTxList = this.getFilteredTxList({status: 'submitted'}) - // in order to keep the nonceTracker accurate we block it while updating pending transactions - const nonceGlobalLock = await this.nonceTracker.getGlobalLock() - try { - await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) - } catch (err) { - console.error('TransactionController - Error updating pending transactions') - console.error(err) - } - nonceGlobalLock.releaseLock() - } - - async _checkPendingTx (txMeta) { - const txHash = txMeta.hash - const txId = txMeta.id - // extra check in case there was an uncaught error during the - // signature and submission process - if (!txHash) { - const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') - noTxHashErr.name = 'NoTxHashError' - this.setTxStatusFailed(txId, noTxHashErr) - } - // get latest transaction status - let txParams - try { - txParams = await this.query.getTransactionByHash(txHash) - if (!txParams) return - if (txParams.blockNumber) { - this.setTxStatusConfirmed(txId) - } - } catch (err) { - if (err || !txParams) { - txMeta.err = { - isWarning: true, - errorCode: err, - message: 'There was a problem loading this transaction.', - } - this.updateTx(txMeta) - throw err - } - } - } }
\ No newline at end of file diff --git a/app/scripts/lib/pending-tx-watchers.js b/app/scripts/lib/pending-tx-watchers.js new file mode 100644 index 000000000..60d0a09ad --- /dev/null +++ b/app/scripts/lib/pending-tx-watchers.js @@ -0,0 +1,162 @@ +const EventEmitter = require('events') +const EthQuery = require('ethjs-query') +const sufficientBalance = require('./util').sufficientBalance +/* + + Utility class for tracking the transactions as they + go from a pending state to a confirmed (mined in a block) state + + As well as continues broadcast while in the pending state + + ~config is not optional~ + requires a: { + provider: //, + nonceTracker: //see nonce tracker, + getBalnce: //(address) a function for getting balances, + getPendingTransactions: //() a function for getting an array of transactions, + publishTransaction: //(rawTx) a async function for publishing raw transactions, + } + +*/ + +module.exports = class PendingTransactionWatchers extends EventEmitter { + constructor (config) { + super() + this.query = new EthQuery(config.provider) + this.nonceTracker = config.nonceTracker + + this.getBalance = config.getBalance + this.getPendingTransactions = config.getPendingTransactions + this.publishTransaction = config.publishTransaction + } + + // checks if a signed tx is in a block and + // if included sets the tx status as 'confirmed' + checkForTxInBlock (block) { + const signedTxList = this.getPendingTransactions() + if (!signedTxList.length) return + signedTxList.forEach((txMeta) => { + const txHash = txMeta.hash + const txId = txMeta.id + + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('txFailed', txId, noTxHashErr) + return + } + + + block.transactions.forEach((tx) => { + if (tx.hash === txHash) this.emit('txConfirmed', txId) + }) + }) + } + + queryPendingTxs ({oldBlock, newBlock}) { + // check pending transactions on start + if (!oldBlock) { + this._checkPendingTxs() + return + } + // if we synced by more than one block, check for missed pending transactions + const diff = Number.parseInt(newBlock.number, 16) - Number.parseInt(oldBlock.number, 16) + if (diff > 1) this._checkPendingTxs() + } + + + resubmitPendingTxs () { + const pending = this.getPendingTransactions('status', 'submitted') + // only try resubmitting if their are transactions to resubmit + if (!pending.length) return + pending.forEach((txMeta) => this._resubmitTx(txMeta).catch((err) => { + /* + Dont marked as failed if the error is a "known" transaction warning + "there is already a transaction with the same sender-nonce + but higher/same gas price" + */ + const errorMessage = err.message.toLowerCase() + const isKnownTx = ( + // geth + errorMessage.includes('replacement transaction underpriced') + || errorMessage.includes('known transaction') + // parity + || errorMessage.includes('gas price too low to replace') + || errorMessage.includes('transaction with the same hash was already imported') + // other + || errorMessage.includes('gateway timeout') + || errorMessage.includes('nonce too low') + ) + // ignore resubmit warnings, return early + if (isKnownTx) return + // encountered real error - transition to error state + this.emit('txFailed', txMeta.id, err) + })) + } + + async _resubmitTx (txMeta) { + const address = txMeta.txParams.from + const balance = this.getBalance(address) + if (!('retryCount' in txMeta)) txMeta.retryCount = 0 + + // if the value of the transaction is greater then the balance, fail. + if (!sufficientBalance(txMeta.txParams, balance)) { + const insufficientFundsError = new Error('Insufficient balance during rebroadcast.') + this.emit('txFailed', txMeta.id, insufficientFundsError) + log.error(insufficientFundsError) + return + } + + // Only auto-submit already-signed txs: + if (!('rawTx' in txMeta)) return + + // Increment a try counter. + txMeta.retryCount++ + const rawTx = txMeta.rawTx + return await this.publishTransaction(rawTx) + } + + async _checkPendingTx (txMeta) { + const txHash = txMeta.hash + const txId = txMeta.id + // extra check in case there was an uncaught error during the + // signature and submission process + if (!txHash) { + const noTxHashErr = new Error('We had an error while submitting this transaction, please try again.') + noTxHashErr.name = 'NoTxHashError' + this.emit('txFailed', txId, noTxHashErr) + return + } + // get latest transaction status + let txParams + try { + txParams = await this.query.getTransactionByHash(txHash) + if (!txParams) return + if (txParams.blockNumber) { + this.emit('txConfirmed', txId) + } + } catch (err) { + txMeta.warning = { + error: err, + message: 'There was a problem loading this transaction.', + } + this.emit('txWarning', txMeta) + throw err + } + } + + // checks the network for signed txs and + // if confirmed sets the tx status as 'confirmed' + async _checkPendingTxs () { + const signedTxList = this.getPendingTransactions() + // in order to keep the nonceTracker accurate we block it while updating pending transactions + const nonceGlobalLock = await this.nonceTracker.getGlobalLock() + try { + await Promise.all(signedTxList.map((txMeta) => this._checkPendingTx(txMeta))) + } catch (err) { + console.error('PendingTransactionWatcher - Error updating pending transactions') + console.error(err) + } + nonceGlobalLock.releaseLock() + } +}
\ No newline at end of file diff --git a/app/scripts/lib/tx-utils.js b/app/scripts/lib/tx-utils.js index 3687a9652..a2db4abd8 100644 --- a/app/scripts/lib/tx-utils.js +++ b/app/scripts/lib/tx-utils.js @@ -1,7 +1,11 @@ -const ethUtil = require('ethereumjs-util') +const EthQuery = require('ethjs-query') const Transaction = require('ethereumjs-tx') const normalize = require('eth-sig-util').normalize -const BN = ethUtil.BN +const { + hexToBn, + BnMultiplyByFraction, + bnToHex, +} = require('./util') /* tx-utils are utility methods for Transaction manager @@ -10,8 +14,8 @@ and used to do things like calculate gas of a tx. */ module.exports = class txProvideUtils { - constructor (ethQuery) { - this.query = ethQuery + constructor (provider) { + this.query = new EthQuery(provider) } async analyzeGasUsage (txMeta) { @@ -91,31 +95,4 @@ module.exports = class txProvideUtils { throw new Error(`Invalid transaction value of ${txParams.value} not a positive number.`) } } - - sufficientBalance (txParams, hexBalance) { - const balance = hexToBn(hexBalance) - const value = hexToBn(txParams.value) - const gasLimit = hexToBn(txParams.gas) - const gasPrice = hexToBn(txParams.gasPrice) - - const maxCost = value.add(gasLimit.mul(gasPrice)) - return balance.gte(maxCost) - } - -} - -// util - -function bnToHex (inputBn) { - return ethUtil.addHexPrefix(inputBn.toString(16)) -} - -function hexToBn (inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16) -} - -function BnMultiplyByFraction (targetBN, numerator, denominator) { - const numBN = new BN(numerator) - const denomBN = new BN(denominator) - return targetBN.mul(numBN).div(denomBN) -} +}
\ No newline at end of file diff --git a/app/scripts/lib/util.js b/app/scripts/lib/util.js index bddd60ee8..70390e95c 100644 --- a/app/scripts/lib/util.js +++ b/app/scripts/lib/util.js @@ -1,8 +1,39 @@ +const ethUtil = require('ethereumjs-util') +const BN = ethUtil.BN + module.exports = { getStack, + sufficientBalance, + hexToBn, + bnToHex, + BnMultiplyByFraction, } function getStack () { const stack = new Error('Stack trace generator - not an error').stack return stack } + +function sufficientBalance (txParams, hexBalance) { + const balance = hexToBn(hexBalance) + const value = hexToBn(txParams.value) + const gasLimit = hexToBn(txParams.gas) + const gasPrice = hexToBn(txParams.gasPrice) + + const maxCost = value.add(gasLimit.mul(gasPrice)) + return balance.gte(maxCost) +} + +function bnToHex (inputBn) { + return ethUtil.addHexPrefix(inputBn.toString(16)) +} + +function hexToBn (inputHex) { + return new BN(ethUtil.stripHexPrefix(inputHex), 16) +} + +function BnMultiplyByFraction (targetBN, numerator, denominator) { + const numBN = new BN(numerator) + const denomBN = new BN(denominator) + return targetBN.mul(numBN).div(denomBN) +} diff --git a/package.json b/package.json index ce8545fc4..a275247ba 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "babel-runtime": "^6.23.0", "bip39": "^2.2.0", "bluebird": "^3.5.0", + "bn.js": "^4.11.7", "browser-passworder": "^2.0.3", "browserify-derequire": "^0.9.4", "client-sw-ready-event": "^3.3.0", diff --git a/test/unit/message-manager-test.js b/test/unit/message-manager-test.js index 30cb4f067..9b76241ed 100644 --- a/test/unit/message-manager-test.js +++ b/test/unit/message-manager-test.js @@ -1,7 +1,7 @@ const assert = require('assert') const MessageManger = require('../../app/scripts/lib/message-manager') -describe('Transaction Manager', function () { +describe('Message Manager', function () { let messageManager beforeEach(function () { diff --git a/test/unit/pending-tx-test.js b/test/unit/pending-tx-test.js new file mode 100644 index 000000000..d0606bb17 --- /dev/null +++ b/test/unit/pending-tx-test.js @@ -0,0 +1,271 @@ +const assert = require('assert') +const ethUtil = require('ethereumjs-util') +const EthTx = require('ethereumjs-tx') +const ObservableStore = require('obs-store') +const clone = require('clone') +const PendingTransactionWatcher = require('../../app/scripts/lib/pending-tx-watchers') +const noop = () => true +const currentNetworkId = 42 +const otherNetworkId = 36 +const privKey = new Buffer('8718b9618a37d1fc78c436511fc6df3c8258d3250635bba617f33003270ec03e', 'hex') + +describe('PendingTransactionWatcher', function () { + let pendingTxWatcher, txMeta, txMetaNoHash, txMetaNoRawTx + this.timeout(10000) + beforeEach(function () { + txMeta = { + id: 1, + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eebBlock', + status: 'signed', + txParams: { + from: '0x1678a085c290ebd122dc42cba69373b5953b831d', + nonce: '0x1', + value: '0xfffff', + }, + rawTx: '0xf86c808504a817c800827b0d940c62bb85faa3311a998d3aba8098c1235c564966880de0b6b3a7640000802aa08ff665feb887a25d4099e40e11f0fef93ee9608f404bd3f853dd9e84ed3317a6a02ec9d3d1d6e176d4d2593dd760e74ccac753e6a0ea0d00cc9789d0d7ff1f471d', + } + txMetaNoHash = { + id: 2, + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + txMetaNoRawTx = { + hash: '0x0593ee121b92e10d63150ad08b4b8f9c7857d1bd160195ee648fb9a0f8d00eebBlock', + status: 'signed', + txParams: { from: '0x1678a085c290ebd122dc42cba69373b5953b831d'}, + } + + pendingTxWatcher = new PendingTransactionWatcher({ + provider: { sendAsync: noop }, + getBalance: () => {}, + nonceTracker: { + getGlobalLock: async () => { + return { releaseLock: () => {} } + } + }, + getPendingTransactions: () => {return []}, + sufficientBalance: () => {}, + publishTransaction: () => {}, + }) + + pendingTxWatcher.query = new Proxy({}, { + get: (queryStubResult, key) => { + if (key === 'stubResult') { + return function (method, ...args) { + queryStubResult[method] = args + } + } else { + const returnValues = queryStubResult[key] + return () => Promise.resolve(...returnValues) + } + }, + }) + + }) + + describe('#checkForTxInBlock', function () { + it('should return if no pending transactions', function () { + // throw a type error if it trys to do anything on the block + // thus failing the test + const block = Proxy.revocable({}, {}).revoke() + pendingTxWatcher.checkForTxInBlock(block) + }) + it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + const block = Proxy.revocable({}, {}).revoke() + pendingTxWatcher.getPendingTransactions = () => [txMetaNoHash] + pendingTxWatcher.once('txFailed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxWatcher.checkForTxInBlock(block) + }) + it('should emit \'txConfirmed\' if the tx is in the block', function (done) { + const block = { transactions: [txMeta]} + pendingTxWatcher.getPendingTransactions = () => [txMeta] + pendingTxWatcher.once('txConfirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxWatcher.once('txFailed', (_, err) => { done(err) }) + pendingTxWatcher.checkForTxInBlock(block) + }) + }) + describe('#queryPendingTxs', function () { + it('should call #_checkPendingTxs if their is no oldBlock', function (done) { + let newBlock, oldBlock + newBlock = { number: '0x01' } + pendingTxWatcher._checkPendingTxs = done + pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) + }) + it('should call #_checkPendingTxs if oldBlock and the newBlock have a diff of greater then 1', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x01' } + newBlock = { number: '0x03' } + pendingTxWatcher._checkPendingTxs = done + pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) + }) + it('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less', function (done) { + let newBlock, oldBlock + oldBlock = { number: '0x1' } + newBlock = { number: '0x2' } + pendingTxWatcher._checkPendingTxs = () => { + const err = new Error('should not call #_checkPendingTxs if oldBlock and the newBlock have a diff of 1 or less') + done(err) + } + pendingTxWatcher.queryPendingTxs({oldBlock, newBlock}) + done() + }) + }) + + describe('#_checkPendingTx', function () { + it('should emit \'txFailed\' if the txMeta does not have a hash', function (done) { + pendingTxWatcher.once('txFailed', (txId, err) => { + assert(txId, txMetaNoHash.id, 'should pass txId') + done() + }) + pendingTxWatcher._checkPendingTx(txMetaNoHash) + }) + + it('should should return if query does not return txParams', function () { + pendingTxWatcher.query.stubResult('getTransactionByHash', null) + pendingTxWatcher._checkPendingTx(txMeta) + }) + + it('should emit \'txConfirmed\'', function (done) { + pendingTxWatcher.query.stubResult('getTransactionByHash', {blockNumber: '0x01'}) + pendingTxWatcher.once('txConfirmed', (txId) => { + assert(txId, txMeta.id, 'should pass txId') + done() + }) + pendingTxWatcher.once('txFailed', (_, err) => { done(err) }) + pendingTxWatcher._checkPendingTx(txMeta) + }) + }) + + describe('#_checkPendingTxs', function () { + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txMeta2.id = 2 + txMeta3.id = 3 + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should warp all txMeta\'s in #_checkPendingTx', function (done) { + pendingTxWatcher.getPendingTransactions = () => txList + pendingTxWatcher._checkPendingTx = (tx) => { tx.resolve(tx) } + const list = txList.map + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxWatcher._checkPendingTxs() + }) + }) + + describe('#resubmitPendingTxs', function () { + beforeEach(function () { + const txMeta2 = txMeta3 = txMeta + txList = [txMeta, txMeta2, txMeta3].map((tx) => { + tx.processed = new Promise ((resolve) => { tx.resolve = resolve }) + return tx + }) + }) + + it('should return if no pending transactions', function () { + pendingTxWatcher.resubmitPendingTxs() + }) + it('should call #_resubmitTx for all pending tx\'s', function (done) { + pendingTxWatcher.getPendingTransactions = () => txList + pendingTxWatcher._resubmitTx = async (tx) => { tx.resolve(tx) } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + pendingTxWatcher.resubmitPendingTxs() + }) + it('should not emit \'txFailed\' if the txMeta throws a known txError', function (done) { + knownErrors =[ + // geth + ' Replacement transaction Underpriced ', + ' known transaction', + // parity + 'Gas price too low to replace ', + ' transaction with the sAme hash was already imported', + // other + ' gateway timeout', + ' noncE too low ', + ] + const enoughForAllErrors = txList.concat(txList) + + pendingTxWatcher.on('txFailed', (_, err) => done(err)) + + pendingTxWatcher.getPendingTransactions = () => enoughForAllErrors + pendingTxWatcher._resubmitTx = async (tx) => { + tx.resolve() + throw new Error(knownErrors.pop()) + } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxWatcher.resubmitPendingTxs() + }) + it('should emit \'txFailed\' if it encountered a real error', function (done) { + pendingTxWatcher.once('txFailed', (id, err) => err.message === 'im some real error' ? txList[id - 1].resolve() : done(err)) + + pendingTxWatcher.getPendingTransactions = () => txList + pendingTxWatcher._resubmitTx = async (tx) => { throw new TypeError('im some real error') } + Promise.all(txList.map((tx) => tx.processed)) + .then((txCompletedList) => done()) + .catch(done) + + pendingTxWatcher.resubmitPendingTxs() + }) + }) + describe.only('#_resubmitTx with a too-low balance', function () { + it('should return before publishing the transaction because to low of balance', function (done) { + const lowBalance = '0x0' + pendingTxWatcher.getBalance = (address) => { + assert.equal(address, txMeta.txParams.from, 'Should pass the address') + return lowBalance + } + pendingTxWatcher.publishTransaction = async (rawTx) => { + done(new Error('tried to publish transaction')) + } + + // Stubbing out current account state: + // Adding the fake tx: + pendingTxWatcher.once('txFailed', (txId, err) => { + assert(err, 'Should have a error') + done() + }) + pendingTxWatcher._resubmitTx(txMeta) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + }) + + it('should publishing the transaction', function (done) { + const enoughBalance = '0x100000' + pendingTxWatcher.getBalance = (address) => { + assert.equal(address, txMeta.txParams.from, 'Should pass the address') + return enoughBalance + } + pendingTxWatcher.publishTransaction = async (rawTx) => { + assert.equal(rawTx, txMeta.rawTx, 'Should pass the rawTx') + } + + // Stubbing out current account state: + // Adding the fake tx: + pendingTxWatcher._resubmitTx(txMeta) + .then(() => done()) + .catch((err) => { + assert.ifError(err, 'should not throw an error') + done(err) + }) + }) + }) +})
\ No newline at end of file diff --git a/test/unit/tx-controller-test.js b/test/unit/tx-controller-test.js index f290088a1..e54dc9719 100644 --- a/test/unit/tx-controller-test.js +++ b/test/unit/tx-controller-test.js @@ -27,7 +27,8 @@ describe('Transaction Controller', function () { }), }) txController.nonceTracker.getNonceLock = () => Promise.resolve({ nextNonce: 0, releaseLock: noop }) - txController.query = new Proxy({}, { + txController.txProviderUtils = new TxProvideUtils(txController.provider) + txController.query = txController.txProviderUtils.query = new Proxy({}, { get: (queryStubResult, key) => { if (key === 'stubResult') { return function (method, ...args) { @@ -39,7 +40,7 @@ describe('Transaction Controller', function () { } }, }) - txController.txProviderUtils = new TxProvideUtils(txController.query) + }) describe('#newUnapprovedTransaction', function () { @@ -76,7 +77,6 @@ describe('Transaction Controller', function () { it('should resolve when finished and status is submitted and resolve with the hash', function (done) { txController.once('newUnaprovedTx', (txMetaFromEmit) => { setTimeout(() => { - console.log('HELLLO') txController.setTxHash(txMetaFromEmit.id, '0x0') txController.setTxStatusSubmitted(txMetaFromEmit.id) }, 10) @@ -93,7 +93,6 @@ describe('Transaction Controller', function () { it('should reject when finished and status is rejected', function (done) { txController.once('newUnaprovedTx', (txMetaFromEmit) => { setTimeout(() => { - console.log('HELLLO') txController.setTxStatusRejected(txMetaFromEmit.id) }, 10) }) @@ -429,46 +428,4 @@ describe('Transaction Controller', function () { }).catch(done) }) }) - - describe('#_resubmitTx with a too-low balance', function () { - it('should fail the transaction', function (done) { - const from = '0xda0da0' - const txMeta = { - id: 1, - status: 'submitted', - metamaskNetworkId: currentNetworkId, - txParams: { - from, - nonce: '0x1', - value: '0xfffff', - }, - } - - const lowBalance = '0x0' - const fakeStoreState = { accounts: {} } - fakeStoreState.accounts[from] = { - balance: lowBalance, - nonce: '0x0', - } - - // Stubbing out current account state: - const getStateStub = sinon.stub(txController.ethStore, 'getState') - .returns(fakeStoreState) - - // Adding the fake tx: - txController.addTx(clone(txMeta)) - - txController._resubmitTx(txMeta) - .then(() => { - const updatedMeta = txController.getTx(txMeta.id) - assert.notEqual(updatedMeta.status, txMeta.status, 'status changed.') - assert.equal(updatedMeta.status, 'failed', 'tx set to failed.') - done() - }) - .catch((err) => { - assert.ifError(err, 'should not throw an error') - done(err) - }) - }) - }) })
\ No newline at end of file diff --git a/test/unit/tx-utils-test.js b/test/unit/tx-utils-test.js index a43bcfb35..43128b977 100644 --- a/test/unit/tx-utils-test.js +++ b/test/unit/tx-utils-test.js @@ -1,7 +1,7 @@ const assert = require('assert') -const ethUtil = require('ethereumjs-util') -const BN = ethUtil.BN +const BN = require('bn.js') +const { hexToBn, bnToHex } = require('../../app/scripts/lib/util') const TxUtils = require('../../app/scripts/lib/tx-utils') @@ -16,44 +16,6 @@ describe('txUtils', function () { })) }) - describe('#sufficientBalance', function () { - it('returns true if max tx cost is equal to balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x8' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(result, 'sufficient balance found.') - }) - - it('returns true if max tx cost is less than balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x9' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(result, 'sufficient balance found.') - }) - - it('returns false if max tx cost is more than balance.', function () { - const tx = { - 'value': '0x1', - 'gas': '0x2', - 'gasPrice': '0x3', - } - const balance = '0x6' - - const result = txUtils.sufficientBalance(tx, balance) - assert.ok(!result, 'insufficient balance found.') - }) - }) - describe('chain Id', function () { it('prepares a transaction with the provided chainId', function () { const txParams = { @@ -96,7 +58,7 @@ describe('txUtils', function () { assert(outputBn.eq(expectedBn), 'returns the original estimatedGas value') }) - it('buffers up to reccomend gas limit reccomended ceiling', function () { + it('buffers up to recommend gas limit recommended ceiling', function () { // naive estimatedGas: 0x16e360 (1.5 mil) const inputHex = '0x16e360' // dummy gas limit: 0x1e8480 (2 mil) @@ -107,17 +69,7 @@ describe('txUtils', function () { // const inputBn = hexToBn(inputHex) // const outputBn = hexToBn(output) const expectedHex = bnToHex(ceilGasLimitBn) - assert.equal(output, expectedHex, 'returns the gas limit reccomended ceiling value') + assert.equal(output, expectedHex, 'returns the gas limit recommended ceiling value') }) }) -}) - -// util - -function hexToBn (inputHex) { - return new BN(ethUtil.stripHexPrefix(inputHex), 16) -} - -function bnToHex (inputBn) { - return ethUtil.addHexPrefix(inputBn.toString(16)) -} +})
\ No newline at end of file diff --git a/test/unit/util-test.js b/test/unit/util-test.js new file mode 100644 index 000000000..6da185b2c --- /dev/null +++ b/test/unit/util-test.js @@ -0,0 +1,41 @@ +const assert = require('assert') +const { sufficientBalance } = require('../../app/scripts/lib/util') + + +describe('SufficientBalance', function () { + it('returns true if max tx cost is equal to balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x8' + + const result = sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns true if max tx cost is less than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x9' + + const result = sufficientBalance(tx, balance) + assert.ok(result, 'sufficient balance found.') + }) + + it('returns false if max tx cost is more than balance.', function () { + const tx = { + 'value': '0x1', + 'gas': '0x2', + 'gasPrice': '0x3', + } + const balance = '0x6' + + const result = sufficientBalance(tx, balance) + assert.ok(!result, 'insufficient balance found.') + }) +})
\ No newline at end of file diff --git a/ui/app/components/transaction-list-item.js b/ui/app/components/transaction-list-item.js index dbda66a31..01355abad 100644 --- a/ui/app/components/transaction-list-item.js +++ b/ui/app/components/transaction-list-item.js @@ -154,12 +154,21 @@ function failIfFailed (transaction) { if (transaction.status === 'rejected') { return h('span.error', ' (Rejected)') } - if (transaction.err) { + if (transaction.err || transaction.warning) { + const { err, warning = {} } = transaction + const errFirst = !!(( err && warning ) || err) + const message = errFirst ? err.message : warning.message + + errFirst ? err.message : warning.message + + return h(Tooltip, { - title: transaction.err.message, + title: message, position: 'bottom', }, [ - h('span.error', ' (Failed)'), + h(`span.${errFirst ? 'error' : 'warning'}`, + ` (${errFirst ? 'Failed' : 'Warning'})` + ), ]) } } |