@atomiqlabs/chain-evm 1.0.0-dev.50 → 1.0.0-dev.52

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.
@@ -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,5 @@ 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";
43
+ export * from "./evm/ReconnectingWebSocketProvider";
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,5 @@ __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);
59
+ __exportStar(require("./evm/ReconnectingWebSocketProvider"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-evm",
3
- "version": "1.0.0-dev.50",
3
+ "version": "1.0.0-dev.52",
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 as any).websocket!=null ?
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 as any).websocket!=null ?
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,82 @@
1
+ import {JsonRpcApiProviderOptions} from "ethers";
2
+ import type {Networkish, WebSocketLike} from "ethers";
3
+ import {SocketProvider} from "./SocketProvider";
4
+
5
+
6
+ export class ReconnectingWebSocketProvider extends SocketProvider {
7
+
8
+ reconnectSeconds: number = 15;
9
+ pingIntervalSeconds: number = 60;
10
+
11
+ pingInterval: any;
12
+ reconnectTimer: any;
13
+
14
+ url: string;
15
+ websocket: null | WebSocketLike & {onclose?: (...args: any[]) => void, ping?: () => void};
16
+
17
+ constructor(url: string, network?: Networkish, options?: JsonRpcApiProviderOptions) {
18
+ super(network, options);
19
+ this.url = url;
20
+ this.connect();
21
+ }
22
+
23
+ private connect() {
24
+ this.websocket = new WebSocket(this.url);
25
+
26
+ this.websocket.onopen = () => {
27
+ this._connected();
28
+ this._start();
29
+
30
+ this.pingInterval = setInterval(() => {
31
+ this.websocket.ping();
32
+ }, this.pingIntervalSeconds * 1000);
33
+ };
34
+
35
+ this.websocket.onerror = (err) => {
36
+ if(this.destroyed) return;
37
+ this.websocket = null;
38
+ if(this.pingInterval!=null) clearInterval(this.pingInterval);
39
+
40
+ //Fail all in-flight requests
41
+ this._disconnected();
42
+
43
+ console.error(`Websocket connection error retrying in ${this.reconnectSeconds} seconds...`, err);
44
+ this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectSeconds * 1000);
45
+ };
46
+
47
+ this.websocket.onmessage = (message: { data: string }) => {
48
+ this._processMessage(message.data);
49
+ };
50
+
51
+ this.websocket.onclose = (event) => {
52
+ if(this.destroyed) return;
53
+ this.websocket = null;
54
+ if(this.pingInterval!=null) clearInterval(this.pingInterval);
55
+
56
+ //Fail all in-flight requests
57
+ this._disconnected();
58
+
59
+ console.error(`Websocket connection closed retrying in ${this.reconnectSeconds} seconds...`, event);
60
+ this.reconnectTimer = setTimeout(() => this.connect(), this.reconnectSeconds * 1000);
61
+ };
62
+ }
63
+
64
+ async _write(message: string): Promise<void> {
65
+ this.websocket.send(message);
66
+ }
67
+
68
+ async destroy(): Promise<void> {
69
+ if (this.websocket != null) {
70
+ this.websocket.close();
71
+ this.websocket = null;
72
+ }
73
+ if(this.reconnectTimer!=null) {
74
+ clearTimeout(this.reconnectTimer);
75
+ }
76
+ if(this.pingInterval!=null) {
77
+ clearInterval(this.pingInterval);
78
+ }
79
+ super.destroy();
80
+ }
81
+
82
+ }