@atomiqlabs/chain-evm 1.0.0-dev.49 → 1.0.0-dev.51

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.
@@ -10,5 +10,4 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import { EVMSwapContract } from "../../evm/swaps/EVMSwapContract";
11
11
  import { EVMBtcRelay } from "../../evm/btcrelay/EVMBtcRelay";
12
12
  import { EVMSpvVaultContract } from "../../evm/spv_swap/EVMSpvVaultContract";
13
- import { EVMChainEventsBrowserWS } from "../../evm/events/EVMChainEventsBrowserWS";
14
- export type BotanixChainType = ChainType<"BOTANIX", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, EVMSwapContract<"BOTANIX">, EVMChainInterface<"BOTANIX", 3636>, EVMChainEventsBrowser | EVMChainEventsBrowserWS, EVMBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, EVMSpvVaultContract<"BOTANIX">>;
13
+ export type BotanixChainType = ChainType<"BOTANIX", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, EVMSwapContract<"BOTANIX">, EVMChainInterface<"BOTANIX", 3636>, EVMChainEventsBrowser, EVMBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, EVMSpvVaultContract<"BOTANIX">>;
@@ -12,7 +12,6 @@ const EVMChainEventsBrowser_1 = require("../../evm/events/EVMChainEventsBrowser"
12
12
  const EVMSwapData_1 = require("../../evm/swaps/EVMSwapData");
13
13
  const EVMSpvVaultData_1 = require("../../evm/spv_swap/EVMSpvVaultData");
14
14
  const EVMSpvWithdrawalData_1 = require("../../evm/spv_swap/EVMSpvWithdrawalData");
15
- const EVMChainEventsBrowserWS_1 = require("../../evm/events/EVMChainEventsBrowserWS");
16
15
  const BotanixChainIds = {
17
16
  MAINNET: null,
18
17
  TESTNET: 3636
@@ -99,9 +98,7 @@ function initializeBotanix(options, bitcoinRpc, network) {
99
98
  }
100
99
  });
101
100
  const spvVaultContract = new EVMSpvVaultContract_1.EVMSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract ?? defaultContractAddresses.spvVaultContract, options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight);
102
- const chainEvents = provider instanceof ethers_1.WebSocketProvider ?
103
- new EVMChainEventsBrowserWS_1.EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
104
- new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
101
+ const chainEvents = new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
105
102
  return {
106
103
  chainId: "BOTANIX",
107
104
  btcRelay,
@@ -10,5 +10,4 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import { CitreaSwapContract } from "./CitreaSwapContract";
11
11
  import { CitreaBtcRelay } from "./CitreaBtcRelay";
12
12
  import { CitreaSpvVaultContract } from "./CitreaSpvVaultContract";
13
- import { EVMChainEventsBrowserWS } from "../../evm/events/EVMChainEventsBrowserWS";
14
- export type CitreaChainType = ChainType<"CITREA", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, CitreaSwapContract, EVMChainInterface<"CITREA", 5115>, EVMChainEventsBrowser | EVMChainEventsBrowserWS, CitreaBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, CitreaSpvVaultContract>;
13
+ export type CitreaChainType = ChainType<"CITREA", never, EVMPreFetchVerification, EVMTx, EVMSigner, EVMSwapData, CitreaSwapContract, EVMChainInterface<"CITREA", 5115>, EVMChainEventsBrowser, CitreaBtcRelay<any>, EVMSpvVaultData, EVMSpvWithdrawalData, CitreaSpvVaultContract>;
@@ -13,7 +13,6 @@ const CitreaBtcRelay_1 = require("./CitreaBtcRelay");
13
13
  const CitreaSwapContract_1 = require("./CitreaSwapContract");
14
14
  const CitreaTokens_1 = require("./CitreaTokens");
15
15
  const CitreaSpvVaultContract_1 = require("./CitreaSpvVaultContract");
16
- const EVMChainEventsBrowserWS_1 = require("../../evm/events/EVMChainEventsBrowserWS");
17
16
  const CitreaChainIds = {
18
17
  MAINNET: null,
19
18
  TESTNET4: 5115
@@ -106,9 +105,7 @@ function initializeCitrea(options, bitcoinRpc, network) {
106
105
  }
107
106
  });
108
107
  const spvVaultContract = new CitreaSpvVaultContract_1.CitreaSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract ?? defaultContractAddresses.spvVaultContract, options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight);
109
- const chainEvents = provider instanceof ethers_1.WebSocketProvider ?
110
- new EVMChainEventsBrowserWS_1.EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
111
- new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
108
+ const chainEvents = new EVMChainEventsBrowser_1.EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
112
109
  return {
113
110
  chainId: "CITREA",
114
111
  btcRelay,
@@ -0,0 +1,15 @@
1
+ import { JsonRpcApiProviderOptions, WebSocketProvider } from "ethers";
2
+ import type { Networkish, WebSocketCreator, WebSocketLike } from "ethers";
3
+ export declare class WebSocketProviderWithRetries extends WebSocketProvider {
4
+ readonly retryPolicy?: {
5
+ maxRetries?: number;
6
+ delay?: number;
7
+ exponential?: boolean;
8
+ };
9
+ constructor(url: string | WebSocketLike | WebSocketCreator, network?: Networkish, options?: JsonRpcApiProviderOptions & {
10
+ maxRetries?: number;
11
+ delay?: number;
12
+ exponential?: boolean;
13
+ });
14
+ send(method: string, params: Array<any> | Record<string, any>): Promise<any>;
15
+ }
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebSocketProviderWithRetries = void 0;
4
+ const ethers_1 = require("ethers");
5
+ const Utils_1 = require("../utils/Utils");
6
+ class WebSocketProviderWithRetries extends ethers_1.WebSocketProvider {
7
+ constructor(url, network, options) {
8
+ super(url, network, options);
9
+ this.retryPolicy = options;
10
+ }
11
+ send(method, params) {
12
+ return (0, Utils_1.tryWithRetries)(() => super.send(method, params), this.retryPolicy, e => {
13
+ return false;
14
+ // if(e?.error?.code!=null) return false; //Error returned by the RPC
15
+ // return true;
16
+ });
17
+ }
18
+ }
19
+ exports.WebSocketProviderWithRetries = WebSocketProviderWithRetries;
@@ -61,6 +61,8 @@ class EVMChainEvents extends EVMChainEventsBrowser_1.EVMChainEventsBrowser {
61
61
  }
62
62
  async init() {
63
63
  const lastState = await this.getLastEventData();
64
+ if (this.provider.websocket != null)
65
+ await this.setupWebsocket();
64
66
  await this.setupPoll(lastState, (newState) => this.saveLastEventData(newState));
65
67
  }
66
68
  }
@@ -1,7 +1,7 @@
1
1
  import { ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent } from "@atomiqlabs/base";
2
2
  import { IClaimHandler } from "../swaps/handlers/claim/ClaimHandlers";
3
3
  import { EVMSwapData } from "../swaps/EVMSwapData";
4
- import { Block, JsonRpcApiProvider } from "ethers";
4
+ import { Block, JsonRpcApiProvider, EventFilter, Log } from "ethers";
5
5
  import { EVMSwapContract } from "../swaps/EVMSwapContract";
6
6
  import { EVMSpvVaultContract } from "../spv_swap/EVMSpvVaultContract";
7
7
  import { EVMChainInterface } from "../chain/EVMChainInterface";
@@ -16,12 +16,16 @@ export type EVMEventListenerState = {
16
16
  logIndex: number;
17
17
  };
18
18
  };
19
+ type AtomiqTypedEvent = (TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> | TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>);
19
20
  /**
20
21
  * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
21
22
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
22
23
  * rely purely on events
23
24
  */
24
25
  export declare class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
26
+ private eventsProcessing;
27
+ private processedEvents;
28
+ private processedEventsIndex;
25
29
  protected readonly listeners: EventListener<EVMSwapData>[];
26
30
  protected readonly provider: JsonRpcApiProvider;
27
31
  protected readonly chainInterface: EVMChainInterface;
@@ -31,7 +35,12 @@ export declare class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
31
35
  protected stopped: boolean;
32
36
  protected pollIntervalSeconds: number;
33
37
  private timeout;
38
+ protected readonly spvVaultContractLogFilter: EventFilter;
39
+ protected readonly swapContractLogFilter: EventFilter;
40
+ protected unconfirmedEventQueue: AtomiqTypedEvent[];
34
41
  constructor(chainInterface: EVMChainInterface, evmSwapContract: EVMSwapContract, evmSpvVaultContract: EVMSpvVaultContract<any>, pollIntervalSeconds?: number);
42
+ private addProcessedEvent;
43
+ private isEventProcessed;
35
44
  findInitSwapData(call: EVMTxTrace, escrowHash: string, claimHandler: IClaimHandler<any, any>): EVMSwapData;
36
45
  /**
37
46
  * Returns async getter for fetching on-demand initialize event swap data
@@ -57,7 +66,7 @@ export declare class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
57
66
  * @param currentBlock
58
67
  * @protected
59
68
  */
60
- protected processEvents(events: (TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> | TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>)[], currentBlock: Block): Promise<void>;
69
+ protected processEvents(events: (TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> | TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>)[], currentBlock?: Block): Promise<void>;
61
70
  protected checkEventsEcrowManager(currentBlock: Block, lastProcessedEvent?: {
62
71
  blockHash: string;
63
72
  logIndex: number;
@@ -79,8 +88,15 @@ export declare class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
79
88
  * @protected
80
89
  */
81
90
  protected setupPoll(lastState?: EVMEventListenerState[], saveLatestProcessedBlockNumber?: (newState: EVMEventListenerState[]) => Promise<void>): Promise<void>;
91
+ protected handleWsEvents(events: AtomiqTypedEvent[]): Promise<void>;
92
+ protected spvVaultContractListener: (log: Log) => void;
93
+ protected swapContractListener: (log: Log) => void;
94
+ protected blockListener: (blockNumber: number) => Promise<void>;
95
+ protected wsStarted: boolean;
96
+ protected setupWebsocket(): Promise<void>;
82
97
  init(): Promise<void>;
83
98
  stop(): Promise<void>;
84
99
  registerListener(cbk: EventListener<EVMSwapData>): void;
85
100
  unregisterListener(cbk: EventListener<EVMSwapData>): boolean;
86
101
  }
102
+ export {};
@@ -7,6 +7,7 @@ const ethers_1 = require("ethers");
7
7
  const Utils_1 = require("../../utils/Utils");
8
8
  const EVMSpvVaultContract_1 = require("../spv_swap/EVMSpvVaultContract");
9
9
  const LOGS_SLIDING_WINDOW_LENGTH = 60;
10
+ const PROCESSED_EVENTS_BACKLOG = 1000;
10
11
  /**
11
12
  * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
12
13
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
@@ -14,13 +15,33 @@ const LOGS_SLIDING_WINDOW_LENGTH = 60;
14
15
  */
15
16
  class EVMChainEventsBrowser {
16
17
  constructor(chainInterface, evmSwapContract, evmSpvVaultContract, pollIntervalSeconds = 5) {
18
+ this.eventsProcessing = {};
19
+ this.processedEvents = [];
20
+ this.processedEventsIndex = 0;
17
21
  this.listeners = [];
18
22
  this.logger = (0, Utils_1.getLogger)("EVMChainEventsBrowser: ");
23
+ this.unconfirmedEventQueue = [];
24
+ this.wsStarted = false;
19
25
  this.chainInterface = chainInterface;
20
26
  this.provider = chainInterface.provider;
21
27
  this.evmSwapContract = evmSwapContract;
22
28
  this.evmSpvVaultContract = evmSpvVaultContract;
23
29
  this.pollIntervalSeconds = pollIntervalSeconds;
30
+ this.spvVaultContractLogFilter = {
31
+ address: this.evmSpvVaultContract.contractAddress
32
+ };
33
+ this.swapContractLogFilter = {
34
+ address: this.evmSwapContract.contractAddress
35
+ };
36
+ }
37
+ addProcessedEvent(event) {
38
+ this.processedEvents[this.processedEventsIndex] = event.transactionHash + ":" + event.transactionIndex;
39
+ this.processedEventsIndex += 1;
40
+ if (this.processedEventsIndex >= PROCESSED_EVENTS_BACKLOG)
41
+ this.processedEventsIndex = 0;
42
+ }
43
+ isEventProcessed(event) {
44
+ return this.processedEvents.includes(event.transactionHash + ":" + event.transactionIndex);
24
45
  }
25
46
  findInitSwapData(call, escrowHash, claimHandler) {
26
47
  if (call.to.toLowerCase() === this.evmSwapContract.contractAddress.toLowerCase()) {
@@ -141,8 +162,12 @@ class EVMChainEventsBrowser {
141
162
  * @protected
142
163
  */
143
164
  async processEvents(events, currentBlock) {
144
- const parsedEvents = [];
145
165
  for (let event of events) {
166
+ const eventIdentifier = event.transactionHash + ":" + event.transactionIndex;
167
+ if (this.isEventProcessed(event)) {
168
+ this.logger.debug("processEvents(): skipping already processed event: " + eventIdentifier);
169
+ continue;
170
+ }
146
171
  let parsedEvent;
147
172
  switch (event.eventName) {
148
173
  case "Claim":
@@ -170,16 +195,32 @@ class EVMChainEventsBrowser {
170
195
  parsedEvent = this.parseSpvCloseEvent(event);
171
196
  break;
172
197
  }
173
- const timestamp = event.blockNumber === currentBlock.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
174
- parsedEvent.meta = {
175
- blockTime: timestamp,
176
- txId: event.transactionHash,
177
- timestamp //Maybe deprecated
178
- };
179
- parsedEvents.push(parsedEvent);
180
- }
181
- for (let listener of this.listeners) {
182
- await listener(parsedEvents);
198
+ if (this.eventsProcessing[eventIdentifier] != null) {
199
+ this.logger.debug("processEvents(): awaiting event that is currently processing: " + eventIdentifier);
200
+ await this.eventsProcessing[eventIdentifier];
201
+ continue;
202
+ }
203
+ const promise = (async () => {
204
+ const timestamp = event.blockNumber === currentBlock?.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
205
+ parsedEvent.meta = {
206
+ blockTime: timestamp,
207
+ txId: event.transactionHash,
208
+ timestamp //Maybe deprecated
209
+ };
210
+ for (let listener of this.listeners) {
211
+ await listener([parsedEvent]);
212
+ }
213
+ this.addProcessedEvent(event);
214
+ })();
215
+ this.eventsProcessing[eventIdentifier] = promise;
216
+ try {
217
+ await promise;
218
+ delete this.eventsProcessing[eventIdentifier];
219
+ }
220
+ catch (e) {
221
+ delete this.eventsProcessing[eventIdentifier];
222
+ throw e;
223
+ }
183
224
  }
184
225
  }
185
226
  async checkEventsEcrowManager(currentBlock, lastProcessedEvent, lastBlockNumber) {
@@ -269,8 +310,67 @@ class EVMChainEventsBrowser {
269
310
  };
270
311
  await func();
271
312
  }
313
+ //Websocket
314
+ handleWsEvents(events) {
315
+ if (this.chainInterface.config.safeBlockTag === "latest" || this.chainInterface.config.safeBlockTag === "pending") {
316
+ return this.processEvents(events);
317
+ }
318
+ this.unconfirmedEventQueue.push(...events);
319
+ }
320
+ async setupWebsocket() {
321
+ this.wsStarted = true;
322
+ await this.provider.on(this.spvVaultContractLogFilter, this.spvVaultContractListener = (log) => {
323
+ let events = this.evmSpvVaultContract.Events.toTypedEvents([log]);
324
+ events = events.filter(val => !val.removed);
325
+ this.handleWsEvents(events);
326
+ });
327
+ await this.provider.on(this.swapContractLogFilter, this.swapContractListener = (log) => {
328
+ let events = this.evmSwapContract.Events.toTypedEvents([log]);
329
+ events = events.filter(val => !val.removed && (val.eventName === "Initialize" || val.eventName === "Refund" || val.eventName === "Claim"));
330
+ this.handleWsEvents(events);
331
+ });
332
+ const safeBlockTag = this.chainInterface.config.safeBlockTag;
333
+ let processing = false;
334
+ if (safeBlockTag !== "latest" && safeBlockTag !== "pending")
335
+ await this.provider.on("block", this.blockListener = async (blockNumber) => {
336
+ if (processing)
337
+ return;
338
+ processing = true;
339
+ try {
340
+ const latestSafeBlock = await this.provider.getBlock(this.chainInterface.config.safeBlockTag);
341
+ const events = [];
342
+ this.unconfirmedEventQueue = this.unconfirmedEventQueue.filter(event => {
343
+ if (event.blockNumber <= latestSafeBlock.number) {
344
+ events.push(event);
345
+ return false;
346
+ }
347
+ return true;
348
+ });
349
+ const blocks = {};
350
+ for (let event of events) {
351
+ const block = blocks[event.blockNumber] ?? (blocks[event.blockNumber] = await this.provider.getBlock(event.blockNumber));
352
+ if (block.hash === event.blockHash) {
353
+ //Valid event
354
+ await this.processEvents([event], block);
355
+ }
356
+ else {
357
+ //Block hash doesn't match
358
+ }
359
+ }
360
+ }
361
+ catch (e) {
362
+ this.logger.error(`on('block'): Error when processing new block ${blockNumber}:`, e);
363
+ }
364
+ processing = false;
365
+ });
366
+ }
272
367
  async init() {
273
- this.setupPoll();
368
+ if (this.provider.websocket != null) {
369
+ await this.setupWebsocket();
370
+ }
371
+ else {
372
+ await this.setupPoll();
373
+ }
274
374
  this.stopped = false;
275
375
  return Promise.resolve();
276
376
  }
@@ -278,6 +378,12 @@ class EVMChainEventsBrowser {
278
378
  this.stopped = true;
279
379
  if (this.timeout != null)
280
380
  clearTimeout(this.timeout);
381
+ if (this.wsStarted) {
382
+ await this.provider.off(this.spvVaultContractLogFilter, this.spvVaultContractListener);
383
+ await this.provider.off(this.swapContractLogFilter, this.swapContractListener);
384
+ await this.provider.off("block", this.blockListener);
385
+ this.wsStarted = false;
386
+ }
281
387
  }
282
388
  registerListener(cbk) {
283
389
  this.listeners.push(cbk);
package/dist/index.d.ts CHANGED
@@ -14,7 +14,6 @@ export * from "./evm/contract/modules/EVMContractEvents";
14
14
  export * from "./evm/contract/EVMContractBase";
15
15
  export * from "./evm/contract/EVMContractModule";
16
16
  export * from "./evm/events/EVMChainEventsBrowser";
17
- export * from "./evm/events/EVMChainEventsBrowserWS";
18
17
  export * from "./evm/spv_swap/EVMSpvVaultContract";
19
18
  export * from "./evm/spv_swap/EVMSpvWithdrawalData";
20
19
  export * from "./evm/spv_swap/EVMSpvVaultData";
@@ -40,3 +39,4 @@ export * from "./chains/citrea/CitreaFees";
40
39
  export * from "./chains/botanix/BotanixInitializer";
41
40
  export * from "./chains/botanix/BotanixChainType";
42
41
  export * from "./evm/JsonRpcProviderWithRetries";
42
+ export * from "./evm/WebSocketProviderWithRetries";
package/dist/index.js CHANGED
@@ -30,7 +30,6 @@ __exportStar(require("./evm/contract/modules/EVMContractEvents"), exports);
30
30
  __exportStar(require("./evm/contract/EVMContractBase"), exports);
31
31
  __exportStar(require("./evm/contract/EVMContractModule"), exports);
32
32
  __exportStar(require("./evm/events/EVMChainEventsBrowser"), exports);
33
- __exportStar(require("./evm/events/EVMChainEventsBrowserWS"), exports);
34
33
  __exportStar(require("./evm/spv_swap/EVMSpvVaultContract"), exports);
35
34
  __exportStar(require("./evm/spv_swap/EVMSpvWithdrawalData"), exports);
36
35
  __exportStar(require("./evm/spv_swap/EVMSpvVaultData"), exports);
@@ -56,3 +55,4 @@ __exportStar(require("./chains/citrea/CitreaFees"), exports);
56
55
  __exportStar(require("./chains/botanix/BotanixInitializer"), exports);
57
56
  __exportStar(require("./chains/botanix/BotanixChainType"), exports);
58
57
  __exportStar(require("./evm/JsonRpcProviderWithRetries"), exports);
58
+ __exportStar(require("./evm/WebSocketProviderWithRetries"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-evm",
3
- "version": "1.0.0-dev.49",
3
+ "version": "1.0.0-dev.51",
4
4
  "description": "EVM specific base implementation",
5
5
  "main": "./dist/index.js",
6
6
  "types:": "./dist/index.d.ts",
@@ -10,7 +10,6 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import {EVMSwapContract} from "../../evm/swaps/EVMSwapContract";
11
11
  import {EVMBtcRelay} from "../../evm/btcrelay/EVMBtcRelay";
12
12
  import {EVMSpvVaultContract} from "../../evm/spv_swap/EVMSpvVaultContract";
13
- import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
14
13
 
15
14
  export type BotanixChainType = ChainType<
16
15
  "BOTANIX",
@@ -21,7 +20,7 @@ export type BotanixChainType = ChainType<
21
20
  EVMSwapData,
22
21
  EVMSwapContract<"BOTANIX">,
23
22
  EVMChainInterface<"BOTANIX", 3636>,
24
- EVMChainEventsBrowser | EVMChainEventsBrowserWS,
23
+ EVMChainEventsBrowser,
25
24
  EVMBtcRelay<any>,
26
25
  EVMSpvVaultData,
27
26
  EVMSpvWithdrawalData,
@@ -10,7 +10,6 @@ import {EVMSwapData} from "../../evm/swaps/EVMSwapData";
10
10
  import {EVMSpvVaultData} from "../../evm/spv_swap/EVMSpvVaultData";
11
11
  import {EVMSpvWithdrawalData} from "../../evm/spv_swap/EVMSpvWithdrawalData";
12
12
  import {BotanixChainType} from "./BotanixChainType";
13
- import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
14
13
 
15
14
  const BotanixChainIds = {
16
15
  MAINNET: null,
@@ -147,9 +146,7 @@ export function initializeBotanix(
147
146
  options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight
148
147
  )
149
148
 
150
- const chainEvents = provider instanceof WebSocketProvider ?
151
- new EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
152
- new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
149
+ const chainEvents = new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
153
150
 
154
151
  return {
155
152
  chainId: "BOTANIX",
@@ -10,7 +10,6 @@ import { EVMSpvWithdrawalData } from "../../evm/spv_swap/EVMSpvWithdrawalData";
10
10
  import {CitreaSwapContract} from "./CitreaSwapContract";
11
11
  import {CitreaBtcRelay} from "./CitreaBtcRelay";
12
12
  import {CitreaSpvVaultContract} from "./CitreaSpvVaultContract";
13
- import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
14
13
 
15
14
  export type CitreaChainType = ChainType<
16
15
  "CITREA",
@@ -21,7 +20,7 @@ export type CitreaChainType = ChainType<
21
20
  EVMSwapData,
22
21
  CitreaSwapContract,
23
22
  EVMChainInterface<"CITREA", 5115>,
24
- EVMChainEventsBrowser | EVMChainEventsBrowserWS,
23
+ EVMChainEventsBrowser,
25
24
  CitreaBtcRelay<any>,
26
25
  EVMSpvVaultData,
27
26
  EVMSpvWithdrawalData,
@@ -11,7 +11,6 @@ import {CitreaBtcRelay} from "./CitreaBtcRelay";
11
11
  import {CitreaSwapContract} from "./CitreaSwapContract";
12
12
  import {CitreaTokens} from "./CitreaTokens";
13
13
  import {CitreaSpvVaultContract} from "./CitreaSpvVaultContract";
14
- import {EVMChainEventsBrowserWS} from "../../evm/events/EVMChainEventsBrowserWS";
15
14
 
16
15
  const CitreaChainIds = {
17
16
  MAINNET: null,
@@ -154,9 +153,7 @@ export function initializeCitrea(
154
153
  options.spvVaultDeploymentHeight ?? defaultContractAddresses.spvVaultDeploymentHeight
155
154
  )
156
155
 
157
- const chainEvents = provider instanceof WebSocketProvider ?
158
- new EVMChainEventsBrowserWS(chainInterface, swapContract, spvVaultContract) :
159
- new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
156
+ const chainEvents = new EVMChainEventsBrowser(chainInterface, swapContract, spvVaultContract);
160
157
 
161
158
  return {
162
159
  chainId: "CITREA",
@@ -0,0 +1,27 @@
1
+ import {JsonRpcApiProviderOptions, WebSocketProvider} from "ethers";
2
+ import type {Networkish, WebSocketCreator, WebSocketLike} from "ethers";
3
+ import {tryWithRetries} from "../utils/Utils";
4
+
5
+
6
+ export class WebSocketProviderWithRetries extends WebSocketProvider {
7
+
8
+ readonly retryPolicy?: {
9
+ maxRetries?: number, delay?: number, exponential?: boolean
10
+ };
11
+
12
+ constructor(url: string | WebSocketLike | WebSocketCreator, network?: Networkish, options?: JsonRpcApiProviderOptions & {
13
+ maxRetries?: number, delay?: number, exponential?: boolean
14
+ }) {
15
+ super(url, network, options);
16
+ this.retryPolicy = options;
17
+ }
18
+
19
+ send(method: string, params: Array<any> | Record<string, any>): Promise<any> {
20
+ return tryWithRetries(() => super.send(method, params), this.retryPolicy, e => {
21
+ return false;
22
+ // if(e?.error?.code!=null) return false; //Error returned by the RPC
23
+ // return true;
24
+ });
25
+ }
26
+
27
+ }
@@ -72,6 +72,7 @@ export class EVMChainEvents extends EVMChainEventsBrowser {
72
72
 
73
73
  async init(): Promise<void> {
74
74
  const lastState = await this.getLastEventData();
75
+ if((this.provider as any).websocket!=null) await this.setupWebsocket();
75
76
  await this.setupPoll(
76
77
  lastState,
77
78
  (newState: EVMEventListenerState[]) => this.saveLastEventData(newState)
@@ -9,7 +9,7 @@ import {
9
9
  } from "@atomiqlabs/base";
10
10
  import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
11
11
  import {EVMSwapData} from "../swaps/EVMSwapData";
12
- import {Block, hexlify, JsonRpcApiProvider} from "ethers";
12
+ import {Block, hexlify, JsonRpcApiProvider, EventFilter, Log} from "ethers";
13
13
  import { EVMSwapContract } from "../swaps/EVMSwapContract";
14
14
  import {getLogger, onceAsync} from "../../utils/Utils";
15
15
  import {EVMSpvVaultContract, unpackOwnerAndVaultId} from "../spv_swap/EVMSpvVaultContract";
@@ -21,8 +21,15 @@ import {EVMTxTrace} from "../chain/modules/EVMTransactions";
21
21
 
22
22
  const LOGS_SLIDING_WINDOW_LENGTH = 60;
23
23
 
24
+ const PROCESSED_EVENTS_BACKLOG = 1000;
25
+
24
26
  export type EVMEventListenerState = {lastBlockNumber: number, lastEvent?: {blockHash: string, logIndex: number}};
25
27
 
28
+ type AtomiqTypedEvent = (
29
+ TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> |
30
+ TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>
31
+ );
32
+
26
33
  /**
27
34
  * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
28
35
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
@@ -30,6 +37,12 @@ export type EVMEventListenerState = {lastBlockNumber: number, lastEvent?: {block
30
37
  */
31
38
  export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
32
39
 
40
+ private eventsProcessing: {
41
+ [signature: string]: Promise<void>
42
+ } = {};
43
+ private processedEvents: string[] = [];
44
+ private processedEventsIndex: number = 0;
45
+
33
46
  protected readonly listeners: EventListener<EVMSwapData>[] = [];
34
47
  protected readonly provider: JsonRpcApiProvider;
35
48
  protected readonly chainInterface: EVMChainInterface;
@@ -42,6 +55,12 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
42
55
 
43
56
  private timeout: number;
44
57
 
58
+ //Websocket
59
+ protected readonly spvVaultContractLogFilter: EventFilter;
60
+ protected readonly swapContractLogFilter: EventFilter;
61
+
62
+ protected unconfirmedEventQueue: AtomiqTypedEvent[] = [];
63
+
45
64
  constructor(
46
65
  chainInterface: EVMChainInterface,
47
66
  evmSwapContract: EVMSwapContract,
@@ -53,6 +72,23 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
53
72
  this.evmSwapContract = evmSwapContract;
54
73
  this.evmSpvVaultContract = evmSpvVaultContract;
55
74
  this.pollIntervalSeconds = pollIntervalSeconds;
75
+
76
+ this.spvVaultContractLogFilter = {
77
+ address: this.evmSpvVaultContract.contractAddress
78
+ };
79
+ this.swapContractLogFilter = {
80
+ address: this.evmSwapContract.contractAddress
81
+ };
82
+ }
83
+
84
+ private addProcessedEvent(event: AtomiqTypedEvent) {
85
+ this.processedEvents[this.processedEventsIndex] = event.transactionHash+":"+event.transactionIndex;
86
+ this.processedEventsIndex += 1;
87
+ if(this.processedEventsIndex >= PROCESSED_EVENTS_BACKLOG) this.processedEventsIndex = 0;
88
+ }
89
+
90
+ private isEventProcessed(event: AtomiqTypedEvent): boolean {
91
+ return this.processedEvents.includes(event.transactionHash+":"+event.transactionIndex);
56
92
  }
57
93
 
58
94
  findInitSwapData(call: EVMTxTrace, escrowHash: string, claimHandler: IClaimHandler<any, any>): EVMSwapData {
@@ -216,11 +252,16 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
216
252
  TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> |
217
253
  TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>
218
254
  )[],
219
- currentBlock: Block
255
+ currentBlock?: Block
220
256
  ) {
221
- const parsedEvents: ChainEvent<EVMSwapData>[] = [];
222
-
223
257
  for(let event of events) {
258
+ const eventIdentifier = event.transactionHash+":"+event.transactionIndex;
259
+
260
+ if(this.isEventProcessed(event)) {
261
+ this.logger.debug("processEvents(): skipping already processed event: "+eventIdentifier);
262
+ continue;
263
+ }
264
+
224
265
  let parsedEvent: ChainEvent<EVMSwapData>;
225
266
  switch(event.eventName) {
226
267
  case "Claim":
@@ -248,17 +289,33 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
248
289
  parsedEvent = this.parseSpvCloseEvent(event as any);
249
290
  break;
250
291
  }
251
- const timestamp = event.blockNumber===currentBlock.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
252
- parsedEvent.meta = {
253
- blockTime: timestamp,
254
- txId: event.transactionHash,
255
- timestamp //Maybe deprecated
256
- } as any;
257
- parsedEvents.push(parsedEvent);
258
- }
259
292
 
260
- for(let listener of this.listeners) {
261
- await listener(parsedEvents);
293
+ if(this.eventsProcessing[eventIdentifier]!=null) {
294
+ this.logger.debug("processEvents(): awaiting event that is currently processing: "+eventIdentifier);
295
+ await this.eventsProcessing[eventIdentifier];
296
+ continue;
297
+ }
298
+
299
+ const promise = (async() => {
300
+ const timestamp = event.blockNumber===currentBlock?.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
301
+ parsedEvent.meta = {
302
+ blockTime: timestamp,
303
+ txId: event.transactionHash,
304
+ timestamp //Maybe deprecated
305
+ } as any;
306
+ for(let listener of this.listeners) {
307
+ await listener([parsedEvent]);
308
+ }
309
+ this.addProcessedEvent(event);
310
+ })();
311
+ this.eventsProcessing[eventIdentifier] = promise;
312
+ try {
313
+ await promise;
314
+ delete this.eventsProcessing[eventIdentifier];
315
+ } catch (e) {
316
+ delete this.eventsProcessing[eventIdentifier];
317
+ throw e;
318
+ }
262
319
  }
263
320
  }
264
321
 
@@ -364,8 +421,78 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
364
421
  await func();
365
422
  }
366
423
 
424
+ //Websocket
425
+
426
+ protected handleWsEvents(
427
+ events : AtomiqTypedEvent[]
428
+ ): Promise<void> {
429
+ if(this.chainInterface.config.safeBlockTag==="latest" || this.chainInterface.config.safeBlockTag==="pending") {
430
+ return this.processEvents(events);
431
+ }
432
+ this.unconfirmedEventQueue.push(...events);
433
+ }
434
+
435
+ protected spvVaultContractListener: (log: Log) => void;
436
+ protected swapContractListener: (log: Log) => void;
437
+ protected blockListener: (blockNumber: number) => Promise<void>;
438
+
439
+ protected wsStarted: boolean = false;
440
+
441
+ protected async setupWebsocket() {
442
+ this.wsStarted = true;
443
+
444
+ await this.provider.on(this.spvVaultContractLogFilter, this.spvVaultContractListener = (log) => {
445
+ let events = this.evmSpvVaultContract.Events.toTypedEvents([log]);
446
+ events = events.filter(val => !val.removed);
447
+ this.handleWsEvents(events);
448
+ });
449
+
450
+ await this.provider.on(this.swapContractLogFilter, this.swapContractListener = (log) => {
451
+ let events = this.evmSwapContract.Events.toTypedEvents([log]);
452
+ events = events.filter(val => !val.removed && (val.eventName==="Initialize" || val.eventName==="Refund" || val.eventName==="Claim"));
453
+ this.handleWsEvents(events);
454
+ });
455
+
456
+ const safeBlockTag = this.chainInterface.config.safeBlockTag;
457
+ let processing = false;
458
+ if(safeBlockTag!=="latest" && safeBlockTag!=="pending") await this.provider.on("block", this.blockListener = async (blockNumber: number) => {
459
+ if(processing) return;
460
+ processing = true;
461
+ try {
462
+ const latestSafeBlock = await this.provider.getBlock(this.chainInterface.config.safeBlockTag);
463
+
464
+ const events = [];
465
+ this.unconfirmedEventQueue = this.unconfirmedEventQueue.filter(event => {
466
+ if(event.blockNumber <= latestSafeBlock.number) {
467
+ events.push(event);
468
+ return false;
469
+ }
470
+ return true;
471
+ });
472
+
473
+ const blocks: {[blockNumber: number]: Block} = {};
474
+ for(let event of events) {
475
+ const block = blocks[event.blockNumber] ?? (blocks[event.blockNumber] = await this.provider.getBlock(event.blockNumber));
476
+ if(block.hash===event.blockHash) {
477
+ //Valid event
478
+ await this.processEvents([event], block);
479
+ } else {
480
+ //Block hash doesn't match
481
+ }
482
+ }
483
+ } catch (e) {
484
+ this.logger.error(`on('block'): Error when processing new block ${blockNumber}:`, e);
485
+ }
486
+ processing = false;
487
+ });
488
+ }
489
+
367
490
  async init(): Promise<void> {
368
- this.setupPoll();
491
+ if((this.provider as any).websocket!=null) {
492
+ await this.setupWebsocket();
493
+ } else {
494
+ await this.setupPoll();
495
+ }
369
496
  this.stopped = false;
370
497
  return Promise.resolve();
371
498
  }
@@ -373,6 +500,12 @@ export class EVMChainEventsBrowser implements ChainEvents<EVMSwapData> {
373
500
  async stop(): Promise<void> {
374
501
  this.stopped = true;
375
502
  if(this.timeout!=null) clearTimeout(this.timeout);
503
+ if(this.wsStarted) {
504
+ await this.provider.off(this.spvVaultContractLogFilter, this.spvVaultContractListener);
505
+ await this.provider.off(this.swapContractLogFilter, this.swapContractListener);
506
+ await this.provider.off("block", this.blockListener);
507
+ this.wsStarted = false;
508
+ }
376
509
  }
377
510
 
378
511
  registerListener(cbk: EventListener<EVMSwapData>): void {
package/src/index.ts CHANGED
@@ -17,7 +17,6 @@ export * from "./evm/contract/EVMContractBase";
17
17
  export * from "./evm/contract/EVMContractModule";
18
18
 
19
19
  export * from "./evm/events/EVMChainEventsBrowser";
20
- export * from "./evm/events/EVMChainEventsBrowserWS";
21
20
 
22
21
  export * from "./evm/spv_swap/EVMSpvVaultContract";
23
22
  export * from "./evm/spv_swap/EVMSpvWithdrawalData";
@@ -49,3 +48,4 @@ export * from "./chains/botanix/BotanixInitializer";
49
48
  export * from "./chains/botanix/BotanixChainType";
50
49
 
51
50
  export * from "./evm/JsonRpcProviderWithRetries";
51
+ export * from "./evm/WebSocketProviderWithRetries";
@@ -1,354 +0,0 @@
1
- import {
2
- ChainEvent,
3
- ChainEvents,
4
- ChainSwapType,
5
- ClaimEvent,
6
- EventListener,
7
- InitializeEvent,
8
- RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent
9
- } from "@atomiqlabs/base";
10
- import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
11
- import {EVMSwapData} from "../swaps/EVMSwapData";
12
- import {Block, EventFilter, hexlify, Log, WebSocketProvider} from "ethers";
13
- import { EVMSwapContract } from "../swaps/EVMSwapContract";
14
- import {getLogger, onceAsync} from "../../utils/Utils";
15
- import {EVMSpvVaultContract, unpackOwnerAndVaultId} from "../spv_swap/EVMSpvVaultContract";
16
- import { EVMChainInterface } from "../chain/EVMChainInterface";
17
- import {TypedEventLog} from "../typechain/common";
18
- import {EscrowManager} from "../swaps/EscrowManagerTypechain";
19
- import {SpvVaultManager} from "../spv_swap/SpvVaultContractTypechain";
20
- import {EVMTxTrace} from "../chain/modules/EVMTransactions";
21
-
22
- type AtomiqTypedEvent = (
23
- TypedEventLog<EscrowManager["filters"]["Initialize" | "Refund" | "Claim"]> |
24
- TypedEventLog<SpvVaultManager["filters"]["Opened" | "Deposited" | "Fronted" | "Claimed" | "Closed"]>
25
- );
26
-
27
- /**
28
- * EVM on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
29
- * out on some events if the network is unreliable, front-end systems should take this into consideration and not
30
- * rely purely on events
31
- */
32
- export class EVMChainEventsBrowserWS implements ChainEvents<EVMSwapData> {
33
-
34
- protected readonly listeners: EventListener<EVMSwapData>[] = [];
35
- protected readonly provider: WebSocketProvider;
36
- protected readonly chainInterface: EVMChainInterface;
37
- protected readonly evmSwapContract: EVMSwapContract;
38
- protected readonly evmSpvVaultContract: EVMSpvVaultContract<any>;
39
- protected readonly logger = getLogger("EVMChainEventsBrowser: ");
40
-
41
- protected readonly spvVaultContractLogFilter: EventFilter;
42
- protected readonly swapContractLogFilter: EventFilter;
43
-
44
- protected unconfirmedEventQueue: AtomiqTypedEvent[] = [];
45
-
46
- protected stopped: boolean;
47
-
48
- constructor(
49
- chainInterface: EVMChainInterface,
50
- evmSwapContract: EVMSwapContract,
51
- evmSpvVaultContract: EVMSpvVaultContract<any>
52
- ) {
53
- this.chainInterface = chainInterface;
54
- this.provider = chainInterface.provider as WebSocketProvider;
55
- this.evmSwapContract = evmSwapContract;
56
- this.evmSpvVaultContract = evmSpvVaultContract;
57
-
58
- this.spvVaultContractLogFilter = {
59
- address: this.evmSpvVaultContract.contractAddress
60
- };
61
- this.swapContractLogFilter = {
62
- address: this.evmSwapContract.contractAddress
63
- };
64
- }
65
-
66
- findInitSwapData(call: EVMTxTrace, escrowHash: string, claimHandler: IClaimHandler<any, any>): EVMSwapData {
67
- if(call.to.toLowerCase() === this.evmSwapContract.contractAddress.toLowerCase()) {
68
- const result = this.evmSwapContract.parseCalldata<typeof this.evmSwapContract.contract.initialize>(call.input);
69
- if(result!=null && result.name==="initialize") {
70
- //Found, check correct escrow hash
71
- const [escrowData, signature, timeout, extraData] = result.args;
72
- const escrow = EVMSwapData.deserializeFromStruct(escrowData, claimHandler);
73
- if("0x"+escrow.getEscrowHash()===escrowHash) {
74
- const extraDataHex = hexlify(extraData);
75
- if(extraDataHex.length>2) {
76
- escrow.setExtraData(extraDataHex.substring(2));
77
- }
78
- return escrow;
79
- }
80
- }
81
- }
82
- for(let _call of call.calls) {
83
- const found = this.findInitSwapData(_call, escrowHash, claimHandler);
84
- if(found!=null) return found;
85
- }
86
- return null;
87
- }
88
-
89
- /**
90
- * Returns async getter for fetching on-demand initialize event swap data
91
- *
92
- * @param event
93
- * @param claimHandler
94
- * @private
95
- * @returns {() => Promise<EVMSwapData>} getter to be passed to InitializeEvent constructor
96
- */
97
- private getSwapDataGetter(
98
- event: TypedEventLog<EscrowManager["filters"]["Initialize"]>,
99
- claimHandler: IClaimHandler<any, any>
100
- ): () => Promise<EVMSwapData> {
101
- return async () => {
102
- const trace = await this.chainInterface.Transactions.traceTransaction(event.transactionHash);
103
- if(trace==null) return null;
104
- return this.findInitSwapData(trace, event.args.escrowHash, claimHandler);
105
- }
106
- }
107
-
108
- protected parseInitializeEvent(
109
- event: TypedEventLog<EscrowManager["filters"]["Initialize"]>
110
- ): InitializeEvent<EVMSwapData> {
111
- const escrowHash = event.args.escrowHash.substring(2);
112
- const claimHandlerHex = event.args.claimHandler;
113
- const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
114
- if(claimHandler==null) {
115
- this.logger.warn("parseInitializeEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
116
- return null;
117
- }
118
- const swapType: ChainSwapType = claimHandler.getType();
119
-
120
- this.logger.debug("InitializeEvent escrowHash: "+escrowHash);
121
- return new InitializeEvent<EVMSwapData>(
122
- escrowHash,
123
- swapType,
124
- onceAsync<EVMSwapData>(this.getSwapDataGetter(event, claimHandler))
125
- );
126
- }
127
-
128
- protected parseRefundEvent(
129
- event: TypedEventLog<EscrowManager["filters"]["Refund"]>
130
- ): RefundEvent<EVMSwapData> {
131
- const escrowHash = event.args.escrowHash.substring(2);
132
- this.logger.debug("RefundEvent escrowHash: "+escrowHash);
133
- return new RefundEvent<EVMSwapData>(escrowHash);
134
- }
135
-
136
- protected parseClaimEvent(
137
- event: TypedEventLog<EscrowManager["filters"]["Claim"]>
138
- ): ClaimEvent<EVMSwapData> {
139
- const escrowHash = event.args.escrowHash.substring(2);
140
- const claimHandlerHex = event.args.claimHandler;
141
- const claimHandler = this.evmSwapContract.claimHandlersByAddress[claimHandlerHex.toLowerCase()];
142
- if(claimHandler==null) {
143
- this.logger.warn("parseClaimEvent("+escrowHash+"): Unknown claim handler with claim: "+claimHandlerHex);
144
- return null;
145
- }
146
- const witnessResult = event.args.witnessResult.substring(2);
147
- this.logger.debug("ClaimEvent witnessResult: "+witnessResult+" escrowHash: "+escrowHash);
148
- return new ClaimEvent<EVMSwapData>(escrowHash, witnessResult);
149
- }
150
-
151
- protected parseSpvOpenEvent(
152
- event: TypedEventLog<SpvVaultManager["filters"]["Opened"]>
153
- ): SpvVaultOpenEvent {
154
- const owner = event.args.owner;
155
- const vaultId = event.args.vaultId;
156
- const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
157
- const vout = Number(event.args.vout);
158
-
159
- this.logger.debug("SpvOpenEvent owner: "+owner+" vaultId: "+vaultId+" utxo: "+btcTxId+":"+vout);
160
- return new SpvVaultOpenEvent(owner, vaultId, btcTxId, vout);
161
- }
162
-
163
- protected parseSpvDepositEvent(
164
- event: TypedEventLog<SpvVaultManager["filters"]["Deposited"]>
165
- ): SpvVaultDepositEvent {
166
- const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
167
- const amounts = [event.args.amount0, event.args.amount1];
168
- const depositCount = Number(event.args.depositCount);
169
-
170
- this.logger.debug("SpvDepositEvent owner: "+owner+" vaultId: "+vaultId+" depositCount: "+depositCount+" amounts: ", amounts);
171
- return new SpvVaultDepositEvent(owner, vaultId, amounts, depositCount);
172
- }
173
-
174
- protected parseSpvFrontEvent(
175
- event: TypedEventLog<SpvVaultManager["filters"]["Fronted"]>
176
- ): SpvVaultFrontEvent {
177
- const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
178
- const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
179
- const recipient = event.args.recipient;
180
- const executionHash = event.args.executionHash;
181
- const amounts = [event.args.amount0, event.args.amount1];
182
- const frontingAddress = event.args.caller;
183
-
184
- this.logger.debug("SpvFrontEvent owner: "+owner+" vaultId: "+vaultId+" btcTxId: "+btcTxId+
185
- " recipient: "+recipient+" frontedBy: "+frontingAddress+" amounts: ", amounts);
186
- return new SpvVaultFrontEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, frontingAddress);
187
- }
188
-
189
- protected parseSpvClaimEvent(
190
- event: TypedEventLog<SpvVaultManager["filters"]["Claimed"]>
191
- ): SpvVaultClaimEvent {
192
- const [owner, vaultId] = unpackOwnerAndVaultId(event.args.ownerAndVaultId);
193
- const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
194
- const recipient = event.args.recipient;
195
- const executionHash = event.args.executionHash;
196
- const amounts = [event.args.amount0, event.args.amount1];
197
- const caller = event.args.caller;
198
- const frontingAddress = event.args.frontingAddress;
199
- const withdrawCount = Number(event.args.withdrawCount);
200
-
201
- this.logger.debug("SpvClaimEvent owner: "+owner+" vaultId: "+vaultId+" btcTxId: "+btcTxId+" withdrawCount: "+withdrawCount+
202
- " recipient: "+recipient+" frontedBy: "+frontingAddress+" claimedBy: "+caller+" amounts: ", amounts);
203
-
204
- return new SpvVaultClaimEvent(owner, vaultId, btcTxId, recipient, executionHash, amounts, caller, frontingAddress, withdrawCount);
205
- }
206
-
207
- protected parseSpvCloseEvent(
208
- event: TypedEventLog<SpvVaultManager["filters"]["Closed"]>
209
- ): SpvVaultCloseEvent {
210
- const btcTxId = Buffer.from(event.args.btcTxHash.substring(2), "hex").reverse().toString("hex");
211
-
212
- return new SpvVaultCloseEvent(event.args.owner, event.args.vaultId, btcTxId, event.args.error);
213
- }
214
-
215
- /**
216
- * Processes event as received from the chain, parses it & calls event listeners
217
- *
218
- * @param events
219
- * @param currentBlock
220
- * @protected
221
- */
222
- protected async processEvents(
223
- events: AtomiqTypedEvent[],
224
- currentBlock?: Block
225
- ) {
226
- const parsedEvents: ChainEvent<EVMSwapData>[] = [];
227
-
228
- for(let event of events) {
229
- let parsedEvent: ChainEvent<EVMSwapData>;
230
- switch(event.eventName) {
231
- case "Claim":
232
- parsedEvent = this.parseClaimEvent(event as any);
233
- break;
234
- case "Refund":
235
- parsedEvent = this.parseRefundEvent(event as any);
236
- break;
237
- case "Initialize":
238
- parsedEvent = this.parseInitializeEvent(event as any);
239
- break;
240
- case "Opened":
241
- parsedEvent = this.parseSpvOpenEvent(event as any);
242
- break;
243
- case "Deposited":
244
- parsedEvent = this.parseSpvDepositEvent(event as any);
245
- break;
246
- case "Fronted":
247
- parsedEvent = this.parseSpvFrontEvent(event as any);
248
- break;
249
- case "Claimed":
250
- parsedEvent = this.parseSpvClaimEvent(event as any);
251
- break;
252
- case "Closed":
253
- parsedEvent = this.parseSpvCloseEvent(event as any);
254
- break;
255
- }
256
- const timestamp = event.blockNumber===currentBlock?.number ? currentBlock.timestamp : await this.chainInterface.Blocks.getBlockTime(event.blockNumber);
257
- parsedEvent.meta = {
258
- blockTime: timestamp,
259
- txId: event.transactionHash,
260
- timestamp //Maybe deprecated
261
- } as any;
262
- parsedEvents.push(parsedEvent);
263
- }
264
-
265
- for(let listener of this.listeners) {
266
- await listener(parsedEvents);
267
- }
268
- }
269
-
270
- protected handleEvents(
271
- events : AtomiqTypedEvent[]
272
- ): Promise<void> {
273
- if(this.chainInterface.config.safeBlockTag==="latest" || this.chainInterface.config.safeBlockTag==="pending") {
274
- return this.processEvents(events);
275
- }
276
- this.unconfirmedEventQueue.push(...events);
277
- }
278
-
279
- protected spvVaultContractListener: (log: Log) => void;
280
- protected swapContractListener: (log: Log) => void;
281
- protected blockListener: (blockNumber: number) => Promise<void>;
282
-
283
- protected async setupWebsocket() {
284
- await this.provider.on(this.spvVaultContractLogFilter, this.spvVaultContractListener = (log) => {
285
- let events = this.evmSpvVaultContract.Events.toTypedEvents([log]);
286
- events = events.filter(val => !val.removed);
287
- this.handleEvents(events);
288
- });
289
-
290
- await this.provider.on(this.swapContractLogFilter, this.swapContractListener = (log) => {
291
- let events = this.evmSwapContract.Events.toTypedEvents([log]);
292
- events = events.filter(val => !val.removed && (val.eventName==="Initialize" || val.eventName==="Refund" || val.eventName==="Claim"));
293
- this.handleEvents(events);
294
- });
295
-
296
- const safeBlockTag = this.chainInterface.config.safeBlockTag;
297
- let processing = false;
298
- if(safeBlockTag!=="latest" && safeBlockTag!=="pending") await this.provider.on("block", this.blockListener = async (blockNumber: number) => {
299
- if(processing) return;
300
- processing = true;
301
- try {
302
- const latestSafeBlock = await this.provider.getBlock(this.chainInterface.config.safeBlockTag);
303
-
304
- const events = [];
305
- this.unconfirmedEventQueue = this.unconfirmedEventQueue.filter(event => {
306
- if(event.blockNumber <= latestSafeBlock.number) {
307
- events.push(event);
308
- return false;
309
- }
310
- return true;
311
- });
312
-
313
- const blocks: {[blockNumber: number]: Block} = {};
314
- for(let event of events) {
315
- const block = blocks[event.blockNumber] ?? (blocks[event.blockNumber] = await this.provider.getBlock(event.blockNumber));
316
- if(block.hash===event.blockHash) {
317
- //Valid event
318
- await this.processEvents([event], block);
319
- } else {
320
- //Block hash doesn't match
321
- }
322
- }
323
- } catch (e) {
324
- this.logger.error(`on('block'): Error when processing new block ${blockNumber}:`, e);
325
- }
326
- processing = false;
327
- });
328
- }
329
-
330
- async init(): Promise<void> {
331
- await this.setupWebsocket();
332
- this.stopped = false;
333
- }
334
-
335
- async stop(): Promise<void> {
336
- this.stopped = true;
337
- await this.provider.off(this.spvVaultContractLogFilter, this.spvVaultContractListener);
338
- await this.provider.off(this.swapContractLogFilter, this.swapContractListener);
339
- await this.provider.off("block", this.blockListener);
340
- }
341
-
342
- registerListener(cbk: EventListener<EVMSwapData>): void {
343
- this.listeners.push(cbk);
344
- }
345
-
346
- unregisterListener(cbk: EventListener<EVMSwapData>): boolean {
347
- const index = this.listeners.indexOf(cbk);
348
- if(index>=0) {
349
- this.listeners.splice(index, 1);
350
- return true;
351
- }
352
- return false;
353
- }
354
- }