@atomiqlabs/chain-starknet 7.0.4 → 7.0.6

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
@@ -45,6 +45,8 @@ class StarknetChainEvents extends StarknetChainEventsBrowser_1.StarknetChainEven
45
45
  }
46
46
  async init() {
47
47
  const lastEventsState = await this.getLastEventData();
48
+ if (this.wsChannel != null)
49
+ await this.setupWebsocket();
48
50
  await this.setupPoll(lastEventsState, (newState) => this.saveLastEventData(newState));
49
51
  }
50
52
  }
@@ -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";
@@ -25,17 +25,26 @@ export type StarknetEventListenerState = {
25
25
  * rely purely on events
26
26
  */
27
27
  export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
28
+ private eventsProcessing;
29
+ private processedEvents;
30
+ protected readonly Chain: StarknetChainInterface;
28
31
  protected readonly listeners: EventListener<StarknetSwapData>[];
32
+ protected readonly wsChannel?: WebSocketChannel;
29
33
  protected readonly provider: Provider;
30
34
  protected readonly starknetSwapContract: StarknetSwapContract;
31
35
  protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
32
36
  protected readonly logger: import("../../utils/Utils").LoggerType;
37
+ protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
38
+ protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
33
39
  protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType>;
34
40
  protected initEntryPointSelector: bigint;
35
41
  protected stopped: boolean;
36
42
  protected pollIntervalSeconds: number;
37
43
  private timeout;
38
44
  constructor(chainInterface: StarknetChainInterface, starknetSwapContract: StarknetSwapContract, starknetSpvVaultContract: StarknetSpvVaultContract, pollIntervalSeconds?: number);
45
+ private getEventFingerprint;
46
+ private addProcessedEvent;
47
+ private isEventProcessed;
39
48
  findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData;
40
49
  /**
41
50
  * Returns async getter for fetching on-demand initialize event swap data
@@ -62,7 +71,7 @@ export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetS
62
71
  * @param currentBlockTimestamp
63
72
  * @protected
64
73
  */
65
- 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>;
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>;
66
75
  protected checkEventsEcrowManager(currentBlock: {
67
76
  timestamp: number;
68
77
  block_number: number;
@@ -78,6 +87,8 @@ export declare class StarknetChainEventsBrowser implements ChainEvents<StarknetS
78
87
  * @protected
79
88
  */
80
89
  protected setupPoll(lastState?: StarknetEventListenerState[], saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>): Promise<void>;
90
+ protected wsStarted: boolean;
91
+ protected setupWebsocket(): Promise<void>;
81
92
  init(): Promise<void>;
82
93
  stop(): Promise<void>;
83
94
  registerListener(cbk: EventListener<StarknetSwapData>): void;
@@ -4,6 +4,9 @@ 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;
7
10
  const LOGS_SLIDING_WINDOW = 60;
8
11
  /**
9
12
  * Starknet on-chain event handler for front-end systems without access to fs, uses WS or long-polling to subscribe, might lose
@@ -12,15 +15,37 @@ const LOGS_SLIDING_WINDOW = 60;
12
15
  */
13
16
  class StarknetChainEventsBrowser {
14
17
  constructor(chainInterface, starknetSwapContract, starknetSpvVaultContract, pollIntervalSeconds = 5) {
18
+ this.eventsProcessing = {};
19
+ this.processedEvents = new Set();
15
20
  this.listeners = [];
16
21
  this.logger = (0, Utils_1.getLogger)("StarknetChainEventsBrowser: ");
17
22
  this.initFunctionName = "initialize";
18
23
  this.initEntryPointSelector = BigInt(starknet_1.hash.starknetKeccak(this.initFunctionName));
24
+ this.wsStarted = false;
25
+ this.Chain = chainInterface;
26
+ this.wsChannel = chainInterface.wsChannel;
19
27
  this.provider = chainInterface.provider;
20
28
  this.starknetSwapContract = starknetSwapContract;
21
29
  this.starknetSpvVaultContract = starknetSpvVaultContract;
22
30
  this.pollIntervalSeconds = pollIntervalSeconds;
23
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
+ }
24
49
  findInitSwapData(call, escrowHash, claimHandler) {
25
50
  if (BigInt(call.contract_address) === BigInt(this.starknetSwapContract.contract.address) &&
26
51
  BigInt(call.entry_point_selector) === this.initEntryPointSelector) {
@@ -165,11 +190,15 @@ class StarknetChainEventsBrowser {
165
190
  if (blockNumber === currentBlockNumber)
166
191
  return currentBlockTimestamp;
167
192
  const blockNumberString = blockNumber.toString();
168
- blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = (await this.provider.getBlockWithTxHashes(blockNumber)).timestamp);
193
+ blockTimestampsCache[blockNumberString] ?? (blockTimestampsCache[blockNumberString] = await this.Chain.Blocks.getBlockTime(blockNumber));
169
194
  return blockTimestampsCache[blockNumberString];
170
195
  };
171
- const parsedEvents = [];
172
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
+ }
173
202
  let parsedEvent;
174
203
  switch (event.name) {
175
204
  case "escrow_manager::events::Claim":
@@ -197,21 +226,37 @@ class StarknetChainEventsBrowser {
197
226
  parsedEvent = this.parseSpvCloseEvent(event);
198
227
  break;
199
228
  }
200
- 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];
201
232
  continue;
202
- //We are not trusting pre-confs for events, so this shall never happen
203
- if (event.blockNumber == null)
204
- throw new Error("Event block number cannot be null!");
205
- const timestamp = await getBlockTimestamp(event.blockNumber);
206
- parsedEvent.meta = {
207
- blockTime: timestamp,
208
- txId: event.txHash,
209
- timestamp //Maybe deprecated
210
- };
211
- parsedEvents.push(parsedEvent);
212
- }
213
- for (let listener of this.listeners) {
214
- 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
+ }
215
260
  }
216
261
  }
217
262
  async checkEventsEcrowManager(currentBlock, lastTxHash, lastBlockNumber) {
@@ -274,7 +319,7 @@ class StarknetChainEventsBrowser {
274
319
  }
275
320
  async checkEvents(lastState) {
276
321
  lastState ?? (lastState = []);
277
- const currentBlock = await this.provider.getBlockWithTxHashes(starknet_1.BlockTag.LATEST);
322
+ const currentBlock = await this.Chain.Blocks.getBlock(starknet_1.BlockTag.LATEST);
278
323
  const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
279
324
  const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
280
325
  return [
@@ -288,7 +333,6 @@ class StarknetChainEventsBrowser {
288
333
  * @protected
289
334
  */
290
335
  async setupPoll(lastState, saveLatestProcessedBlockNumber) {
291
- this.stopped = false;
292
336
  let func;
293
337
  func = async () => {
294
338
  await this.checkEvents(lastState).then(newState => {
@@ -304,14 +348,60 @@ class StarknetChainEventsBrowser {
304
348
  };
305
349
  await func();
306
350
  }
307
- init() {
308
- this.setupPoll();
309
- return Promise.resolve();
351
+ async setupWebsocket() {
352
+ this.wsStarted = true;
353
+ this.wsChannel.on("open", () => {
354
+ this.logger.info("setupWebsocket(): Websocket connection opened!");
355
+ });
356
+ this.wsChannel.on("close", () => {
357
+ this.logger.warn("setupWebsocket(): Websocket connection closed!");
358
+ });
359
+ this.wsChannel.on("error", (err) => {
360
+ this.logger.error("setupWebsocket(): Websocket connection error: ", err);
361
+ });
362
+ await this.wsChannel.waitForConnection();
363
+ this.logger.info("setupWebsocket(): Websocket connection awaited successfully!");
364
+ const [escrowContractSubscription, spvVaultContractSubscription] = await Promise.all([
365
+ this.wsChannel.subscribeEvents({
366
+ fromAddress: this.starknetSwapContract.contract.address,
367
+ keys: this.starknetSwapContract.Events.toFilter(["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"], []),
368
+ finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
369
+ }),
370
+ this.wsChannel.subscribeEvents({
371
+ fromAddress: this.starknetSpvVaultContract.contract.address,
372
+ 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"], []),
373
+ finalityStatus: starknet_1.TransactionFinalityStatus.ACCEPTED_ON_L2
374
+ })
375
+ ]);
376
+ escrowContractSubscription.on((event) => {
377
+ const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents([event]);
378
+ this.processEvents(parsedEvents, event.block_number);
379
+ });
380
+ this.escrowContractSubscription = escrowContractSubscription;
381
+ spvVaultContractSubscription.on((event) => {
382
+ const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents([event]);
383
+ this.processEvents(parsedEvents, event.block_number);
384
+ });
385
+ this.spvVaultContractSubscription = spvVaultContractSubscription;
386
+ }
387
+ async init() {
388
+ if (this.wsChannel != null) {
389
+ await this.setupWebsocket();
390
+ }
391
+ else {
392
+ await this.setupPoll();
393
+ }
394
+ this.stopped = false;
310
395
  }
311
396
  async stop() {
312
397
  this.stopped = true;
313
398
  if (this.timeout != null)
314
399
  clearTimeout(this.timeout);
400
+ if (this.wsStarted) {
401
+ await this.escrowContractSubscription.unsubscribe();
402
+ await this.spvVaultContractSubscription.unsubscribe();
403
+ this.wsStarted = false;
404
+ }
315
405
  }
316
406
  registerListener(cbk) {
317
407
  this.listeners.push(cbk);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atomiqlabs/chain-starknet",
3
- "version": "7.0.4",
3
+ "version": "7.0.6",
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[][] {
@@ -59,6 +59,7 @@ export class StarknetChainEvents extends StarknetChainEventsBrowser {
59
59
 
60
60
  async init(): Promise<void> {
61
61
  const lastEventsState = await this.getLastEventData();
62
+ if(this.wsChannel!=null) await this.setupWebsocket();
62
63
  await this.setupPoll(
63
64
  lastEventsState,
64
65
  (newState: StarknetEventListenerState[]) => this.saveLastEventData(newState)
@@ -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,7 +33,10 @@ 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";
28
38
 
39
+ const PROCESSED_EVENTS_BACKLOG = 5000;
29
40
  const LOGS_SLIDING_WINDOW = 60;
30
41
 
31
42
  export type StarknetTraceCall = {
@@ -44,12 +55,22 @@ export type StarknetEventListenerState = {lastBlockNumber: number, lastTxHash?:
44
55
  */
45
56
  export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData> {
46
57
 
58
+ private eventsProcessing: {
59
+ [eventFingerprint: string]: Promise<void>
60
+ } = {};
61
+ private processedEvents: Set<string> = new Set();
62
+
63
+ protected readonly Chain: StarknetChainInterface;
47
64
  protected readonly listeners: EventListener<StarknetSwapData>[] = [];
65
+ protected readonly wsChannel?: WebSocketChannel;
48
66
  protected readonly provider: Provider;
49
67
  protected readonly starknetSwapContract: StarknetSwapContract;
50
68
  protected readonly starknetSpvVaultContract: StarknetSpvVaultContract;
51
69
  protected readonly logger = getLogger("StarknetChainEventsBrowser: ");
52
70
 
71
+ protected escrowContractSubscription: SubscriptionStarknetEventsEvent;
72
+ protected spvVaultContractSubscription: SubscriptionStarknetEventsEvent;
73
+
53
74
  protected initFunctionName: ExtractAbiFunctionNames<EscrowManagerAbiType> = "initialize";
54
75
  protected initEntryPointSelector = BigInt(hash.starknetKeccak(this.initFunctionName));
55
76
 
@@ -64,12 +85,34 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
64
85
  starknetSpvVaultContract: StarknetSpvVaultContract,
65
86
  pollIntervalSeconds: number = 5
66
87
  ) {
88
+ this.Chain = chainInterface;
89
+ this.wsChannel = chainInterface.wsChannel;
67
90
  this.provider = chainInterface.provider;
68
91
  this.starknetSwapContract = starknetSwapContract;
69
92
  this.starknetSpvVaultContract = starknetSpvVaultContract;
70
93
  this.pollIntervalSeconds = pollIntervalSeconds;
71
94
  }
72
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
+
73
116
  findInitSwapData(call: StarknetTraceCall, escrowHash: BigNumberish, claimHandler: IClaimHandler<any, any>): StarknetSwapData {
74
117
  if(
75
118
  BigInt(call.contract_address)===BigInt(this.starknetSwapContract.contract.address) &&
@@ -253,19 +296,24 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
253
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"
254
297
  >)[],
255
298
  currentBlockNumber: number,
256
- currentBlockTimestamp: number
299
+ currentBlockTimestamp?: number
257
300
  ) {
258
301
  const blockTimestampsCache: {[blockNumber: string]: number} = {};
259
302
  const getBlockTimestamp: (blockNumber: number) => Promise<number> = async (blockNumber: number)=> {
260
303
  if(blockNumber===currentBlockNumber) return currentBlockTimestamp;
261
304
  const blockNumberString = blockNumber.toString();
262
- blockTimestampsCache[blockNumberString] ??= (await this.provider.getBlockWithTxHashes(blockNumber)).timestamp;
305
+ blockTimestampsCache[blockNumberString] ??= await this.Chain.Blocks.getBlockTime(blockNumber);
263
306
  return blockTimestampsCache[blockNumberString];
264
307
  }
265
308
 
266
- const parsedEvents: ChainEvent<StarknetSwapData>[] = [];
267
-
268
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
+
269
317
  let parsedEvent: ChainEvent<StarknetSwapData>;
270
318
  switch(event.name) {
271
319
  case "escrow_manager::events::Claim":
@@ -293,20 +341,37 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
293
341
  parsedEvent = this.parseSpvCloseEvent(event as any);
294
342
  break;
295
343
  }
296
- if(parsedEvent==null) continue;
297
- //We are not trusting pre-confs for events, so this shall never happen
298
- if(event.blockNumber==null) throw new Error("Event block number cannot be null!");
299
- const timestamp = await getBlockTimestamp(event.blockNumber);
300
- parsedEvent.meta = {
301
- blockTime: timestamp,
302
- txId: event.txHash,
303
- timestamp //Maybe deprecated
304
- } as any;
305
- parsedEvents.push(parsedEvent);
306
- }
307
344
 
308
- for(let listener of this.listeners) {
309
- 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
+ }
310
375
  }
311
376
  }
312
377
 
@@ -379,7 +444,7 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
379
444
  protected async checkEvents(lastState: StarknetEventListenerState[]): Promise<StarknetEventListenerState[]> {
380
445
  lastState ??= [];
381
446
 
382
- const currentBlock = await this.provider.getBlockWithTxHashes(BlockTag.LATEST);
447
+ const currentBlock = await this.Chain.Blocks.getBlock(BlockTag.LATEST);
383
448
 
384
449
  const [lastEscrowTxHash, lastEscrowHeight] = await this.checkEventsEcrowManager(currentBlock as any, lastState?.[0]?.lastTxHash, lastState?.[0]?.lastBlockNumber);
385
450
  const [lastSpvVaultTxHash, lastSpvVaultHeight] = await this.checkEventsSpvVaults(currentBlock as any, lastState?.[1]?.lastTxHash, lastState?.[1]?.lastBlockNumber);
@@ -399,7 +464,6 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
399
464
  lastState?: StarknetEventListenerState[],
400
465
  saveLatestProcessedBlockNumber?: (newState: StarknetEventListenerState[]) => Promise<void>
401
466
  ) {
402
- this.stopped = false;
403
467
  let func;
404
468
  func = async () => {
405
469
  await this.checkEvents(lastState).then(newState => {
@@ -414,14 +478,79 @@ export class StarknetChainEventsBrowser implements ChainEvents<StarknetSwapData>
414
478
  await func();
415
479
  }
416
480
 
417
- init(): Promise<void> {
418
- this.setupPoll();
419
- return Promise.resolve();
481
+ protected wsStarted: boolean = false;
482
+
483
+ protected async setupWebsocket() {
484
+ this.wsStarted = true;
485
+
486
+ this.wsChannel.on("open", () => {
487
+ this.logger.info("setupWebsocket(): Websocket connection opened!");
488
+ });
489
+ this.wsChannel.on("close", () => {
490
+ this.logger.warn("setupWebsocket(): Websocket connection closed!");
491
+ });
492
+ this.wsChannel.on("error", (err) => {
493
+ this.logger.error("setupWebsocket(): Websocket connection error: ", err);
494
+ });
495
+ await this.wsChannel.waitForConnection();
496
+ this.logger.info("setupWebsocket(): Websocket connection awaited successfully!");
497
+
498
+ const [
499
+ escrowContractSubscription,
500
+ spvVaultContractSubscription
501
+ ] = await Promise.all([
502
+ this.wsChannel.subscribeEvents({
503
+ fromAddress: this.starknetSwapContract.contract.address,
504
+ keys: this.starknetSwapContract.Events.toFilter(
505
+ ["escrow_manager::events::Initialize", "escrow_manager::events::Claim", "escrow_manager::events::Refund"],
506
+ []
507
+ ),
508
+ finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
509
+ }),
510
+ this.wsChannel.subscribeEvents({
511
+ fromAddress: this.starknetSpvVaultContract.contract.address,
512
+ keys: this.starknetSpvVaultContract.Events.toFilter(
513
+ ["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"],
514
+ []
515
+ ),
516
+ finalityStatus: TransactionFinalityStatus.ACCEPTED_ON_L2
517
+ })
518
+ ]);
519
+
520
+ escrowContractSubscription.on((event) => {
521
+ const parsedEvents = this.starknetSwapContract.Events.toStarknetAbiEvents<
522
+ "escrow_manager::events::Initialize" | "escrow_manager::events::Claim" | "escrow_manager::events::Refund"
523
+ >([event]);
524
+ this.processEvents(parsedEvents, event.block_number);
525
+ });
526
+ this.escrowContractSubscription = escrowContractSubscription;
527
+
528
+ spvVaultContractSubscription.on((event) => {
529
+ const parsedEvents = this.starknetSpvVaultContract.Events.toStarknetAbiEvents<
530
+ "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"
531
+ >([event]);
532
+ this.processEvents(parsedEvents, event.block_number);
533
+ });
534
+ this.spvVaultContractSubscription = spvVaultContractSubscription;
535
+ }
536
+
537
+ async init(): Promise<void> {
538
+ if(this.wsChannel!=null) {
539
+ await this.setupWebsocket();
540
+ } else {
541
+ await this.setupPoll();
542
+ }
543
+ this.stopped = false;
420
544
  }
421
545
 
422
546
  async stop(): Promise<void> {
423
547
  this.stopped = true;
424
548
  if(this.timeout!=null) clearTimeout(this.timeout);
549
+ if(this.wsStarted) {
550
+ await this.escrowContractSubscription.unsubscribe();
551
+ await this.spvVaultContractSubscription.unsubscribe();
552
+ this.wsStarted = false;
553
+ }
425
554
  }
426
555
 
427
556
  registerListener(cbk: EventListener<StarknetSwapData>): void {