1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
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
// default is one day
this.retryTimePeriod = config.retryTimePeriod || 86400000
this.getPendingTransactions = config.getPendingTransactions
this.getCompletedTransactions = config.getCompletedTransactions
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')
)
// ignore resubmit warnings, return early
if (isKnownTx) return
// encountered real error - transition to error state
txMeta.warning = {
error: errorMessage,
message: 'There was an error when resubmitting this transaction.',
}
this.emit('tx:warning', txMeta, err)
}))
}
async _resubmitTx (txMeta) {
if (Date.now() > txMeta.time + this.retryTimePeriod) {
const hours = (this.retryTimePeriod / 3.6e+6).toFixed(1)
const err = new Error(`Gave up submitting after ${hours} hours.`)
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
}
// If another tx with the same nonce is mined, set as failed.
const taken = await this._checkIfNonceIsTaken(txMeta)
if (taken) {
const nonceTakenErr = new Error('Another transaction with this nonce has been mined.')
nonceTakenErr.name = 'NonceTakenErr'
return this.emit('tx:failed', txId, nonceTakenErr)
}
// 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,
message: 'There was a problem loading this transaction.',
}
this.emit('tx:warning', txMeta, 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()
}
async _checkIfNonceIsTaken (txMeta) {
const completed = this.getCompletedTransactions()
const sameNonce = completed.filter((otherMeta) => {
return otherMeta.txParams.nonce === txMeta.txParams.nonce
})
return sameNonce.length > 0
}
}
|