-
Notifications
You must be signed in to change notification settings - Fork 7
/
TransactionManager.ts
152 lines (143 loc) · 4.67 KB
/
TransactionManager.ts
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
import { ethers } from 'ethers'
import { DB } from 'anondb'
export class TransactionManager {
wallet?: ethers.Wallet
db?: DB
configure(wallet: ethers.Wallet, db: DB) {
this.wallet = wallet
this.db = db
}
async start() {
if (!this.wallet || !this.db) throw new Error('Not initialized')
const latestNonce = await this.wallet.getTransactionCount()
await this.db.upsert('AccountNonce', {
where: {
address: this.wallet.address,
},
create: {
address: this.wallet.address,
nonce: latestNonce,
},
update: {},
})
this.startDaemon()
}
async startDaemon() {
if (!this.db) throw new Error('No db connected')
for (;;) {
const nextTx = await this.db.findOne('AccountTransaction', {
where: {},
orderBy: {
nonce: 'asc',
},
})
if (!nextTx) {
await new Promise((r) => setTimeout(r, 5000))
continue
}
const sent = await this.tryBroadcastTransaction(nextTx.signedData)
if (sent) {
await this.db.delete('AccountTransaction', {
where: {
signedData: nextTx.signedData,
},
})
} else {
const randWait = Math.random() * 2000
await new Promise((r) => setTimeout(r, 1000 + randWait))
}
}
}
async tryBroadcastTransaction(signedData: string) {
if (!this.wallet) throw new Error('Not initialized')
try {
console.log(`Sending tx ${ethers.utils.keccak256(signedData)}`)
await this.wallet.provider.sendTransaction(signedData)
return true
} catch (err: any) {
if (
err
.toString()
.indexOf('VM Exception while processing transaction') !== -1
) {
// if the transaction is reverted the nonce is still used, so we return true
return true
} else if (
err
.toString()
.indexOf(
'Your app has exceeded its compute units per second capacity'
) !== -1
) {
await new Promise((r) => setTimeout(r, 1000))
return this.tryBroadcastTransaction(signedData)
} else {
console.log(err)
return false
}
}
}
async getNonce(address: string) {
const latest = await this.db?.findOne('AccountNonce', {
where: {
address,
},
})
const updated = await this.db?.update('AccountNonce', {
where: {
address,
nonce: latest.nonce,
},
update: {
nonce: latest.nonce + 1,
},
})
if (updated === 0) {
await new Promise((r) => setTimeout(r, Math.random() * 500))
return this.getNonce(address)
}
return latest.nonce
}
async wait(hash: string) {
return this.wallet?.provider.waitForTransaction(hash)
}
async queueTransaction(to: string, data: string | any = {}) {
const args = {} as any
if (typeof data === 'string') {
// assume it's input data
args.data = data
} else {
Object.assign(args, data)
}
if (!this.wallet) throw new Error('Not initialized')
if (!args.gasLimit) {
// don't estimate, use this for unpredictable gas limit tx's
// transactions may revert with this
const gasLimit = await this.wallet.provider.estimateGas({
to,
from: this.wallet.address,
...args,
})
Object.assign(args, {
gasLimit: gasLimit.add(50000),
})
}
const nonce = await this.getNonce(this.wallet.address)
const { chainId } = await this.wallet.provider.getNetwork()
const signedData = await this.wallet.signTransaction({
nonce,
to,
gasPrice: 2 * 10 ** 9, // 2 gwei
chainId,
// gasPrice: 10000,
...args,
})
await this.db?.create('AccountTransaction', {
address: this.wallet.address,
signedData,
nonce,
})
return ethers.utils.keccak256(signedData)
}
}
export default new TransactionManager()