break out tx status pendding watchers

feature/default_network_editable
frankiebee 7 years ago
parent 54739cb798
commit 087cd9fb1a
  1. 172
      app/scripts/controllers/transactions.js
  2. 165
      app/scripts/lib/pending-tx-watchers.js

@ -5,6 +5,7 @@ const ObservableStore = require('obs-store')
const ethUtil = require('ethereumjs-util')
const EthQuery = require('ethjs-query')
const TxProviderUtil = require('../lib/tx-utils')
const PendingTransactionUtils = require('../lib/pending-tx-watchers')
const getStack = require('../lib/util').getStack
const createId = require('../lib/random-id')
const NonceTracker = require('../lib/nonce-tracker')
@ -21,6 +22,9 @@ module.exports = class TransactionController extends EventEmitter {
this.txHistoryLimit = opts.txHistoryLimit
this.provider = opts.provider
this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
this.ethStore = opts.ethStore
this.nonceTracker = new NonceTracker({
provider: this.provider,
getPendingTransactions: (address) => {
@ -32,15 +36,31 @@ module.exports = class TransactionController extends EventEmitter {
},
})
this.query = new EthQuery(this.provider)
this.txProviderUtils = new TxProviderUtil(this.query)
this.blockTracker.on('rawBlock', this.checkForTxInBlock.bind(this))
this.txProviderUtils = new TxProviderUtil(this.provider)
this.pendingTxUtils = new PendingTransactionUtils({
provider: this.provider,
nonceTracker: this.nonceTracker,
getBalance: (address) => this.ethStore.getState().accounts[address].balance,
publishTransaction: this.txProviderUtils.publishTransaction.bind(this.txProviderUtils),
getPendingTransactions: (address) => {
return this.getFilteredTxList({
from: address,
status: 'submitted',
})
},
})
this.pendingTxUtils.on('txWarning', this.updateTx.bind(this))
this.pendingTxUtils.on('txFailed', this.setTxStatusFailed.bind(this))
this.pendingTxUtils.on('txConfirmed', this.setTxStatusConfirmed.bind(this))
this.blockTracker.on('rawBlock', this.pendingTxUtils.checkForTxInBlock.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.ethStore = opts.ethStore
this.blockTracker.once('latest', () => this.blockTracker.on('latest', this.pendingTxUtils.resubmitPendingTxs.bind(this)))
this.blockTracker.on('sync', this.pendingTxUtils.queryPendingTxs.bind(this))
// memstore is computed from a few different stores
this._updateMemstore()
this.store.subscribe(() => this._updateMemstore())
@ -229,7 +249,7 @@ module.exports = class TransactionController extends EventEmitter {
// must set transaction to submitted/failed before releasing lock
nonceLock.releaseLock()
} catch (err) {
if(!err.message) err.message = 'Transaction failed during approval'
if (!err.message) err.message = 'Transaction failed during approval'
this.setTxStatusFailed(txId, err)
// must set transaction to submitted/failed before releasing lock
if (nonceLock) nonceLock.releaseLock()
@ -374,73 +394,6 @@ module.exports = class TransactionController extends EventEmitter {
this.updateTx(txMeta)
}
// checks if a signed tx is in a block and
// if included sets the tx status as 'confirmed'
checkForTxInBlock (block) {
const signedTxList = this.getFilteredTxList({status: 'submitted'})
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.setTxStatusFailed(txId, noTxHashErr)
}
block.transactions.forEach((tx) => {
if (tx.hash === txHash) this.setTxStatusConfirmed(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) - Number.parseInt(oldBlock.number)
if (diff > 1) this._checkPendingTxs()
}
resubmitPendingTxs () {
const pending = this.getTxsByMetaData('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.setTxStatusFailed(txMeta.id, {
stack: err.stack || err.message,
errCode: err.errCode || err,
message: err.message,
})
}))
}
/* _____________________________________
| |
| PRIVATE METHODS |
@ -482,75 +435,4 @@ module.exports = class TransactionController extends EventEmitter {
})
this.memStore.updateState({ unapprovedTxs, selectedAddressTxList })
}
async _resubmitTx (txMeta) {
const address = txMeta.txParams.from
const balance = this.ethStore.getState().accounts[address].balance
if (!('retryCount' in txMeta)) txMeta.retryCount = 0
// 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, {
stack: '_resubmitTx: custom tx-controller error',
message,
})
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.txProviderUtils.publishTransaction(rawTx)
}
// checks the network for signed txs and
// if confirmed sets the tx status as 'confirmed'
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 noTxHashErr = new Error('We had an error while submitting this transaction, please try again.')
noTxHashErr.name = 'NoTxHashError'
this.setTxStatusFailed(txId, noTxHashErr)
}
// get latest transaction status
let txParams
try {
txParams = await this.query.getTransactionByHash(txHash)
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)
throw err
}
}
}
}

@ -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…
Cancel
Save