|
|
|
const EventEmitter = require('events')
|
|
|
|
const EthQuery = require('ethjs-query')
|
|
|
|
/*
|
|
|
|
|
|
|
|
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,
|
|
|
|
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.retryLimit = config.retryLimit || Infinity
|
|
|
|
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('tx:failed', txId, noTxHashErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
block.transactions.forEach((tx) => {
|
|
|
|
if (tx.hash === txHash) this.emit('tx:confirmed', 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"
|
|
|
|
|
|
|
|
Also don't mark as failed if it has ever been broadcast successfully.
|
|
|
|
A successful broadcast means it may still be mined.
|
|
|
|
*/
|
|
|
|
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')
|
|
|
|
|| txMeta.retryCount > 1
|
|
|
|
)
|
|
|
|
// ignore resubmit warnings, return early
|
|
|
|
if (isKnownTx) return
|
|
|
|
// encountered real error - transition to error state
|
|
|
|
this.emit('tx:failed', txMeta.id, err)
|
|
|
|
}))
|
|
|
|
}
|
|
|
|
|
|
|
|
async _resubmitTx (txMeta) {
|
|
|
|
if (txMeta.retryCount > this.retryLimit) {
|
|
|
|
const err = new Error(`Gave up submitting after ${this.retryLimit} blocks un-mined.`)
|
|
|
|
return this.emit('tx:failed', txMeta.id, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Only auto-submit already-signed txs:
|
|
|
|
if (!('rawTx' in txMeta)) return
|
|
|
|
|
|
|
|
const rawTx = txMeta.rawTx
|
|
|
|
const txHash = await this.publishTransaction(rawTx)
|
|
|
|
|
|
|
|
// Increment successful tries:
|
|
|
|
this.emit('tx:retry', txMeta)
|
|
|
|
return txHash
|
|
|
|
}
|
|
|
|
|
|
|
|
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('tx:failed', txId, noTxHashErr)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// get latest transaction status
|
|
|
|
let txParams
|
|
|
|
try {
|
|
|
|
txParams = await this.query.getTransactionByHash(txHash)
|
|
|
|
if (!txParams) return
|
|
|
|
if (txParams.blockNumber) {
|
|
|
|
this.emit('tx:confirmed', txId)
|
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
txMeta.warning = {
|
|
|
|
error: err,
|
|
|
|
message: 'There was a problem loading this transaction.',
|
|
|
|
}
|
|
|
|
this.emit('tx:warning', txMeta)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
}
|