parent
54739cb798
commit
087cd9fb1a
@ -0,0 +1,165 @@ |
||||
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 PendingTransactionWatcher 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 message = 'Insufficient balance during rebroadcast.' |
||||
txMeta.warning = { |
||||
message, |
||||
} |
||||
this.emit('txWarning', txMeta) |
||||
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.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() |
||||
} |
||||
} |
Loading…
Reference in new issue