aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/controllers/transactions.js
diff options
context:
space:
mode:
authorDan Finlay <dan@danfinlay.com>2017-07-21 03:39:06 +0800
committerDan Finlay <dan@danfinlay.com>2017-07-21 03:39:06 +0800
commita41e5788cf4a8e680fdbc4d16690a8f5cc8af265 (patch)
tree5c546e149ce0cf2d88bbef1d1645b124f11d0eb5 /app/scripts/controllers/transactions.js
parent86d367957fe8ac04462f716fe0ba2bfa4e5ff3f6 (diff)
parent9b24ab0e70d72471ddc928a763bc2096790ab966 (diff)
downloadtangerine-wallet-browser-a41e5788cf4a8e680fdbc4d16690a8f5cc8af265.tar.gz
tangerine-wallet-browser-a41e5788cf4a8e680fdbc4d16690a8f5cc8af265.tar.zst
tangerine-wallet-browser-a41e5788cf4a8e680fdbc4d16690a8f5cc8af265.zip
Merge branch 'master' into NewUI
Diffstat (limited to 'app/scripts/controllers/transactions.js')
-rw-r--r--app/scripts/controllers/transactions.js223
1 files changed, 139 insertions, 84 deletions
diff --git a/app/scripts/controllers/transactions.js b/app/scripts/controllers/transactions.js
index 52251d66e..5f3d84ebe 100644
--- a/app/scripts/controllers/transactions.js
+++ b/app/scripts/controllers/transactions.js
@@ -1,12 +1,12 @@
const EventEmitter = require('events')
const async = require('async')
const extend = require('xtend')
-const Semaphore = require('semaphore')
const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
+const pify = require('pify')
const TxProviderUtil = require('../lib/tx-utils')
const createId = require('../lib/random-id')
-const denodeify = require('denodeify')
+const NonceTracker = require('../lib/nonce-tracker')
module.exports = class TransactionController extends EventEmitter {
constructor (opts) {
@@ -20,13 +20,26 @@ module.exports = class TransactionController extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider
this.blockTracker = opts.blockTracker
+ this.nonceTracker = new NonceTracker({
+ provider: this.provider,
+ blockTracker: this.provider._blockTracker,
+ getPendingTransactions: (address) => {
+ return this.getFilteredTxList({
+ from: address,
+ status: 'submitted',
+ err: undefined,
+ })
+ },
+ })
this.query = opts.ethQuery
this.txProviderUtils = new TxProviderUtil(this.query)
this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this))
- this.blockTracker.on('latest', this.resubmitPendingTxs.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.nonceLock = Semaphore(1)
this.ethStore = opts.ethStore
// memstore is computed from a few different stores
this._updateMemstore()
@@ -170,29 +183,32 @@ module.exports = class TransactionController extends EventEmitter {
}, {})
}
- approveTransaction (txId, cb = warn) {
- const self = this
- // approve
- self.setTxStatusApproved(txId)
- // only allow one tx at a time for atomic nonce usage
- self.nonceLock.take(() => {
- // begin signature process
- async.waterfall([
- (cb) => self.fillInTxParams(txId, cb),
- (cb) => self.signTransaction(txId, cb),
- (rawTx, cb) => self.publishTransaction(txId, rawTx, cb),
- ], (err) => {
- self.nonceLock.leave()
- if (err) {
- this.setTxStatusFailed(txId, {
- errCode: err.errCode || err,
- message: err.message || 'Transaction failed during approval',
- })
- return cb(err)
- }
- cb()
+ async approveTransaction (txId) {
+ let nonceLock
+ try {
+ // approve
+ this.setTxStatusApproved(txId)
+ // get next nonce
+ const txMeta = this.getTx(txId)
+ const fromAddress = txMeta.txParams.from
+ nonceLock = await this.nonceTracker.getNonceLock(fromAddress)
+ txMeta.txParams.nonce = nonceLock.nextNonce
+ this.updateTx(txMeta)
+ // sign transaction
+ const rawTx = await this.signTransaction(txId)
+ await this.publishTransaction(txId, rawTx)
+ // must set transaction to submitted/failed before releasing lock
+ nonceLock.releaseLock()
+ } catch (err) {
+ this.setTxStatusFailed(txId, {
+ errCode: err.errCode || err,
+ message: err.message || 'Transaction failed during approval',
})
- })
+ // must set transaction to submitted/failed before releasing lock
+ if (nonceLock) nonceLock.releaseLock()
+ // continue with error chain
+ throw err
+ }
}
cancelTransaction (txId, cb = warn) {
@@ -200,13 +216,9 @@ module.exports = class TransactionController extends EventEmitter {
cb()
}
- fillInTxParams (txId, cb) {
- const txMeta = this.getTx(txId)
- this.txProviderUtils.fillInTxParams(txMeta.txParams, (err) => {
- if (err) return cb(err)
- this.updateTx(txMeta)
- cb()
- })
+ async updateAndApproveTransaction (txMeta) {
+ this.updateTx(txMeta)
+ await this.approveTransaction(txMeta.id)
}
getChainId () {
@@ -219,31 +231,27 @@ module.exports = class TransactionController extends EventEmitter {
}
}
- signTransaction (txId, cb) {
+ async signTransaction (txId) {
const txMeta = this.getTx(txId)
const txParams = txMeta.txParams
const fromAddress = txParams.from
// add network/chain id
txParams.chainId = this.getChainId()
const ethTx = this.txProviderUtils.buildEthTxFromParams(txParams)
- this.signEthTx(ethTx, fromAddress).then(() => {
+ const rawTx = await this.signEthTx(ethTx, fromAddress).then(() => {
this.setTxStatusSigned(txMeta.id)
- cb(null, ethUtil.bufferToHex(ethTx.serialize()))
- }).catch((err) => {
- cb(err)
+ return ethUtil.bufferToHex(ethTx.serialize())
})
+ return rawTx
}
- publishTransaction (txId, rawTx, cb = warn) {
+ async publishTransaction (txId, rawTx) {
const txMeta = this.getTx(txId)
txMeta.rawTx = rawTx
this.updateTx(txMeta)
-
- this.txProviderUtils.publishTransaction(rawTx, (err, txHash) => {
- if (err) return cb(err)
+ await this.txProviderUtils.publishTransaction(rawTx).then((txHash) => {
this.setTxHash(txId, txHash)
this.setTxStatusSubmitted(txId)
- cb()
})
}
@@ -261,10 +269,19 @@ module.exports = class TransactionController extends EventEmitter {
to: '0x0..',
from: '0x0..',
status: 'signed',
+ err: undefined,
}
and returns a list of tx with all
options matching
+ ****************HINT****************
+ | `err: undefined` is like looking |
+ | for a tx with no err |
+ | so you can also search txs that |
+ | dont have something as well by |
+ | setting the value as undefined |
+ ************************************
+
this is for things like filtering a the tx list
for only tx's from 1 account
or for filltering for all txs from one account
@@ -413,65 +430,103 @@ module.exports = class TransactionController extends EventEmitter {
const pending = this.getTxsByMetaData('status', 'submitted')
// only try resubmitting if their are transactions to resubmit
if (!pending.length) return
- const resubmit = denodeify(this._resubmitTx.bind(this))
- Promise.all(pending.map(txMeta => resubmit(txMeta)))
- .catch((reason) => {
- log.info('Problem resubmitting tx', reason)
- })
+ 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, {
+ errCode: err.errCode || err,
+ message: err.message,
+ })
+ }))
}
- _resubmitTx (txMeta, cb) {
+ async _resubmitTx (txMeta, cb) {
const address = txMeta.txParams.from
const balance = this.ethStore.getState().accounts[address].balance
- const nonce = Number.parseInt(this.ethStore.getState().accounts[address].nonce)
- const txNonce = Number.parseInt(txMeta.txParams.nonce)
- const gtBalance = Number.parseInt(txMeta.txParams.value) > Number.parseInt(balance)
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
- // if the value of the transaction is greater then the balance
- // or the nonce of the transaction is lower then the accounts nonce
- // dont resubmit the tx
- if (gtBalance || txNonce < nonce) return cb()
+ // 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, { message })
+ cb()
+ return log.error(message)
+ }
+
// Only auto-submit already-signed txs:
if (!('rawTx' in txMeta)) return cb()
// Increment a try counter.
txMeta.retryCount++
const rawTx = txMeta.rawTx
- this.txProviderUtils.publishTransaction(rawTx, cb)
+ return await this.txProviderUtils.publishTransaction(rawTx, cb)
}
// checks the network for signed txs and
// if confirmed sets the tx status as 'confirmed'
- _checkPendingTxs () {
- var signedTxList = this.getFilteredTxList({status: 'submitted'})
- if (!signedTxList.length) return
- signedTxList.forEach((txMeta) => {
- var txHash = txMeta.hash
- var txId = txMeta.id
- if (!txHash) {
- const errReason = {
- errCode: 'No hash was provided',
- message: 'We had an error while submitting this transaction, please try again.',
- }
- return this.setTxStatusFailed(txId, errReason)
+ 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 errReason = {
+ errCode: 'No hash was provided',
+ message: 'We had an error while submitting this transaction, please try again.',
}
- this.query.getTransactionByHash(txHash, (err, txParams) => {
- if (err || !txParams) {
- if (!txParams) return
- txMeta.err = {
- isWarning: true,
- errorCode: err,
- message: 'There was a problem loading this transaction.',
- }
- this.updateTx(txMeta)
- return log.error(err)
- }
- if (txParams.blockNumber) {
- this.setTxStatusConfirmed(txId)
+ this.setTxStatusFailed(txId, errReason)
+ return
+ }
+ // get latest transaction status
+ let txParams
+ try {
+ txParams = await pify((cb) => this.query.getTransactionByHash(txHash, cb))()
+ 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)
+ log.error(err)
+ }
+ }
}
}