@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.
@@ -20,7 +20,7 @@ export declare class EVMFees {
20
20
  */
21
21
  private _getFeeRate;
22
22
  /**
23
- * Gets the gas price with caching, format: <gas price in Wei>;<transaction version: v1/v3>
23
+ * Gets the gas price with caching, format: <base fee Wei>,<priority fee Wei>
24
24
  *
25
25
  * @private
26
26
  */
@@ -25,7 +25,7 @@ class EVMFees {
25
25
  return baseFee;
26
26
  }
27
27
  /**
28
- * Gets the gas price with caching, format: <gas price in Wei>;<transaction version: v1/v3>
28
+ * Gets the gas price with caching, format: <base fee Wei>,<priority fee Wei>
29
29
  *
30
30
  * @private
31
31
  */
@@ -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 cbkBeforeTxSigned;
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 WS subscription and occasional HTTP polling, also re-sends
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 starknet transaction with a specific txId
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 WS subscription and occasional HTTP polling, also re-sends
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
- state = await this.getTxIdStatus(tx.hash);
26
- //Don't re-send transactions
27
- // if(state==="not_found") await this.sendSignedTransaction(tx).catch(e => {
28
- // if(e.baseError?.code === 59) return; //Transaction already in the mempool
29
- // this.logger.error("confirmTransaction(): Error on transaction re-send: ", e);
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 currentNonce = this.latestConfirmedNonces[tx.from];
34
- if (currentNonce == null || nextAccountNonce > currentNonce) {
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
- let nonce = (await signer.getNonce()) ?? await this.root.provider.getTransactionCount(signer.getAddress(), "pending");
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
- if (tx.nonce != null)
59
- nonce = tx.nonce; //Take the nonce from last tx
60
- if (nonce == null)
61
- nonce = await this.root.provider.getTransactionCount(signer.getAddress(), "pending"); //Fetch the nonce
62
- if (tx.nonce == null)
63
- tx.nonce = nonce;
64
- this.logger.debug("sendAndConfirm(): transaction prepared (" + (i + 1) + "/" + txs.length + "), nonce: " + tx.nonce);
65
- nonce++;
66
- if (this.cbkBeforeTxSigned != null)
67
- await this.cbkBeforeTxSigned(tx);
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 (!signer.isBrowserWallet)
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.account.signTransaction(tx));
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.isBrowserWallet) {
124
- tx = await signer.account.sendTransaction(txs[i]);
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.isBrowserWallet) {
147
- tx = await signer.account.sendTransaction(txs[i]);
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 starknet transaction with a specific txId
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.cbkBeforeTxSigned = callback;
246
+ this.cbksBeforeTxSigned.push(callback);
214
247
  }
215
248
  offBeforeTxSigned(callback) {
216
- this.cbkBeforeTxSigned = null;
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,5 @@
1
+ import { Signer } from "ethers";
2
+ import { EVMSigner } from "./EVMSigner";
3
+ export declare class EVMBrowserSigner extends EVMSigner {
4
+ constructor(account: Signer, address: string);
5
+ }
@@ -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 isBrowserWallet: boolean;
7
- constructor(account: Signer, address: string, isBrowserWallet?: boolean);
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, isBrowserWallet = false) {
6
+ constructor(account, address, isManagingNoncesInternally = false) {
6
7
  this.account = account;
7
8
  this.address = address;
8
- this.isBrowserWallet = isBrowserWallet;
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);
@@ -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;
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-evm",
3
- "version": "1.0.0-dev.65",
3
+ "version": "1.0.0-dev.67",
4
4
  "description": "EVM specific base implementation",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -50,7 +50,7 @@ export class EVMFees {
50
50
  }
51
51
 
52
52
  /**
53
- * Gets the gas price with caching, format: <gas price in Wei>;<transaction version: v1/v3>
53
+ * Gets the gas price with caching, format: <base fee Wei>,<priority fee Wei>
54
54
  *
55
55
  * @private
56
56
  */
@@ -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
- private cbkBeforeTxSigned: (tx: TransactionRequest) => Promise<void>;
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 WS subscription and occasional HTTP polling, also re-sends
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: {nonce: number, from: string, hash: string}, abortSignal?: AbortSignal) {
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
- state = await this.getTxIdStatus(tx.hash);
43
- //Don't re-send transactions
44
- // if(state==="not_found") await this.sendSignedTransaction(tx).catch(e => {
45
- // if(e.baseError?.code === 59) return; //Transaction already in the mempool
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 currentNonce = this.latestConfirmedNonces[tx.from];
51
- if(currentNonce==null || nextAccountNonce > currentNonce) {
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 nonce: number = (await signer.getNonce()) ?? await this.root.provider.getTransactionCount(signer.getAddress(), "pending");
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
- if(tx.nonce!=null) nonce = tx.nonce; //Take the nonce from last tx
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
- this.logger.debug("sendAndConfirm(): transaction prepared ("+(i+1)+"/"+txs.length+"), nonce: "+tx.nonce);
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
- nonce++;
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
- if(this.cbkBeforeTxSigned!=null) await this.cbkBeforeTxSigned(tx);
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(!signer.isBrowserWallet) for(let i=0;i<txs.length;i++) {
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.account.signTransaction(tx));
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: {nonce: number, from: string, hash: string};
148
- if(signer.isBrowserWallet) {
149
- tx = await signer.account.sendTransaction(txs[i]);
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: {nonce: number, from: string, hash: string};
167
- if(signer.isBrowserWallet) {
168
- tx = await signer.account.sendTransaction(txs[i]);
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 starknet transaction with a specific txId
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.cbkBeforeTxSigned = callback;
279
+ this.cbksBeforeTxSigned.push(callback);
238
280
  }
239
281
 
240
282
  public offBeforeTxSigned(callback: (tx: TransactionRequest) => Promise<void>): boolean {
241
- this.cbkBeforeTxSigned = null;
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 isBrowserWallet: boolean;
9
+ public readonly isManagingNoncesInternally: boolean;
10
10
 
11
- constructor(account: Signer, address: string, isBrowserWallet: boolean = false) {
11
+ constructor(account: Signer, address: string, isManagingNoncesInternally: boolean = false) {
12
12
  this.account = account;
13
13
  this.address = address;
14
- this.isBrowserWallet = isBrowserWallet;
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";
@@ -79,3 +79,7 @@ export function uint32ReverseEndianness(value: number): number {
79
79
  ((valueBN >> 8n) & 0xFF00n) |
80
80
  ((valueBN >> 24n) & 0xFFn));
81
81
  }
82
+
83
+ export function bigIntMax(a: bigint, b: bigint) {
84
+ return a>b ? a : b;
85
+ }