aboutsummaryrefslogblamecommitdiffstats
path: root/app/scripts/lib/pending-tx-tracker.js
blob: 19720db3f8a22abfaf48eabe072e4263595ee44c (plain) (tree)




















                                                                                   
                                                                       













































                                                                                                             
                                                 





























                                                                                       
                                     



                                                                         

                                                                                          
                                       























































                                                                                                           
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()
    // 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()
  }
}