@atomiqlabs/chain-starknet 7.0.3 → 7.0.5

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,4 +1,4 @@
1
- import { constants, Provider } from "starknet";
1
+ import { constants, Provider, WebSocketChannel } from "starknet";
2
2
  import { StarknetFees } from "./chain/modules/StarknetFees";
3
3
  import { StarknetConfig, StarknetRetryPolicy } from "./chain/StarknetChainInterface";
4
4
  import { BaseTokenType, BitcoinNetwork, BitcoinRpc, ChainData, ChainInitializer, ChainSwapType } from "@atomiqlabs/base";
@@ -7,6 +7,7 @@ export type StarknetAssetsType = BaseTokenType<"ETH" | "STRK" | "WBTC" | "TBTC"
7
7
  export declare const StarknetAssets: StarknetAssetsType;
8
8
  export type StarknetOptions = {
9
9
  rpcUrl: string | Provider;
10
+ wsUrl?: string | WebSocketChannel;
10
11
  retryPolicy?: StarknetRetryPolicy;
11
12
  chainId?: constants.StarknetChainId;
12
13
  swapContract?: string;
@@ -42,10 +42,15 @@ function initializeStarknet(options, bitcoinRpc, network) {
42
42
  const provider = typeof (options.rpcUrl) === "string" ?
43
43
  new RpcProviderWithRetries_1.RpcProviderWithRetries({ nodeUrl: options.rpcUrl }) :
44
44
  options.rpcUrl;
45
+ let wsChannel;
46
+ if (options.wsUrl != null)
47
+ wsChannel = typeof (options.wsUrl) === "string" ?
48
+ new starknet_1.WebSocketChannel({ nodeUrl: options.wsUrl, websocket: typeof window !== "undefined" && typeof window.WebSocket !== "undefined" ? window.WebSocket : require("ws") }) :
49
+ options.wsUrl;
45
50
  const Fees = options.fees ?? new StarknetFees_1.StarknetFees(provider);
46
51
  const chainId = options.chainId ??
47
52
  (network === base_1.BitcoinNetwork.MAINNET ? starknet_1.constants.StarknetChainId.SN_MAIN : starknet_1.constants.StarknetChainId.SN_SEPOLIA);
48
- const chainInterface = new StarknetChainInterface_1.StarknetChainInterface(chainId, provider, options.retryPolicy, Fees, options.starknetConfig);
53
+ const chainInterface = new StarknetChainInterface_1.StarknetChainInterface(chainId, provider, wsChannel, options.retryPolicy, Fees, options.starknetConfig);
49
54
  const btcRelay = new StarknetBtcRelay_1.StarknetBtcRelay(chainInterface, bitcoinRpc, network, options.btcRelayContract);
50
55
  const swapContract = new StarknetSwapContract_1.StarknetSwapContract(chainInterface, btcRelay, options.swapContract, options.handlerContracts);
51
56
  const spvVaultContract = new StarknetSpvVaultContract_1.StarknetSpvVaultContract(chainInterface, btcRelay, bitcoinRpc, options.spvVaultContract);
@@ -1,4 +1,4 @@
1
- import { Provider, constants, Account } from "starknet";
1
+ import { Provider, constants, Account, WebSocketChannel } from "starknet";
2
2
  import { StarknetTransactions, StarknetTx } from "./modules/StarknetTransactions";
3
3
  import { StarknetFees } from "./modules/StarknetFees";
4
4
  import { StarknetTokens } from "./modules/StarknetTokens";
@@ -21,6 +21,7 @@ export type StarknetConfig = {
21
21
  };
22
22
  export declare class StarknetChainInterface implements ChainInterface<StarknetTx, StarknetSigner, "STARKNET", Account> {
23
23
  readonly chainId = "STARKNET";
24
+ readonly wsChannel?: WebSocketChannel;
24
25
  readonly provider: Provider;
25
26
  readonly retryPolicy: StarknetRetryPolicy;
26
27
  readonly starknetChainId: constants.StarknetChainId;
@@ -33,7 +34,7 @@ export declare class StarknetChainInterface implements ChainInterface<StarknetTx
33
34
  readonly Blocks: StarknetBlocks;
34
35
  protected readonly logger: import("../../utils/Utils").LoggerType;
35
36
  readonly config: StarknetConfig;
36
- constructor(chainId: constants.StarknetChainId, provider: Provider, retryPolicy?: StarknetRetryPolicy, feeEstimator?: StarknetFees, options?: StarknetConfig);
37
+ constructor(chainId: constants.StarknetChainId, provider: Provider, wsChannel?: WebSocketChannel, retryPolicy?: StarknetRetryPolicy, feeEstimator?: StarknetFees, options?: StarknetConfig);
37
38
  getBalance(signer: string, tokenAddress: string): Promise<bigint>;
38
39
  getNativeCurrencyAddress(): string;
39
40
  isValidToken(tokenIdentifier: string): boolean;
@@ -16,7 +16,7 @@ const buffer_1 = require("buffer");
16
16
  const StarknetKeypairWallet_1 = require("../wallet/accounts/StarknetKeypairWallet");
17
17
  const StarknetBrowserSigner_1 = require("../wallet/StarknetBrowserSigner");
18
18
  class StarknetChainInterface {
19
- constructor(chainId, provider, retryPolicy, feeEstimator = new StarknetFees_1.StarknetFees(provider), options) {
19
+ constructor(chainId, provider, wsChannel, retryPolicy, feeEstimator = new StarknetFees_1.StarknetFees(provider), options) {
20
20
  var _a, _b, _c, _d;
21
21
  this.chainId = "STARKNET";
22
22
  this.logger = (0, Utils_1.getLogger)("StarknetChainInterface: ");
@@ -28,6 +28,7 @@ class StarknetChainInterface {
28
28
  (_b = this.config).getLogChunkSize ?? (_b.getLogChunkSize = 100);
29
29
  (_c = this.config).maxGetLogKeys ?? (_c.maxGetLogKeys = 64);
30
30
  (_d = this.config).maxParallelCalls ?? (_d.maxParallelCalls = 10);
31
+ this.wsChannel = wsChannel;
31
32
  this.Fees = feeEstimator;
32
33
  this.Tokens = new StarknetTokens_1.StarknetTokens(this);
33
34
  this.Transactions = new StarknetTransactions_1.StarknetTransactions(this);
@@ -1,6 +1,6 @@
1
1
  import { Abi } from "abi-wan-kanabi";
2
2
  import { EventToPrimitiveType, ExtractAbiEventNames } from "abi-wan-kanabi/dist/kanabi";
3
- import { StarknetEvents } from "../../chain/modules/StarknetEvents";
3
+ import { StarknetEvent, StarknetEvents } from "../../chain/modules/StarknetEvents";
4
4
  import { StarknetContractBase } from "../StarknetContractBase";
5
5
  import { StarknetChainInterface } from "../../chain/StarknetChainInterface";
6
6
  export type StarknetAbiEvent<TAbi extends Abi, TEventName extends ExtractAbiEventNames<TAbi>> = {
@@ -16,8 +16,8 @@ export declare class StarknetContractEvents<TAbi extends Abi> extends StarknetEv
16
16
  readonly contract: StarknetContractBase<TAbi>;
17
17
  readonly abi: TAbi;
18
18
  constructor(chainInterface: StarknetChainInterface, contract: StarknetContractBase<TAbi>, abi: TAbi);
19
- private toStarknetAbiEvents;
20
- private toFilter;
19
+ toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[];
20
+ toFilter<T extends ExtractAbiEventNames<TAbi>>(events: T[], keys: (string | string[])[]): string[][];
21
21
  /**
22
22
  * Returns the events occuring in a range of starknet block as identified by the contract and keys,
23
23
  * returns pending events if no startHeight & endHeight is passed
@@ -18,22 +18,21 @@ class StarknetChainEvents extends StarknetChainEventsBrowser_1.StarknetChainEven
18
18
  async getLastEventData() {
19
19
  try {
20
20
  const txt = (await fs.readFile(this.directory + BLOCKHEIGHT_FILENAME)).toString();
21
- const arr = txt.split(";");
22
- if (arr.length < 2)
23
- return {
24
- blockNumber: parseInt(arr[0]),
25
- txHashes: null
26
- };
27
- return {
28
- blockNumber: parseInt(arr[0]),
29
- txHashes: arr.slice(1)
30
- };
21
+ const arr = txt.split(",");
22
+ if (arr.length < 2) {
23
+ const blockNumber = parseInt(arr[0].split(";")[0]);
24
+ return [
25
+ { lastBlockNumber: blockNumber, lastTxHash: null },
26
+ { lastBlockNumber: blockNumber, lastTxHash: null }
27
+ ];
28
+ }
29
+ return arr.map(arrValue => {
30
+ const subArray = arrValue.split(";");
31
+ return { lastBlockNumber: parseInt(subArray[0]), lastTxHash: subArray[1] };
32
+ });
31
33
  }
32
34
  catch (e) {
33
- return {
34
- blockNumber: null,
35
- txHashes: null
36
- };
35
+ return [];
37
36
  }
38
37
  }
39
38
  /**
@@ -41,12 +40,14 @@ class StarknetChainEvents extends StarknetChainEventsBrowser_1.StarknetChainEven
41
40
  *
42
41
  * @private
43
42
  */
44
- saveLastEventData(blockNumber, txHashes) {
45
- return fs.writeFile(this.directory + BLOCKHEIGHT_FILENAME, blockNumber.toString() + ";" + txHashes.join(";"));
43
+ saveLastEventData(newState) {
44
+ return fs.writeFile(this.directory + BLOCKHEIGHT_FILENAME, newState.map(value => value.lastTxHash == null ? value.lastBlockNumber.toString(10) : value.lastBlockNumber.toString(10) + ";" + value.lastTxHash).join(","));
46
45
  }
47
46
  async init() {
48
- const { blockNumber, txHashes } = await this.getLastEventData();
49
- await this.setupPoll(blockNumber, txHashes, (blockNumber, txHashes) => this.saveLastEventData(blockNumber, txHashes));
47
+ const lastEventsState = await this.getLastEventData();
48
+ if (this.wsChannel != null)
49
+ await this.setupWebsocket();
50
+ await this.setupPoll(lastEventsState, (newState) => this.saveLastEventData(newState));
50
51
  }
51
52
  }
52
53
  exports.StarknetChainEvents = StarknetChainEvents;
@@ -1,7 +1,7 @@
1
1
  import { ChainEvents, ClaimEvent, EventListener, InitializeEvent, RefundEvent, SpvVaultClaimEvent, SpvVaultCloseEvent, SpvVaultDepositEvent, SpvVaultFrontEvent, SpvVaultOpenEvent } from "@atomiqlabs/base";
2
2
  import { StarknetSwapData } from "../swaps/StarknetSwapData";
3
3
  import { StarknetSwapContract } from "../swaps/StarknetSwapContract";
4
- import { BigNumberish, Provider } from "starknet";
4
+ import { BigNumberish, Provider, SubscriptionStarknetEventsEvent, WebSocketChannel } from "starknet";
5
5
  import { StarknetAbiEvent } from "../contract/modules/StarknetContractEvents";
6
6
  import { EscrowManagerAbiType } from "../swaps/EscrowManagerAbi";
7
7
  import { ExtractAbiFunctionNames } from "abi-wan-kanabi/dist/kanabi";
@@ -15,23 +15,36 @@ export type StarknetTraceCall = {
15
15
  entry_point_selector: string;
16
16
  calls: StarknetTraceCall[];
17
17
  };
18
+ export type StarknetEventListenerState = {
19
+ lastBlockNumber: number;
20
+ lastTxHash?: string;
21
+ };
18
22
  /**
19
23
  * Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
20
24
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
21
25
  * rely purely on events
22
26
  */
23
27
  export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
28
+ private eventsProcessing;
29
+ private processedEvents;
30
+ protected readonly Chain: StarknetChainInterface;
24
31
  protected readonly listeners: EventListener<StarknetSwapData>[];
32
+ protected readonly wsChannel?: WebSocketChannel;
25
33
  protected readonly provider: Provider;
26
34
  protected readonly starknetSwapContract: StarknetSwapContract;
27
35
  protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
28
36
  protected readonly logger: import("../../utils/Utils").LoggerType;
37
+ protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
38
+ protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
29
39
  protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType>;
30
40
  protected initEntryPointSelector: bigint;
31
41
  protected stopped: boolean;
32
42
  protected pollIntervalSeconds: number;
33
43
  private timeout;
34
44
  constructor(chainInterface: StarknetChainInterface, starknetSwapContract: StarknetSwapContract, starknetSpvVaultContract: StarknetSpvVaultContract, pollIntervalSeconds?: number);
45
+ private getEventFingerprint;
46
+ private addProcessedEvent;
47
+ private isEventProcessed;
35
48
  findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData;
36
49
  /**
37
50
  * Returns async getter for fetching on-demand initialize event swap data
@@ -58,25 +71,24 @@ export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetS
58
71
  * @param currentBlockTimestamp
59
72
  * @protected
60
73
  */
61
- protected processEvents(events: (StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize" | "escrow_manager::events::Refund" | "escrow_manager::events::Claim"> | StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Opened" | "spv_swap_vault::events::Deposited" | "spv_swap_vault::events::Fronted" | "spv_swap_vault::events::Claimed" | "spv_swap_vault::events::Closed">)[], currentBlockNumber: number, currentBlockTimestamp: number): Promise<void>;
62
- protected checkEventsEcrowManager(lastTxHash: string, lastBlockNumber?: number, currentBlock?: {
74
+ protected processEvents(events: (StarknetAbiEvent<EscrowManagerAbiType, "escrow_manager::events::Initialize" | "escrow_manager::events::Refund" | "escrow_manager::events::Claim"> | StarknetAbiEvent<SpvVaultContractAbiType, "spv_swap_vault::events::Opened" | "spv_swap_vault::events::Deposited" | "spv_swap_vault::events::Fronted" | "spv_swap_vault::events::Claimed" | "spv_swap_vault::events::Closed">)[], currentBlockNumber: number, currentBlockTimestamp?: number): Promise<void>;
75
+ protected checkEventsEcrowManager(currentBlock: {
63
76
  timestamp: number;
64
77
  block_number: number;
65
- }): Promise<string>;
66
- protected checkEventsSpvVaults(lastTxHash: string, lastBlockNumber?: number, currentBlock?: {
78
+ }, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]>;
79
+ protected checkEventsSpvVaults(currentBlock: {
67
80
  timestamp: number;
68
81
  block_number: number;
69
- }): Promise<string>;
70
- protected checkEvents(lastBlockNumber: number, lastTxHashes: string[]): Promise<{
71
- txHashes: string[];
72
- blockNumber: number;
73
- }>;
82
+ }, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]>;
83
+ protected checkEvents(lastState: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]>;
74
84
  /**
75
85
  * Sets up event handlers listening for swap events over websocket
76
86
  *
77
87
  * @protected
78
88
  */
79
- protected setupPoll(lastBlockNumber?: number, lastTxHashes?: string[], saveLatestProcessedBlockNumber?: (blockNumber: number, lastTxHashes: string[]) => Promise<void>): Promise<void>;
89
+ protected setupPoll(lastState?: StarknetEventListenerState[], saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>): Promise<void>;
90
+ protected wsStarted: boolean;
91
+ protected setupWebsocket(): Promise<void>;
80
92
  init(): Promise<void>;
81
93
  stop(): Promise<void>;
82
94
  registerListener(cbk: EventListener<StarknetSwapData>): void;
@@ -4,6 +4,10 @@ exports.StarknetChainEventsBrowser = void 0;
4
4
  const base_1 = require("@atomiqlabs/base");
5
5
  const Utils_1 = require("../../utils/Utils");
6
6
  const starknet_1 = require("starknet");
7
+ const sha2_1 = require("@noble/hashes/sha2");
8
+ const buffer_1 = require("buffer");
9
+ const PROCESSED_EVENTS_BACKLOG = 5000;
10
+ const LOGS_SLIDING_WINDOW = 60;
7
11
  /**
8
12
  * Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
9
13
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
@@ -11,15 +15,37 @@ const starknet_1 = require("starknet");
11
15
  */
12
16
  class StarknetChainEventsBrowser {
13
17
  constructor(chainInterface, starknetSwapContract, starknetSpvVaultContract, pollIntervalSeconds = 5) {
18
+ this.eventsProcessing = {};
19
+ this.processedEvents = new Set();
14
20
  this.listeners = [];
15
21
  this.logger = (0, Utils_1.getLogger)("StarknetChainEventsBrowser: ");
16
22
  this.initFunctionName = "initialize";
17
23
  this.initEntryPointSelector = BigInt(starknet_1.hash.starknetKeccak(this.initFunctionName));
24
+ this.wsStarted = false;
25
+ this.Chain = chainInterface;
26
+ this.wsChannel = chainInterface.wsChannel;
18
27
  this.provider = chainInterface.provider;
19
28
  this.starknetSwapContract = starknetSwapContract;
20
29
  this.starknetSpvVaultContract = starknetSpvVaultContract;
21
30
  this.pollIntervalSeconds = pollIntervalSeconds;
22
31
  }
32
+ getEventFingerprint(event) {
33
+ const eventData = buffer_1.Buffer.concat([
34
+ ...event.keys.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 64)),
35
+ ...event.data.map(value => (0, Utils_1.bigNumberishToBuffer)(value, 64))
36
+ ]);
37
+ const fingerprint = buffer_1.Buffer.from((0, sha2_1.sha256)(eventData));
38
+ return event.txHash + ":" + fingerprint.toString("hex");
39
+ }
40
+ addProcessedEvent(event) {
41
+ this.processedEvents.add(this.getEventFingerprint(event));
42
+ if (this.processedEvents.size > PROCESSED_EVENTS_BACKLOG)
43
+ this.processedEvents.delete(this.processedEvents.keys().next().value);
44
+ }
45
+ isEventProcessed(eventOrFingerprint) {
46
+ const eventFingerprint = typeof (eventOrFingerprint) === "string" ? eventOrFingerprint : this.getEventFingerprint(eventOrFingerprint);
47
+ return this.processedEvents.has(eventFingerprint);
48
+ }
23
49
  findInitSwapData(call, escrowHash, claimHandler) {
24
50
  if (BigInt(call.contract_address) === BigInt(this.starknetSwapContract.contract.address) &&
25
51
  BigInt(call.entry_point_selector) === this.initEntryPointSelector) {
@@ -164,11 +190,15 @@ class StarknetChainEventsBrowser {
164
190
  if (blockNumber === currentBlockNumber)
165
191
  return currentBlockTimestamp;
166
192
  const blockNumberString = blockNumber.toString();
167
- blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = (await this.provider.getBlockWithTxHashes(blockNumber)).timestamp);
193
+ blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = await this.Chain.Blocks.getBlockTime(blockNumber));
168
194
  return blockTimestampsCache[blockNumberString];
169
195
  };
170
- const parsedEvents = [];
171
196
  for (let event of events) {
197
+ const eventIdentifier = this.getEventFingerprint(event);
198
+ if (this.isEventProcessed(eventIdentifier)) {
199
+ this.logger.debug("processEvents(): skipping already processed event: " + eventIdentifier);
200
+ continue;
201
+ }
172
202
  let parsedEvent;
173
203
  switch (event.name) {
174
204
  case "escrow_manager::events::Claim":
@@ -196,84 +226,119 @@ class StarknetChainEventsBrowser {
196
226
  parsedEvent = this.parseSpvCloseEvent(event);
197
227
  break;
198
228
  }
199
- if (parsedEvent == null)
229
+ if (this.eventsProcessing[eventIdentifier] != null) {
230
+ this.logger.debug("processEvents(): awaiting event that is currently processing: " + eventIdentifier);
231
+ await this.eventsProcessing[eventIdentifier];
200
232
  continue;
201
- //We are not trusting pre-confs for events, so this shall never happen
202
- if (event.blockNumber == null)
203
- throw new Error("Event block number cannot be null!");
204
- const timestamp = await getBlockTimestamp(event.blockNumber);
205
- parsedEvent.meta = {
206
- blockTime: timestamp,
207
- txId: event.txHash,
208
- timestamp //Maybe deprecated
209
- };
210
- parsedEvents.push(parsedEvent);
211
- }
212
- for (let listener of this.listeners) {
213
- await listener(parsedEvents);
233
+ }
234
+ const promise = (async () => {
235
+ if (parsedEvent == null)
236
+ return;
237
+ //We are not trusting pre-confs for events, so this shall never happen
238
+ if (event.blockNumber == null)
239
+ throw new Error("Event block number cannot be null!");
240
+ const timestamp = await getBlockTimestamp(event.blockNumber);
241
+ parsedEvent.meta = {
242
+ blockTime: timestamp,
243
+ txId: event.txHash,
244
+ timestamp //Maybe deprecated
245
+ };
246
+ for (let listener of this.listeners) {
247
+ await listener([parsedEvent]);
248
+ }
249
+ this.addProcessedEvent(event);
250
+ })();
251
+ this.eventsProcessing[eventIdentifier] = promise;
252
+ try {
253
+ await promise;
254
+ delete this.eventsProcessing[eventIdentifier];
255
+ }
256
+ catch (e) {
257
+ delete this.eventsProcessing[eventIdentifier];
258
+ throw e;
259
+ }
214
260
  }
215
261
  }
216
- async checkEventsEcrowManager(lastTxHash, lastBlockNumber, currentBlock) {
262
+ async checkEventsEcrowManager(currentBlock, lastTxHash, lastBlockNumber) {
217
263
  const currentBlockNumber = currentBlock.block_number;
218
264
  lastBlockNumber ?? (lastBlockNumber = currentBlockNumber);
265
+ if (currentBlockNumber < lastBlockNumber) {
266
+ this.logger.warn(`checkEventsEscrowManager(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
267
+ return;
268
+ }
219
269
  // this.logger.debug("checkEvents(EscrowManager): Requesting logs: "+logStartHeight+"...pending");
220
270
  let events = await this.starknetSwapContract.Events.getContractBlockEvents(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], [], lastBlockNumber, null);
221
271
  if (lastTxHash != null) {
222
272
  const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash);
223
273
  if (latestProcessedEventIndex !== -1) {
224
274
  events.splice(0, latestProcessedEventIndex + 1);
225
- // this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: "+events.length);
275
+ this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: " + events.length);
226
276
  }
227
277
  }
228
278
  if (events.length > 0) {
229
279
  await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
230
- lastTxHash = events[events.length - 1].txHash;
280
+ const lastProcessed = events[events.length - 1];
281
+ lastTxHash = lastProcessed.txHash;
282
+ if (lastProcessed.blockNumber > lastBlockNumber)
283
+ lastBlockNumber = lastProcessed.blockNumber;
284
+ }
285
+ else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
286
+ lastTxHash = null;
287
+ lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
231
288
  }
232
- return lastTxHash;
289
+ return [lastTxHash, lastBlockNumber];
233
290
  }
234
- async checkEventsSpvVaults(lastTxHash, lastBlockNumber, currentBlock) {
291
+ async checkEventsSpvVaults(currentBlock, lastTxHash, lastBlockNumber) {
235
292
  const currentBlockNumber = currentBlock.block_number;
236
293
  lastBlockNumber ?? (lastBlockNumber = currentBlockNumber);
294
+ if (currentBlockNumber < lastBlockNumber) {
295
+ this.logger.warn(`checkEventsSpvVaults(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
296
+ return;
297
+ }
237
298
  // this.logger.debug("checkEvents(SpvVaults): Requesting logs: "+logStartHeight+"...pending");
238
299
  let events = await this.starknetSpvVaultContract.Events.getContractBlockEvents(["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"], [], lastBlockNumber, null);
239
300
  if (lastTxHash != null) {
240
301
  const latestProcessedEventIndex = (0, Utils_1.findLastIndex)(events, val => val.txHash === lastTxHash);
241
302
  if (latestProcessedEventIndex !== -1) {
242
303
  events.splice(0, latestProcessedEventIndex + 1);
243
- // this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: "+events.length);
304
+ this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: " + events.length);
244
305
  }
245
306
  }
246
307
  if (events.length > 0) {
247
308
  await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
248
- lastTxHash = events[events.length - 1].txHash;
309
+ const lastProcessed = events[events.length - 1];
310
+ lastTxHash = lastProcessed.txHash;
311
+ if (lastProcessed.blockNumber > lastBlockNumber)
312
+ lastBlockNumber = lastProcessed.blockNumber;
249
313
  }
250
- return lastTxHash;
314
+ else if (currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
315
+ lastTxHash = null;
316
+ lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
317
+ }
318
+ return [lastTxHash, lastBlockNumber];
251
319
  }
252
- async checkEvents(lastBlockNumber, lastTxHashes) {
253
- lastTxHashes ?? (lastTxHashes = []);
254
- const currentBlock = await this.provider.getBlockWithTxHashes(starknet_1.BlockTag.LATEST);
255
- const currentBlockNumber = currentBlock.block_number;
256
- lastTxHashes[0] = await this.checkEventsEcrowManager(lastTxHashes[0], lastBlockNumber, currentBlock);
257
- lastTxHashes[1] = await this.checkEventsSpvVaults(lastTxHashes[1], lastBlockNumber, currentBlock);
258
- return {
259
- txHashes: lastTxHashes,
260
- blockNumber: currentBlockNumber
261
- };
320
+ async checkEvents(lastState) {
321
+ lastState ?? (lastState = []);
322
+ const currentBlock = await this.Chain.Blocks.getBlock(starknet_1.BlockTag.LATEST);
323
+ const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
324
+ const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
325
+ return [
326
+ { lastBlockNumber: lastEscrowHeight, lastTxHash: lastEscrowTxHash },
327
+ { lastBlockNumber: lastSpvVaultHeight, lastTxHash: lastSpvVaultTxHash }
328
+ ];
262
329
  }
263
330
  /**
264
331
  * Sets up event handlers listening for swap events over websocket
265
332
  *
266
333
  * @protected
267
334
  */
268
- async setupPoll(lastBlockNumber, lastTxHashes, saveLatestProcessedBlockNumber) {
269
- this.stopped = false;
335
+ async setupPoll(lastState, saveLatestProcessedBlockNumber) {
270
336
  let func;
271
337
  func = async () => {
272
- await this.checkEvents(lastBlockNumber, lastTxHashes).then(({ blockNumber, txHashes }) => {
273
- lastBlockNumber = blockNumber;
274
- lastTxHashes = txHashes;
338
+ await this.checkEvents(lastState).then(newState => {
339
+ lastState = newState;
275
340
  if (saveLatestProcessedBlockNumber != null)
276
- return saveLatestProcessedBlockNumber(blockNumber, lastTxHashes);
341
+ return saveLatestProcessedBlockNumber(newState);
277
342
  }).catch(e => {
278
343
  this.logger.error("setupPoll(): Failed to fetch starknet log: ", e);
279
344
  });
@@ -283,14 +348,49 @@ class StarknetChainEventsBrowser {
283
348
  };
284
349
  await func();
285
350
  }
286
- init() {
287
- this.setupPoll();
288
- return Promise.resolve();
351
+ async setupWebsocket() {
352
+ this.wsStarted = true;
353
+ const [escrowContractSubscription, spvVaultContractSubscription] = await Promise.all([
354
+ this.wsChannel.subscribeEvents({
355
+ fromAddress: this.starknetSwapContract.contract.address,
356
+ keys: this.starknetSwapContract.Events.toFilter(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], []),
357
+ finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
358
+ }),
359
+ this.wsChannel.subscribeEvents({
360
+ fromAddress: this.starknetSpvVaultContract.contract.address,
361
+ keys: this.starknetSpvVaultContract.Events.toFilter(["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"], []),
362
+ finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
363
+ })
364
+ ]);
365
+ escrowContractSubscription.on((event) => {
366
+ const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents([event]);
367
+ this.processEvents(parsedEvents, event.block_number);
368
+ });
369
+ this.escrowContractSubscription = escrowContractSubscription;
370
+ spvVaultContractSubscription.on((event) => {
371
+ const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents([event]);
372
+ this.processEvents(parsedEvents, event.block_number);
373
+ });
374
+ this.spvVaultContractSubscription = spvVaultContractSubscription;
375
+ }
376
+ async init() {
377
+ if (this.wsChannel != null) {
378
+ await this.setupWebsocket();
379
+ }
380
+ else {
381
+ await this.setupPoll();
382
+ }
383
+ this.stopped = false;
289
384
  }
290
385
  async stop() {
291
386
  this.stopped = true;
292
387
  if (this.timeout != null)
293
388
  clearTimeout(this.timeout);
389
+ if (this.wsStarted) {
390
+ await this.escrowContractSubscription.unsubscribe();
391
+ await this.spvVaultContractSubscription.unsubscribe();
392
+ this.wsStarted = false;
393
+ }
294
394
  }
295
395
  registerListener(cbk) {
296
396
  this.listeners.push(cbk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-starknet",
3
- "version": "7.0.3",
3
+ "version": "7.0.5",
4
4
  "description": "Starknet specific base implementation",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -33,7 +33,8 @@
33
33
  "@scure/btc-signer": "^1.6.0",
34
34
  "abi-wan-kanabi": "2.2.4",
35
35
  "buffer": "6.0.3",
36
- "promise-queue-ts": "^1.0.0"
36
+ "promise-queue-ts": "^1.0.0",
37
+ "ws": "^8.18.3"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "starknet": "^8.5.0"
@@ -1,4 +1,4 @@
1
- import {constants, Provider} from "starknet";
1
+ import {constants, Provider, WebSocketChannel} from "starknet";
2
2
  import {StarknetFees} from "./chain/modules/StarknetFees";
3
3
  import {StarknetChainInterface, StarknetConfig, StarknetRetryPolicy} from "./chain/StarknetChainInterface";
4
4
  import {StarknetBtcRelay} from "./btcrelay/StarknetBtcRelay";
@@ -41,6 +41,7 @@ export const StarknetAssets: StarknetAssetsType = {
41
41
 
42
42
  export type StarknetOptions = {
43
43
  rpcUrl: string | Provider,
44
+ wsUrl?: string | WebSocketChannel,
44
45
  retryPolicy?: StarknetRetryPolicy,
45
46
  chainId?: constants.StarknetChainId,
46
47
 
@@ -69,13 +70,17 @@ export function initializeStarknet(
69
70
  const provider = typeof(options.rpcUrl)==="string" ?
70
71
  new RpcProviderWithRetries({nodeUrl: options.rpcUrl}) :
71
72
  options.rpcUrl;
73
+ let wsChannel: WebSocketChannel;
74
+ if(options.wsUrl!=null) wsChannel = typeof(options.wsUrl)==="string" ?
75
+ new WebSocketChannel({nodeUrl: options.wsUrl, websocket: typeof window !== "undefined" && typeof window.WebSocket !== "undefined" ? window.WebSocket : require("ws")}) :
76
+ options.wsUrl;
72
77
 
73
78
  const Fees = options.fees ?? new StarknetFees(provider);
74
79
 
75
80
  const chainId = options.chainId ??
76
81
  (network===BitcoinNetwork.MAINNET ? constants.StarknetChainId.SN_MAIN : constants.StarknetChainId.SN_SEPOLIA);
77
82
 
78
- const chainInterface = new StarknetChainInterface(chainId, provider, options.retryPolicy, Fees, options.starknetConfig);
83
+ const chainInterface = new StarknetChainInterface(chainId, provider, wsChannel, options.retryPolicy, Fees, options.starknetConfig);
79
84
 
80
85
  const btcRelay = new StarknetBtcRelay(
81
86
  chainInterface, bitcoinRpc, network, options.btcRelayContract
@@ -1,4 +1,4 @@
1
- import {Provider, constants, stark, ec, Account, provider, wallet} from "starknet";
1
+ import {Provider, constants, stark, ec, Account, provider, wallet, WebSocketChannel} from "starknet";
2
2
  import {getLogger, toHex} from "../../utils/Utils";
3
3
  import {StarknetTransactions, StarknetTx} from "./modules/StarknetTransactions";
4
4
  import {StarknetFees} from "./modules/StarknetFees";
@@ -32,6 +32,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
32
32
 
33
33
  readonly chainId = "STARKNET";
34
34
 
35
+ readonly wsChannel?: WebSocketChannel;
35
36
  readonly provider: Provider;
36
37
  readonly retryPolicy: StarknetRetryPolicy;
37
38
 
@@ -52,6 +53,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
52
53
  constructor(
53
54
  chainId: constants.StarknetChainId,
54
55
  provider: Provider,
56
+ wsChannel?: WebSocketChannel,
55
57
  retryPolicy?: StarknetRetryPolicy,
56
58
  feeEstimator: StarknetFees = new StarknetFees(provider),
57
59
  options?: StarknetConfig
@@ -64,6 +66,7 @@ export class StarknetChainInterface implements ChainInterface<StarknetTx, Starkn
64
66
  this.config.getLogChunkSize ??= 100;
65
67
  this.config.maxGetLogKeys ??= 64;
66
68
  this.config.maxParallelCalls ??= 10;
69
+ this.wsChannel = wsChannel;
67
70
 
68
71
  this.Fees = feeEstimator;
69
72
  this.Tokens = new StarknetTokens(this);
@@ -27,7 +27,7 @@ export class StarknetContractEvents<TAbi extends Abi> extends StarknetEvents {
27
27
  this.abi = abi;
28
28
  }
29
29
 
30
- private toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[] {
30
+ public toStarknetAbiEvents<T extends ExtractAbiEventNames<TAbi>>(blockEvents: StarknetEvent[]): StarknetAbiEvent<TAbi, T>[] {
31
31
  const abiEvents = events.getAbiEvents(this.abi);
32
32
  const abiStructs = CallData.getAbiStruct(this.abi);
33
33
  const abiEnums = CallData.getAbiEnum(this.abi);
@@ -49,7 +49,7 @@ export class StarknetContractEvents<TAbi extends Abi> extends StarknetEvents {
49
49
  });
50
50
  }
51
51
 
52
- private toFilter<T extends ExtractAbiEventNames<TAbi>>(
52
+ public toFilter<T extends ExtractAbiEventNames<TAbi>>(
53
53
  events: T[],
54
54
  keys: (string | string[])[],
55
55
  ): string[][] {
@@ -1,4 +1,4 @@
1
- import {StarknetChainEventsBrowser} from "./StarknetChainEventsBrowser";
1
+ import {StarknetChainEventsBrowser, StarknetEventListenerState} from "./StarknetChainEventsBrowser";
2
2
  //@ts-ignore
3
3
  import * as fs from "fs/promises";
4
4
  import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
@@ -27,23 +27,24 @@ export class StarknetChainEvents extends StarknetChainEventsBrowser {
27
27
  *
28
28
  * @private
29
29
  */
30
- private async getLastEventData(): Promise<{blockNumber: number, txHashes: string[]}> {
30
+ private async getLastEventData(): Promise<StarknetEventListenerState[]> {
31
31
  try {
32
32
  const txt: string = (await fs.readFile(this.directory+BLOCKHEIGHT_FILENAME)).toString();
33
- const arr = txt.split(";");
34
- if(arr.length<2) return {
35
- blockNumber: parseInt(arr[0]),
36
- txHashes: null
37
- };
38
- return {
39
- blockNumber: parseInt(arr[0]),
40
- txHashes: arr.slice(1)
41
- };
33
+ const arr = txt.split(",");
34
+ if(arr.length<2) {
35
+ const blockNumber = parseInt(arr[0].split(";")[0]);
36
+ return [
37
+ {lastBlockNumber: blockNumber, lastTxHash: null},
38
+ {lastBlockNumber: blockNumber, lastTxHash: null}
39
+ ];
40
+ }
41
+
42
+ return arr.map(arrValue => {
43
+ const subArray = arrValue.split(";");
44
+ return {lastBlockNumber: parseInt(subArray[0]), lastTxHash: subArray[1]};
45
+ })
42
46
  } catch (e) {
43
- return {
44
- blockNumber: null,
45
- txHashes: null
46
- };
47
+ return [];
47
48
  }
48
49
  }
49
50
 
@@ -52,16 +53,16 @@ export class StarknetChainEvents extends StarknetChainEventsBrowser {
52
53
  *
53
54
  * @private
54
55
  */
55
- private saveLastEventData(blockNumber: number, txHashes: string[]): Promise<void> {
56
- return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME, blockNumber.toString()+";"+txHashes.join(";"));
56
+ private saveLastEventData(newState: StarknetEventListenerState[]): Promise<void> {
57
+ return fs.writeFile(this.directory+BLOCKHEIGHT_FILENAME, newState.map(value => value.lastTxHash==null ? value.lastBlockNumber.toString(10) : value.lastBlockNumber.toString(10)+";"+value.lastTxHash).join(","));
57
58
  }
58
59
 
59
60
  async init(): Promise<void> {
60
- const {blockNumber, txHashes} = await this.getLastEventData();
61
+ const lastEventsState = await this.getLastEventData();
62
+ if(this.wsChannel!=null) await this.setupWebsocket();
61
63
  await this.setupPoll(
62
- blockNumber,
63
- txHashes,
64
- (blockNumber: number, txHashes: string[]) => this.saveLastEventData(blockNumber, txHashes)
64
+ lastEventsState,
65
+ (newState: StarknetEventListenerState[]) => this.saveLastEventData(newState)
65
66
  );
66
67
  }
67
68
 
@@ -17,7 +17,15 @@ import {
17
17
  toHex
18
18
  } from "../../utils/Utils";
19
19
  import {StarknetSwapContract} from "../swaps/StarknetSwapContract";
20
- import {BigNumberish, BlockTag, hash, Provider} from "starknet";
20
+ import {
21
+ BigNumberish,
22
+ BlockTag,
23
+ hash,
24
+ Provider,
25
+ SubscriptionStarknetEventsEvent,
26
+ TransactionFinalityStatus,
27
+ WebSocketChannel
28
+ } from "starknet";
21
29
  import {StarknetAbiEvent} from "../contract/modules/StarknetContractEvents";
22
30
  import {EscrowManagerAbiType} from "../swaps/EscrowManagerAbi";
23
31
  import {ExtractAbiFunctionNames} from "abi-wan-kanabi/dist/kanabi";
@@ -25,6 +33,11 @@ import {IClaimHandler} from "../swaps/handlers/claim/ClaimHandlers";
25
33
  import {StarknetSpvVaultContract} from "../spv_swap/StarknetSpvVaultContract";
26
34
  import {StarknetChainInterface} from "../chain/StarknetChainInterface";
27
35
  import {SpvVaultContractAbiType} from "../spv_swap/SpvVaultContractAbi";
36
+ import {sha256} from "@noble/hashes/sha2";
37
+ import {Buffer} from "buffer";
38
+
39
+ const PROCESSED_EVENTS_BACKLOG = 5000;
40
+ const LOGS_SLIDING_WINDOW = 60;
28
41
 
29
42
  export type StarknetTraceCall = {
30
43
  calldata: string[],
@@ -33,6 +46,8 @@ export type StarknetTraceCall = {
33
46
  calls: StarknetTraceCall[]
34
47
  };
35
48
 
49
+ export type StarknetEventListenerState = {lastBlockNumber: number, lastTxHash?: string};
50
+
36
51
  /**
37
52
  * Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
38
53
  * out on some events if the network is unreliable, front-end systems should take this into consideration and not
@@ -40,12 +55,22 @@ export type StarknetTraceCall = {
40
55
  */
41
56
  export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
42
57
 
58
+ private eventsProcessing: {
59
+ [eventFingerprint: string]: Promise<void>
60
+ } = {};
61
+ private processedEvents: Set<string> = new Set();
62
+
63
+ protected readonly Chain: StarknetChainInterface;
43
64
  protected readonly listeners: EventListener<StarknetSwapData>[] = [];
65
+ protected readonly wsChannel?: WebSocketChannel;
44
66
  protected readonly provider: Provider;
45
67
  protected readonly starknetSwapContract: StarknetSwapContract;
46
68
  protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
47
69
  protected readonly logger = getLogger("StarknetChainEventsBrowser: ");
48
70
 
71
+ protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
72
+ protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
73
+
49
74
  protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType> = "initialize";
50
75
  protected initEntryPointSelector = BigInt(hash.starknetKeccak(this.initFunctionName));
51
76
 
@@ -60,12 +85,34 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
60
85
  starknetSpvVaultContract: StarknetSpvVaultContract,
61
86
  pollIntervalSeconds: number = 5
62
87
  ) {
88
+ this.Chain = chainInterface;
89
+ this.wsChannel = chainInterface.wsChannel;
63
90
  this.provider = chainInterface.provider;
64
91
  this.starknetSwapContract = starknetSwapContract;
65
92
  this.starknetSpvVaultContract = starknetSpvVaultContract;
66
93
  this.pollIntervalSeconds = pollIntervalSeconds;
67
94
  }
68
95
 
96
+ private getEventFingerprint(event: {keys: string[], data: string[], txHash: string}): string {
97
+ const eventData = Buffer.concat([
98
+ ...event.keys.map(value => bigNumberishToBuffer(value, 64)),
99
+ ...event.data.map(value => bigNumberishToBuffer(value, 64))
100
+ ]);
101
+ const fingerprint = Buffer.from(sha256(eventData));
102
+
103
+ return event.txHash+":"+fingerprint.toString("hex");
104
+ }
105
+
106
+ private addProcessedEvent(event: {keys: string[], data: string[], txHash: string}) {
107
+ this.processedEvents.add(this.getEventFingerprint(event));
108
+ if(this.processedEvents.size > PROCESSED_EVENTS_BACKLOG) this.processedEvents.delete(this.processedEvents.keys().next().value);
109
+ }
110
+
111
+ private isEventProcessed(eventOrFingerprint: {keys: string[], data: string[], txHash: string} | string): boolean {
112
+ const eventFingerprint: string = typeof(eventOrFingerprint)==="string" ? eventOrFingerprint : this.getEventFingerprint(eventOrFingerprint);
113
+ return this.processedEvents.has(eventFingerprint);
114
+ }
115
+
69
116
  findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData {
70
117
  if(
71
118
  BigInt(call.contract_address)===BigInt(this.starknetSwapContract.contract.address) &&
@@ -249,19 +296,24 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
249
296
  "spv_swap_vault::events::Opened" | "spv_swap_vault::events::Deposited" | "spv_swap_vault::events::Fronted" | "spv_swap_vault::events::Claimed" | "spv_swap_vault::events::Closed"
250
297
  >)[],
251
298
  currentBlockNumber: number,
252
- currentBlockTimestamp: number
299
+ currentBlockTimestamp?: number
253
300
  ) {
254
301
  const blockTimestampsCache: {[blockNumber: string]: number} = {};
255
302
  const getBlockTimestamp: (blockNumber: number) => Promise<number> = async (blockNumber: number)=> {
256
303
  if(blockNumber===currentBlockNumber) return currentBlockTimestamp;
257
304
  const blockNumberString = blockNumber.toString();
258
- blockTimestampsCache[blockNumberString] ??= (await this.provider.getBlockWithTxHashes(blockNumber)).timestamp;
305
+ blockTimestampsCache[blockNumberString] ??= await this.Chain.Blocks.getBlockTime(blockNumber);
259
306
  return blockTimestampsCache[blockNumberString];
260
307
  }
261
308
 
262
- const parsedEvents: ChainEvent<StarknetSwapData>[] = [];
263
-
264
309
  for(let event of events) {
310
+ const eventIdentifier = this.getEventFingerprint(event);
311
+
312
+ if(this.isEventProcessed(eventIdentifier)) {
313
+ this.logger.debug("processEvents(): skipping already processed event: "+eventIdentifier);
314
+ continue;
315
+ }
316
+
265
317
  let parsedEvent: ChainEvent<StarknetSwapData>;
266
318
  switch(event.name) {
267
319
  case "escrow_manager::events::Claim":
@@ -289,26 +341,47 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
289
341
  parsedEvent = this.parseSpvCloseEvent(event as any);
290
342
  break;
291
343
  }
292
- if(parsedEvent==null) continue;
293
- //We are not trusting pre-confs for events, so this shall never happen
294
- if(event.blockNumber==null) throw new Error("Event block number cannot be null!");
295
- const timestamp = await getBlockTimestamp(event.blockNumber);
296
- parsedEvent.meta = {
297
- blockTime: timestamp,
298
- txId: event.txHash,
299
- timestamp //Maybe deprecated
300
- } as any;
301
- parsedEvents.push(parsedEvent);
302
- }
303
344
 
304
- for(let listener of this.listeners) {
305
- await listener(parsedEvents);
345
+ if(this.eventsProcessing[eventIdentifier]!=null) {
346
+ this.logger.debug("processEvents(): awaiting event that is currently processing: "+eventIdentifier);
347
+ await this.eventsProcessing[eventIdentifier];
348
+ continue;
349
+ }
350
+
351
+ const promise = (async() => {
352
+ if(parsedEvent==null) return;
353
+ //We are not trusting pre-confs for events, so this shall never happen
354
+ if(event.blockNumber==null) throw new Error("Event block number cannot be null!");
355
+ const timestamp = await getBlockTimestamp(event.blockNumber);
356
+ parsedEvent.meta = {
357
+ blockTime: timestamp,
358
+ txId: event.txHash,
359
+ timestamp //Maybe deprecated
360
+ } as any;
361
+ for(let listener of this.listeners) {
362
+ await listener([parsedEvent]);
363
+ }
364
+ this.addProcessedEvent(event);
365
+ })();
366
+
367
+ this.eventsProcessing[eventIdentifier] = promise;
368
+ try {
369
+ await promise;
370
+ delete this.eventsProcessing[eventIdentifier];
371
+ } catch (e) {
372
+ delete this.eventsProcessing[eventIdentifier];
373
+ throw e;
374
+ }
306
375
  }
307
376
  }
308
377
 
309
- protected async checkEventsEcrowManager(lastTxHash: string, lastBlockNumber?: number, currentBlock?: {timestamp: number, block_number: number}): Promise<string> {
310
- const currentBlockNumber: number = (currentBlock as any).block_number;
378
+ protected async checkEventsEcrowManager(currentBlock: {timestamp: number, block_number: number}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]> {
379
+ const currentBlockNumber: number = currentBlock.block_number;
311
380
  lastBlockNumber ??= currentBlockNumber;
381
+ if(currentBlockNumber < lastBlockNumber) {
382
+ this.logger.warn(`checkEventsEscrowManager(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
383
+ return;
384
+ }
312
385
  // this.logger.debug("checkEvents(EscrowManager): Requesting logs: "+logStartHeight+"...pending");
313
386
  let events = await this.starknetSwapContract.Events.getContractBlockEvents(
314
387
  ["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
@@ -320,19 +393,28 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
320
393
  const latestProcessedEventIndex = findLastIndex(events, val => val.txHash===lastTxHash);
321
394
  if(latestProcessedEventIndex!==-1) {
322
395
  events.splice(0, latestProcessedEventIndex+1);
323
- // this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: "+events.length);
396
+ this.logger.debug("checkEvents(EscrowManager): Splicing processed events, resulting size: "+events.length);
324
397
  }
325
398
  }
326
399
  if(events.length>0) {
327
400
  await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
328
- lastTxHash = events[events.length-1].txHash;
401
+ const lastProcessed = events[events.length-1];
402
+ lastTxHash = lastProcessed.txHash;
403
+ if(lastProcessed.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessed.blockNumber;
404
+ } else if(currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
405
+ lastTxHash = null;
406
+ lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
329
407
  }
330
- return lastTxHash;
408
+ return [lastTxHash, lastBlockNumber];
331
409
  }
332
410
 
333
- protected async checkEventsSpvVaults(lastTxHash: string, lastBlockNumber?: number, currentBlock?: {timestamp: number, block_number: number}): Promise<string> {
334
- const currentBlockNumber: number = (currentBlock as any).block_number;
411
+ protected async checkEventsSpvVaults(currentBlock: {timestamp: number, block_number: number}, lastTxHash?: string, lastBlockNumber?: number): Promise<[string, number]> {
412
+ const currentBlockNumber: number = currentBlock.block_number;
335
413
  lastBlockNumber ??= currentBlockNumber;
414
+ if(currentBlockNumber < lastBlockNumber) {
415
+ this.logger.warn(`checkEventsSpvVaults(): Sanity check triggered - not processing events, currentBlock: ${currentBlockNumber}, lastBlock: ${lastBlockNumber}`);
416
+ return;
417
+ }
336
418
  // this.logger.debug("checkEvents(SpvVaults): Requesting logs: "+logStartHeight+"...pending");
337
419
  let events = await this.starknetSpvVaultContract.Events.getContractBlockEvents(
338
420
  ["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"],
@@ -344,29 +426,33 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
344
426
  const latestProcessedEventIndex = findLastIndex(events, val => val.txHash===lastTxHash);
345
427
  if(latestProcessedEventIndex!==-1) {
346
428
  events.splice(0, latestProcessedEventIndex+1);
347
- // this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: "+events.length);
429
+ this.logger.debug("checkEvents(SpvVaults): Splicing processed events, resulting size: "+events.length);
348
430
  }
349
431
  }
350
432
  if(events.length>0) {
351
433
  await this.processEvents(events, currentBlock?.block_number, currentBlock?.timestamp);
352
- lastTxHash = events[events.length-1].txHash;
434
+ const lastProcessed = events[events.length-1];
435
+ lastTxHash = lastProcessed.txHash;
436
+ if(lastProcessed.blockNumber > lastBlockNumber) lastBlockNumber = lastProcessed.blockNumber;
437
+ } else if(currentBlockNumber - lastBlockNumber > LOGS_SLIDING_WINDOW) {
438
+ lastTxHash = null;
439
+ lastBlockNumber = currentBlockNumber - LOGS_SLIDING_WINDOW;
353
440
  }
354
- return lastTxHash;
441
+ return [lastTxHash, lastBlockNumber];
355
442
  }
356
443
 
357
- protected async checkEvents(lastBlockNumber: number, lastTxHashes: string[]): Promise<{txHashes: string[], blockNumber: number}> {
358
- lastTxHashes ??= [];
444
+ protected async checkEvents(lastState: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]> {
445
+ lastState ??= [];
359
446
 
360
- const currentBlock = await this.provider.getBlockWithTxHashes(BlockTag.LATEST);
361
- const currentBlockNumber: number = currentBlock.block_number;
447
+ const currentBlock = await this.Chain.Blocks.getBlock(BlockTag.LATEST);
362
448
 
363
- lastTxHashes[0] = await this.checkEventsEcrowManager(lastTxHashes[0], lastBlockNumber, currentBlock as any);
364
- lastTxHashes[1] = await this.checkEventsSpvVaults(lastTxHashes[1], lastBlockNumber, currentBlock as any);
449
+ const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock as any, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
450
+ const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock as any, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
365
451
 
366
- return {
367
- txHashes: lastTxHashes,
368
- blockNumber: currentBlockNumber
369
- };
452
+ return [
453
+ {lastBlockNumber: lastEscrowHeight, lastTxHash: lastEscrowTxHash},
454
+ {lastBlockNumber: lastSpvVaultHeight, lastTxHash: lastSpvVaultTxHash}
455
+ ];
370
456
  }
371
457
 
372
458
  /**
@@ -375,17 +461,14 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
375
461
  * @protected
376
462
  */
377
463
  protected async setupPoll(
378
- lastBlockNumber?: number,
379
- lastTxHashes?: string[],
380
- saveLatestProcessedBlockNumber?: (blockNumber: number, lastTxHashes: string[]) => Promise<void>
464
+ lastState?: StarknetEventListenerState[],
465
+ saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>
381
466
  ) {
382
- this.stopped = false;
383
467
  let func;
384
468
  func = async () => {
385
- await this.checkEvents(lastBlockNumber, lastTxHashes).then(({blockNumber, txHashes}) => {
386
- lastBlockNumber = blockNumber;
387
- lastTxHashes = txHashes;
388
- if(saveLatestProcessedBlockNumber!=null) return saveLatestProcessedBlockNumber(blockNumber, lastTxHashes);
469
+ await this.checkEvents(lastState).then(newState => {
470
+ lastState = newState;
471
+ if(saveLatestProcessedBlockNumber!=null) return saveLatestProcessedBlockNumber(newState);
389
472
  }).catch(e => {
390
473
  this.logger.error("setupPoll(): Failed to fetch starknet log: ", e);
391
474
  });
@@ -395,14 +478,67 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
395
478
  await func();
396
479
  }
397
480
 
398
- init(): Promise<void> {
399
- this.setupPoll();
400
- return Promise.resolve();
481
+ protected wsStarted: boolean = false;
482
+
483
+ protected async setupWebsocket() {
484
+ this.wsStarted = true;
485
+
486
+ const [
487
+ escrowContractSubscription,
488
+ spvVaultContractSubscription
489
+ ] = await Promise.all([
490
+ this.wsChannel.subscribeEvents({
491
+ fromAddress: this.starknetSwapContract.contract.address,
492
+ keys: this.starknetSwapContract.Events.toFilter(
493
+ ["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
494
+ []
495
+ ),
496
+ finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
497
+ }),
498
+ this.wsChannel.subscribeEvents({
499
+ fromAddress: this.starknetSpvVaultContract.contract.address,
500
+ keys: this.starknetSpvVaultContract.Events.toFilter(
501
+ ["spv_swap_vault::events::Opened", "spv_swap_vault::events::Deposited", "spv_swap_vault::events::Closed", "spv_swap_vault::events::Fronted", "spv_swap_vault::events::Claimed"],
502
+ []
503
+ ),
504
+ finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
505
+ })
506
+ ]);
507
+
508
+ escrowContractSubscription.on((event) => {
509
+ const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents<
510
+ "escrow_manager::events::Initialize" | "escrow_manager::events::Claim" | "escrow_manager::events::Refund"
511
+ >([event]);
512
+ this.processEvents(parsedEvents, event.block_number);
513
+ });
514
+ this.escrowContractSubscription = escrowContractSubscription;
515
+
516
+ spvVaultContractSubscription.on((event) => {
517
+ const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents<
518
+ "spv_swap_vault::events::Opened" | "spv_swap_vault::events::Deposited" | "spv_swap_vault::events::Closed" | "spv_swap_vault::events::Fronted" | "spv_swap_vault::events::Claimed"
519
+ >([event]);
520
+ this.processEvents(parsedEvents, event.block_number);
521
+ });
522
+ this.spvVaultContractSubscription = spvVaultContractSubscription;
523
+ }
524
+
525
+ async init(): Promise<void> {
526
+ if(this.wsChannel!=null) {
527
+ await this.setupWebsocket();
528
+ } else {
529
+ await this.setupPoll();
530
+ }
531
+ this.stopped = false;
401
532
  }
402
533
 
403
534
  async stop(): Promise<void> {
404
535
  this.stopped = true;
405
536
  if(this.timeout!=null) clearTimeout(this.timeout);
537
+ if(this.wsStarted) {
538
+ await this.escrowContractSubscription.unsubscribe();
539
+ await this.spvVaultContractSubscription.unsubscribe();
540
+ this.wsStarted = false;
541
+ }
406
542
  }
407
543
 
408
544
  registerListener(cbk: EventListener<StarknetSwapData>): void {