starknet 7.5.0 → 7.5.1

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.
@@ -38,7 +38,6 @@ var starknet = (() => {
38
38
  Contract: () => Contract,
39
39
  ContractFactory: () => ContractFactory,
40
40
  ContractInterface: () => ContractInterface,
41
- CustomError: () => CustomError,
42
41
  EDAMode: () => EDAMode3,
43
42
  EDataAvailabilityMode: () => EDataAvailabilityMode3,
44
43
  ETH_ADDRESS: () => ETH_ADDRESS,
@@ -75,6 +74,8 @@ var starknet = (() => {
75
74
  RpcProvider: () => RpcProvider2,
76
75
  Signer: () => Signer,
77
76
  SignerInterface: () => SignerInterface,
77
+ Subscription: () => Subscription,
78
+ TimeoutError: () => TimeoutError,
78
79
  TransactionExecutionStatus: () => TransactionExecutionStatus,
79
80
  TransactionFinalityStatus: () => TransactionFinalityStatus,
80
81
  TransactionType: () => TransactionType,
@@ -91,9 +92,9 @@ var starknet = (() => {
91
92
  UINT_512_MIN: () => UINT_512_MIN,
92
93
  Uint: () => Uint,
93
94
  ValidateType: () => ValidateType,
94
- WSSubscriptions: () => WSSubscriptions,
95
95
  WalletAccount: () => WalletAccount,
96
96
  WebSocketChannel: () => WebSocketChannel,
97
+ WebSocketNotConnectedError: () => WebSocketNotConnectedError,
97
98
  addAddressPadding: () => addAddressPadding,
98
99
  byteArray: () => byteArray_exports,
99
100
  cairo: () => cairo_exports,
@@ -107,8 +108,6 @@ var starknet = (() => {
107
108
  eth: () => eth_exports,
108
109
  events: () => events_exports,
109
110
  extractContractHashes: () => extractContractHashes,
110
- fixProto: () => fixProto,
111
- fixStack: () => fixStack,
112
111
  getCalldata: () => getCalldata,
113
112
  getChecksumAddress: () => getChecksumAddress,
114
113
  getLedgerPathBuffer: () => getLedgerPathBuffer111,
@@ -12047,6 +12046,18 @@ ${indent}}` : "}";
12047
12046
  return rpc_default[typeName] === this.code;
12048
12047
  }
12049
12048
  };
12049
+ var TimeoutError = class extends LibraryError {
12050
+ constructor(message) {
12051
+ super(message);
12052
+ this.name = "TimeoutError";
12053
+ }
12054
+ };
12055
+ var WebSocketNotConnectedError = class extends LibraryError {
12056
+ constructor(message) {
12057
+ super(message);
12058
+ this.name = "WebSocketNotConnectedError";
12059
+ }
12060
+ };
12050
12061
 
12051
12062
  // src/utils/eth.ts
12052
12063
  var eth_exports = {};
@@ -13615,6 +13626,31 @@ ${indent}}` : "}";
13615
13626
  }
13616
13627
  };
13617
13628
 
13629
+ // src/utils/eventEmitter.ts
13630
+ var EventEmitter = class {
13631
+ listeners = {};
13632
+ on(event, listener) {
13633
+ if (!this.listeners[event]) {
13634
+ this.listeners[event] = [];
13635
+ }
13636
+ this.listeners[event].push(listener);
13637
+ }
13638
+ off(event, listener) {
13639
+ if (!this.listeners[event]) {
13640
+ return;
13641
+ }
13642
+ this.listeners[event] = this.listeners[event].filter((l) => l !== listener);
13643
+ }
13644
+ emit(event, data) {
13645
+ if (this.listeners[event]) {
13646
+ this.listeners[event].forEach((listener) => listener(data));
13647
+ }
13648
+ }
13649
+ clear() {
13650
+ this.listeners = {};
13651
+ }
13652
+ };
13653
+
13618
13654
  // src/utils/connect/ws.ts
13619
13655
  var ws_default = typeof WebSocket !== "undefined" && WebSocket || typeof globalThis !== "undefined" && globalThis.WebSocket || typeof window !== "undefined" && window.WebSocket.bind(window) || typeof global !== "undefined" && global.WebSocket || class {
13620
13656
  constructor() {
@@ -13624,152 +13660,184 @@ ${indent}}` : "}";
13624
13660
  }
13625
13661
  };
13626
13662
 
13627
- // src/channel/ws_0_8.ts
13628
- var WSSubscriptions = {
13629
- NEW_HEADS: "newHeads",
13630
- EVENTS: "events",
13631
- TRANSACTION_STATUS: "transactionStatus",
13632
- PENDING_TRANSACTION: "pendingTransactions"
13633
- };
13634
- var WebSocketChannel = class {
13635
- /**
13636
- * WebSocket RPC Node URL
13637
- * @example 'wss://starknet-node.io/rpc/v0_8'
13638
- */
13639
- nodeUrl;
13640
- // public headers: object;
13641
- // readonly retries: number;
13642
- // public requestId: number;
13643
- // readonly blockIdentifier: BlockIdentifier;
13644
- // private chainId?: StarknetChainId;
13645
- // private specVersion?: string;
13646
- // private transactionRetryIntervalFallback?: number;
13647
- // readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed
13648
- // private batchClient?: BatchClient;
13663
+ // src/channel/ws/subscription.ts
13664
+ var Subscription = class {
13649
13665
  /**
13650
- * ws library object
13666
+ * The containing `WebSocketChannel` instance.
13667
+ * @internal
13651
13668
  */
13652
- websocket;
13669
+ channel;
13653
13670
  /**
13654
- * Assign implementation method to get 'on reorg event data'
13655
- * @example
13656
- * ```typescript
13657
- * webSocketChannel.onReorg = async function (data) {
13658
- * // ... do something when reorg happens
13659
- * }
13660
- * ```
13671
+ * The JSON-RPC method used to create this subscription.
13672
+ * @internal
13661
13673
  */
13662
- onReorg = () => {
13663
- };
13674
+ method;
13664
13675
  /**
13665
- * Assign implementation method to get 'starknet block heads'
13666
- * @example
13667
- * ```typescript
13668
- * webSocketChannel.onNewHeads = async function (data) {
13669
- * // ... do something with head data
13670
- * }
13671
- * ```
13676
+ * The parameters used to create this subscription.
13677
+ * @internal
13672
13678
  */
13673
- onNewHeads = () => {
13674
- };
13679
+ params;
13675
13680
  /**
13676
- * Assign implementation method to get 'starknet events'
13677
- * @example
13678
- * ```typescript
13679
- * webSocketChannel.onEvents = async function (data) {
13680
- * // ... do something with event data
13681
- * }
13682
- * ```
13681
+ * The unique identifier for this subscription.
13682
+ * @internal
13683
13683
  */
13684
- onEvents = () => {
13685
- };
13684
+ id;
13685
+ events = new EventEmitter();
13686
+ buffer = [];
13687
+ maxBufferSize;
13688
+ handler = null;
13689
+ _isClosed = false;
13686
13690
  /**
13687
- * Assign method to get 'starknet transactions status'
13688
- * @example
13689
- * ```typescript
13690
- * webSocketChannel.onTransactionStatus = async function (data) {
13691
- * // ... do something with tx status data
13692
- * }
13693
- * ```
13691
+ * @internal
13692
+ * @param {WebSocketChannel} channel - The WebSocketChannel instance.
13693
+ * @param {string} method - The RPC method used for the subscription.
13694
+ * @param {any} params - The parameters for the subscription.
13695
+ * @param {SUBSCRIPTION_ID} id - The subscription ID.
13696
+ * @param {number} maxBufferSize - The maximum number of events to buffer.
13694
13697
  */
13695
- onTransactionStatus = () => {
13696
- };
13698
+ constructor(channel, method, params, id, maxBufferSize) {
13699
+ this.channel = channel;
13700
+ this.method = method;
13701
+ this.params = params;
13702
+ this.id = id;
13703
+ this.maxBufferSize = maxBufferSize;
13704
+ }
13697
13705
  /**
13698
- * Assign implementation method to get 'starknet pending transactions (mempool)'
13699
- * @example
13700
- * ```typescript
13701
- * webSocketChannel.onPendingTransaction = async function (data) {
13702
- * // ... do something with pending tx data
13703
- * }
13704
- * ```
13706
+ * Indicates if the subscription has been closed.
13707
+ * @returns {boolean} `true` if unsubscribed, `false` otherwise.
13705
13708
  */
13706
- onPendingTransaction = () => {
13707
- };
13709
+ get isClosed() {
13710
+ return this._isClosed;
13711
+ }
13708
13712
  /**
13709
- * Assign implementation to this method to listen open Event
13713
+ * Internal method to handle incoming events from the WebSocket channel.
13714
+ * If a handler is attached, it's invoked immediately. Otherwise, the event is buffered.
13715
+ * @internal
13716
+ * @param {T} data - The event data.
13710
13717
  */
13711
- onOpen = () => {
13712
- };
13718
+ _handleEvent(data) {
13719
+ if (this.handler) {
13720
+ this.handler(data);
13721
+ } else {
13722
+ if (this.buffer.length >= this.maxBufferSize) {
13723
+ const droppedEvent = this.buffer.shift();
13724
+ logger.warn(`Subscription ${this.id}: Buffer full. Dropping oldest event:`, droppedEvent);
13725
+ }
13726
+ this.buffer.push(data);
13727
+ }
13728
+ }
13713
13729
  /**
13714
- * Assign implementation to this method to listen close CloseEvent
13730
+ * Attaches a handler function to be called for each event.
13731
+ *
13732
+ * When a handler is attached, any buffered events will be passed to it sequentially.
13733
+ * Subsequent events will be passed directly as they arrive.
13734
+ *
13735
+ * @param {(data: T) => void} handler - The function to call with event data.
13736
+ * @throws {Error} If a handler is already attached to this subscription.
13715
13737
  */
13716
- onClose = () => {
13717
- };
13738
+ on(handler) {
13739
+ if (this.handler) {
13740
+ throw new Error("A handler is already attached to this subscription.");
13741
+ }
13742
+ this.handler = handler;
13743
+ while (this.buffer.length > 0) {
13744
+ const event = this.buffer.shift();
13745
+ if (event) {
13746
+ this.handler(event);
13747
+ }
13748
+ }
13749
+ }
13718
13750
  /**
13719
- * Assign implementation to this method to listen message MessageEvent
13751
+ * Sends an unsubscribe request to the node and cleans up local resources.
13752
+ * @returns {Promise<boolean>} A Promise that resolves to `true` if the unsubscription was successful.
13720
13753
  */
13721
- onMessage = () => {
13722
- };
13754
+ async unsubscribe() {
13755
+ if (this._isClosed) {
13756
+ return true;
13757
+ }
13758
+ const success = await this.channel.unsubscribe(this.id);
13759
+ if (success) {
13760
+ this._isClosed = true;
13761
+ this.channel.removeSubscription(this.id);
13762
+ this.events.emit("unsubscribe", void 0);
13763
+ this.events.clear();
13764
+ }
13765
+ return success;
13766
+ }
13767
+ };
13768
+
13769
+ // src/channel/ws/ws_0_8.ts
13770
+ var WebSocketChannel = class {
13723
13771
  /**
13724
- * Assign implementation to this method to listen error Event
13772
+ * The URL of the WebSocket RPC Node.
13773
+ * @example 'wss://starknet-sepolia.public.blastapi.io/rpc/v0_8'
13725
13774
  */
13726
- onError = () => {
13727
- };
13775
+ nodeUrl;
13728
13776
  /**
13729
- * Assign implementation to this method to listen unsubscription
13777
+ * The underlying WebSocket instance.
13730
13778
  */
13731
- onUnsubscribe = () => {
13732
- };
13733
- onUnsubscribeLocal = () => {
13734
- };
13779
+ websocket;
13780
+ // Store the WebSocket implementation class to allow for custom implementations.
13781
+ WsImplementation;
13782
+ // Map of active subscriptions, keyed by their ID.
13783
+ activeSubscriptions = /* @__PURE__ */ new Map();
13784
+ maxBufferSize;
13785
+ autoReconnect;
13786
+ reconnectOptions;
13787
+ requestTimeout;
13788
+ isReconnecting = false;
13789
+ reconnectAttempts = 0;
13790
+ userInitiatedClose = false;
13791
+ reconnectTimeoutId = null;
13792
+ requestQueue = [];
13793
+ events = new EventEmitter();
13794
+ openListener = (ev) => this.events.emit("open", ev);
13795
+ closeListener = this.onCloseProxy.bind(this);
13796
+ messageListener = this.onMessageProxy.bind(this);
13797
+ errorListener = (ev) => this.events.emit("error", ev);
13735
13798
  /**
13736
- * JSON RPC latest sent message id
13737
- * expecting receiving message to contain same id
13799
+ * JSON RPC latest sent message ID.
13800
+ * The receiving message is expected to contain the same ID.
13738
13801
  */
13739
13802
  sendId = 0;
13740
13803
  /**
13741
- * subscriptions ids
13742
- * mapped by keys WSSubscriptions
13804
+ * Creates an instance of WebSocketChannel.
13805
+ * @param {WebSocketOptions} options - The options for configuring the channel.
13743
13806
  */
13744
- subscriptions = /* @__PURE__ */ new Map();
13745
- /**
13746
- * Construct class and event listeners
13747
- * @param options WebSocketOptions
13748
- */
13749
- constructor(options = {}) {
13750
- const nodeUrl = options.nodeUrl || "http://localhost:3000 ";
13751
- this.nodeUrl = options.websocket ? options.websocket.url : nodeUrl;
13752
- this.websocket = options.websocket || config.get("websocket") || new ws_default(nodeUrl);
13753
- this.websocket.addEventListener("open", this.onOpen.bind(this));
13754
- this.websocket.addEventListener("close", this.onCloseProxy.bind(this));
13755
- this.websocket.addEventListener("message", this.onMessageProxy.bind(this));
13756
- this.websocket.addEventListener("error", this.onError.bind(this));
13807
+ constructor(options) {
13808
+ this.nodeUrl = options.nodeUrl;
13809
+ this.maxBufferSize = options.maxBufferSize ?? 1e3;
13810
+ this.autoReconnect = options.autoReconnect ?? true;
13811
+ this.reconnectOptions = {
13812
+ retries: options.reconnectOptions?.retries ?? 5,
13813
+ delay: options.reconnectOptions?.delay ?? 2e3
13814
+ };
13815
+ this.requestTimeout = options.requestTimeout ?? 6e4;
13816
+ this.WsImplementation = options.websocket || config.get("websocket") || ws_default;
13817
+ this.websocket = new this.WsImplementation(this.nodeUrl);
13818
+ this.websocket.addEventListener("open", this.openListener);
13819
+ this.websocket.addEventListener("close", this.closeListener);
13820
+ this.websocket.addEventListener("message", this.messageListener);
13821
+ this.websocket.addEventListener("error", this.errorListener);
13757
13822
  }
13758
13823
  idResolver(id) {
13759
13824
  if (id) return id;
13760
13825
  return this.sendId++;
13761
13826
  }
13762
13827
  /**
13763
- * Send data over open ws connection
13764
- * * this would only send data on the line without awaiting 'response message'
13765
- * @example
13766
- * ```typescript
13767
- * const sentId = await this.send('starknet_method', params);
13768
- * ```
13828
+ * Sends a JSON-RPC request over the WebSocket connection without waiting for a response.
13829
+ * This is a low-level method. Prefer `sendReceive` for most use cases.
13830
+ * @param {string} method - The RPC method name.
13831
+ * @param {object} [params] - The parameters for the RPC method.
13832
+ * @param {number} [id] - A specific request ID. If not provided, an auto-incrementing ID is used.
13833
+ * @returns {number} The ID of the sent request.
13834
+ * @throws {WebSocketNotConnectedError} If the WebSocket is not connected.
13769
13835
  */
13770
13836
  send(method, params, id) {
13771
13837
  if (!this.isConnected()) {
13772
- throw Error("WebSocketChannel.send() fail due to socket disconnected");
13838
+ throw new WebSocketNotConnectedError(
13839
+ "WebSocketChannel.send() failed due to socket being disconnected"
13840
+ );
13773
13841
  }
13774
13842
  const usedId = this.idResolver(id);
13775
13843
  const rpcRequestBody = {
@@ -13782,50 +13850,88 @@ ${indent}}` : "}";
13782
13850
  return usedId;
13783
13851
  }
13784
13852
  /**
13785
- * Any Starknet method not just websocket override
13786
- */
13787
- sendReceiveAny(method, params) {
13788
- return this.sendReceive(method, params);
13789
- }
13790
- /**
13791
- * Send request and receive response over ws line
13792
- * This method abstract ws messages into request/response model
13793
- * @param method rpc method name
13794
- * @param params rpc method parameters
13795
- * @example
13796
- * ```typescript
13797
- * const response = await this.sendReceive('starknet_method', params);
13798
- * ```
13853
+ * Sends a JSON-RPC request and returns a Promise that resolves with the result.
13854
+ * This method abstracts the request/response cycle over WebSockets.
13855
+ * If the connection is lost, it will queue the request and send it upon reconnection.
13856
+ * @template T - The expected type of the result.
13857
+ * @param {string} method - The RPC method name.
13858
+ * @param {object} [params] - The parameters for the RPC method.
13859
+ * @returns {Promise<T>} A Promise that resolves with the RPC response result.
13860
+ * @throws {TimeoutError} If the request does not receive a response within the configured `requestTimeout`.
13861
+ * @throws {WebSocketNotConnectedError} If the WebSocket is not connected and auto-reconnect is disabled.
13799
13862
  */
13800
13863
  sendReceive(method, params) {
13864
+ if (this.isReconnecting || !this.isConnected() && this.autoReconnect && !this.userInitiatedClose) {
13865
+ logger.info(`WebSocket: Connection unavailable, queueing request: ${method}`);
13866
+ return new Promise((resolve, reject) => {
13867
+ this.requestQueue.push({ method, params, resolve, reject });
13868
+ });
13869
+ }
13801
13870
  const sendId = this.send(method, params);
13802
13871
  return new Promise((resolve, reject) => {
13803
- if (!this.websocket) return;
13804
- this.websocket.onmessage = ({ data }) => {
13805
- const message = JSON.parse(data);
13872
+ let timeoutId;
13873
+ if (!this.websocket || this.websocket.readyState !== ws_default.OPEN) {
13874
+ reject(new WebSocketNotConnectedError("WebSocket not available or not connected."));
13875
+ return;
13876
+ }
13877
+ const messageHandler = (event) => {
13878
+ if (!isString(event.data)) {
13879
+ logger.warn("WebSocket received non-string message data:", event.data);
13880
+ return;
13881
+ }
13882
+ const message = JSON.parse(event.data);
13806
13883
  if (message.id === sendId) {
13884
+ clearTimeout(timeoutId);
13885
+ this.websocket.removeEventListener("message", messageHandler);
13886
+ this.websocket.removeEventListener("error", errorHandler);
13807
13887
  if ("result" in message) {
13808
13888
  resolve(message.result);
13809
13889
  } else {
13810
- reject(Error(`error on ${method}, ${message.error}`));
13890
+ reject(
13891
+ new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`)
13892
+ );
13811
13893
  }
13812
13894
  }
13813
13895
  };
13814
- this.websocket.onerror = reject;
13896
+ const errorHandler = (event) => {
13897
+ clearTimeout(timeoutId);
13898
+ this.websocket.removeEventListener("message", messageHandler);
13899
+ this.websocket.removeEventListener("error", errorHandler);
13900
+ reject(
13901
+ new Error(
13902
+ `WebSocket error during ${method} (id: ${sendId}): ${event.type || "Unknown error"}`
13903
+ )
13904
+ );
13905
+ };
13906
+ this.websocket.addEventListener("message", messageHandler);
13907
+ this.websocket.addEventListener("error", errorHandler);
13908
+ timeoutId = setTimeout(() => {
13909
+ this.websocket.removeEventListener("message", messageHandler);
13910
+ this.websocket.removeEventListener("error", errorHandler);
13911
+ reject(
13912
+ new TimeoutError(
13913
+ `Request ${method} (id: ${sendId}) timed out after ${this.requestTimeout}ms`
13914
+ )
13915
+ );
13916
+ }, this.requestTimeout);
13815
13917
  });
13816
13918
  }
13817
13919
  /**
13818
- * Helper to check connection is open
13920
+ * Checks if the WebSocket connection is currently open.
13921
+ * @returns {boolean} `true` if the connection is open, `false` otherwise.
13819
13922
  */
13820
13923
  isConnected() {
13821
13924
  return this.websocket.readyState === ws_default.OPEN;
13822
13925
  }
13823
13926
  /**
13824
- * await while websocket is connected
13825
- * * could be used to block the flow until websocket is open
13927
+ * Returns a Promise that resolves when the WebSocket connection is open.
13928
+ * Can be used to block execution until the connection is established.
13929
+ * @returns {Promise<number>} A Promise that resolves with the WebSocket's `readyState` when connected.
13826
13930
  * @example
13827
13931
  * ```typescript
13828
- * const readyState = await webSocketChannel.waitForConnection();
13932
+ * const channel = new WebSocketChannel({ nodeUrl: '...' });
13933
+ * await channel.waitForConnection();
13934
+ * console.log('Connected!');
13829
13935
  * ```
13830
13936
  */
13831
13937
  async waitForConnection() {
@@ -13841,17 +13947,22 @@ ${indent}}` : "}";
13841
13947
  return this.websocket.readyState;
13842
13948
  }
13843
13949
  /**
13844
- * Disconnect the WebSocket connection, optionally using code as the the WebSocket connection close code and reason as the the WebSocket connection close reason.
13950
+ * Closes the WebSocket connection.
13951
+ * This method is user-initiated and will prevent automatic reconnection for this closure.
13952
+ * @param {number} [code] - The WebSocket connection close code.
13953
+ * @param {string} [reason] - The WebSocket connection close reason.
13845
13954
  */
13846
13955
  disconnect(code, reason) {
13956
+ if (this.reconnectTimeoutId) {
13957
+ clearTimeout(this.reconnectTimeoutId);
13958
+ this.reconnectTimeoutId = null;
13959
+ }
13847
13960
  this.websocket.close(code, reason);
13961
+ this.userInitiatedClose = true;
13848
13962
  }
13849
13963
  /**
13850
- * await while websocket is disconnected
13851
- * @example
13852
- * ```typescript
13853
- * const readyState = await webSocketChannel.waitForDisconnection();
13854
- * ```
13964
+ * Returns a Promise that resolves when the WebSocket connection is closed.
13965
+ * @returns {Promise<number | Event>} A Promise that resolves with the WebSocket's `readyState` or a `CloseEvent` when disconnected.
13855
13966
  */
13856
13967
  async waitForDisconnection() {
13857
13968
  if (this.websocket.readyState !== ws_default.CLOSED) {
@@ -13864,206 +13975,233 @@ ${indent}}` : "}";
13864
13975
  return this.websocket.readyState;
13865
13976
  }
13866
13977
  /**
13867
- * Unsubscribe from starknet subscription
13868
- * @param subscriptionId
13869
- * @param ref internal usage, only for managed subscriptions
13978
+ * Unsubscribes from a Starknet subscription.
13979
+ * It is recommended to use the `unsubscribe()` method on the `Subscription` object instead.
13980
+ * @internal
13981
+ * @param {SUBSCRIPTION_ID} subscriptionId - The ID of the subscription to unsubscribe from.
13982
+ * @returns {Promise<boolean>} A Promise that resolves with `true` if the unsubscription was successful.
13870
13983
  */
13871
- async unsubscribe(subscriptionId, ref) {
13984
+ async unsubscribe(subscriptionId) {
13872
13985
  const status = await this.sendReceive("starknet_unsubscribe", {
13873
13986
  subscription_id: subscriptionId
13874
13987
  });
13875
13988
  if (status) {
13876
- if (ref) {
13877
- this.subscriptions.delete(ref);
13878
- }
13879
- this.onUnsubscribeLocal(subscriptionId);
13880
- this.onUnsubscribe(subscriptionId);
13989
+ this.events.emit("unsubscribe", subscriptionId);
13881
13990
  }
13882
13991
  return status;
13883
13992
  }
13884
13993
  /**
13885
- * await while subscription is unsubscribed
13886
- * @param forSubscriptionId if defined trigger on subscriptionId else trigger on any
13887
- * @returns subscriptionId | onerror(Event)
13994
+ * Returns a Promise that resolves when a specific subscription is successfully unsubscribed.
13995
+ * @param {SUBSCRIPTION_ID} targetId - The ID of the subscription to wait for.
13996
+ * @returns {Promise<void>}
13888
13997
  * @example
13889
13998
  * ```typescript
13890
- * const subscriptionId = await webSocketChannel.waitForUnsubscription();
13999
+ * await channel.waitForUnsubscription(mySubscription.id);
14000
+ * console.log('Successfully unsubscribed.');
13891
14001
  * ```
13892
14002
  */
13893
- async waitForUnsubscription(forSubscriptionId) {
13894
- return new Promise((resolve, reject) => {
13895
- if (!this.websocket) return;
13896
- this.onUnsubscribeLocal = (subscriptionId) => {
13897
- if (forSubscriptionId === void 0) {
13898
- resolve(subscriptionId);
13899
- } else if (subscriptionId === forSubscriptionId) {
13900
- resolve(subscriptionId);
14003
+ waitForUnsubscription(targetId) {
14004
+ return new Promise((resolve) => {
14005
+ const listener = (unsubId) => {
14006
+ if (unsubId === targetId) {
14007
+ this.events.off("unsubscribe", listener);
14008
+ resolve();
13901
14009
  }
13902
14010
  };
13903
- this.websocket.onerror = reject;
14011
+ this.events.on("unsubscribe", listener);
13904
14012
  });
13905
14013
  }
13906
14014
  /**
13907
- * Reconnect re-create this.websocket instance
14015
+ * Manually initiates a reconnection attempt.
14016
+ * This creates a new WebSocket instance and re-establishes listeners.
13908
14017
  */
13909
14018
  reconnect() {
13910
- this.websocket = new ws_default(this.nodeUrl);
13911
- this.websocket.addEventListener("open", this.onOpen.bind(this));
13912
- this.websocket.addEventListener("close", this.onCloseProxy.bind(this));
13913
- this.websocket.addEventListener("message", this.onMessageProxy.bind(this));
13914
- this.websocket.addEventListener("error", this.onError.bind(this));
14019
+ this.userInitiatedClose = false;
14020
+ this.websocket = new this.WsImplementation(this.nodeUrl);
14021
+ this.websocket.addEventListener("open", this.openListener);
14022
+ this.websocket.addEventListener("close", this.closeListener);
14023
+ this.websocket.addEventListener("message", this.messageListener);
14024
+ this.websocket.addEventListener("error", this.errorListener);
14025
+ }
14026
+ _processRequestQueue() {
14027
+ logger.info(`WebSocket: Processing ${this.requestQueue.length} queued requests.`);
14028
+ while (this.requestQueue.length > 0) {
14029
+ const { method, params, resolve, reject } = this.requestQueue.shift();
14030
+ this.sendReceive(method, params).then(resolve).catch(reject);
14031
+ }
14032
+ }
14033
+ async _restoreSubscriptions() {
14034
+ const oldSubscriptions = Array.from(this.activeSubscriptions.values());
14035
+ this.activeSubscriptions.clear();
14036
+ const restorePromises = oldSubscriptions.map(async (sub) => {
14037
+ try {
14038
+ const newSubId = await this.sendReceive(sub.method, sub.params);
14039
+ sub.id = newSubId;
14040
+ this.activeSubscriptions.set(newSubId, sub);
14041
+ logger.info(`Subscription ${sub.method} restored with new ID: ${newSubId}`);
14042
+ } catch (error) {
14043
+ logger.error(`Failed to restore subscription ${sub.method}:`, error);
14044
+ }
14045
+ });
14046
+ await Promise.all(restorePromises);
13915
14047
  }
13916
- // TODO: Add/Test ping service. It seems this work out of the box from pathfinder. If net disc. it will auto replay.
13917
- reconnectAndUpdate() {
13918
- this.reconnect();
14048
+ _startReconnect() {
14049
+ if (this.isReconnecting || !this.autoReconnect) {
14050
+ return;
14051
+ }
14052
+ this.isReconnecting = true;
14053
+ this.reconnectAttempts = 0;
14054
+ const tryReconnect = () => {
14055
+ if (this.reconnectAttempts >= this.reconnectOptions.retries) {
14056
+ logger.error("WebSocket: Maximum reconnection retries reached. Giving up.");
14057
+ this.isReconnecting = false;
14058
+ return;
14059
+ }
14060
+ this.reconnectAttempts += 1;
14061
+ logger.info(
14062
+ `WebSocket: Connection lost. Attempting to reconnect... (${this.reconnectAttempts}/${this.reconnectOptions.retries})`
14063
+ );
14064
+ this.reconnect();
14065
+ this.websocket.onopen = async () => {
14066
+ logger.info("WebSocket: Reconnection successful.");
14067
+ this.isReconnecting = false;
14068
+ this.reconnectAttempts = 0;
14069
+ await this._restoreSubscriptions();
14070
+ this._processRequestQueue();
14071
+ this.events.emit("open", new Event("open"));
14072
+ };
14073
+ this.websocket.onerror = () => {
14074
+ const delay = this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1);
14075
+ logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
14076
+ this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
14077
+ };
14078
+ };
14079
+ tryReconnect();
13919
14080
  }
13920
14081
  onCloseProxy(ev) {
13921
- this.websocket.removeEventListener("open", this.onOpen);
13922
- this.websocket.removeEventListener("close", this.onCloseProxy);
13923
- this.websocket.removeEventListener("message", this.onMessageProxy);
13924
- this.websocket.removeEventListener("error", this.onError);
13925
- this.onClose(ev);
14082
+ this.websocket.removeEventListener("open", this.openListener);
14083
+ this.websocket.removeEventListener("close", this.closeListener);
14084
+ this.websocket.removeEventListener("message", this.messageListener);
14085
+ this.websocket.removeEventListener("error", this.errorListener);
14086
+ this.events.emit("close", ev);
14087
+ if (!this.userInitiatedClose) {
14088
+ this._startReconnect();
14089
+ }
13926
14090
  }
13927
14091
  onMessageProxy(event) {
13928
- const message = JSON.parse(event.data);
13929
- const eventName = message.method;
13930
- switch (eventName) {
13931
- case "starknet_subscriptionReorg":
13932
- this.onReorg(message.params);
13933
- break;
13934
- case "starknet_subscriptionNewHeads":
13935
- this.onNewHeads(message.params);
13936
- break;
13937
- case "starknet_subscriptionEvents":
13938
- this.onEvents(message.params);
13939
- break;
13940
- case "starknet_subscriptionTransactionStatus":
13941
- this.onTransactionStatus(message.params);
13942
- break;
13943
- case "starknet_subscriptionPendingTransactions":
13944
- this.onPendingTransaction(message.params);
13945
- break;
13946
- default:
13947
- break;
14092
+ let message;
14093
+ try {
14094
+ message = JSON.parse(event.data);
14095
+ } catch (error) {
14096
+ logger.error(
14097
+ `WebSocketChannel: Error parsing incoming message: ${event.data}, Error: ${error}`
14098
+ );
14099
+ return;
13948
14100
  }
13949
- this.onMessage(event);
13950
- }
13951
- /**
13952
- * subscribe to new block heads
13953
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
13954
- */
13955
- subscribeNewHeadsUnmanaged(blockIdentifier) {
13956
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
13957
- return this.sendReceive("starknet_subscribeNewHeads", {
13958
- ...{ block_id }
13959
- });
14101
+ if (message.method && isObject2(message.params) && "subscription_id" in message.params) {
14102
+ const { result, subscription_id } = message.params;
14103
+ const subscription = this.activeSubscriptions.get(subscription_id);
14104
+ if (subscription) {
14105
+ subscription._handleEvent(result);
14106
+ } else {
14107
+ logger.warn(
14108
+ `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
14109
+ );
14110
+ }
14111
+ }
14112
+ logger.debug("onMessageProxy:", event.data);
14113
+ this.events.emit("message", event);
13960
14114
  }
13961
14115
  /**
13962
- * subscribe to new block heads
14116
+ * Subscribes to new block headers.
14117
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block to start receiving notifications from. Defaults to 'latest'.
14118
+ * @returns {Promise<Subscription<BLOCK_HEADER>>} A Promise that resolves with a `Subscription` object for new block headers.
13963
14119
  */
13964
14120
  async subscribeNewHeads(blockIdentifier) {
13965
- if (this.subscriptions.get(WSSubscriptions.NEW_HEADS)) return false;
13966
- const subId = await this.subscribeNewHeadsUnmanaged(blockIdentifier);
13967
- this.subscriptions.set(WSSubscriptions.NEW_HEADS, subId);
13968
- return subId;
13969
- }
13970
- /**
13971
- * Unsubscribe newHeads subscription
13972
- */
13973
- async unsubscribeNewHeads() {
13974
- const subId = this.subscriptions.get(WSSubscriptions.NEW_HEADS);
13975
- if (!subId) throw Error("There is no subscription on this event");
13976
- return this.unsubscribe(subId, WSSubscriptions.NEW_HEADS);
13977
- }
13978
- /**
13979
- * subscribe to 'starknet events'
13980
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
13981
- */
13982
- subscribeEventsUnmanaged(fromAddress, keys, blockIdentifier) {
13983
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
13984
- return this.sendReceive("starknet_subscribeEvents", {
13985
- ...{ from_address: fromAddress !== void 0 ? toHex(fromAddress) : void 0 },
13986
- ...{ keys },
13987
- ...{ block_id }
13988
- });
14121
+ const method = "starknet_subscribeNewHeads";
14122
+ const params = {
14123
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
14124
+ };
14125
+ const subId = await this.sendReceive(method, params);
14126
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
14127
+ this.activeSubscriptions.set(subId, subscription);
14128
+ return subscription;
13989
14129
  }
13990
14130
  /**
13991
- * subscribe to 'starknet events'
14131
+ * Subscribes to events matching a given filter.
14132
+ * @param {BigNumberish} [fromAddress] - The contract address to filter by.
14133
+ * @param {string[][]} [keys] - The event keys to filter by.
14134
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block to start receiving notifications from. Defaults to 'latest'.
14135
+ * @returns {Promise<Subscription<EMITTED_EVENT>>} A Promise that resolves with a `Subscription` object for the specified events.
13992
14136
  */
13993
14137
  async subscribeEvents(fromAddress, keys, blockIdentifier) {
13994
- if (this.subscriptions.get(WSSubscriptions.EVENTS)) return false;
13995
- const subId = await this.subscribeEventsUnmanaged(fromAddress, keys, blockIdentifier);
13996
- this.subscriptions.set(WSSubscriptions.EVENTS, subId);
13997
- return subId;
13998
- }
13999
- /**
14000
- * Unsubscribe 'starknet events' subscription
14001
- */
14002
- unsubscribeEvents() {
14003
- const subId = this.subscriptions.get(WSSubscriptions.EVENTS);
14004
- if (!subId) throw Error("There is no subscription ID for this event");
14005
- return this.unsubscribe(subId, WSSubscriptions.EVENTS);
14006
- }
14007
- /**
14008
- * subscribe to transaction status
14009
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
14010
- */
14011
- subscribeTransactionStatusUnmanaged(transactionHash, blockIdentifier) {
14012
- const transaction_hash = toHex(transactionHash);
14013
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
14014
- return this.sendReceive("starknet_subscribeTransactionStatus", {
14015
- transaction_hash,
14016
- ...{ block_id }
14017
- });
14138
+ const method = "starknet_subscribeEvents";
14139
+ const params = {
14140
+ from_address: fromAddress !== void 0 ? toHex(fromAddress) : void 0,
14141
+ keys,
14142
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
14143
+ };
14144
+ const subId = await this.sendReceive(method, params);
14145
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
14146
+ this.activeSubscriptions.set(subId, subscription);
14147
+ return subscription;
14018
14148
  }
14019
14149
  /**
14020
- * subscribe to transaction status
14150
+ * Subscribes to status updates for a specific transaction.
14151
+ * @param {BigNumberish} transactionHash - The hash of the transaction to monitor.
14152
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block context. Not typically required.
14153
+ * @returns {Promise<Subscription<NEW_TXN_STATUS>>} A Promise that resolves with a `Subscription` object for the transaction's status.
14021
14154
  */
14022
- async subscribeTransactionStatus(transactionHash) {
14023
- if (this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS)) return false;
14024
- const subId = await this.subscribeTransactionStatusUnmanaged(transactionHash);
14025
- this.subscriptions.set(WSSubscriptions.TRANSACTION_STATUS, subId);
14026
- return subId;
14155
+ async subscribeTransactionStatus(transactionHash, blockIdentifier) {
14156
+ const method = "starknet_subscribeTransactionStatus";
14157
+ const params = {
14158
+ transaction_hash: toHex(transactionHash),
14159
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
14160
+ };
14161
+ const subId = await this.sendReceive(method, params);
14162
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
14163
+ this.activeSubscriptions.set(subId, subscription);
14164
+ return subscription;
14027
14165
  }
14028
14166
  /**
14029
- * unsubscribe 'transaction status' subscription
14167
+ * Subscribes to pending transactions.
14168
+ * @param {boolean} [transactionDetails] - If `true`, the full transaction details are included. Defaults to `false` (hash only).
14169
+ * @param {BigNumberish[]} [senderAddress] - An array of sender addresses to filter by.
14170
+ * @returns {Promise<Subscription<TXN_HASH | TXN_WITH_HASH>>} A Promise that resolves with a `Subscription` object for pending transactions.
14030
14171
  */
14031
- async unsubscribeTransactionStatus() {
14032
- const subId = this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS);
14033
- if (!subId) throw Error("There is no subscription ID for this event");
14034
- return this.unsubscribe(subId, WSSubscriptions.TRANSACTION_STATUS);
14172
+ async subscribePendingTransaction(transactionDetails, senderAddress) {
14173
+ const method = "starknet_subscribePendingTransactions";
14174
+ const params = {
14175
+ transaction_details: transactionDetails,
14176
+ sender_address: senderAddress && bigNumberishArrayToHexadecimalStringArray(senderAddress)
14177
+ };
14178
+ const subId = await this.sendReceive(method, params);
14179
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
14180
+ this.activeSubscriptions.set(subId, subscription);
14181
+ return subscription;
14035
14182
  }
14036
14183
  /**
14037
- * subscribe to pending transactions (mempool)
14038
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
14184
+ * Internal method to remove subscription from active map.
14185
+ * @internal
14039
14186
  */
14040
- subscribePendingTransactionUnmanaged(transactionDetails, senderAddress) {
14041
- return this.sendReceive("starknet_subscribePendingTransactions", {
14042
- ...{ transaction_details: transactionDetails },
14043
- ...{
14044
- sender_address: senderAddress && bigNumberishArrayToHexadecimalStringArray(senderAddress)
14045
- }
14046
- });
14187
+ removeSubscription(id) {
14188
+ this.activeSubscriptions.delete(id);
14047
14189
  }
14048
14190
  /**
14049
- * subscribe to pending transactions (mempool)
14191
+ * Adds a listener for a given event.
14192
+ * @param event The event name.
14193
+ * @param listener The listener function to add.
14050
14194
  */
14051
- async subscribePendingTransaction(transactionDetails, senderAddress) {
14052
- if (this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS)) return false;
14053
- const subId = await this.subscribePendingTransactionUnmanaged(
14054
- transactionDetails,
14055
- senderAddress
14056
- );
14057
- this.subscriptions.set(WSSubscriptions.PENDING_TRANSACTION, subId);
14058
- return subId;
14195
+ on(event, listener) {
14196
+ this.events.on(event, listener);
14059
14197
  }
14060
14198
  /**
14061
- * unsubscribe 'pending transaction' subscription
14199
+ * Removes a listener for a given event.
14200
+ * @param event The event name.
14201
+ * @param listener The listener function to remove.
14062
14202
  */
14063
- async unsubscribePendingTransaction() {
14064
- const subId = this.subscriptions.get(WSSubscriptions.PENDING_TRANSACTION);
14065
- if (!subId) throw Error("There is no subscription ID for this event");
14066
- return this.unsubscribe(subId, WSSubscriptions.PENDING_TRANSACTION);
14203
+ off(event, listener) {
14204
+ this.events.off(event, listener);
14067
14205
  }
14068
14206
  };
14069
14207