@atomiqlabs/chain-evm 1.0.0-dev.65 → 1.0.0-dev.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/evm/chain/modules/EVMFees.d.ts +1 -1
- package/dist/evm/chain/modules/EVMFees.js +1 -1
- package/dist/evm/chain/modules/EVMTransactions.d.ts +9 -4
- package/dist/evm/chain/modules/EVMTransactions.js +84 -38
- package/dist/evm/wallet/EVMBrowserSigner.d.ts +5 -0
- package/dist/evm/wallet/EVMBrowserSigner.js +11 -0
- package/dist/evm/wallet/EVMPersistentSigner.d.ts +28 -0
- package/dist/evm/wallet/EVMPersistentSigner.js +199 -0
- package/dist/evm/wallet/EVMSigner.d.ts +5 -4
- package/dist/evm/wallet/EVMSigner.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/utils/Utils.d.ts +1 -0
- package/dist/utils/Utils.js +5 -1
- package/package.json +1 -1
- package/src/evm/chain/modules/EVMFees.ts +1 -1
- package/src/evm/chain/modules/EVMTransactions.ts +95 -40
- package/src/evm/wallet/EVMBrowserSigner.ts +12 -0
- package/src/evm/wallet/EVMPersistentSigner.ts +276 -0
- package/src/evm/wallet/EVMSigner.ts +14 -8
- package/src/index.ts +1 -0
- package/src/utils/Utils.ts +4 -0
|
@@ -20,7 +20,7 @@ export declare class EVMFees {
|
|
|
20
20
|
*/
|
|
21
21
|
private _getFeeRate;
|
|
22
22
|
/**
|
|
23
|
-
* Gets the gas price with caching, format: <
|
|
23
|
+
* Gets the gas price with caching, format: <base fee Wei>,<priority fee Wei>
|
|
24
24
|
*
|
|
25
25
|
* @private
|
|
26
26
|
*/
|
|
@@ -16,11 +16,14 @@ export type EVMTxTrace = {
|
|
|
16
16
|
};
|
|
17
17
|
export declare class EVMTransactions extends EVMModule<any> {
|
|
18
18
|
private readonly latestConfirmedNonces;
|
|
19
|
-
private
|
|
19
|
+
private readonly latestPendingNonces;
|
|
20
|
+
private readonly latestSignedNonces;
|
|
21
|
+
readonly _cbksBeforeTxReplace: ((oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>)[];
|
|
22
|
+
private readonly cbksBeforeTxSigned;
|
|
20
23
|
private cbkSendTransaction;
|
|
24
|
+
readonly _knownTxSet: Set<string>;
|
|
21
25
|
/**
|
|
22
|
-
* Waits for transaction confirmation using
|
|
23
|
-
* the transaction at regular interval
|
|
26
|
+
* Waits for transaction confirmation using HTTP polling
|
|
24
27
|
*
|
|
25
28
|
* @param tx EVM transaction to wait for confirmation for
|
|
26
29
|
* @param abortSignal signal to abort waiting for tx confirmation
|
|
@@ -76,7 +79,7 @@ export declare class EVMTransactions extends EVMModule<any> {
|
|
|
76
79
|
*/
|
|
77
80
|
getTxStatus(tx: string): Promise<"pending" | "success" | "not_found" | "reverted">;
|
|
78
81
|
/**
|
|
79
|
-
* Gets the status of the
|
|
82
|
+
* Gets the status of the EVM transaction with a specific txId
|
|
80
83
|
*
|
|
81
84
|
* @param txId
|
|
82
85
|
*/
|
|
@@ -85,5 +88,7 @@ export declare class EVMTransactions extends EVMModule<any> {
|
|
|
85
88
|
offBeforeTxSigned(callback: (tx: TransactionRequest) => Promise<void>): boolean;
|
|
86
89
|
onSendTransaction(callback: (tx: string) => Promise<string>): void;
|
|
87
90
|
offSendTransaction(callback: (tx: string) => Promise<string>): boolean;
|
|
91
|
+
onBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): void;
|
|
92
|
+
offBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): boolean;
|
|
88
93
|
traceTransaction(txId: string): Promise<EVMTxTrace>;
|
|
89
94
|
}
|
|
@@ -9,29 +9,40 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
9
9
|
constructor() {
|
|
10
10
|
super(...arguments);
|
|
11
11
|
this.latestConfirmedNonces = {};
|
|
12
|
+
this.latestPendingNonces = {};
|
|
13
|
+
this.latestSignedNonces = {};
|
|
14
|
+
this._cbksBeforeTxReplace = [];
|
|
15
|
+
this.cbksBeforeTxSigned = [];
|
|
16
|
+
this._knownTxSet = new Set();
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
|
-
* Waits for transaction confirmation using
|
|
15
|
-
* the transaction at regular interval
|
|
19
|
+
* Waits for transaction confirmation using HTTP polling
|
|
16
20
|
*
|
|
17
21
|
* @param tx EVM transaction to wait for confirmation for
|
|
18
22
|
* @param abortSignal signal to abort waiting for tx confirmation
|
|
19
23
|
* @private
|
|
20
24
|
*/
|
|
21
25
|
async confirmTransaction(tx, abortSignal) {
|
|
26
|
+
const checkTxns = new Set([tx.hash]);
|
|
27
|
+
const txReplaceListener = (oldTx, oldTxId, newTx, newTxId) => {
|
|
28
|
+
if (checkTxns.has(oldTxId))
|
|
29
|
+
checkTxns.add(newTxId);
|
|
30
|
+
return Promise.resolve();
|
|
31
|
+
};
|
|
32
|
+
this.onBeforeTxReplace(txReplaceListener);
|
|
22
33
|
let state = "pending";
|
|
23
34
|
while (state === "pending" || state === "not_found") {
|
|
24
35
|
await (0, Utils_1.timeoutPromise)(3000, abortSignal);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// });
|
|
36
|
+
for (let txId of checkTxns) {
|
|
37
|
+
state = await this.getTxIdStatus(txId);
|
|
38
|
+
if (state === "reverted" || state === "success")
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
31
41
|
}
|
|
42
|
+
this.offBeforeTxReplace(txReplaceListener);
|
|
32
43
|
const nextAccountNonce = tx.nonce + 1;
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
44
|
+
const currentConfirmedNonce = this.latestConfirmedNonces[tx.from];
|
|
45
|
+
if (currentConfirmedNonce == null || nextAccountNonce > currentConfirmedNonce) {
|
|
35
46
|
this.latestConfirmedNonces[tx.from] = nextAccountNonce;
|
|
36
47
|
}
|
|
37
48
|
if (state === "reverted")
|
|
@@ -45,26 +56,33 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
45
56
|
* @private
|
|
46
57
|
*/
|
|
47
58
|
async prepareTransactions(signer, txs) {
|
|
48
|
-
|
|
49
|
-
const latestConfirmedNonce = this.latestConfirmedNonces[signer.getAddress()];
|
|
50
|
-
if (latestConfirmedNonce != null && latestConfirmedNonce > nonce) {
|
|
51
|
-
this.logger.debug("prepareTransactions(): Using nonce from local cache!");
|
|
52
|
-
nonce = latestConfirmedNonce;
|
|
53
|
-
}
|
|
54
|
-
for (let i = 0; i < txs.length; i++) {
|
|
55
|
-
const tx = txs[i];
|
|
59
|
+
for (let tx of txs) {
|
|
56
60
|
tx.chainId = this.root.evmChainId;
|
|
57
61
|
tx.from = signer.getAddress();
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
}
|
|
63
|
+
if (!signer.isManagingNoncesInternally) {
|
|
64
|
+
let nonce = await this.root.provider.getTransactionCount(signer.getAddress(), "pending");
|
|
65
|
+
const latestKnownNonce = this.latestPendingNonces[signer.getAddress()];
|
|
66
|
+
if (latestKnownNonce != null && latestKnownNonce > nonce) {
|
|
67
|
+
this.logger.debug("prepareTransactions(): Using nonce from local cache!");
|
|
68
|
+
nonce = latestKnownNonce;
|
|
69
|
+
}
|
|
70
|
+
for (let i = 0; i < txs.length; i++) {
|
|
71
|
+
const tx = txs[i];
|
|
72
|
+
if (tx.nonce != null)
|
|
73
|
+
nonce = tx.nonce; //Take the nonce from last tx
|
|
74
|
+
if (nonce == null)
|
|
75
|
+
nonce = await this.root.provider.getTransactionCount(signer.getAddress(), "pending"); //Fetch the nonce
|
|
76
|
+
if (tx.nonce == null)
|
|
77
|
+
tx.nonce = nonce;
|
|
78
|
+
this.logger.debug("sendAndConfirm(): transaction prepared (" + (i + 1) + "/" + txs.length + "), nonce: " + tx.nonce);
|
|
79
|
+
nonce++;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (let tx of txs) {
|
|
83
|
+
for (let callback of this.cbksBeforeTxSigned) {
|
|
84
|
+
await callback(tx);
|
|
85
|
+
}
|
|
68
86
|
}
|
|
69
87
|
}
|
|
70
88
|
/**
|
|
@@ -106,12 +124,17 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
106
124
|
await this.prepareTransactions(signer, txs);
|
|
107
125
|
const signedTxs = [];
|
|
108
126
|
//Don't separate the signing process from the sending when using browser-based wallet
|
|
109
|
-
if (
|
|
127
|
+
if (signer.signTransaction != null)
|
|
110
128
|
for (let i = 0; i < txs.length; i++) {
|
|
111
129
|
const tx = txs[i];
|
|
112
|
-
const signedTx = ethers_1.Transaction.from(await signer.
|
|
130
|
+
const signedTx = ethers_1.Transaction.from(await signer.signTransaction(tx));
|
|
113
131
|
signedTxs.push(signedTx);
|
|
114
132
|
this.logger.debug("sendAndConfirm(): transaction signed (" + (i + 1) + "/" + txs.length + "): " + signedTx);
|
|
133
|
+
const nextAccountNonce = signedTx.nonce + 1;
|
|
134
|
+
const currentSignedNonce = this.latestSignedNonces[signedTx.from];
|
|
135
|
+
if (currentSignedNonce == null || nextAccountNonce > currentSignedNonce) {
|
|
136
|
+
this.latestSignedNonces[signedTx.from] = nextAccountNonce;
|
|
137
|
+
}
|
|
115
138
|
}
|
|
116
139
|
this.logger.debug("sendAndConfirm(): sending transactions, count: " + txs.length +
|
|
117
140
|
" waitForConfirmation: " + waitForConfirmation + " parallel: " + parallel);
|
|
@@ -120,14 +143,19 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
120
143
|
let promises = [];
|
|
121
144
|
for (let i = 0; i < txs.length; i++) {
|
|
122
145
|
let tx;
|
|
123
|
-
if (signer.
|
|
124
|
-
tx = await signer.
|
|
146
|
+
if (signer.signTransaction == null) {
|
|
147
|
+
tx = await signer.sendTransaction(txs[i], onBeforePublish);
|
|
125
148
|
}
|
|
126
149
|
else {
|
|
127
150
|
const signedTx = signedTxs[i];
|
|
128
151
|
await this.sendSignedTransaction(signedTx, onBeforePublish);
|
|
129
152
|
tx = signedTx;
|
|
130
153
|
}
|
|
154
|
+
const nextAccountNonce = tx.nonce + 1;
|
|
155
|
+
const currentPendingNonce = this.latestPendingNonces[tx.from];
|
|
156
|
+
if (currentPendingNonce == null || nextAccountNonce > currentPendingNonce) {
|
|
157
|
+
this.latestPendingNonces[tx.from] = nextAccountNonce;
|
|
158
|
+
}
|
|
131
159
|
if (waitForConfirmation)
|
|
132
160
|
promises.push(this.confirmTransaction(tx, abortSignal));
|
|
133
161
|
txIds.push(tx.hash);
|
|
@@ -143,14 +171,19 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
143
171
|
else {
|
|
144
172
|
for (let i = 0; i < txs.length; i++) {
|
|
145
173
|
let tx;
|
|
146
|
-
if (signer.
|
|
147
|
-
tx = await signer.
|
|
174
|
+
if (signer.signTransaction == null) {
|
|
175
|
+
tx = await signer.sendTransaction(txs[i], onBeforePublish);
|
|
148
176
|
}
|
|
149
177
|
else {
|
|
150
178
|
const signedTx = signedTxs[i];
|
|
151
179
|
await this.sendSignedTransaction(signedTx, onBeforePublish);
|
|
152
180
|
tx = signedTx;
|
|
153
181
|
}
|
|
182
|
+
const nextAccountNonce = tx.nonce + 1;
|
|
183
|
+
const currentPendingNonce = this.latestPendingNonces[tx.from];
|
|
184
|
+
if (currentPendingNonce == null || nextAccountNonce > currentPendingNonce) {
|
|
185
|
+
this.latestPendingNonces[tx.from] = nextAccountNonce;
|
|
186
|
+
}
|
|
154
187
|
const confirmPromise = this.confirmTransaction(tx, abortSignal);
|
|
155
188
|
this.logger.debug("sendAndConfirm(): transaction sent (" + (i + 1) + "/" + txs.length + "): " + tx.hash);
|
|
156
189
|
//Don't await the last promise when !waitForConfirmation
|
|
@@ -189,14 +222,14 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
189
222
|
return await this.getTxIdStatus(parsedTx.hash);
|
|
190
223
|
}
|
|
191
224
|
/**
|
|
192
|
-
* Gets the status of the
|
|
225
|
+
* Gets the status of the EVM transaction with a specific txId
|
|
193
226
|
*
|
|
194
227
|
* @param txId
|
|
195
228
|
*/
|
|
196
229
|
async getTxIdStatus(txId) {
|
|
197
230
|
const txResponse = await this.provider.getTransaction(txId);
|
|
198
231
|
if (txResponse == null)
|
|
199
|
-
return "not_found";
|
|
232
|
+
return this._knownTxSet.has(txId) ? "pending" : "not_found";
|
|
200
233
|
if (txResponse.blockHash == null)
|
|
201
234
|
return "pending";
|
|
202
235
|
const [safeBlockNumber, txReceipt] = await Promise.all([
|
|
@@ -210,10 +243,13 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
210
243
|
return "success";
|
|
211
244
|
}
|
|
212
245
|
onBeforeTxSigned(callback) {
|
|
213
|
-
this.
|
|
246
|
+
this.cbksBeforeTxSigned.push(callback);
|
|
214
247
|
}
|
|
215
248
|
offBeforeTxSigned(callback) {
|
|
216
|
-
|
|
249
|
+
const index = this.cbksBeforeTxSigned.indexOf(callback);
|
|
250
|
+
if (index === -1)
|
|
251
|
+
return false;
|
|
252
|
+
this.cbksBeforeTxSigned.splice(index, 1);
|
|
217
253
|
return true;
|
|
218
254
|
}
|
|
219
255
|
onSendTransaction(callback) {
|
|
@@ -223,6 +259,16 @@ class EVMTransactions extends EVMModule_1.EVMModule {
|
|
|
223
259
|
this.cbkSendTransaction = null;
|
|
224
260
|
return true;
|
|
225
261
|
}
|
|
262
|
+
onBeforeTxReplace(callback) {
|
|
263
|
+
this._cbksBeforeTxReplace.push(callback);
|
|
264
|
+
}
|
|
265
|
+
offBeforeTxReplace(callback) {
|
|
266
|
+
const index = this._cbksBeforeTxReplace.indexOf(callback);
|
|
267
|
+
if (index === -1)
|
|
268
|
+
return false;
|
|
269
|
+
this._cbksBeforeTxReplace.splice(index, 1);
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
226
272
|
traceTransaction(txId) {
|
|
227
273
|
return this.provider.send("debug_traceTransaction", [
|
|
228
274
|
txId,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EVMBrowserSigner = void 0;
|
|
4
|
+
const EVMSigner_1 = require("./EVMSigner");
|
|
5
|
+
class EVMBrowserSigner extends EVMSigner_1.EVMSigner {
|
|
6
|
+
constructor(account, address) {
|
|
7
|
+
super(account, address, false);
|
|
8
|
+
this.signTransaction = null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.EVMBrowserSigner = EVMBrowserSigner;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Signer, TransactionRequest, TransactionResponse } from "ethers";
|
|
2
|
+
import { EVMBlockTag } from "../chain/modules/EVMBlocks";
|
|
3
|
+
import { EVMChainInterface } from "../chain/EVMChainInterface";
|
|
4
|
+
import { EVMSigner } from "./EVMSigner";
|
|
5
|
+
export declare class EVMPersistentSigner extends EVMSigner {
|
|
6
|
+
readonly safeBlockTag: EVMBlockTag;
|
|
7
|
+
private pendingTxs;
|
|
8
|
+
private confirmedNonce;
|
|
9
|
+
private pendingNonce;
|
|
10
|
+
private feeBumper;
|
|
11
|
+
private stopped;
|
|
12
|
+
private readonly directory;
|
|
13
|
+
private readonly waitBeforeBump;
|
|
14
|
+
private readonly minFeeIncreaseAbsolute;
|
|
15
|
+
private readonly minFeeIncreasePpm;
|
|
16
|
+
private readonly chainInterface;
|
|
17
|
+
private readonly logger;
|
|
18
|
+
constructor(account: Signer, address: string, chainInterface: EVMChainInterface, directory: string, minFeeIncreaseAbsolute?: bigint, minFeeIncreasePpm?: bigint, waitBeforeBumpMillis?: number);
|
|
19
|
+
private load;
|
|
20
|
+
private priorSavePromise;
|
|
21
|
+
private saveCount;
|
|
22
|
+
private save;
|
|
23
|
+
private checkPastTransactions;
|
|
24
|
+
private startFeeBumper;
|
|
25
|
+
init(): Promise<void>;
|
|
26
|
+
stop(): Promise<void>;
|
|
27
|
+
sendTransaction(transaction: TransactionRequest, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<TransactionResponse>;
|
|
28
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EVMPersistentSigner = void 0;
|
|
4
|
+
const fs = require("fs/promises");
|
|
5
|
+
const ethers_1 = require("ethers");
|
|
6
|
+
const Utils_1 = require("../../utils/Utils");
|
|
7
|
+
const EVMFees_1 = require("../chain/modules/EVMFees");
|
|
8
|
+
const EVMSigner_1 = require("./EVMSigner");
|
|
9
|
+
const WAIT_BEFORE_BUMP = 15 * 1000;
|
|
10
|
+
const MIN_FEE_INCREASE_ABSOLUTE = 1n * 1000000000n; //1GWei
|
|
11
|
+
const MIN_FEE_INCREASE_PPM = 100000n; // +10%
|
|
12
|
+
class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
|
|
13
|
+
constructor(account, address, chainInterface, directory, minFeeIncreaseAbsolute, minFeeIncreasePpm, waitBeforeBumpMillis) {
|
|
14
|
+
super(account, address, true);
|
|
15
|
+
this.pendingTxs = new Map();
|
|
16
|
+
this.stopped = false;
|
|
17
|
+
this.saveCount = 0;
|
|
18
|
+
this.signTransaction = null;
|
|
19
|
+
this.chainInterface = chainInterface;
|
|
20
|
+
this.directory = directory;
|
|
21
|
+
this.minFeeIncreaseAbsolute = minFeeIncreaseAbsolute ?? MIN_FEE_INCREASE_ABSOLUTE;
|
|
22
|
+
this.minFeeIncreasePpm = minFeeIncreasePpm ?? MIN_FEE_INCREASE_PPM;
|
|
23
|
+
this.waitBeforeBump = waitBeforeBumpMillis ?? WAIT_BEFORE_BUMP;
|
|
24
|
+
this.safeBlockTag = chainInterface.config.safeBlockTag;
|
|
25
|
+
this.logger = (0, Utils_1.getLogger)("EVMPersistentSigner(" + address + "): ");
|
|
26
|
+
}
|
|
27
|
+
async load() {
|
|
28
|
+
const fileExists = await fs.access(this.directory + "/txs.json", fs.constants.F_OK).then(() => true).catch(() => false);
|
|
29
|
+
if (!fileExists)
|
|
30
|
+
return;
|
|
31
|
+
const res = await fs.readFile(this.directory + "/txs.json");
|
|
32
|
+
if (res != null) {
|
|
33
|
+
const pendingTxs = JSON.parse(res.toString());
|
|
34
|
+
for (let nonceStr in pendingTxs) {
|
|
35
|
+
const nonceData = pendingTxs[nonceStr];
|
|
36
|
+
const nonce = parseInt(nonceStr);
|
|
37
|
+
if (this.confirmedNonce >= nonce)
|
|
38
|
+
continue; //Already confirmed
|
|
39
|
+
if (this.pendingNonce < nonce) {
|
|
40
|
+
this.pendingNonce = nonce;
|
|
41
|
+
}
|
|
42
|
+
const parsedPendingTxns = nonceData.txs.map(ethers_1.Transaction.from);
|
|
43
|
+
this.pendingTxs.set(nonce, {
|
|
44
|
+
txs: parsedPendingTxns,
|
|
45
|
+
lastBumped: nonceData.lastBumped
|
|
46
|
+
});
|
|
47
|
+
for (let tx of parsedPendingTxns) {
|
|
48
|
+
this.chainInterface.Transactions._knownTxSet.add(tx.hash);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async save() {
|
|
54
|
+
const pendingTxs = {};
|
|
55
|
+
for (let [nonce, data] of this.pendingTxs) {
|
|
56
|
+
pendingTxs[nonce.toString(10)] = {
|
|
57
|
+
lastBumped: data.lastBumped,
|
|
58
|
+
txs: data.txs.map(tx => tx.serialized)
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const requiredSaveCount = ++this.saveCount;
|
|
62
|
+
if (this.priorSavePromise != null) {
|
|
63
|
+
await this.priorSavePromise;
|
|
64
|
+
}
|
|
65
|
+
if (requiredSaveCount === this.saveCount) {
|
|
66
|
+
this.priorSavePromise = fs.writeFile(this.directory + "/txs.json", JSON.stringify(pendingTxs));
|
|
67
|
+
await this.priorSavePromise;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async checkPastTransactions() {
|
|
71
|
+
let _gasPrice = null;
|
|
72
|
+
let _safeBlockTxCount = null;
|
|
73
|
+
for (let [nonce, data] of this.pendingTxs) {
|
|
74
|
+
if (data.lastBumped < Date.now() - this.waitBeforeBump) {
|
|
75
|
+
_safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
76
|
+
this.confirmedNonce = _safeBlockTxCount;
|
|
77
|
+
if (_safeBlockTxCount > nonce) {
|
|
78
|
+
this.pendingTxs.delete(nonce);
|
|
79
|
+
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
80
|
+
this.logger.info("checkPastTransactions(): Tx confirmed, required fee bumps: ", data.txs.length);
|
|
81
|
+
this.save();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const lastTx = data.txs[data.txs.length - 1];
|
|
85
|
+
if (_gasPrice == null) {
|
|
86
|
+
const feeRate = await this.chainInterface.Fees.getFeeRate();
|
|
87
|
+
const [baseFee, priorityFee] = feeRate.split(",");
|
|
88
|
+
_gasPrice = {
|
|
89
|
+
baseFee: BigInt(baseFee),
|
|
90
|
+
priorityFee: BigInt(priorityFee)
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
let priorityFee = lastTx.maxPriorityFeePerGas;
|
|
94
|
+
let baseFee = lastTx.maxFeePerGas - lastTx.maxPriorityFeePerGas;
|
|
95
|
+
baseFee = (0, Utils_1.bigIntMax)(_gasPrice.baseFee, this.minFeeIncreaseAbsolute + (baseFee * (1000000n + this.minFeeIncreasePpm) / 1000000n));
|
|
96
|
+
priorityFee = (0, Utils_1.bigIntMax)(_gasPrice.priorityFee, this.minFeeIncreaseAbsolute + (priorityFee * (1000000n + this.minFeeIncreasePpm) / 1000000n));
|
|
97
|
+
if (baseFee > (this.minFeeIncreaseAbsolute + (_gasPrice.baseFee * (1000000n + this.minFeeIncreasePpm) / 1000000n)) &&
|
|
98
|
+
priorityFee > (this.minFeeIncreaseAbsolute + (_gasPrice.priorityFee * (1000000n + this.minFeeIncreasePpm) / 1000000n))) {
|
|
99
|
+
//Too big of an increase over the current fee rate, don't fee bump
|
|
100
|
+
this.logger.debug("checkPastTransactions(): Tx yet unconfirmed but not increasing fee for ", lastTx.hash);
|
|
101
|
+
await this.chainInterface.provider.broadcastTransaction(lastTx.serialized).catch(e => this.logger.error("checkPastTransactions(): Tx re-broadcast error", e));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let newTx = lastTx.clone();
|
|
105
|
+
EVMFees_1.EVMFees.applyFeeRate(newTx, null, baseFee.toString(10) + "," + priorityFee.toString(10));
|
|
106
|
+
this.logger.info("checkPastTransactions(): Bump fee for tx: ", lastTx.hash);
|
|
107
|
+
newTx.signature = null;
|
|
108
|
+
const signedRawTx = await this.account.signTransaction(newTx);
|
|
109
|
+
//Double check pending txns still has nonce after async signTransaction was called
|
|
110
|
+
if (!this.pendingTxs.has(nonce))
|
|
111
|
+
continue;
|
|
112
|
+
newTx = ethers_1.Transaction.from(signedRawTx);
|
|
113
|
+
for (let callback of this.chainInterface.Transactions._cbksBeforeTxReplace) {
|
|
114
|
+
try {
|
|
115
|
+
await callback(lastTx.hash, lastTx.serialized, newTx.hash, signedRawTx);
|
|
116
|
+
}
|
|
117
|
+
catch (e) {
|
|
118
|
+
this.logger.error("checkPastTransactions(): beforeTxReplace callback error: ", e);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
data.txs.push(newTx);
|
|
122
|
+
data.lastBumped = Date.now();
|
|
123
|
+
this.save();
|
|
124
|
+
this.chainInterface.Transactions._knownTxSet.add(newTx.hash);
|
|
125
|
+
//TODO: Better error handling when sending tx
|
|
126
|
+
await this.chainInterface.provider.broadcastTransaction(signedRawTx).catch(e => this.logger.error("checkPastTransactions(): Fee-bumped tx broadcast error", e));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
startFeeBumper() {
|
|
131
|
+
let func;
|
|
132
|
+
func = async () => {
|
|
133
|
+
try {
|
|
134
|
+
await this.checkPastTransactions();
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
this.logger.error("startFeeBumper(): Error when check past transactions: ", e);
|
|
138
|
+
}
|
|
139
|
+
if (this.stopped)
|
|
140
|
+
return;
|
|
141
|
+
this.feeBumper = setTimeout(func, 1000);
|
|
142
|
+
};
|
|
143
|
+
func();
|
|
144
|
+
}
|
|
145
|
+
async init() {
|
|
146
|
+
try {
|
|
147
|
+
await fs.mkdir(this.directory);
|
|
148
|
+
}
|
|
149
|
+
catch (e) { }
|
|
150
|
+
const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
151
|
+
this.confirmedNonce = txCount - 1;
|
|
152
|
+
this.pendingNonce = txCount - 1;
|
|
153
|
+
await this.load();
|
|
154
|
+
this.startFeeBumper();
|
|
155
|
+
}
|
|
156
|
+
stop() {
|
|
157
|
+
this.stopped = true;
|
|
158
|
+
if (this.feeBumper != null) {
|
|
159
|
+
clearTimeout(this.feeBumper);
|
|
160
|
+
this.feeBumper = null;
|
|
161
|
+
}
|
|
162
|
+
return Promise.resolve();
|
|
163
|
+
}
|
|
164
|
+
async sendTransaction(transaction, onBeforePublish) {
|
|
165
|
+
if (transaction.nonce != null) {
|
|
166
|
+
if (transaction.nonce !== this.pendingNonce + 1)
|
|
167
|
+
throw new Error("Invalid transaction nonce!");
|
|
168
|
+
this.pendingNonce++;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
this.pendingNonce++;
|
|
172
|
+
transaction.nonce = this.pendingNonce;
|
|
173
|
+
}
|
|
174
|
+
const tx = {};
|
|
175
|
+
for (let key in transaction) {
|
|
176
|
+
if (transaction[key] instanceof Promise) {
|
|
177
|
+
tx[key] = await transaction[key];
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
tx[key] = transaction[key];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const signedRawTx = await this.account.signTransaction(tx);
|
|
184
|
+
const signedTx = ethers_1.Transaction.from(signedRawTx);
|
|
185
|
+
if (onBeforePublish != null) {
|
|
186
|
+
try {
|
|
187
|
+
await onBeforePublish(signedTx.hash, signedRawTx);
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
this.logger.error("sendTransaction(): Error when calling onBeforePublish function: ", e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this.pendingTxs.set(transaction.nonce, { txs: [signedTx], lastBumped: Date.now() });
|
|
194
|
+
this.save();
|
|
195
|
+
this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
|
|
196
|
+
return await this.chainInterface.provider.broadcastTransaction(signedRawTx);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
exports.EVMPersistentSigner = EVMPersistentSigner;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { AbstractSigner } from "@atomiqlabs/base";
|
|
2
|
-
import { Signer } from "ethers";
|
|
2
|
+
import { Signer, TransactionRequest, TransactionResponse } from "ethers";
|
|
3
3
|
export declare class EVMSigner implements AbstractSigner {
|
|
4
4
|
account: Signer;
|
|
5
5
|
readonly address: string;
|
|
6
|
-
readonly
|
|
7
|
-
constructor(account: Signer, address: string,
|
|
8
|
-
getNonce(): Promise<number>;
|
|
6
|
+
readonly isManagingNoncesInternally: boolean;
|
|
7
|
+
constructor(account: Signer, address: string, isManagingNoncesInternally?: boolean);
|
|
9
8
|
getAddress(): string;
|
|
9
|
+
signTransaction?(transaction: TransactionRequest): Promise<string>;
|
|
10
|
+
sendTransaction(transaction: TransactionRequest, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<TransactionResponse>;
|
|
10
11
|
}
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EVMSigner = void 0;
|
|
4
|
+
const ethers_1 = require("ethers");
|
|
4
5
|
class EVMSigner {
|
|
5
|
-
constructor(account, address,
|
|
6
|
+
constructor(account, address, isManagingNoncesInternally = false) {
|
|
6
7
|
this.account = account;
|
|
7
8
|
this.address = address;
|
|
8
|
-
this.
|
|
9
|
-
}
|
|
10
|
-
getNonce() {
|
|
11
|
-
return Promise.resolve(null);
|
|
9
|
+
this.isManagingNoncesInternally = isManagingNoncesInternally;
|
|
12
10
|
}
|
|
13
11
|
getAddress() {
|
|
14
12
|
return this.address;
|
|
15
13
|
}
|
|
14
|
+
async signTransaction(transaction) {
|
|
15
|
+
return this.account.signTransaction(transaction);
|
|
16
|
+
}
|
|
17
|
+
async sendTransaction(transaction, onBeforePublish) {
|
|
18
|
+
const txResponse = await this.account.sendTransaction(transaction);
|
|
19
|
+
if (onBeforePublish != null)
|
|
20
|
+
await onBeforePublish(txResponse.hash, ethers_1.Transaction.from(txResponse).serialized);
|
|
21
|
+
return txResponse;
|
|
22
|
+
}
|
|
16
23
|
}
|
|
17
24
|
exports.EVMSigner = EVMSigner;
|
package/dist/index.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./evm/swaps/handlers/claim/btc/BitcoinTxIdClaimHandler";
|
|
|
33
33
|
export * from "./evm/swaps/handlers/claim/btc/BitcoinOutputClaimHandler";
|
|
34
34
|
export * from "./evm/swaps/handlers/claim/btc/BitcoinNoncedOutputClaimHandler";
|
|
35
35
|
export * from "./evm/wallet/EVMSigner";
|
|
36
|
+
export * from "./evm/wallet/EVMBrowserSigner";
|
|
36
37
|
export * from "./chains/citrea/CitreaInitializer";
|
|
37
38
|
export * from "./chains/citrea/CitreaChainType";
|
|
38
39
|
export * from "./chains/citrea/CitreaFees";
|
package/dist/index.js
CHANGED
|
@@ -49,6 +49,7 @@ __exportStar(require("./evm/swaps/handlers/claim/btc/BitcoinTxIdClaimHandler"),
|
|
|
49
49
|
__exportStar(require("./evm/swaps/handlers/claim/btc/BitcoinOutputClaimHandler"), exports);
|
|
50
50
|
__exportStar(require("./evm/swaps/handlers/claim/btc/BitcoinNoncedOutputClaimHandler"), exports);
|
|
51
51
|
__exportStar(require("./evm/wallet/EVMSigner"), exports);
|
|
52
|
+
__exportStar(require("./evm/wallet/EVMBrowserSigner"), exports);
|
|
52
53
|
__exportStar(require("./chains/citrea/CitreaInitializer"), exports);
|
|
53
54
|
__exportStar(require("./chains/citrea/CitreaChainType"), exports);
|
|
54
55
|
__exportStar(require("./chains/citrea/CitreaFees"), exports);
|
package/dist/utils/Utils.d.ts
CHANGED
|
@@ -13,3 +13,4 @@ export declare function tryWithRetries<T>(func: () => Promise<T>, retryPolicy?:
|
|
|
13
13
|
exponential?: boolean;
|
|
14
14
|
}, errorAllowed?: (e: any) => boolean, abortSignal?: AbortSignal): Promise<T>;
|
|
15
15
|
export declare function uint32ReverseEndianness(value: number): number;
|
|
16
|
+
export declare function bigIntMax(a: bigint, b: bigint): bigint;
|
package/dist/utils/Utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.uint32ReverseEndianness = exports.tryWithRetries = exports.getLogger = exports.onceAsync = exports.timeoutPromise = void 0;
|
|
3
|
+
exports.bigIntMax = exports.uint32ReverseEndianness = exports.tryWithRetries = exports.getLogger = exports.onceAsync = exports.timeoutPromise = void 0;
|
|
4
4
|
function timeoutPromise(timeoutMillis, abortSignal) {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
const timeout = setTimeout(resolve, timeoutMillis);
|
|
@@ -69,3 +69,7 @@ function uint32ReverseEndianness(value) {
|
|
|
69
69
|
((valueBN >> 24n) & 0xffn));
|
|
70
70
|
}
|
|
71
71
|
exports.uint32ReverseEndianness = uint32ReverseEndianness;
|
|
72
|
+
function bigIntMax(a, b) {
|
|
73
|
+
return a > b ? a : b;
|
|
74
|
+
}
|
|
75
|
+
exports.bigIntMax = bigIntMax;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {EVMModule} from "../EVMModule";
|
|
2
|
-
import {Transaction, TransactionRequest} from "ethers";
|
|
2
|
+
import {Transaction, TransactionRequest, TransactionResponse} from "ethers";
|
|
3
3
|
import {timeoutPromise} from "../../../utils/Utils";
|
|
4
4
|
import {EVMSigner} from "../../wallet/EVMSigner";
|
|
5
5
|
|
|
@@ -23,32 +23,45 @@ const MAX_UNCONFIRMED_TXNS = 10;
|
|
|
23
23
|
export class EVMTransactions extends EVMModule<any> {
|
|
24
24
|
|
|
25
25
|
private readonly latestConfirmedNonces: {[address: string]: number} = {};
|
|
26
|
+
private readonly latestPendingNonces: {[address: string]: number} = {};
|
|
27
|
+
private readonly latestSignedNonces: {[address: string]: number} = {};
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
readonly _cbksBeforeTxReplace: ((oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>)[] = [];
|
|
30
|
+
private readonly cbksBeforeTxSigned: ((tx: TransactionRequest) => Promise<void>)[] = [];
|
|
28
31
|
private cbkSendTransaction: (tx: string) => Promise<string>;
|
|
29
32
|
|
|
33
|
+
readonly _knownTxSet: Set<string> = new Set();
|
|
34
|
+
|
|
30
35
|
/**
|
|
31
|
-
* Waits for transaction confirmation using
|
|
32
|
-
* the transaction at regular interval
|
|
36
|
+
* Waits for transaction confirmation using HTTP polling
|
|
33
37
|
*
|
|
34
38
|
* @param tx EVM transaction to wait for confirmation for
|
|
35
39
|
* @param abortSignal signal to abort waiting for tx confirmation
|
|
36
40
|
* @private
|
|
37
41
|
*/
|
|
38
|
-
private async confirmTransaction(tx:
|
|
42
|
+
private async confirmTransaction(tx: TransactionResponse | Transaction, abortSignal?: AbortSignal) {
|
|
43
|
+
const checkTxns: Set<string> = new Set([tx.hash]);
|
|
44
|
+
|
|
45
|
+
const txReplaceListener = (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => {
|
|
46
|
+
if(checkTxns.has(oldTxId)) checkTxns.add(newTxId);
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
};
|
|
49
|
+
this.onBeforeTxReplace(txReplaceListener);
|
|
50
|
+
|
|
39
51
|
let state = "pending";
|
|
40
52
|
while(state==="pending" || state==="not_found") {
|
|
41
53
|
await timeoutPromise(3000, abortSignal);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// this.logger.error("confirmTransaction(): Error on transaction re-send: ", e);
|
|
47
|
-
// });
|
|
54
|
+
for(let txId of checkTxns) {
|
|
55
|
+
state = await this.getTxIdStatus(txId);
|
|
56
|
+
if(state==="reverted" || state==="success") break;
|
|
57
|
+
}
|
|
48
58
|
}
|
|
59
|
+
|
|
60
|
+
this.offBeforeTxReplace(txReplaceListener);
|
|
61
|
+
|
|
49
62
|
const nextAccountNonce = tx.nonce + 1;
|
|
50
|
-
const
|
|
51
|
-
if(
|
|
63
|
+
const currentConfirmedNonce = this.latestConfirmedNonces[tx.from];
|
|
64
|
+
if(currentConfirmedNonce==null || nextAccountNonce > currentConfirmedNonce) {
|
|
52
65
|
this.latestConfirmedNonces[tx.from] = nextAccountNonce;
|
|
53
66
|
}
|
|
54
67
|
if(state==="reverted") throw new Error("Transaction reverted!");
|
|
@@ -62,26 +75,35 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
62
75
|
* @private
|
|
63
76
|
*/
|
|
64
77
|
private async prepareTransactions(signer: EVMSigner, txs: TransactionRequest[]): Promise<void> {
|
|
65
|
-
let
|
|
66
|
-
const latestConfirmedNonce = this.latestConfirmedNonces[signer.getAddress()];
|
|
67
|
-
if(latestConfirmedNonce!=null && latestConfirmedNonce > nonce) {
|
|
68
|
-
this.logger.debug("prepareTransactions(): Using nonce from local cache!");
|
|
69
|
-
nonce = latestConfirmedNonce;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
for(let i=0;i<txs.length;i++) {
|
|
73
|
-
const tx = txs[i];
|
|
78
|
+
for(let tx of txs) {
|
|
74
79
|
tx.chainId = this.root.evmChainId;
|
|
75
80
|
tx.from = signer.getAddress();
|
|
76
|
-
|
|
77
|
-
if(nonce==null) nonce = await this.root.provider.getTransactionCount(signer.getAddress(), "pending"); //Fetch the nonce
|
|
78
|
-
if(tx.nonce==null) tx.nonce = nonce;
|
|
81
|
+
}
|
|
79
82
|
|
|
80
|
-
|
|
83
|
+
if(!signer.isManagingNoncesInternally) {
|
|
84
|
+
let nonce: number = await this.root.provider.getTransactionCount(signer.getAddress(), "pending");
|
|
85
|
+
const latestKnownNonce = this.latestPendingNonces[signer.getAddress()];
|
|
86
|
+
if(latestKnownNonce!=null && latestKnownNonce > nonce) {
|
|
87
|
+
this.logger.debug("prepareTransactions(): Using nonce from local cache!");
|
|
88
|
+
nonce = latestKnownNonce;
|
|
89
|
+
}
|
|
81
90
|
|
|
82
|
-
|
|
91
|
+
for(let i=0;i<txs.length;i++) {
|
|
92
|
+
const tx = txs[i];
|
|
93
|
+
if(tx.nonce!=null) nonce = tx.nonce; //Take the nonce from last tx
|
|
94
|
+
if(nonce==null) nonce = await this.root.provider.getTransactionCount(signer.getAddress(), "pending"); //Fetch the nonce
|
|
95
|
+
if(tx.nonce==null) tx.nonce = nonce;
|
|
96
|
+
|
|
97
|
+
this.logger.debug("sendAndConfirm(): transaction prepared ("+(i+1)+"/"+txs.length+"), nonce: "+tx.nonce);
|
|
98
|
+
|
|
99
|
+
nonce++;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
83
102
|
|
|
84
|
-
|
|
103
|
+
for(let tx of txs) {
|
|
104
|
+
for(let callback of this.cbksBeforeTxSigned) {
|
|
105
|
+
await callback(tx);
|
|
106
|
+
}
|
|
85
107
|
}
|
|
86
108
|
}
|
|
87
109
|
|
|
@@ -130,11 +152,17 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
130
152
|
const signedTxs: Transaction[] = [];
|
|
131
153
|
|
|
132
154
|
//Don't separate the signing process from the sending when using browser-based wallet
|
|
133
|
-
if(
|
|
155
|
+
if(signer.signTransaction!=null) for(let i=0;i<txs.length;i++) {
|
|
134
156
|
const tx = txs[i];
|
|
135
|
-
const signedTx = Transaction.from(await signer.
|
|
157
|
+
const signedTx = Transaction.from(await signer.signTransaction(tx));
|
|
136
158
|
signedTxs.push(signedTx);
|
|
137
159
|
this.logger.debug("sendAndConfirm(): transaction signed ("+(i+1)+"/"+txs.length+"): "+signedTx);
|
|
160
|
+
|
|
161
|
+
const nextAccountNonce = signedTx.nonce + 1;
|
|
162
|
+
const currentSignedNonce = this.latestSignedNonces[signedTx.from];
|
|
163
|
+
if(currentSignedNonce==null || nextAccountNonce > currentSignedNonce) {
|
|
164
|
+
this.latestSignedNonces[signedTx.from] = nextAccountNonce;
|
|
165
|
+
}
|
|
138
166
|
}
|
|
139
167
|
|
|
140
168
|
this.logger.debug("sendAndConfirm(): sending transactions, count: "+txs.length+
|
|
@@ -144,14 +172,21 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
144
172
|
if(parallel) {
|
|
145
173
|
let promises: Promise<void>[] = [];
|
|
146
174
|
for(let i=0;i<txs.length;i++) {
|
|
147
|
-
let tx:
|
|
148
|
-
if(signer.
|
|
149
|
-
tx = await signer.
|
|
175
|
+
let tx: TransactionResponse | Transaction;
|
|
176
|
+
if(signer.signTransaction==null) {
|
|
177
|
+
tx = await signer.sendTransaction(txs[i], onBeforePublish);
|
|
150
178
|
} else {
|
|
151
179
|
const signedTx = signedTxs[i];
|
|
152
180
|
await this.sendSignedTransaction(signedTx, onBeforePublish);
|
|
153
181
|
tx = signedTx;
|
|
154
182
|
}
|
|
183
|
+
|
|
184
|
+
const nextAccountNonce = tx.nonce + 1;
|
|
185
|
+
const currentPendingNonce = this.latestPendingNonces[tx.from];
|
|
186
|
+
if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) {
|
|
187
|
+
this.latestPendingNonces[tx.from] = nextAccountNonce;
|
|
188
|
+
}
|
|
189
|
+
|
|
155
190
|
if(waitForConfirmation) promises.push(this.confirmTransaction(tx, abortSignal));
|
|
156
191
|
txIds.push(tx.hash);
|
|
157
192
|
this.logger.debug("sendAndConfirm(): transaction sent ("+(i+1)+"/"+signedTxs.length+"): "+tx.hash);
|
|
@@ -163,14 +198,21 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
163
198
|
if(promises.length>0) await Promise.all(promises);
|
|
164
199
|
} else {
|
|
165
200
|
for(let i=0;i<txs.length;i++) {
|
|
166
|
-
let tx:
|
|
167
|
-
if(signer.
|
|
168
|
-
tx = await signer.
|
|
201
|
+
let tx: TransactionResponse | Transaction;
|
|
202
|
+
if(signer.signTransaction==null) {
|
|
203
|
+
tx = await signer.sendTransaction(txs[i], onBeforePublish);
|
|
169
204
|
} else {
|
|
170
205
|
const signedTx = signedTxs[i];
|
|
171
206
|
await this.sendSignedTransaction(signedTx, onBeforePublish);
|
|
172
207
|
tx = signedTx;
|
|
173
208
|
}
|
|
209
|
+
|
|
210
|
+
const nextAccountNonce = tx.nonce + 1;
|
|
211
|
+
const currentPendingNonce = this.latestPendingNonces[tx.from];
|
|
212
|
+
if(currentPendingNonce==null || nextAccountNonce > currentPendingNonce) {
|
|
213
|
+
this.latestPendingNonces[tx.from] = nextAccountNonce;
|
|
214
|
+
}
|
|
215
|
+
|
|
174
216
|
const confirmPromise = this.confirmTransaction(tx, abortSignal);
|
|
175
217
|
this.logger.debug("sendAndConfirm(): transaction sent ("+(i+1)+"/"+txs.length+"): "+tx.hash);
|
|
176
218
|
//Don't await the last promise when !waitForConfirmation
|
|
@@ -214,13 +256,13 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
214
256
|
}
|
|
215
257
|
|
|
216
258
|
/**
|
|
217
|
-
* Gets the status of the
|
|
259
|
+
* Gets the status of the EVM transaction with a specific txId
|
|
218
260
|
*
|
|
219
261
|
* @param txId
|
|
220
262
|
*/
|
|
221
263
|
public async getTxIdStatus(txId: string): Promise<"pending" | "success" | "not_found" | "reverted"> {
|
|
222
264
|
const txResponse = await this.provider.getTransaction(txId);
|
|
223
|
-
if(txResponse==null) return "not_found";
|
|
265
|
+
if(txResponse==null) return this._knownTxSet.has(txId) ? "pending" : "not_found";
|
|
224
266
|
if(txResponse.blockHash==null) return "pending";
|
|
225
267
|
|
|
226
268
|
const [safeBlockNumber, txReceipt] = await Promise.all([
|
|
@@ -234,11 +276,13 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
234
276
|
}
|
|
235
277
|
|
|
236
278
|
public onBeforeTxSigned(callback: (tx: TransactionRequest) => Promise<void>): void {
|
|
237
|
-
this.
|
|
279
|
+
this.cbksBeforeTxSigned.push(callback);
|
|
238
280
|
}
|
|
239
281
|
|
|
240
282
|
public offBeforeTxSigned(callback: (tx: TransactionRequest) => Promise<void>): boolean {
|
|
241
|
-
|
|
283
|
+
const index = this.cbksBeforeTxSigned.indexOf(callback);
|
|
284
|
+
if(index===-1) return false;
|
|
285
|
+
this.cbksBeforeTxSigned.splice(index, 1);
|
|
242
286
|
return true;
|
|
243
287
|
}
|
|
244
288
|
|
|
@@ -251,6 +295,17 @@ export class EVMTransactions extends EVMModule<any> {
|
|
|
251
295
|
return true;
|
|
252
296
|
}
|
|
253
297
|
|
|
298
|
+
onBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): void {
|
|
299
|
+
this._cbksBeforeTxReplace.push(callback);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
offBeforeTxReplace(callback: (oldTx: string, oldTxId: string, newTx: string, newTxId: string) => Promise<void>): boolean {
|
|
303
|
+
const index = this._cbksBeforeTxReplace.indexOf(callback);
|
|
304
|
+
if(index===-1) return false;
|
|
305
|
+
this._cbksBeforeTxReplace.splice(index, 1);
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
254
309
|
public traceTransaction(txId: string): Promise<EVMTxTrace> {
|
|
255
310
|
return this.provider.send("debug_traceTransaction", [
|
|
256
311
|
txId,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {Signer} from "ethers";
|
|
2
|
+
import {EVMSigner} from "./EVMSigner";
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export class EVMBrowserSigner extends EVMSigner {
|
|
6
|
+
|
|
7
|
+
constructor(account: Signer, address: string) {
|
|
8
|
+
super(account, address, false);
|
|
9
|
+
this.signTransaction = null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as fs from "fs/promises";
|
|
2
|
+
import {
|
|
3
|
+
Signer,
|
|
4
|
+
Transaction,
|
|
5
|
+
TransactionRequest,
|
|
6
|
+
TransactionResponse
|
|
7
|
+
} from "ethers";
|
|
8
|
+
import {bigIntMax, getLogger, LoggerType} from "../../utils/Utils";
|
|
9
|
+
import {EVMBlockTag} from "../chain/modules/EVMBlocks";
|
|
10
|
+
import {EVMChainInterface} from "../chain/EVMChainInterface";
|
|
11
|
+
import {EVMFees} from "../chain/modules/EVMFees";
|
|
12
|
+
import {EVMSigner} from "./EVMSigner";
|
|
13
|
+
|
|
14
|
+
const WAIT_BEFORE_BUMP = 15*1000;
|
|
15
|
+
const MIN_FEE_INCREASE_ABSOLUTE = 1n*1_000_000_000n; //1GWei
|
|
16
|
+
const MIN_FEE_INCREASE_PPM = 100_000n; // +10%
|
|
17
|
+
|
|
18
|
+
export class EVMPersistentSigner extends EVMSigner {
|
|
19
|
+
|
|
20
|
+
readonly safeBlockTag: EVMBlockTag;
|
|
21
|
+
|
|
22
|
+
private pendingTxs: Map<number, {
|
|
23
|
+
txs: Transaction[],
|
|
24
|
+
lastBumped: number
|
|
25
|
+
}> = new Map();
|
|
26
|
+
|
|
27
|
+
private confirmedNonce: number;
|
|
28
|
+
private pendingNonce: number;
|
|
29
|
+
|
|
30
|
+
private feeBumper: any;
|
|
31
|
+
private stopped: boolean = false;
|
|
32
|
+
|
|
33
|
+
private readonly directory: string;
|
|
34
|
+
|
|
35
|
+
private readonly waitBeforeBump: number;
|
|
36
|
+
private readonly minFeeIncreaseAbsolute: bigint;
|
|
37
|
+
private readonly minFeeIncreasePpm: bigint;
|
|
38
|
+
|
|
39
|
+
private readonly chainInterface: EVMChainInterface;
|
|
40
|
+
|
|
41
|
+
private readonly logger: LoggerType;
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
account: Signer,
|
|
45
|
+
address: string,
|
|
46
|
+
chainInterface: EVMChainInterface,
|
|
47
|
+
directory: string,
|
|
48
|
+
minFeeIncreaseAbsolute?: bigint,
|
|
49
|
+
minFeeIncreasePpm?: bigint,
|
|
50
|
+
waitBeforeBumpMillis?: number
|
|
51
|
+
) {
|
|
52
|
+
super(account, address, true);
|
|
53
|
+
this.signTransaction = null;
|
|
54
|
+
this.chainInterface = chainInterface;
|
|
55
|
+
this.directory = directory;
|
|
56
|
+
this.minFeeIncreaseAbsolute = minFeeIncreaseAbsolute ?? MIN_FEE_INCREASE_ABSOLUTE;
|
|
57
|
+
this.minFeeIncreasePpm = minFeeIncreasePpm ?? MIN_FEE_INCREASE_PPM;
|
|
58
|
+
this.waitBeforeBump = waitBeforeBumpMillis ?? WAIT_BEFORE_BUMP;
|
|
59
|
+
this.safeBlockTag = chainInterface.config.safeBlockTag;
|
|
60
|
+
this.logger = getLogger("EVMPersistentSigner("+address+"): ");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private async load() {
|
|
64
|
+
const fileExists = await fs.access(this.directory+"/txs.json", fs.constants.F_OK).then(() => true).catch(() => false);
|
|
65
|
+
if(!fileExists) return;
|
|
66
|
+
const res = await fs.readFile(this.directory+"/txs.json");
|
|
67
|
+
if(res!=null) {
|
|
68
|
+
const pendingTxs: {
|
|
69
|
+
[nonce: string]: {
|
|
70
|
+
txs: string[],
|
|
71
|
+
lastBumped: number
|
|
72
|
+
}
|
|
73
|
+
} = JSON.parse((res as Buffer).toString());
|
|
74
|
+
|
|
75
|
+
for(let nonceStr in pendingTxs) {
|
|
76
|
+
const nonceData = pendingTxs[nonceStr];
|
|
77
|
+
|
|
78
|
+
const nonce = parseInt(nonceStr);
|
|
79
|
+
if(this.confirmedNonce>=nonce) continue; //Already confirmed
|
|
80
|
+
|
|
81
|
+
if(this.pendingNonce<nonce) {
|
|
82
|
+
this.pendingNonce = nonce;
|
|
83
|
+
}
|
|
84
|
+
const parsedPendingTxns = nonceData.txs.map(Transaction.from);
|
|
85
|
+
this.pendingTxs.set(nonce, {
|
|
86
|
+
txs: parsedPendingTxns,
|
|
87
|
+
lastBumped: nonceData.lastBumped
|
|
88
|
+
})
|
|
89
|
+
for(let tx of parsedPendingTxns) {
|
|
90
|
+
this.chainInterface.Transactions._knownTxSet.add(tx.hash);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private priorSavePromise: Promise<void>;
|
|
97
|
+
private saveCount: number = 0;
|
|
98
|
+
|
|
99
|
+
private async save() {
|
|
100
|
+
const pendingTxs: {
|
|
101
|
+
[nonce: string]: {
|
|
102
|
+
txs: string[],
|
|
103
|
+
lastBumped: number
|
|
104
|
+
}
|
|
105
|
+
} = {};
|
|
106
|
+
for(let [nonce, data] of this.pendingTxs) {
|
|
107
|
+
pendingTxs[nonce.toString(10)] = {
|
|
108
|
+
lastBumped: data.lastBumped,
|
|
109
|
+
txs: data.txs.map(tx => tx.serialized)
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const requiredSaveCount = ++this.saveCount;
|
|
113
|
+
if(this.priorSavePromise!=null) {
|
|
114
|
+
await this.priorSavePromise;
|
|
115
|
+
}
|
|
116
|
+
if(requiredSaveCount===this.saveCount) {
|
|
117
|
+
this.priorSavePromise = fs.writeFile(this.directory+"/txs.json", JSON.stringify(pendingTxs));
|
|
118
|
+
await this.priorSavePromise;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private async checkPastTransactions() {
|
|
123
|
+
let _gasPrice: {
|
|
124
|
+
baseFee: bigint,
|
|
125
|
+
priorityFee: bigint
|
|
126
|
+
} = null;
|
|
127
|
+
let _safeBlockTxCount: number = null;
|
|
128
|
+
|
|
129
|
+
for(let [nonce, data] of this.pendingTxs) {
|
|
130
|
+
if(data.lastBumped<Date.now()-this.waitBeforeBump) {
|
|
131
|
+
_safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
132
|
+
this.confirmedNonce = _safeBlockTxCount;
|
|
133
|
+
if(_safeBlockTxCount > nonce) {
|
|
134
|
+
this.pendingTxs.delete(nonce);
|
|
135
|
+
data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
|
|
136
|
+
this.logger.info("checkPastTransactions(): Tx confirmed, required fee bumps: ", data.txs.length);
|
|
137
|
+
this.save();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const lastTx = data.txs[data.txs.length-1];
|
|
142
|
+
if(_gasPrice==null) {
|
|
143
|
+
const feeRate = await this.chainInterface.Fees.getFeeRate();
|
|
144
|
+
const [baseFee, priorityFee] = feeRate.split(",");
|
|
145
|
+
_gasPrice = {
|
|
146
|
+
baseFee: BigInt(baseFee),
|
|
147
|
+
priorityFee: BigInt(priorityFee)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let priorityFee = lastTx.maxPriorityFeePerGas;
|
|
152
|
+
let baseFee = lastTx.maxFeePerGas - lastTx.maxPriorityFeePerGas;
|
|
153
|
+
|
|
154
|
+
baseFee = bigIntMax(_gasPrice.baseFee, this.minFeeIncreaseAbsolute + (baseFee * (1_000_000n + this.minFeeIncreasePpm) / 1_000_000n));
|
|
155
|
+
priorityFee = bigIntMax(_gasPrice.priorityFee, this.minFeeIncreaseAbsolute + (priorityFee * (1_000_000n + this.minFeeIncreasePpm) / 1_000_000n));
|
|
156
|
+
|
|
157
|
+
if(
|
|
158
|
+
baseFee > (this.minFeeIncreaseAbsolute + (_gasPrice.baseFee * (1_000_000n + this.minFeeIncreasePpm) / 1_000_000n)) &&
|
|
159
|
+
priorityFee > (this.minFeeIncreaseAbsolute + (_gasPrice.priorityFee * (1_000_000n + this.minFeeIncreasePpm) / 1_000_000n))
|
|
160
|
+
) {
|
|
161
|
+
//Too big of an increase over the current fee rate, don't fee bump
|
|
162
|
+
this.logger.debug("checkPastTransactions(): Tx yet unconfirmed but not increasing fee for ", lastTx.hash);
|
|
163
|
+
await this.chainInterface.provider.broadcastTransaction(lastTx.serialized).catch(e => this.logger.error("checkPastTransactions(): Tx re-broadcast error", e));
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let newTx = lastTx.clone();
|
|
168
|
+
EVMFees.applyFeeRate(newTx, null, baseFee.toString(10)+","+priorityFee.toString(10));
|
|
169
|
+
this.logger.info("checkPastTransactions(): Bump fee for tx: ", lastTx.hash);
|
|
170
|
+
|
|
171
|
+
newTx.signature = null;
|
|
172
|
+
const signedRawTx = await this.account.signTransaction(newTx);
|
|
173
|
+
|
|
174
|
+
//Double check pending txns still has nonce after async signTransaction was called
|
|
175
|
+
if(!this.pendingTxs.has(nonce)) continue;
|
|
176
|
+
|
|
177
|
+
newTx = Transaction.from(signedRawTx);
|
|
178
|
+
|
|
179
|
+
for(let callback of this.chainInterface.Transactions._cbksBeforeTxReplace) {
|
|
180
|
+
try {
|
|
181
|
+
await callback(lastTx.hash, lastTx.serialized, newTx.hash, signedRawTx)
|
|
182
|
+
} catch (e) {
|
|
183
|
+
this.logger.error("checkPastTransactions(): beforeTxReplace callback error: ", e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
data.txs.push(newTx);
|
|
188
|
+
data.lastBumped = Date.now();
|
|
189
|
+
this.save();
|
|
190
|
+
|
|
191
|
+
this.chainInterface.Transactions._knownTxSet.add(newTx.hash);
|
|
192
|
+
|
|
193
|
+
//TODO: Better error handling when sending tx
|
|
194
|
+
await this.chainInterface.provider.broadcastTransaction(signedRawTx).catch(e => this.logger.error("checkPastTransactions(): Fee-bumped tx broadcast error", e));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
private startFeeBumper() {
|
|
200
|
+
let func: () => Promise<void>;
|
|
201
|
+
func = async () => {
|
|
202
|
+
try {
|
|
203
|
+
await this.checkPastTransactions();
|
|
204
|
+
} catch (e) {
|
|
205
|
+
this.logger.error("startFeeBumper(): Error when check past transactions: ", e);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if(this.stopped) return;
|
|
209
|
+
|
|
210
|
+
this.feeBumper = setTimeout(func, 1000);
|
|
211
|
+
};
|
|
212
|
+
func();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async init(): Promise<void> {
|
|
216
|
+
try {
|
|
217
|
+
await fs.mkdir(this.directory)
|
|
218
|
+
} catch (e) {}
|
|
219
|
+
|
|
220
|
+
const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
|
|
221
|
+
this.confirmedNonce = txCount-1;
|
|
222
|
+
this.pendingNonce = txCount-1;
|
|
223
|
+
|
|
224
|
+
await this.load();
|
|
225
|
+
|
|
226
|
+
this.startFeeBumper();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
stop(): Promise<void> {
|
|
230
|
+
this.stopped = true;
|
|
231
|
+
if(this.feeBumper!=null) {
|
|
232
|
+
clearTimeout(this.feeBumper);
|
|
233
|
+
this.feeBumper = null;
|
|
234
|
+
}
|
|
235
|
+
return Promise.resolve();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async sendTransaction(transaction: TransactionRequest, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<TransactionResponse> {
|
|
239
|
+
if(transaction.nonce!=null) {
|
|
240
|
+
if(transaction.nonce !== this.pendingNonce + 1)
|
|
241
|
+
throw new Error("Invalid transaction nonce!");
|
|
242
|
+
this.pendingNonce++;
|
|
243
|
+
} else {
|
|
244
|
+
this.pendingNonce++;
|
|
245
|
+
transaction.nonce = this.pendingNonce;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const tx: TransactionRequest = {};
|
|
249
|
+
for(let key in transaction) {
|
|
250
|
+
if(transaction[key] instanceof Promise) {
|
|
251
|
+
tx[key] = await transaction[key];
|
|
252
|
+
} else {
|
|
253
|
+
tx[key] = transaction[key];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const signedRawTx = await this.account.signTransaction(tx);
|
|
258
|
+
const signedTx = Transaction.from(signedRawTx);
|
|
259
|
+
|
|
260
|
+
if(onBeforePublish!=null) {
|
|
261
|
+
try {
|
|
262
|
+
await onBeforePublish(signedTx.hash, signedRawTx);
|
|
263
|
+
} catch (e) {
|
|
264
|
+
this.logger.error("sendTransaction(): Error when calling onBeforePublish function: ", e);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.pendingTxs.set(transaction.nonce, {txs: [signedTx], lastBumped: Date.now()});
|
|
269
|
+
this.save();
|
|
270
|
+
|
|
271
|
+
this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
|
|
272
|
+
|
|
273
|
+
return await this.chainInterface.provider.broadcastTransaction(signedRawTx);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
}
|
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import {AbstractSigner} from "@atomiqlabs/base";
|
|
2
|
-
import {Signer} from "ethers";
|
|
2
|
+
import {Signer, Transaction, TransactionRequest, TransactionResponse} from "ethers";
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
export class EVMSigner implements AbstractSigner {
|
|
6
6
|
|
|
7
7
|
account: Signer;
|
|
8
8
|
public readonly address: string;
|
|
9
|
-
public readonly
|
|
9
|
+
public readonly isManagingNoncesInternally: boolean;
|
|
10
10
|
|
|
11
|
-
constructor(account: Signer, address: string,
|
|
11
|
+
constructor(account: Signer, address: string, isManagingNoncesInternally: boolean = false) {
|
|
12
12
|
this.account = account;
|
|
13
13
|
this.address = address;
|
|
14
|
-
this.
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
getNonce(): Promise<number> {
|
|
18
|
-
return Promise.resolve(null);
|
|
14
|
+
this.isManagingNoncesInternally = isManagingNoncesInternally;
|
|
19
15
|
}
|
|
20
16
|
|
|
21
17
|
getAddress(): string {
|
|
22
18
|
return this.address;
|
|
23
19
|
}
|
|
24
20
|
|
|
21
|
+
async signTransaction?(transaction: TransactionRequest): Promise<string> {
|
|
22
|
+
return this.account.signTransaction(transaction);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async sendTransaction(transaction: TransactionRequest, onBeforePublish?: (txId: string, rawTx: string) => Promise<void>): Promise<TransactionResponse> {
|
|
26
|
+
const txResponse = await this.account.sendTransaction(transaction);
|
|
27
|
+
if(onBeforePublish!=null) await onBeforePublish(txResponse.hash, Transaction.from(txResponse).serialized);
|
|
28
|
+
return txResponse;
|
|
29
|
+
}
|
|
30
|
+
|
|
25
31
|
}
|
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export * from "./evm/swaps/handlers/claim/btc/BitcoinOutputClaimHandler";
|
|
|
39
39
|
export * from "./evm/swaps/handlers/claim/btc/BitcoinNoncedOutputClaimHandler";
|
|
40
40
|
|
|
41
41
|
export * from "./evm/wallet/EVMSigner";
|
|
42
|
+
export * from "./evm/wallet/EVMBrowserSigner";
|
|
42
43
|
|
|
43
44
|
export * from "./chains/citrea/CitreaInitializer";
|
|
44
45
|
export * from "./chains/citrea/CitreaChainType";
|