aboutsummaryrefslogtreecommitdiffstats
path: root/app/scripts/lib/pending-tx-tracker.js
diff options
context:
space:
mode:
Diffstat (limited to 'app/scripts/lib/pending-tx-tracker.js')
-rw-r--r--app/scripts/lib/pending-tx-tracker.js163
1 files changed, 163 insertions, 0 deletions
diff --git a/app/scripts/lib/pending-tx-tracker.js b/app/scripts/lib/pending-tx-tracker.js
new file mode 100644
index 000000000..6921997b2
--- /dev/null
+++ b/app/scripts/lib/pending-tx-tracker.js
@@ -0,0 +1,163 @@
+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 PendingTransactionTracker 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 (balance === undefined) return
+ 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