@atomiqlabs/chain-evm 1.0.0-dev.99 → 1.0.0

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.
Files changed (28) hide show
  1. package/dist/chains/botanix/BotanixInitializer.d.ts +1 -1
  2. package/dist/chains/botanix/BotanixInitializer.js +1 -0
  3. package/dist/chains/citrea/CitreaInitializer.d.ts +1 -1
  4. package/dist/chains/citrea/CitreaInitializer.js +1 -0
  5. package/dist/evm/chain/EVMChainInterface.d.ts +5 -0
  6. package/dist/evm/chain/EVMChainInterface.js +9 -1
  7. package/dist/evm/chain/modules/EVMBlocks.d.ts +7 -0
  8. package/dist/evm/chain/modules/EVMBlocks.js +16 -7
  9. package/dist/evm/chain/modules/EVMEvents.js +2 -1
  10. package/dist/evm/chain/modules/EVMTransactions.js +2 -1
  11. package/dist/evm/contract/modules/EVMContractEvents.d.ts +2 -1
  12. package/dist/evm/contract/modules/EVMContractEvents.js +3 -2
  13. package/dist/evm/providers/ReconnectingWebSocketProvider.js +2 -2
  14. package/dist/evm/spv_swap/EVMSpvVaultContract.d.ts +6 -3
  15. package/dist/evm/spv_swap/EVMSpvVaultContract.js +85 -34
  16. package/dist/evm/wallet/EVMPersistentSigner.d.ts +1 -0
  17. package/dist/evm/wallet/EVMPersistentSigner.js +29 -4
  18. package/package.json +2 -2
  19. package/src/chains/botanix/BotanixInitializer.ts +2 -1
  20. package/src/chains/citrea/CitreaInitializer.ts +2 -1
  21. package/src/evm/chain/EVMChainInterface.ts +10 -0
  22. package/src/evm/chain/modules/EVMBlocks.ts +20 -9
  23. package/src/evm/chain/modules/EVMEvents.ts +1 -0
  24. package/src/evm/chain/modules/EVMTransactions.ts +2 -1
  25. package/src/evm/contract/modules/EVMContractEvents.ts +3 -1
  26. package/src/evm/providers/ReconnectingWebSocketProvider.ts +2 -2
  27. package/src/evm/spv_swap/EVMSpvVaultContract.ts +100 -44
  28. package/src/evm/wallet/EVMPersistentSigner.ts +30 -4
@@ -23,7 +23,7 @@ export type BotanixOptions = {
23
23
  };
24
24
  };
25
25
  fees?: EVMFees;
26
- evmConfig?: Omit<EVMConfiguration, "safeBlockTag">;
26
+ evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">;
27
27
  };
28
28
  export declare function initializeBotanix(options: BotanixOptions, bitcoinRpc: BitcoinRpc<any>, network: BitcoinNetwork): ChainData<BotanixChainType>;
29
29
  export type BotanixInitializerType = ChainInitializer<BotanixOptions, BotanixChainType, BotanixAssetsType>;
@@ -84,6 +84,7 @@ function initializeBotanix(options, bitcoinRpc, network) {
84
84
  const Fees = options.fees ?? new EVMFees_1.EVMFees(provider, 2n * 1000000000n, 1000000n);
85
85
  const chainInterface = new EVMChainInterface_1.EVMChainInterface("BOTANIX", chainId, provider, {
86
86
  safeBlockTag: "finalized",
87
+ finalizedBlockTag: "finalized",
87
88
  maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
88
89
  maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
89
90
  maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
@@ -23,7 +23,7 @@ export type CitreaOptions = {
23
23
  };
24
24
  };
25
25
  fees?: CitreaFees;
26
- evmConfig?: Omit<EVMConfiguration, "safeBlockTag">;
26
+ evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">;
27
27
  };
28
28
  export declare function initializeCitrea(options: CitreaOptions, bitcoinRpc: BitcoinRpc<any>, network: BitcoinNetwork): ChainData<CitreaChainType>;
29
29
  export type CitreaInitializerType = ChainInitializer<CitreaOptions, CitreaChainType, CitreaAssetsType>;
@@ -90,6 +90,7 @@ function initializeCitrea(options, bitcoinRpc, network) {
90
90
  const Fees = options.fees ?? new CitreaFees_1.CitreaFees(provider, 2n * 1000000000n, 1000000n);
91
91
  const chainInterface = new EVMChainInterface_1.EVMChainInterface("CITREA", chainId, provider, {
92
92
  safeBlockTag: "latest",
93
+ finalizedBlockTag: "safe",
93
94
  maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
94
95
  maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
95
96
  maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
@@ -15,6 +15,7 @@ export type EVMRetryPolicy = {
15
15
  };
16
16
  export type EVMConfiguration = {
17
17
  safeBlockTag: EVMBlockTag;
18
+ finalizedBlockTag: EVMBlockTag;
18
19
  maxLogsBlockRange: number;
19
20
  maxParallelLogRequests: number;
20
21
  maxParallelCalls: number;
@@ -50,6 +51,10 @@ export declare class EVMChainInterface<ChainId extends string = string> implemen
50
51
  deserializeTx(txData: string): Promise<Transaction>;
51
52
  getTxIdStatus(txId: string): Promise<"not_found" | "pending" | "success" | "reverted">;
52
53
  getTxStatus(tx: string): Promise<"not_found" | "pending" | "success" | "reverted">;
54
+ getFinalizedBlock(): Promise<{
55
+ height: number;
56
+ blockHash: string;
57
+ }>;
53
58
  txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<TransactionRequest[]>;
54
59
  transfer(signer: EVMSigner, token: string, amount: bigint, dstAddress: string, txOptions?: TransactionConfirmationOptions): Promise<string>;
55
60
  wrapSigner(signer: Signer): Promise<EVMSigner>;
@@ -14,13 +14,14 @@ const EVMSigner_1 = require("../wallet/EVMSigner");
14
14
  const EVMBrowserSigner_1 = require("../wallet/EVMBrowserSigner");
15
15
  class EVMChainInterface {
16
16
  constructor(chainId, evmChainId, provider, config, retryPolicy, evmFeeEstimator = new EVMFees_1.EVMFees(provider)) {
17
- var _a;
17
+ var _a, _b;
18
18
  this.chainId = chainId;
19
19
  this.evmChainId = evmChainId;
20
20
  this.provider = provider;
21
21
  this.retryPolicy = retryPolicy;
22
22
  this.config = config;
23
23
  (_a = this.config).safeBlockTag ?? (_a.safeBlockTag = "safe");
24
+ (_b = this.config).finalizedBlockTag ?? (_b.finalizedBlockTag = "finalized");
24
25
  this.logger = (0, Utils_1.getLogger)("EVMChainInterface(" + this.evmChainId + "): ");
25
26
  this.Fees = evmFeeEstimator;
26
27
  this.Tokens = new EVMTokens_1.EVMTokens(this);
@@ -81,6 +82,13 @@ class EVMChainInterface {
81
82
  getTxStatus(tx) {
82
83
  return this.Transactions.getTxStatus(tx);
83
84
  }
85
+ async getFinalizedBlock() {
86
+ const block = await this.Blocks.getBlock(this.config.finalizedBlockTag);
87
+ return {
88
+ height: block.number,
89
+ blockHash: block.hash
90
+ };
91
+ }
84
92
  async txsTransfer(signer, token, amount, dstAddress, feeRate) {
85
93
  return [await this.Tokens.Transfer(signer, token, amount, dstAddress, feeRate)];
86
94
  }
@@ -1,4 +1,5 @@
1
1
  import { EVMModule } from "../EVMModule";
2
+ import { Block } from "ethers";
2
3
  export type EVMBlockTag = "safe" | "pending" | "latest" | "finalized";
3
4
  export declare class EVMBlocks extends EVMModule<any> {
4
5
  private BLOCK_CACHE_TIME;
@@ -16,5 +17,11 @@ export declare class EVMBlocks extends EVMModule<any> {
16
17
  *
17
18
  * @param blockTag
18
19
  */
20
+ getBlock(blockTag: EVMBlockTag | number): Promise<Block>;
21
+ /**
22
+ * Gets the block time for a given blocktag, with caching
23
+ *
24
+ * @param blockTag
25
+ */
19
26
  getBlockTime(blockTag: EVMBlockTag | number): Promise<number>;
20
27
  }
@@ -16,19 +16,19 @@ class EVMBlocks extends EVMModule_1.EVMModule {
16
16
  */
17
17
  fetchAndSaveBlockTime(blockTag) {
18
18
  const blockTagStr = blockTag.toString(10);
19
- const blockTimePromise = this.provider.getBlock(blockTag, false).then(result => result.timestamp);
19
+ const blockPromise = this.provider.getBlock(blockTag, false);
20
20
  const timestamp = Date.now();
21
21
  this.blockCache[blockTagStr] = {
22
- blockTime: blockTimePromise,
22
+ block: blockPromise,
23
23
  timestamp
24
24
  };
25
- blockTimePromise.catch(e => {
26
- if (this.blockCache[blockTagStr] != null && this.blockCache[blockTagStr].blockTime === blockTimePromise)
25
+ blockPromise.catch(e => {
26
+ if (this.blockCache[blockTagStr] != null && this.blockCache[blockTagStr].block === blockPromise)
27
27
  delete this.blockCache[blockTagStr];
28
28
  throw e;
29
29
  });
30
30
  return {
31
- blockTime: blockTimePromise,
31
+ block: blockPromise,
32
32
  timestamp
33
33
  };
34
34
  }
@@ -52,13 +52,22 @@ class EVMBlocks extends EVMModule_1.EVMModule {
52
52
  *
53
53
  * @param blockTag
54
54
  */
55
- getBlockTime(blockTag) {
55
+ getBlock(blockTag) {
56
56
  this.cleanupBlocks();
57
57
  let cachedBlockData = this.blockCache[blockTag.toString(10)];
58
58
  if (cachedBlockData == null || Date.now() - cachedBlockData.timestamp > this.BLOCK_CACHE_TIME) {
59
59
  cachedBlockData = this.fetchAndSaveBlockTime(blockTag);
60
60
  }
61
- return cachedBlockData.blockTime;
61
+ return cachedBlockData.block;
62
+ }
63
+ /**
64
+ * Gets the block time for a given blocktag, with caching
65
+ *
66
+ * @param blockTag
67
+ */
68
+ async getBlockTime(blockTag) {
69
+ const block = await this.getBlock(blockTag);
70
+ return block.timestamp;
62
71
  }
63
72
  }
64
73
  exports.EVMBlocks = EVMBlocks;
@@ -22,7 +22,8 @@ class EVMEvents extends EVMModule_1.EVMModule {
22
22
  });
23
23
  }
24
24
  catch (e) {
25
- if (e.error?.code === -32008 || //Response is too big
25
+ if ((e.error?.code === -32602 && e.error?.message?.startsWith("query exceeds max results")) || //Query exceeds max results
26
+ e.error?.code === -32008 || //Response is too big
26
27
  e.error?.code === -32005 //Limit exceeded
27
28
  ) {
28
29
  if (startBlock === endBlock)
@@ -4,6 +4,7 @@ exports.EVMTransactions = void 0;
4
4
  const EVMModule_1 = require("../EVMModule");
5
5
  const ethers_1 = require("ethers");
6
6
  const Utils_1 = require("../../../utils/Utils");
7
+ const base_1 = require("@atomiqlabs/base");
7
8
  const MAX_UNCONFIRMED_TXNS = 10;
8
9
  class EVMTransactions extends EVMModule_1.EVMModule {
9
10
  constructor() {
@@ -68,7 +69,7 @@ class EVMTransactions extends EVMModule_1.EVMModule {
68
69
  this.latestConfirmedNonces[tx.from] = nextAccountNonce;
69
70
  }
70
71
  if (state === "reverted")
71
- throw new Error("Transaction reverted!");
72
+ throw new base_1.TransactionRevertedError("Transaction reverted!");
72
73
  return confirmedTxId;
73
74
  }
74
75
  /**
@@ -36,7 +36,8 @@ export declare class EVMContractEvents<T extends BaseContract> extends EVMEvents
36
36
  * @param keys
37
37
  * @param processor called for every event, should return a value if the correct event was found, or null
38
38
  * if the search should continue
39
+ * @param startHeight
39
40
  * @param abortSignal
40
41
  */
41
- findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(events: TEventName[], keys: (string | string[])[], processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>, abortSignal?: AbortSignal): Promise<TResult>;
42
+ findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(events: TEventName[], keys: (string | string[])[], processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>, startHeight?: number, abortSignal?: AbortSignal): Promise<TResult>;
42
43
  }
@@ -59,9 +59,10 @@ class EVMContractEvents extends EVMEvents_1.EVMEvents {
59
59
  * @param keys
60
60
  * @param processor called for every event, should return a value if the correct event was found, or null
61
61
  * if the search should continue
62
+ * @param startHeight
62
63
  * @param abortSignal
63
64
  */
64
- async findInContractEventsForward(events, keys, processor, abortSignal) {
65
+ async findInContractEventsForward(events, keys, processor, startHeight, abortSignal) {
65
66
  return this.findInEventsForward(await this.baseContract.getAddress(), this.toFilter(events, keys), async (events) => {
66
67
  const parsedEvents = this.toTypedEvents(events);
67
68
  for (let event of parsedEvents) {
@@ -69,7 +70,7 @@ class EVMContractEvents extends EVMEvents_1.EVMEvents {
69
70
  if (result != null)
70
71
  return result;
71
72
  }
72
- }, abortSignal, this.contract.contractDeploymentHeight);
73
+ }, abortSignal, Math.max(this.contract.contractDeploymentHeight, startHeight ?? 0));
73
74
  }
74
75
  }
75
76
  exports.EVMContractEvents = EVMContractEvents;
@@ -36,7 +36,7 @@ class ReconnectingWebSocketProvider extends SocketProvider_1.SocketProvider {
36
36
  logger.info("connect(): Websocket connected!");
37
37
  };
38
38
  this.websocket.onerror = (err) => {
39
- logger.error(`connect(): onerror: Websocket connection error: `, err);
39
+ logger.error(`connect(): onerror: Websocket connection error: `, err.error ?? err);
40
40
  this.disconnectedAndScheduleReconnect();
41
41
  };
42
42
  this.websocket.onmessage = (message) => {
@@ -59,7 +59,7 @@ class ReconnectingWebSocketProvider extends SocketProvider_1.SocketProvider {
59
59
  return;
60
60
  this.websocket.onclose = null;
61
61
  //Register dummy handler, otherwise we get unhandled `error` event which crashes the whole thing
62
- this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err);
62
+ this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err.error ?? err);
63
63
  this.websocket.onmessage = null;
64
64
  this.websocket.onopen = null;
65
65
  this.websocket = null;
@@ -40,7 +40,7 @@ export declare class EVMSpvVaultContract<ChainId extends string> extends EVMCont
40
40
  protected Claim(signer: string, vault: EVMSpvVaultData, data: EVMSpvWithdrawalData, blockheader: EVMBtcStoredHeader, merkle: Buffer[], position: number, feeRate: string): Promise<TransactionRequest>;
41
41
  checkWithdrawalTx(tx: SpvWithdrawalTransactionData): Promise<void>;
42
42
  createVaultData(owner: string, vaultId: bigint, utxo: string, confirmations: number, tokenData: SpvVaultTokenData[]): Promise<EVMSpvVaultData>;
43
- getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string>;
43
+ getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null>;
44
44
  getFronterAddresses(withdrawals: {
45
45
  owner: string;
46
46
  vaultId: bigint;
@@ -69,8 +69,11 @@ export declare class EVMSpvVaultContract<ChainId extends string> extends EVMCont
69
69
  }>;
70
70
  getAllVaults(owner?: string): Promise<EVMSpvVaultData[]>;
71
71
  private parseWithdrawalEvent;
72
- getWithdrawalState(btcTxId: string): Promise<SpvWithdrawalState>;
73
- getWithdrawalStates(btcTxIds: string[]): Promise<{
72
+ getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState>;
73
+ getWithdrawalStates(withdrawalTxs: {
74
+ withdrawal: EVMSpvWithdrawalData;
75
+ scStartBlockheight?: number;
76
+ }[]): Promise<{
74
77
  [btcTxId: string]: SpvWithdrawalState;
75
78
  }>;
76
79
  getWithdrawalData(btcTx: BtcTx): Promise<EVMSpvWithdrawalData>;
@@ -262,49 +262,100 @@ class EVMSpvVaultContract extends EVMContractBase_1.EVMContractBase {
262
262
  return null;
263
263
  }
264
264
  }
265
- async getWithdrawalState(btcTxId) {
266
- const txHash = buffer_1.Buffer.from(btcTxId, "hex").reverse();
267
- let result = await this.Events.findInContractEvents(["Fronted", "Claimed", "Closed"], [
268
- null,
269
- null,
270
- (0, ethers_1.hexlify)(txHash)
271
- ], async (event) => {
272
- const result = this.parseWithdrawalEvent(event);
273
- if (result != null)
265
+ async getWithdrawalState(withdrawalTx, scStartHeight) {
266
+ const txHash = buffer_1.Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
267
+ const events = ["Fronted", "Claimed", "Closed"];
268
+ const keys = [null, null, (0, ethers_1.hexlify)(txHash)];
269
+ let result;
270
+ if (scStartHeight == null) {
271
+ result = await this.Events.findInContractEvents(events, keys, async (event) => {
272
+ const result = this.parseWithdrawalEvent(event);
273
+ if (result != null)
274
+ return result;
275
+ });
276
+ }
277
+ else {
278
+ result = await this.Events.findInContractEventsForward(events, keys, async (event) => {
279
+ const result = this.parseWithdrawalEvent(event);
280
+ if (result == null)
281
+ return;
282
+ if (result.type === base_1.SpvWithdrawalStateType.FRONTED) {
283
+ //Check if still fronted
284
+ const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx);
285
+ //Not fronted now, there should be a claim/close event after the front event, continue
286
+ if (fronterAddress == null)
287
+ return;
288
+ }
274
289
  return result;
275
- });
290
+ }, scStartHeight);
291
+ }
276
292
  result ?? (result = {
277
293
  type: base_1.SpvWithdrawalStateType.NOT_FOUND
278
294
  });
279
295
  return result;
280
296
  }
281
- async getWithdrawalStates(btcTxIds) {
297
+ async getWithdrawalStates(withdrawalTxs) {
298
+ var _a;
282
299
  const result = {};
283
- for (let i = 0; i < btcTxIds.length; i += this.Chain.config.maxLogTopics) {
284
- const checkBtcTxIds = btcTxIds.slice(i, i + this.Chain.config.maxLogTopics);
285
- const checkBtcTxIdsSet = new Set(checkBtcTxIds);
286
- await this.Events.findInContractEvents(["Fronted", "Claimed", "Closed"], [
287
- null,
288
- null,
289
- checkBtcTxIds.map(btcTxId => (0, ethers_1.hexlify)(buffer_1.Buffer.from(btcTxId, "hex").reverse()))
290
- ], async (event) => {
291
- const _event = event;
292
- const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
293
- if (!checkBtcTxIdsSet.has(btcTxId)) {
294
- this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
295
- return null;
300
+ const events = ["Fronted", "Claimed", "Closed"];
301
+ for (let i = 0; i < withdrawalTxs.length; i += this.Chain.config.maxLogTopics) {
302
+ const checkWithdrawalTxs = withdrawalTxs.slice(i, i + this.Chain.config.maxLogTopics);
303
+ const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId(), val.withdrawal]));
304
+ let scStartHeight = null;
305
+ for (let val of checkWithdrawalTxs) {
306
+ if (val.scStartBlockheight == null) {
307
+ scStartHeight = null;
308
+ break;
296
309
  }
297
- const eventResult = this.parseWithdrawalEvent(event);
298
- if (eventResult == null)
299
- return null;
300
- checkBtcTxIdsSet.delete(btcTxId);
301
- result[btcTxId] = eventResult;
302
- if (checkBtcTxIdsSet.size === 0)
303
- return true; //All processed
304
- });
310
+ scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight);
311
+ }
312
+ const keys = [null, null, checkWithdrawalTxs.map(withdrawal => (0, ethers_1.hexlify)(buffer_1.Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))];
313
+ if (scStartHeight == null) {
314
+ await this.Events.findInContractEvents(events, keys, async (event) => {
315
+ const _event = event;
316
+ const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
317
+ if (!checkWithdrawalTxsMap.has(btcTxId)) {
318
+ this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
319
+ return null;
320
+ }
321
+ const eventResult = this.parseWithdrawalEvent(event);
322
+ if (eventResult == null)
323
+ return null;
324
+ checkWithdrawalTxsMap.delete(btcTxId);
325
+ result[btcTxId] = eventResult;
326
+ if (checkWithdrawalTxsMap.size === 0)
327
+ return true; //All processed
328
+ });
329
+ }
330
+ else {
331
+ await this.Events.findInContractEventsForward(events, keys, async (event) => {
332
+ const _event = event;
333
+ const btcTxId = buffer_1.Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
334
+ const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId);
335
+ if (withdrawalTx == null) {
336
+ this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`);
337
+ return;
338
+ }
339
+ const eventResult = this.parseWithdrawalEvent(event);
340
+ if (eventResult == null)
341
+ return;
342
+ if (eventResult.type === base_1.SpvWithdrawalStateType.FRONTED) {
343
+ //Check if still fronted
344
+ const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx);
345
+ //Not fronted now, so there should be a claim/close event after the front event, continue
346
+ if (fronterAddress == null)
347
+ return;
348
+ //Fronted still, so this should be the latest current state
349
+ }
350
+ checkWithdrawalTxsMap.delete(btcTxId);
351
+ result[btcTxId] = eventResult;
352
+ if (checkWithdrawalTxsMap.size === 0)
353
+ return true; //All processed
354
+ }, scStartHeight);
355
+ }
305
356
  }
306
- for (let btcTxId of btcTxIds) {
307
- result[btcTxId] ?? (result[btcTxId] = {
357
+ for (let val of withdrawalTxs) {
358
+ result[_a = val.withdrawal.getTxId()] ?? (result[_a] = {
308
359
  type: base_1.SpvWithdrawalStateType.NOT_FOUND
309
360
  });
310
361
  }
@@ -22,6 +22,7 @@ export declare class EVMPersistentSigner extends EVMSigner {
22
22
  private save;
23
23
  private checkPastTransactions;
24
24
  private startFeeBumper;
25
+ private syncNonceFromChain;
25
26
  init(): Promise<void>;
26
27
  stop(): Promise<void>;
27
28
  private readonly sendTransactionQueue;
@@ -74,12 +74,14 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
74
74
  let _safeBlockTxCount = null;
75
75
  for (let [nonce, data] of this.pendingTxs) {
76
76
  if (!data.sending && data.lastBumped < Date.now() - this.waitBeforeBump) {
77
- _safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
78
- this.confirmedNonce = _safeBlockTxCount;
79
- if (_safeBlockTxCount > nonce) {
77
+ if (_safeBlockTxCount == null) {
78
+ _safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
79
+ this.confirmedNonce = _safeBlockTxCount - 1;
80
+ }
81
+ if (this.confirmedNonce >= nonce) {
80
82
  this.pendingTxs.delete(nonce);
81
83
  data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
82
- this.logger.info("checkPastTransactions(): Tx confirmed, required fee bumps: ", data.txs.length);
84
+ this.logger.info(`checkPastTransactions(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
83
85
  this.save();
84
86
  continue;
85
87
  }
@@ -159,6 +161,22 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
159
161
  };
160
162
  func();
161
163
  }
164
+ async syncNonceFromChain() {
165
+ const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
166
+ this.confirmedNonce = txCount - 1;
167
+ if (this.pendingNonce < this.confirmedNonce) {
168
+ this.logger.info(`syncNonceFromChain(): Re-synced latest nonce from chain, adjusting local pending nonce ${this.pendingNonce} -> ${this.confirmedNonce}`);
169
+ this.pendingNonce = this.confirmedNonce;
170
+ for (let [nonce, data] of this.pendingTxs) {
171
+ if (nonce <= this.pendingNonce) {
172
+ this.pendingTxs.delete(nonce);
173
+ data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
174
+ this.logger.info(`syncNonceFromChain(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
175
+ }
176
+ }
177
+ this.save();
178
+ }
179
+ }
162
180
  async init() {
163
181
  try {
164
182
  await fs.mkdir(this.directory);
@@ -213,11 +231,18 @@ class EVMPersistentSigner extends EVMSigner_1.EVMSigner {
213
231
  this.save();
214
232
  this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
215
233
  try {
234
+ //TODO: This can fail due to not receiving a response from the server, however the transaction
235
+ // might already be broadcasted!
216
236
  const result = await this.chainInterface.provider.broadcastTransaction(signedRawTx);
217
237
  pendingTxObject.sending = false;
218
238
  return result;
219
239
  }
220
240
  catch (e) {
241
+ if (e.code === "NONCE_EXPIRED") {
242
+ //Re-check nonce from on-chain
243
+ this.logger.info("sendTransaction(): Got NONCE_EXPIRED back from backend, re-checking latest nonce from chain!");
244
+ await this.syncNonceFromChain();
245
+ }
221
246
  this.chainInterface.Transactions._knownTxSet.delete(signedTx.hash);
222
247
  this.pendingTxs.delete(transaction.nonce);
223
248
  this.pendingNonce--;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-evm",
3
- "version": "1.0.0-dev.99",
3
+ "version": "1.0.0",
4
4
  "description": "EVM specific base implementation",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -23,7 +23,7 @@
23
23
  "author": "adambor",
24
24
  "license": "Apache-2.0",
25
25
  "dependencies": {
26
- "@atomiqlabs/base": "^10.0.0-dev.18",
26
+ "@atomiqlabs/base": "^10.0.0-dev.21",
27
27
  "@noble/hashes": "^1.8.0",
28
28
  "@scure/btc-signer": "^1.6.0",
29
29
  "buffer": "6.0.3",
@@ -87,7 +87,7 @@ export type BotanixOptions = {
87
87
 
88
88
  fees?: EVMFees,
89
89
 
90
- evmConfig?: Omit<EVMConfiguration, "safeBlockTag">
90
+ evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">
91
91
  }
92
92
 
93
93
  export function initializeBotanix(
@@ -121,6 +121,7 @@ export function initializeBotanix(
121
121
 
122
122
  const chainInterface = new EVMChainInterface("BOTANIX", chainId, provider, {
123
123
  safeBlockTag: "finalized",
124
+ finalizedBlockTag: "finalized",
124
125
  maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
125
126
  maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
126
127
  maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
@@ -93,7 +93,7 @@ export type CitreaOptions = {
93
93
 
94
94
  fees?: CitreaFees,
95
95
 
96
- evmConfig?: Omit<EVMConfiguration, "safeBlockTag">
96
+ evmConfig?: Omit<EVMConfiguration, "safeBlockTag" | "finalizedBlockTag">
97
97
  }
98
98
 
99
99
  export function initializeCitrea(
@@ -127,6 +127,7 @@ export function initializeCitrea(
127
127
 
128
128
  const chainInterface = new EVMChainInterface("CITREA", chainId, provider, {
129
129
  safeBlockTag: "latest",
130
+ finalizedBlockTag: "safe",
130
131
  maxLogsBlockRange: options?.evmConfig?.maxLogsBlockRange ?? 950,
131
132
  maxLogTopics: options?.evmConfig?.maxLogTopics ?? 64,
132
133
  maxParallelLogRequests: options?.evmConfig?.maxParallelLogRequests ?? 5,
@@ -28,6 +28,7 @@ export type EVMRetryPolicy = {
28
28
 
29
29
  export type EVMConfiguration = {
30
30
  safeBlockTag: EVMBlockTag,
31
+ finalizedBlockTag: EVMBlockTag,
31
32
  maxLogsBlockRange: number,
32
33
  maxParallelLogRequests: number,
33
34
  maxParallelCalls: number,
@@ -68,6 +69,7 @@ export class EVMChainInterface<ChainId extends string = string> implements Chain
68
69
  this.retryPolicy = retryPolicy;
69
70
  this.config = config;
70
71
  this.config.safeBlockTag ??= "safe";
72
+ this.config.finalizedBlockTag ??= "finalized";
71
73
 
72
74
  this.logger = getLogger("EVMChainInterface("+this.evmChainId+"): ");
73
75
 
@@ -154,6 +156,14 @@ export class EVMChainInterface<ChainId extends string = string> implements Chain
154
156
  return this.Transactions.getTxStatus(tx);
155
157
  }
156
158
 
159
+ async getFinalizedBlock(): Promise<{ height: number; blockHash: string }> {
160
+ const block = await this.Blocks.getBlock(this.config.finalizedBlockTag);
161
+ return {
162
+ height: block.number,
163
+ blockHash: block.hash
164
+ };
165
+ }
166
+
157
167
  async txsTransfer(signer: string, token: string, amount: bigint, dstAddress: string, feeRate?: string): Promise<TransactionRequest[]> {
158
168
  return [await this.Tokens.Transfer(signer, token, amount, dstAddress, feeRate)];
159
169
  }
@@ -1,4 +1,5 @@
1
1
  import {EVMModule} from "../EVMModule";
2
+ import {Block} from "ethers";
2
3
 
3
4
  export type EVMBlockTag = "safe" | "pending" | "latest" | "finalized";
4
5
 
@@ -8,7 +9,7 @@ export class EVMBlocks extends EVMModule<any> {
8
9
 
9
10
  private blockCache: {
10
11
  [key: string]: {
11
- blockTime: Promise<number>,
12
+ block: Promise<Block>,
12
13
  timestamp: number
13
14
  }
14
15
  } = {};
@@ -20,23 +21,23 @@ export class EVMBlocks extends EVMModule<any> {
20
21
  * @param blockTag
21
22
  */
22
23
  private fetchAndSaveBlockTime(blockTag: EVMBlockTag | number): {
23
- blockTime: Promise<number>,
24
+ block: Promise<Block>,
24
25
  timestamp: number
25
26
  } {
26
27
  const blockTagStr = blockTag.toString(10);
27
28
 
28
- const blockTimePromise = this.provider.getBlock(blockTag, false).then(result => result.timestamp);
29
+ const blockPromise = this.provider.getBlock(blockTag, false);
29
30
  const timestamp = Date.now();
30
31
  this.blockCache[blockTagStr] = {
31
- blockTime: blockTimePromise,
32
+ block: blockPromise,
32
33
  timestamp
33
34
  };
34
- blockTimePromise.catch(e => {
35
- if(this.blockCache[blockTagStr]!=null && this.blockCache[blockTagStr].blockTime===blockTimePromise) delete this.blockCache[blockTagStr];
35
+ blockPromise.catch(e => {
36
+ if(this.blockCache[blockTagStr]!=null && this.blockCache[blockTagStr].block===blockPromise) delete this.blockCache[blockTagStr];
36
37
  throw e;
37
38
  })
38
39
  return {
39
- blockTime: blockTimePromise,
40
+ block: blockPromise,
40
41
  timestamp
41
42
  };
42
43
  }
@@ -61,7 +62,7 @@ export class EVMBlocks extends EVMModule<any> {
61
62
  *
62
63
  * @param blockTag
63
64
  */
64
- public getBlockTime(blockTag: EVMBlockTag | number): Promise<number> {
65
+ public getBlock(blockTag: EVMBlockTag | number): Promise<Block> {
65
66
  this.cleanupBlocks();
66
67
  let cachedBlockData = this.blockCache[blockTag.toString(10)];
67
68
 
@@ -69,7 +70,17 @@ export class EVMBlocks extends EVMModule<any> {
69
70
  cachedBlockData = this.fetchAndSaveBlockTime(blockTag);
70
71
  }
71
72
 
72
- return cachedBlockData.blockTime;
73
+ return cachedBlockData.block;
74
+ }
75
+
76
+ /**
77
+ * Gets the block time for a given blocktag, with caching
78
+ *
79
+ * @param blockTag
80
+ */
81
+ public async getBlockTime(blockTag: EVMBlockTag | number): Promise<number> {
82
+ const block = await this.getBlock(blockTag);
83
+ return block.timestamp;
73
84
  }
74
85
 
75
86
  }
@@ -24,6 +24,7 @@ export class EVMEvents extends EVMModule<any> {
24
24
  });
25
25
  } catch (e) {
26
26
  if(
27
+ (e.error?.code===-32602 && e.error?.message?.startsWith("query exceeds max results")) || //Query exceeds max results
27
28
  e.error?.code===-32008 || //Response is too big
28
29
  e.error?.code===-32005 //Limit exceeded
29
30
  ) {
@@ -2,6 +2,7 @@ import {EVMModule} from "../EVMModule";
2
2
  import {Transaction, TransactionRequest, TransactionResponse} from "ethers";
3
3
  import {timeoutPromise} from "../../../utils/Utils";
4
4
  import {EVMSigner} from "../../wallet/EVMSigner";
5
+ import {TransactionRevertedError} from "@atomiqlabs/base";
5
6
 
6
7
  export type EVMTx = TransactionRequest;
7
8
 
@@ -87,7 +88,7 @@ export class EVMTransactions extends EVMModule<any> {
87
88
  if(currentConfirmedNonce==null || nextAccountNonce > currentConfirmedNonce) {
88
89
  this.latestConfirmedNonces[tx.from] = nextAccountNonce;
89
90
  }
90
- if(state==="reverted") throw new Error("Transaction reverted!");
91
+ if(state==="reverted") throw new TransactionRevertedError("Transaction reverted!");
91
92
 
92
93
  return confirmedTxId;
93
94
  }
@@ -81,12 +81,14 @@ export class EVMContractEvents<T extends BaseContract> extends EVMEvents {
81
81
  * @param keys
82
82
  * @param processor called for every event, should return a value if the correct event was found, or null
83
83
  * if the search should continue
84
+ * @param startHeight
84
85
  * @param abortSignal
85
86
  */
86
87
  public async findInContractEventsForward<TResult, TEventName extends keyof T["filters"]>(
87
88
  events: TEventName[],
88
89
  keys: (string | string[])[],
89
90
  processor: (event: TypedEventLog<T["filters"][TEventName]>) => Promise<TResult>,
91
+ startHeight?: number,
90
92
  abortSignal?: AbortSignal
91
93
  ): Promise<TResult> {
92
94
  return this.findInEventsForward<TResult>(await this.baseContract.getAddress(), this.toFilter(events, keys), async (events: Log[]) => {
@@ -95,7 +97,7 @@ export class EVMContractEvents<T extends BaseContract> extends EVMEvents {
95
97
  const result: TResult = await processor(event);
96
98
  if(result!=null) return result;
97
99
  }
98
- }, abortSignal, this.contract.contractDeploymentHeight);
100
+ }, abortSignal, Math.max(this.contract.contractDeploymentHeight, startHeight ?? 0));
99
101
  }
100
102
 
101
103
  }
@@ -51,7 +51,7 @@ export class ReconnectingWebSocketProvider extends SocketProvider {
51
51
  };
52
52
 
53
53
  this.websocket.onerror = (err) => {
54
- logger.error(`connect(): onerror: Websocket connection error: `, err);
54
+ logger.error(`connect(): onerror: Websocket connection error: `, err.error ?? err);
55
55
  this.disconnectedAndScheduleReconnect();
56
56
  };
57
57
 
@@ -76,7 +76,7 @@ export class ReconnectingWebSocketProvider extends SocketProvider {
76
76
  if(this.websocket==null) return;
77
77
  this.websocket.onclose = null;
78
78
  //Register dummy handler, otherwise we get unhandled `error` event which crashes the whole thing
79
- this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err);
79
+ this.websocket.onerror = (err) => logger.error("disconnectedAndScheduleReconnect(): Post-close onerror: ", err.error ?? err);
80
80
  this.websocket.onmessage = null;
81
81
  this.websocket.onopen = null;
82
82
  this.websocket = null;
@@ -11,9 +11,9 @@ import {
11
11
  TransactionConfirmationOptions
12
12
  } from "@atomiqlabs/base";
13
13
  import {Buffer} from "buffer";
14
- import { EVMTx } from "../chain/modules/EVMTransactions";
15
- import { EVMContractBase } from "../contract/EVMContractBase";
16
- import { EVMSigner } from "../wallet/EVMSigner";
14
+ import {EVMTx} from "../chain/modules/EVMTransactions";
15
+ import {EVMContractBase} from "../contract/EVMContractBase";
16
+ import {EVMSigner} from "../wallet/EVMSigner";
17
17
  import {SpvVaultContractAbi} from "./SpvVaultContractAbi";
18
18
  import {SpvVaultManager, SpvVaultParametersStructOutput} from "./SpvVaultContractTypechain";
19
19
  import {EVMBtcRelay} from "../btcrelay/EVMBtcRelay";
@@ -204,7 +204,7 @@ export class EVMSpvVaultContract<ChainId extends string>
204
204
  }
205
205
 
206
206
  //Getters
207
- async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string> {
207
+ async getFronterAddress(owner: string, vaultId: bigint, withdrawal: EVMSpvWithdrawalData): Promise<string | null> {
208
208
  const frontingAddress = await this.contract.getFronterById(owner, vaultId, "0x"+withdrawal.getFrontingId());
209
209
  if(frontingAddress===ZeroAddress) return null;
210
210
  return frontingAddress;
@@ -370,58 +370,114 @@ export class EVMSpvVaultContract<ChainId extends string>
370
370
  }
371
371
  }
372
372
 
373
- async getWithdrawalState(btcTxId: string): Promise<SpvWithdrawalState> {
374
- const txHash = Buffer.from(btcTxId, "hex").reverse();
375
- let result: SpvWithdrawalState = await this.Events.findInContractEvents(
376
- ["Fronted", "Claimed", "Closed"],
377
- [
378
- null,
379
- null,
380
- hexlify(txHash)
381
- ],
382
- async (event) => {
383
- const result = this.parseWithdrawalEvent(event);
384
- if(result!=null) return result;
385
- }
386
- );
373
+ async getWithdrawalState(withdrawalTx: EVMSpvWithdrawalData, scStartHeight?: number): Promise<SpvWithdrawalState> {
374
+ const txHash = Buffer.from(withdrawalTx.getTxId(), "hex").reverse();
375
+
376
+ const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
377
+ const keys = [null, null, hexlify(txHash)];
378
+
379
+ let result: SpvWithdrawalState;
380
+ if(scStartHeight==null) {
381
+ result = await this.Events.findInContractEvents(
382
+ events, keys,
383
+ async (event) => {
384
+ const result = this.parseWithdrawalEvent(event);
385
+ if(result!=null) return result;
386
+ }
387
+ );
388
+ } else {
389
+ result = await this.Events.findInContractEventsForward(
390
+ events, keys,
391
+ async (event) => {
392
+ const result = this.parseWithdrawalEvent(event);
393
+ if(result==null) return;
394
+ if(result.type===SpvWithdrawalStateType.FRONTED) {
395
+ //Check if still fronted
396
+ const fronterAddress = await this.getFronterAddress(result.owner, result.vaultId, withdrawalTx);
397
+ //Not fronted now, there should be a claim/close event after the front event, continue
398
+ if(fronterAddress==null) return;
399
+ }
400
+ return result;
401
+ },
402
+ scStartHeight
403
+ );
404
+ }
387
405
  result ??= {
388
406
  type: SpvWithdrawalStateType.NOT_FOUND
389
407
  };
390
408
  return result;
391
409
  }
392
410
 
393
- async getWithdrawalStates(btcTxIds: string[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> {
411
+ async getWithdrawalStates(withdrawalTxs: {withdrawal: EVMSpvWithdrawalData, scStartBlockheight?: number}[]): Promise<{[btcTxId: string]: SpvWithdrawalState}> {
394
412
  const result: {[btcTxId: string]: SpvWithdrawalState} = {};
395
413
 
396
- for(let i=0;i<btcTxIds.length;i+=this.Chain.config.maxLogTopics) {
397
- const checkBtcTxIds = btcTxIds.slice(i, i+this.Chain.config.maxLogTopics);
398
- const checkBtcTxIdsSet = new Set(checkBtcTxIds);
414
+ const events: ["Fronted", "Claimed", "Closed"] = ["Fronted", "Claimed", "Closed"];
399
415
 
400
- await this.Events.findInContractEvents(
401
- ["Fronted", "Claimed", "Closed"],
402
- [
403
- null,
404
- null,
405
- checkBtcTxIds.map(btcTxId => hexlify(Buffer.from(btcTxId, "hex").reverse()))
406
- ],
407
- async (event) => {
408
- const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
409
- const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
410
- if(!checkBtcTxIdsSet.has(btcTxId)) {
411
- this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
412
- return null;
413
- }
414
- const eventResult = this.parseWithdrawalEvent(event);
415
- if(eventResult==null) return null;
416
- checkBtcTxIdsSet.delete(btcTxId);
417
- result[btcTxId] = eventResult;
418
- if(checkBtcTxIdsSet.size===0) return true; //All processed
416
+ for(let i=0;i<withdrawalTxs.length;i+=this.Chain.config.maxLogTopics) {
417
+ const checkWithdrawalTxs = withdrawalTxs.slice(i, i+this.Chain.config.maxLogTopics);
418
+ const checkWithdrawalTxsMap = new Map(checkWithdrawalTxs.map(val => [val.withdrawal.getTxId() as string, val.withdrawal]));
419
+
420
+ let scStartHeight = null;
421
+ for(let val of checkWithdrawalTxs) {
422
+ if(val.scStartBlockheight==null) {
423
+ scStartHeight = null;
424
+ break;
419
425
  }
420
- );
426
+ scStartHeight = Math.min(scStartHeight ?? Infinity, val.scStartBlockheight);
427
+ }
428
+
429
+ const keys = [null, null, checkWithdrawalTxs.map(withdrawal => hexlify(Buffer.from(withdrawal.withdrawal.getTxId(), "hex").reverse()))];
430
+
431
+ if(scStartHeight==null) {
432
+ await this.Events.findInContractEvents(
433
+ events, keys,
434
+ async (event) => {
435
+ const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
436
+ const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
437
+ if(!checkWithdrawalTxsMap.has(btcTxId)) {
438
+ this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
439
+ return null;
440
+ }
441
+ const eventResult = this.parseWithdrawalEvent(event);
442
+ if(eventResult==null) return null;
443
+ checkWithdrawalTxsMap.delete(btcTxId);
444
+ result[btcTxId] = eventResult;
445
+ if(checkWithdrawalTxsMap.size===0) return true; //All processed
446
+ }
447
+ );
448
+ } else {
449
+ await this.Events.findInContractEventsForward(
450
+ events, keys,
451
+ async (event) => {
452
+ const _event = event as TypedEventLog<SpvVaultManager["filters"]["Fronted" | "Claimed" | "Closed"]>;
453
+ const btcTxId = Buffer.from(_event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
454
+ const withdrawalTx = checkWithdrawalTxsMap.get(btcTxId);
455
+ if(withdrawalTx==null) {
456
+ this.logger.warn(`getWithdrawalStates(): findInContractEvents-callback: loaded event for ${btcTxId}, but transaction not found in input params!`)
457
+ return;
458
+ }
459
+ const eventResult = this.parseWithdrawalEvent(event);
460
+ if(eventResult==null) return;
461
+
462
+ if(eventResult.type===SpvWithdrawalStateType.FRONTED) {
463
+ //Check if still fronted
464
+ const fronterAddress = await this.getFronterAddress(eventResult.owner, eventResult.vaultId, withdrawalTx);
465
+ //Not fronted now, so there should be a claim/close event after the front event, continue
466
+ if(fronterAddress==null) return;
467
+ //Fronted still, so this should be the latest current state
468
+ }
469
+
470
+ checkWithdrawalTxsMap.delete(btcTxId);
471
+ result[btcTxId] = eventResult;
472
+ if(checkWithdrawalTxsMap.size===0) return true; //All processed
473
+ },
474
+ scStartHeight
475
+ );
476
+ }
421
477
  }
422
478
 
423
- for(let btcTxId of btcTxIds) {
424
- result[btcTxId] ??= {
479
+ for(let val of withdrawalTxs) {
480
+ result[val.withdrawal.getTxId()] ??= {
425
481
  type: SpvWithdrawalStateType.NOT_FOUND
426
482
  };
427
483
  }
@@ -130,12 +130,14 @@ export class EVMPersistentSigner extends EVMSigner {
130
130
 
131
131
  for(let [nonce, data] of this.pendingTxs) {
132
132
  if(!data.sending && data.lastBumped<Date.now()-this.waitBeforeBump) {
133
- _safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
134
- this.confirmedNonce = _safeBlockTxCount;
135
- if(_safeBlockTxCount > nonce) {
133
+ if(_safeBlockTxCount==null) {
134
+ _safeBlockTxCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
135
+ this.confirmedNonce = _safeBlockTxCount - 1;
136
+ }
137
+ if(this.confirmedNonce >= nonce) {
136
138
  this.pendingTxs.delete(nonce);
137
139
  data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
138
- this.logger.info("checkPastTransactions(): Tx confirmed, required fee bumps: ", data.txs.length);
140
+ this.logger.info(`checkPastTransactions(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
139
141
  this.save();
140
142
  continue;
141
143
  }
@@ -228,6 +230,23 @@ export class EVMPersistentSigner extends EVMSigner {
228
230
  func();
229
231
  }
230
232
 
233
+ private async syncNonceFromChain() {
234
+ const txCount = await this.chainInterface.provider.getTransactionCount(this.address, this.safeBlockTag);
235
+ this.confirmedNonce = txCount-1;
236
+ if(this.pendingNonce < this.confirmedNonce) {
237
+ this.logger.info(`syncNonceFromChain(): Re-synced latest nonce from chain, adjusting local pending nonce ${this.pendingNonce} -> ${this.confirmedNonce}`);
238
+ this.pendingNonce = this.confirmedNonce;
239
+ for(let [nonce, data] of this.pendingTxs) {
240
+ if(nonce <= this.pendingNonce) {
241
+ this.pendingTxs.delete(nonce);
242
+ data.txs.forEach(tx => this.chainInterface.Transactions._knownTxSet.delete(tx.hash));
243
+ this.logger.info(`syncNonceFromChain(): Tx confirmed, nonce: ${nonce}, required fee bumps: `, data.txs.length);
244
+ }
245
+ }
246
+ this.save();
247
+ }
248
+ }
249
+
231
250
  async init(): Promise<void> {
232
251
  try {
233
252
  await fs.mkdir(this.directory)
@@ -291,10 +310,17 @@ export class EVMPersistentSigner extends EVMSigner {
291
310
  this.chainInterface.Transactions._knownTxSet.add(signedTx.hash);
292
311
 
293
312
  try {
313
+ //TODO: This can fail due to not receiving a response from the server, however the transaction
314
+ // might already be broadcasted!
294
315
  const result = await this.chainInterface.provider.broadcastTransaction(signedRawTx);
295
316
  pendingTxObject.sending = false;
296
317
  return result;
297
318
  } catch (e) {
319
+ if(e.code==="NONCE_EXPIRED") {
320
+ //Re-check nonce from on-chain
321
+ this.logger.info("sendTransaction(): Got NONCE_EXPIRED back from backend, re-checking latest nonce from chain!");
322
+ await this.syncNonceFromChain();
323
+ }
298
324
  this.chainInterface.Transactions._knownTxSet.delete(signedTx.hash);
299
325
  this.pendingTxs.delete(transaction.nonce);
300
326
  this.pendingNonce--;