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
|
const EthQuery = require('eth-query')
const assert = require('assert')
const Mutex = require('await-semaphore').Mutex
class NonceTracker {
constructor ({ provider, getPendingTransactions, getConfirmedTransactions }) {
this.provider = provider
this.ethQuery = new EthQuery(provider)
this.getPendingTransactions = getPendingTransactions
this.getConfirmedTransactions = getConfirmedTransactions
this.lockMap = {}
}
async getGlobalLock () {
const globalMutex = this._lookupMutex('global')
// await global mutex free
const releaseLock = await globalMutex.acquire()
return { releaseLock }
}
// releaseLock must be called
// releaseLock must be called after adding signed tx to pending transactions (or discarding)
async getNonceLock (address) {
// await global mutex free
await this._globalMutexFree()
// await lock free, then take lock
const releaseLock = await this._takeMutex(address)
const localNextNonce = this._getLocalNextNonce(address)
const nonceDetails = await this._getNetworkNonceAndDetails(address)
const networkNonce = nonceDetails.networkNonce
const nextNonce = Math.max(networkNonce, localNextNonce)
assert(Number.isInteger(nextNonce), `nonce-tracker - nextNonce is not an integer - got: (${typeof nextNonce}) "${nextNonce}"`)
// collect the numbers used to calculate the nonce for debugging
nonceDetails.localNextNonce = localNextNonce
// return nonce and release cb
return { nextNonce, nonceDetails, releaseLock }
}
async _getCurrentBlock () {
const blockTracker = this._getBlockTracker()
const currentBlock = blockTracker.getCurrentBlock()
if (currentBlock) return currentBlock
return await Promise((reject, resolve) => {
blockTracker.once('latest', resolve)
})
}
async _getTxCount (address, currentBlock) {
const blockNumber = currentBlock.number
return new Promise((resolve, reject) => {
this.ethQuery.getTransactionCount(address, blockNumber, (err, result) => {
err ? reject(err) : resolve(result)
})
})
}
async _globalMutexFree () {
const globalMutex = this._lookupMutex('global')
const release = await globalMutex.acquire()
release()
}
async _takeMutex (lockId) {
const mutex = this._lookupMutex(lockId)
const releaseLock = await mutex.acquire()
return releaseLock
}
_lookupMutex (lockId) {
let mutex = this.lockMap[lockId]
if (!mutex) {
mutex = new Mutex()
this.lockMap[lockId] = mutex
}
return mutex
}
async _getNetworkNonceAndDetails (address) {
// calculate next nonce
// we need to make sure our base count
// and pending count are from the same block
const currentBlock = await this._getCurrentBlock()
const blockNumber = currentBlock.blockNumber
const pendingNonce = this._getLocalPendingNonce(address)
const pendingCount = this._getPendingTransactionCount(address)
assert(Number.isInteger(pendingCount), `nonce-tracker - pendingCount is not an integer - got: (${typeof pendingCount}) "${pendingCount}"`)
const baseCountHex = await this._getTxCount(address, currentBlock)
const baseCount = parseInt(baseCountHex, 16)
assert(Number.isInteger(baseCount), `nonce-tracker - baseCount is not an integer - got: (${typeof baseCount}) "${baseCount}"`)
// if the nonce provided by the network is higher then a pending tx
// toss out the pending txCount
const networkNonce = pendingNonce > baseCount ? baseCount + pendingCount : baseCount
return {networkNonce, blockNumber, baseCountHex, baseCount, pendingCount, pendingNonce}
}
_getLocalNextNonce (address) {
const confirmedTransactions = this._reduceTxListToUniqueNonces(this.getConfirmedTransactions(address))
const pendingTransactions = this._reduceTxListToUniqueNonces(this.getPendingTransactions(address))
const transactions = this._reduceTxListToUniqueNonces(confirmedTransactions.concat(pendingTransactions))
let localNonce = this._getHighestNonce(transactions)
// throw out localNonce if not a number
if (!Number.isInteger(localNonce)) localNonce = 0
if (
// the local nonce is not 0
localNonce ||
// or their are pending or confirmed transactions
this._getPendingTransactionCount(address) ||
confirmedTransactions.length
) ++localNonce
return localNonce
}
_getLocalPendingNonce (address) {
const pendingTransactions = this._reduceTxListToUniqueNonces(this.getPendingTransactions(address))
const localNonce = this._getHighestNonce(pendingTransactions)
return localNonce
}
_getPendingTransactionCount (address) {
const pendingTransactions = this.getPendingTransactions(address)
return this._reduceTxListToUniqueNonces(pendingTransactions).length
}
_reduceTxListToUniqueNonces (txList) {
const reducedTxList = txList.reduce((reducedList, txMeta, index) => {
if (!index) return [txMeta]
const nonceMatches = txList.filter((txData) => {
return txMeta.txParams.nonce === txData.txParams.nonce
})
if (nonceMatches.length > 1) return reducedList
reducedList.push(txMeta)
return reducedList
}, [])
return reducedTxList
}
_getHighestNonce (txList) {
const nonces = txList.map((txMeta) => txMeta.txParams.nonce)
const nonceHex = nonces.reduce((highestNonce, nonce) => {
return parseInt(nonce, 16) > parseInt(highestNonce, 16) ? nonce : highestNonce
}, '0x0')
return parseInt(nonceHex, 16)
}
// this is a hotfix for the fact that the blockTracker will
// change when the network changes
_getBlockTracker () {
return this.provider._blockTracker
}
}
module.exports = NonceTracker
|