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.
package/dist/index.js CHANGED
@@ -48,7 +48,6 @@ __export(index_exports, {
48
48
  Contract: () => Contract,
49
49
  ContractFactory: () => ContractFactory,
50
50
  ContractInterface: () => ContractInterface,
51
- CustomError: () => CustomError,
52
51
  EDAMode: () => EDAMode,
53
52
  EDataAvailabilityMode: () => EDataAvailabilityMode,
54
53
  ETH_ADDRESS: () => ETH_ADDRESS,
@@ -85,6 +84,8 @@ __export(index_exports, {
85
84
  RpcProvider: () => RpcProvider2,
86
85
  Signer: () => Signer,
87
86
  SignerInterface: () => SignerInterface,
87
+ Subscription: () => Subscription,
88
+ TimeoutError: () => TimeoutError,
88
89
  TransactionExecutionStatus: () => TransactionExecutionStatus,
89
90
  TransactionFinalityStatus: () => TransactionFinalityStatus,
90
91
  TransactionType: () => TransactionType,
@@ -101,9 +102,9 @@ __export(index_exports, {
101
102
  UINT_512_MIN: () => UINT_512_MIN,
102
103
  Uint: () => Uint,
103
104
  ValidateType: () => ValidateType,
104
- WSSubscriptions: () => WSSubscriptions,
105
105
  WalletAccount: () => WalletAccount,
106
106
  WebSocketChannel: () => WebSocketChannel,
107
+ WebSocketNotConnectedError: () => WebSocketNotConnectedError,
107
108
  addAddressPadding: () => addAddressPadding,
108
109
  byteArray: () => byteArray_exports,
109
110
  cairo: () => cairo_exports,
@@ -117,8 +118,6 @@ __export(index_exports, {
117
118
  eth: () => eth_exports,
118
119
  events: () => events_exports,
119
120
  extractContractHashes: () => extractContractHashes,
120
- fixProto: () => fixProto,
121
- fixStack: () => fixStack,
122
121
  getCalldata: () => getCalldata,
123
122
  getChecksumAddress: () => getChecksumAddress,
124
123
  getLedgerPathBuffer: () => getLedgerPathBuffer111,
@@ -3985,6 +3984,18 @@ var RpcError = class extends LibraryError {
3985
3984
  return rpc_default[typeName] === this.code;
3986
3985
  }
3987
3986
  };
3987
+ var TimeoutError = class extends LibraryError {
3988
+ constructor(message) {
3989
+ super(message);
3990
+ this.name = "TimeoutError";
3991
+ }
3992
+ };
3993
+ var WebSocketNotConnectedError = class extends LibraryError {
3994
+ constructor(message) {
3995
+ super(message);
3996
+ this.name = "WebSocketNotConnectedError";
3997
+ }
3998
+ };
3988
3999
 
3989
4000
  // src/utils/eth.ts
3990
4001
  var eth_exports = {};
@@ -5472,6 +5483,31 @@ var RpcChannel2 = class {
5472
5483
  }
5473
5484
  };
5474
5485
 
5486
+ // src/utils/eventEmitter.ts
5487
+ var EventEmitter = class {
5488
+ listeners = {};
5489
+ on(event, listener) {
5490
+ if (!this.listeners[event]) {
5491
+ this.listeners[event] = [];
5492
+ }
5493
+ this.listeners[event].push(listener);
5494
+ }
5495
+ off(event, listener) {
5496
+ if (!this.listeners[event]) {
5497
+ return;
5498
+ }
5499
+ this.listeners[event] = this.listeners[event].filter((l) => l !== listener);
5500
+ }
5501
+ emit(event, data) {
5502
+ if (this.listeners[event]) {
5503
+ this.listeners[event].forEach((listener) => listener(data));
5504
+ }
5505
+ }
5506
+ clear() {
5507
+ this.listeners = {};
5508
+ }
5509
+ };
5510
+
5475
5511
  // src/utils/connect/ws.ts
5476
5512
  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 {
5477
5513
  constructor() {
@@ -5481,152 +5517,184 @@ var ws_default = typeof WebSocket !== "undefined" && WebSocket || typeof globalT
5481
5517
  }
5482
5518
  };
5483
5519
 
5484
- // src/channel/ws_0_8.ts
5485
- var WSSubscriptions = {
5486
- NEW_HEADS: "newHeads",
5487
- EVENTS: "events",
5488
- TRANSACTION_STATUS: "transactionStatus",
5489
- PENDING_TRANSACTION: "pendingTransactions"
5490
- };
5491
- var WebSocketChannel = class {
5520
+ // src/channel/ws/subscription.ts
5521
+ var Subscription = class {
5492
5522
  /**
5493
- * WebSocket RPC Node URL
5494
- * @example 'wss://starknet-node.io/rpc/v0_8'
5523
+ * The containing `WebSocketChannel` instance.
5524
+ * @internal
5495
5525
  */
5496
- nodeUrl;
5497
- // public headers: object;
5498
- // readonly retries: number;
5499
- // public requestId: number;
5500
- // readonly blockIdentifier: BlockIdentifier;
5501
- // private chainId?: StarknetChainId;
5502
- // private specVersion?: string;
5503
- // private transactionRetryIntervalFallback?: number;
5504
- // readonly waitMode: Boolean; // behave like web2 rpc and return when tx is processed
5505
- // private batchClient?: BatchClient;
5506
- /**
5507
- * ws library object
5508
- */
5509
- websocket;
5526
+ channel;
5510
5527
  /**
5511
- * Assign implementation method to get 'on reorg event data'
5512
- * @example
5513
- * ```typescript
5514
- * webSocketChannel.onReorg = async function (data) {
5515
- * // ... do something when reorg happens
5516
- * }
5517
- * ```
5528
+ * The JSON-RPC method used to create this subscription.
5529
+ * @internal
5518
5530
  */
5519
- onReorg = () => {
5520
- };
5531
+ method;
5521
5532
  /**
5522
- * Assign implementation method to get 'starknet block heads'
5523
- * @example
5524
- * ```typescript
5525
- * webSocketChannel.onNewHeads = async function (data) {
5526
- * // ... do something with head data
5527
- * }
5528
- * ```
5533
+ * The parameters used to create this subscription.
5534
+ * @internal
5529
5535
  */
5530
- onNewHeads = () => {
5531
- };
5536
+ params;
5532
5537
  /**
5533
- * Assign implementation method to get 'starknet events'
5534
- * @example
5535
- * ```typescript
5536
- * webSocketChannel.onEvents = async function (data) {
5537
- * // ... do something with event data
5538
- * }
5539
- * ```
5538
+ * The unique identifier for this subscription.
5539
+ * @internal
5540
5540
  */
5541
- onEvents = () => {
5542
- };
5541
+ id;
5542
+ events = new EventEmitter();
5543
+ buffer = [];
5544
+ maxBufferSize;
5545
+ handler = null;
5546
+ _isClosed = false;
5543
5547
  /**
5544
- * Assign method to get 'starknet transactions status'
5545
- * @example
5546
- * ```typescript
5547
- * webSocketChannel.onTransactionStatus = async function (data) {
5548
- * // ... do something with tx status data
5549
- * }
5550
- * ```
5548
+ * @internal
5549
+ * @param {WebSocketChannel} channel - The WebSocketChannel instance.
5550
+ * @param {string} method - The RPC method used for the subscription.
5551
+ * @param {any} params - The parameters for the subscription.
5552
+ * @param {SUBSCRIPTION_ID} id - The subscription ID.
5553
+ * @param {number} maxBufferSize - The maximum number of events to buffer.
5551
5554
  */
5552
- onTransactionStatus = () => {
5553
- };
5555
+ constructor(channel, method, params, id, maxBufferSize) {
5556
+ this.channel = channel;
5557
+ this.method = method;
5558
+ this.params = params;
5559
+ this.id = id;
5560
+ this.maxBufferSize = maxBufferSize;
5561
+ }
5554
5562
  /**
5555
- * Assign implementation method to get 'starknet pending transactions (mempool)'
5556
- * @example
5557
- * ```typescript
5558
- * webSocketChannel.onPendingTransaction = async function (data) {
5559
- * // ... do something with pending tx data
5560
- * }
5561
- * ```
5563
+ * Indicates if the subscription has been closed.
5564
+ * @returns {boolean} `true` if unsubscribed, `false` otherwise.
5562
5565
  */
5563
- onPendingTransaction = () => {
5564
- };
5566
+ get isClosed() {
5567
+ return this._isClosed;
5568
+ }
5565
5569
  /**
5566
- * Assign implementation to this method to listen open Event
5570
+ * Internal method to handle incoming events from the WebSocket channel.
5571
+ * If a handler is attached, it's invoked immediately. Otherwise, the event is buffered.
5572
+ * @internal
5573
+ * @param {T} data - The event data.
5567
5574
  */
5568
- onOpen = () => {
5569
- };
5575
+ _handleEvent(data) {
5576
+ if (this.handler) {
5577
+ this.handler(data);
5578
+ } else {
5579
+ if (this.buffer.length >= this.maxBufferSize) {
5580
+ const droppedEvent = this.buffer.shift();
5581
+ logger.warn(`Subscription ${this.id}: Buffer full. Dropping oldest event:`, droppedEvent);
5582
+ }
5583
+ this.buffer.push(data);
5584
+ }
5585
+ }
5570
5586
  /**
5571
- * Assign implementation to this method to listen close CloseEvent
5572
- */
5573
- onClose = () => {
5574
- };
5587
+ * Attaches a handler function to be called for each event.
5588
+ *
5589
+ * When a handler is attached, any buffered events will be passed to it sequentially.
5590
+ * Subsequent events will be passed directly as they arrive.
5591
+ *
5592
+ * @param {(data: T) => void} handler - The function to call with event data.
5593
+ * @throws {Error} If a handler is already attached to this subscription.
5594
+ */
5595
+ on(handler) {
5596
+ if (this.handler) {
5597
+ throw new Error("A handler is already attached to this subscription.");
5598
+ }
5599
+ this.handler = handler;
5600
+ while (this.buffer.length > 0) {
5601
+ const event = this.buffer.shift();
5602
+ if (event) {
5603
+ this.handler(event);
5604
+ }
5605
+ }
5606
+ }
5575
5607
  /**
5576
- * Assign implementation to this method to listen message MessageEvent
5608
+ * Sends an unsubscribe request to the node and cleans up local resources.
5609
+ * @returns {Promise<boolean>} A Promise that resolves to `true` if the unsubscription was successful.
5577
5610
  */
5578
- onMessage = () => {
5579
- };
5611
+ async unsubscribe() {
5612
+ if (this._isClosed) {
5613
+ return true;
5614
+ }
5615
+ const success = await this.channel.unsubscribe(this.id);
5616
+ if (success) {
5617
+ this._isClosed = true;
5618
+ this.channel.removeSubscription(this.id);
5619
+ this.events.emit("unsubscribe", void 0);
5620
+ this.events.clear();
5621
+ }
5622
+ return success;
5623
+ }
5624
+ };
5625
+
5626
+ // src/channel/ws/ws_0_8.ts
5627
+ var WebSocketChannel = class {
5580
5628
  /**
5581
- * Assign implementation to this method to listen error Event
5629
+ * The URL of the WebSocket RPC Node.
5630
+ * @example 'wss://starknet-sepolia.public.blastapi.io/rpc/v0_8'
5582
5631
  */
5583
- onError = () => {
5584
- };
5632
+ nodeUrl;
5585
5633
  /**
5586
- * Assign implementation to this method to listen unsubscription
5634
+ * The underlying WebSocket instance.
5587
5635
  */
5588
- onUnsubscribe = () => {
5589
- };
5590
- onUnsubscribeLocal = () => {
5591
- };
5592
- /**
5593
- * JSON RPC latest sent message id
5594
- * expecting receiving message to contain same id
5636
+ websocket;
5637
+ // Store the WebSocket implementation class to allow for custom implementations.
5638
+ WsImplementation;
5639
+ // Map of active subscriptions, keyed by their ID.
5640
+ activeSubscriptions = /* @__PURE__ */ new Map();
5641
+ maxBufferSize;
5642
+ autoReconnect;
5643
+ reconnectOptions;
5644
+ requestTimeout;
5645
+ isReconnecting = false;
5646
+ reconnectAttempts = 0;
5647
+ userInitiatedClose = false;
5648
+ reconnectTimeoutId = null;
5649
+ requestQueue = [];
5650
+ events = new EventEmitter();
5651
+ openListener = (ev) => this.events.emit("open", ev);
5652
+ closeListener = this.onCloseProxy.bind(this);
5653
+ messageListener = this.onMessageProxy.bind(this);
5654
+ errorListener = (ev) => this.events.emit("error", ev);
5655
+ /**
5656
+ * JSON RPC latest sent message ID.
5657
+ * The receiving message is expected to contain the same ID.
5595
5658
  */
5596
5659
  sendId = 0;
5597
5660
  /**
5598
- * subscriptions ids
5599
- * mapped by keys WSSubscriptions
5600
- */
5601
- subscriptions = /* @__PURE__ */ new Map();
5602
- /**
5603
- * Construct class and event listeners
5604
- * @param options WebSocketOptions
5661
+ * Creates an instance of WebSocketChannel.
5662
+ * @param {WebSocketOptions} options - The options for configuring the channel.
5605
5663
  */
5606
- constructor(options = {}) {
5607
- const nodeUrl = options.nodeUrl || "http://localhost:3000 ";
5608
- this.nodeUrl = options.websocket ? options.websocket.url : nodeUrl;
5609
- this.websocket = options.websocket || config.get("websocket") || new ws_default(nodeUrl);
5610
- this.websocket.addEventListener("open", this.onOpen.bind(this));
5611
- this.websocket.addEventListener("close", this.onCloseProxy.bind(this));
5612
- this.websocket.addEventListener("message", this.onMessageProxy.bind(this));
5613
- this.websocket.addEventListener("error", this.onError.bind(this));
5664
+ constructor(options) {
5665
+ this.nodeUrl = options.nodeUrl;
5666
+ this.maxBufferSize = options.maxBufferSize ?? 1e3;
5667
+ this.autoReconnect = options.autoReconnect ?? true;
5668
+ this.reconnectOptions = {
5669
+ retries: options.reconnectOptions?.retries ?? 5,
5670
+ delay: options.reconnectOptions?.delay ?? 2e3
5671
+ };
5672
+ this.requestTimeout = options.requestTimeout ?? 6e4;
5673
+ this.WsImplementation = options.websocket || config.get("websocket") || ws_default;
5674
+ this.websocket = new this.WsImplementation(this.nodeUrl);
5675
+ this.websocket.addEventListener("open", this.openListener);
5676
+ this.websocket.addEventListener("close", this.closeListener);
5677
+ this.websocket.addEventListener("message", this.messageListener);
5678
+ this.websocket.addEventListener("error", this.errorListener);
5614
5679
  }
5615
5680
  idResolver(id) {
5616
5681
  if (id) return id;
5617
5682
  return this.sendId++;
5618
5683
  }
5619
5684
  /**
5620
- * Send data over open ws connection
5621
- * * this would only send data on the line without awaiting 'response message'
5622
- * @example
5623
- * ```typescript
5624
- * const sentId = await this.send('starknet_method', params);
5625
- * ```
5685
+ * Sends a JSON-RPC request over the WebSocket connection without waiting for a response.
5686
+ * This is a low-level method. Prefer `sendReceive` for most use cases.
5687
+ * @param {string} method - The RPC method name.
5688
+ * @param {object} [params] - The parameters for the RPC method.
5689
+ * @param {number} [id] - A specific request ID. If not provided, an auto-incrementing ID is used.
5690
+ * @returns {number} The ID of the sent request.
5691
+ * @throws {WebSocketNotConnectedError} If the WebSocket is not connected.
5626
5692
  */
5627
5693
  send(method, params, id) {
5628
5694
  if (!this.isConnected()) {
5629
- throw Error("WebSocketChannel.send() fail due to socket disconnected");
5695
+ throw new WebSocketNotConnectedError(
5696
+ "WebSocketChannel.send() failed due to socket being disconnected"
5697
+ );
5630
5698
  }
5631
5699
  const usedId = this.idResolver(id);
5632
5700
  const rpcRequestBody = {
@@ -5639,50 +5707,88 @@ var WebSocketChannel = class {
5639
5707
  return usedId;
5640
5708
  }
5641
5709
  /**
5642
- * Any Starknet method not just websocket override
5643
- */
5644
- sendReceiveAny(method, params) {
5645
- return this.sendReceive(method, params);
5646
- }
5647
- /**
5648
- * Send request and receive response over ws line
5649
- * This method abstract ws messages into request/response model
5650
- * @param method rpc method name
5651
- * @param params rpc method parameters
5652
- * @example
5653
- * ```typescript
5654
- * const response = await this.sendReceive('starknet_method', params);
5655
- * ```
5710
+ * Sends a JSON-RPC request and returns a Promise that resolves with the result.
5711
+ * This method abstracts the request/response cycle over WebSockets.
5712
+ * If the connection is lost, it will queue the request and send it upon reconnection.
5713
+ * @template T - The expected type of the result.
5714
+ * @param {string} method - The RPC method name.
5715
+ * @param {object} [params] - The parameters for the RPC method.
5716
+ * @returns {Promise<T>} A Promise that resolves with the RPC response result.
5717
+ * @throws {TimeoutError} If the request does not receive a response within the configured `requestTimeout`.
5718
+ * @throws {WebSocketNotConnectedError} If the WebSocket is not connected and auto-reconnect is disabled.
5656
5719
  */
5657
5720
  sendReceive(method, params) {
5721
+ if (this.isReconnecting || !this.isConnected() && this.autoReconnect && !this.userInitiatedClose) {
5722
+ logger.info(`WebSocket: Connection unavailable, queueing request: ${method}`);
5723
+ return new Promise((resolve, reject) => {
5724
+ this.requestQueue.push({ method, params, resolve, reject });
5725
+ });
5726
+ }
5658
5727
  const sendId = this.send(method, params);
5659
5728
  return new Promise((resolve, reject) => {
5660
- if (!this.websocket) return;
5661
- this.websocket.onmessage = ({ data }) => {
5662
- const message = JSON.parse(data);
5729
+ let timeoutId;
5730
+ if (!this.websocket || this.websocket.readyState !== ws_default.OPEN) {
5731
+ reject(new WebSocketNotConnectedError("WebSocket not available or not connected."));
5732
+ return;
5733
+ }
5734
+ const messageHandler = (event) => {
5735
+ if (!isString(event.data)) {
5736
+ logger.warn("WebSocket received non-string message data:", event.data);
5737
+ return;
5738
+ }
5739
+ const message = JSON.parse(event.data);
5663
5740
  if (message.id === sendId) {
5741
+ clearTimeout(timeoutId);
5742
+ this.websocket.removeEventListener("message", messageHandler);
5743
+ this.websocket.removeEventListener("error", errorHandler);
5664
5744
  if ("result" in message) {
5665
5745
  resolve(message.result);
5666
5746
  } else {
5667
- reject(Error(`error on ${method}, ${message.error}`));
5747
+ reject(
5748
+ new Error(`Error on ${method} (id: ${sendId}): ${JSON.stringify(message.error)}`)
5749
+ );
5668
5750
  }
5669
5751
  }
5670
5752
  };
5671
- this.websocket.onerror = reject;
5753
+ const errorHandler = (event) => {
5754
+ clearTimeout(timeoutId);
5755
+ this.websocket.removeEventListener("message", messageHandler);
5756
+ this.websocket.removeEventListener("error", errorHandler);
5757
+ reject(
5758
+ new Error(
5759
+ `WebSocket error during ${method} (id: ${sendId}): ${event.type || "Unknown error"}`
5760
+ )
5761
+ );
5762
+ };
5763
+ this.websocket.addEventListener("message", messageHandler);
5764
+ this.websocket.addEventListener("error", errorHandler);
5765
+ timeoutId = setTimeout(() => {
5766
+ this.websocket.removeEventListener("message", messageHandler);
5767
+ this.websocket.removeEventListener("error", errorHandler);
5768
+ reject(
5769
+ new TimeoutError(
5770
+ `Request ${method} (id: ${sendId}) timed out after ${this.requestTimeout}ms`
5771
+ )
5772
+ );
5773
+ }, this.requestTimeout);
5672
5774
  });
5673
5775
  }
5674
5776
  /**
5675
- * Helper to check connection is open
5777
+ * Checks if the WebSocket connection is currently open.
5778
+ * @returns {boolean} `true` if the connection is open, `false` otherwise.
5676
5779
  */
5677
5780
  isConnected() {
5678
5781
  return this.websocket.readyState === ws_default.OPEN;
5679
5782
  }
5680
5783
  /**
5681
- * await while websocket is connected
5682
- * * could be used to block the flow until websocket is open
5784
+ * Returns a Promise that resolves when the WebSocket connection is open.
5785
+ * Can be used to block execution until the connection is established.
5786
+ * @returns {Promise<number>} A Promise that resolves with the WebSocket's `readyState` when connected.
5683
5787
  * @example
5684
5788
  * ```typescript
5685
- * const readyState = await webSocketChannel.waitForConnection();
5789
+ * const channel = new WebSocketChannel({ nodeUrl: '...' });
5790
+ * await channel.waitForConnection();
5791
+ * console.log('Connected!');
5686
5792
  * ```
5687
5793
  */
5688
5794
  async waitForConnection() {
@@ -5698,17 +5804,22 @@ var WebSocketChannel = class {
5698
5804
  return this.websocket.readyState;
5699
5805
  }
5700
5806
  /**
5701
- * Disconnect the WebSocket connection, optionally using code as the the WebSocket connection close code and reason as the the WebSocket connection close reason.
5807
+ * Closes the WebSocket connection.
5808
+ * This method is user-initiated and will prevent automatic reconnection for this closure.
5809
+ * @param {number} [code] - The WebSocket connection close code.
5810
+ * @param {string} [reason] - The WebSocket connection close reason.
5702
5811
  */
5703
5812
  disconnect(code, reason) {
5813
+ if (this.reconnectTimeoutId) {
5814
+ clearTimeout(this.reconnectTimeoutId);
5815
+ this.reconnectTimeoutId = null;
5816
+ }
5704
5817
  this.websocket.close(code, reason);
5818
+ this.userInitiatedClose = true;
5705
5819
  }
5706
5820
  /**
5707
- * await while websocket is disconnected
5708
- * @example
5709
- * ```typescript
5710
- * const readyState = await webSocketChannel.waitForDisconnection();
5711
- * ```
5821
+ * Returns a Promise that resolves when the WebSocket connection is closed.
5822
+ * @returns {Promise<number | Event>} A Promise that resolves with the WebSocket's `readyState` or a `CloseEvent` when disconnected.
5712
5823
  */
5713
5824
  async waitForDisconnection() {
5714
5825
  if (this.websocket.readyState !== ws_default.CLOSED) {
@@ -5721,206 +5832,233 @@ var WebSocketChannel = class {
5721
5832
  return this.websocket.readyState;
5722
5833
  }
5723
5834
  /**
5724
- * Unsubscribe from starknet subscription
5725
- * @param subscriptionId
5726
- * @param ref internal usage, only for managed subscriptions
5835
+ * Unsubscribes from a Starknet subscription.
5836
+ * It is recommended to use the `unsubscribe()` method on the `Subscription` object instead.
5837
+ * @internal
5838
+ * @param {SUBSCRIPTION_ID} subscriptionId - The ID of the subscription to unsubscribe from.
5839
+ * @returns {Promise<boolean>} A Promise that resolves with `true` if the unsubscription was successful.
5727
5840
  */
5728
- async unsubscribe(subscriptionId, ref) {
5841
+ async unsubscribe(subscriptionId) {
5729
5842
  const status = await this.sendReceive("starknet_unsubscribe", {
5730
5843
  subscription_id: subscriptionId
5731
5844
  });
5732
5845
  if (status) {
5733
- if (ref) {
5734
- this.subscriptions.delete(ref);
5735
- }
5736
- this.onUnsubscribeLocal(subscriptionId);
5737
- this.onUnsubscribe(subscriptionId);
5846
+ this.events.emit("unsubscribe", subscriptionId);
5738
5847
  }
5739
5848
  return status;
5740
5849
  }
5741
5850
  /**
5742
- * await while subscription is unsubscribed
5743
- * @param forSubscriptionId if defined trigger on subscriptionId else trigger on any
5744
- * @returns subscriptionId | onerror(Event)
5851
+ * Returns a Promise that resolves when a specific subscription is successfully unsubscribed.
5852
+ * @param {SUBSCRIPTION_ID} targetId - The ID of the subscription to wait for.
5853
+ * @returns {Promise<void>}
5745
5854
  * @example
5746
5855
  * ```typescript
5747
- * const subscriptionId = await webSocketChannel.waitForUnsubscription();
5856
+ * await channel.waitForUnsubscription(mySubscription.id);
5857
+ * console.log('Successfully unsubscribed.');
5748
5858
  * ```
5749
5859
  */
5750
- async waitForUnsubscription(forSubscriptionId) {
5751
- return new Promise((resolve, reject) => {
5752
- if (!this.websocket) return;
5753
- this.onUnsubscribeLocal = (subscriptionId) => {
5754
- if (forSubscriptionId === void 0) {
5755
- resolve(subscriptionId);
5756
- } else if (subscriptionId === forSubscriptionId) {
5757
- resolve(subscriptionId);
5860
+ waitForUnsubscription(targetId) {
5861
+ return new Promise((resolve) => {
5862
+ const listener = (unsubId) => {
5863
+ if (unsubId === targetId) {
5864
+ this.events.off("unsubscribe", listener);
5865
+ resolve();
5758
5866
  }
5759
5867
  };
5760
- this.websocket.onerror = reject;
5868
+ this.events.on("unsubscribe", listener);
5761
5869
  });
5762
5870
  }
5763
5871
  /**
5764
- * Reconnect re-create this.websocket instance
5872
+ * Manually initiates a reconnection attempt.
5873
+ * This creates a new WebSocket instance and re-establishes listeners.
5765
5874
  */
5766
5875
  reconnect() {
5767
- this.websocket = new ws_default(this.nodeUrl);
5768
- this.websocket.addEventListener("open", this.onOpen.bind(this));
5769
- this.websocket.addEventListener("close", this.onCloseProxy.bind(this));
5770
- this.websocket.addEventListener("message", this.onMessageProxy.bind(this));
5771
- this.websocket.addEventListener("error", this.onError.bind(this));
5876
+ this.userInitiatedClose = false;
5877
+ this.websocket = new this.WsImplementation(this.nodeUrl);
5878
+ this.websocket.addEventListener("open", this.openListener);
5879
+ this.websocket.addEventListener("close", this.closeListener);
5880
+ this.websocket.addEventListener("message", this.messageListener);
5881
+ this.websocket.addEventListener("error", this.errorListener);
5882
+ }
5883
+ _processRequestQueue() {
5884
+ logger.info(`WebSocket: Processing ${this.requestQueue.length} queued requests.`);
5885
+ while (this.requestQueue.length > 0) {
5886
+ const { method, params, resolve, reject } = this.requestQueue.shift();
5887
+ this.sendReceive(method, params).then(resolve).catch(reject);
5888
+ }
5889
+ }
5890
+ async _restoreSubscriptions() {
5891
+ const oldSubscriptions = Array.from(this.activeSubscriptions.values());
5892
+ this.activeSubscriptions.clear();
5893
+ const restorePromises = oldSubscriptions.map(async (sub) => {
5894
+ try {
5895
+ const newSubId = await this.sendReceive(sub.method, sub.params);
5896
+ sub.id = newSubId;
5897
+ this.activeSubscriptions.set(newSubId, sub);
5898
+ logger.info(`Subscription ${sub.method} restored with new ID: ${newSubId}`);
5899
+ } catch (error) {
5900
+ logger.error(`Failed to restore subscription ${sub.method}:`, error);
5901
+ }
5902
+ });
5903
+ await Promise.all(restorePromises);
5772
5904
  }
5773
- // TODO: Add/Test ping service. It seems this work out of the box from pathfinder. If net disc. it will auto replay.
5774
- reconnectAndUpdate() {
5775
- this.reconnect();
5905
+ _startReconnect() {
5906
+ if (this.isReconnecting || !this.autoReconnect) {
5907
+ return;
5908
+ }
5909
+ this.isReconnecting = true;
5910
+ this.reconnectAttempts = 0;
5911
+ const tryReconnect = () => {
5912
+ if (this.reconnectAttempts >= this.reconnectOptions.retries) {
5913
+ logger.error("WebSocket: Maximum reconnection retries reached. Giving up.");
5914
+ this.isReconnecting = false;
5915
+ return;
5916
+ }
5917
+ this.reconnectAttempts += 1;
5918
+ logger.info(
5919
+ `WebSocket: Connection lost. Attempting to reconnect... (${this.reconnectAttempts}/${this.reconnectOptions.retries})`
5920
+ );
5921
+ this.reconnect();
5922
+ this.websocket.onopen = async () => {
5923
+ logger.info("WebSocket: Reconnection successful.");
5924
+ this.isReconnecting = false;
5925
+ this.reconnectAttempts = 0;
5926
+ await this._restoreSubscriptions();
5927
+ this._processRequestQueue();
5928
+ this.events.emit("open", new Event("open"));
5929
+ };
5930
+ this.websocket.onerror = () => {
5931
+ const delay = this.reconnectOptions.delay * 2 ** (this.reconnectAttempts - 1);
5932
+ logger.info(`WebSocket: Reconnect attempt failed. Retrying in ${delay}ms.`);
5933
+ this.reconnectTimeoutId = setTimeout(tryReconnect, delay);
5934
+ };
5935
+ };
5936
+ tryReconnect();
5776
5937
  }
5777
5938
  onCloseProxy(ev) {
5778
- this.websocket.removeEventListener("open", this.onOpen);
5779
- this.websocket.removeEventListener("close", this.onCloseProxy);
5780
- this.websocket.removeEventListener("message", this.onMessageProxy);
5781
- this.websocket.removeEventListener("error", this.onError);
5782
- this.onClose(ev);
5939
+ this.websocket.removeEventListener("open", this.openListener);
5940
+ this.websocket.removeEventListener("close", this.closeListener);
5941
+ this.websocket.removeEventListener("message", this.messageListener);
5942
+ this.websocket.removeEventListener("error", this.errorListener);
5943
+ this.events.emit("close", ev);
5944
+ if (!this.userInitiatedClose) {
5945
+ this._startReconnect();
5946
+ }
5783
5947
  }
5784
5948
  onMessageProxy(event) {
5785
- const message = JSON.parse(event.data);
5786
- const eventName = message.method;
5787
- switch (eventName) {
5788
- case "starknet_subscriptionReorg":
5789
- this.onReorg(message.params);
5790
- break;
5791
- case "starknet_subscriptionNewHeads":
5792
- this.onNewHeads(message.params);
5793
- break;
5794
- case "starknet_subscriptionEvents":
5795
- this.onEvents(message.params);
5796
- break;
5797
- case "starknet_subscriptionTransactionStatus":
5798
- this.onTransactionStatus(message.params);
5799
- break;
5800
- case "starknet_subscriptionPendingTransactions":
5801
- this.onPendingTransaction(message.params);
5802
- break;
5803
- default:
5804
- break;
5949
+ let message;
5950
+ try {
5951
+ message = JSON.parse(event.data);
5952
+ } catch (error) {
5953
+ logger.error(
5954
+ `WebSocketChannel: Error parsing incoming message: ${event.data}, Error: ${error}`
5955
+ );
5956
+ return;
5805
5957
  }
5806
- this.onMessage(event);
5807
- }
5808
- /**
5809
- * subscribe to new block heads
5810
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
5811
- */
5812
- subscribeNewHeadsUnmanaged(blockIdentifier) {
5813
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
5814
- return this.sendReceive("starknet_subscribeNewHeads", {
5815
- ...{ block_id }
5816
- });
5958
+ if (message.method && isObject(message.params) && "subscription_id" in message.params) {
5959
+ const { result, subscription_id } = message.params;
5960
+ const subscription = this.activeSubscriptions.get(subscription_id);
5961
+ if (subscription) {
5962
+ subscription._handleEvent(result);
5963
+ } else {
5964
+ logger.warn(
5965
+ `WebSocketChannel: Received event for untracked subscription ID: ${subscription_id}.`
5966
+ );
5967
+ }
5968
+ }
5969
+ logger.debug("onMessageProxy:", event.data);
5970
+ this.events.emit("message", event);
5817
5971
  }
5818
5972
  /**
5819
- * subscribe to new block heads
5973
+ * Subscribes to new block headers.
5974
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block to start receiving notifications from. Defaults to 'latest'.
5975
+ * @returns {Promise<Subscription<BLOCK_HEADER>>} A Promise that resolves with a `Subscription` object for new block headers.
5820
5976
  */
5821
5977
  async subscribeNewHeads(blockIdentifier) {
5822
- if (this.subscriptions.get(WSSubscriptions.NEW_HEADS)) return false;
5823
- const subId = await this.subscribeNewHeadsUnmanaged(blockIdentifier);
5824
- this.subscriptions.set(WSSubscriptions.NEW_HEADS, subId);
5825
- return subId;
5826
- }
5827
- /**
5828
- * Unsubscribe newHeads subscription
5829
- */
5830
- async unsubscribeNewHeads() {
5831
- const subId = this.subscriptions.get(WSSubscriptions.NEW_HEADS);
5832
- if (!subId) throw Error("There is no subscription on this event");
5833
- return this.unsubscribe(subId, WSSubscriptions.NEW_HEADS);
5834
- }
5835
- /**
5836
- * subscribe to 'starknet events'
5837
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
5838
- */
5839
- subscribeEventsUnmanaged(fromAddress, keys, blockIdentifier) {
5840
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
5841
- return this.sendReceive("starknet_subscribeEvents", {
5842
- ...{ from_address: fromAddress !== void 0 ? toHex(fromAddress) : void 0 },
5843
- ...{ keys },
5844
- ...{ block_id }
5845
- });
5978
+ const method = "starknet_subscribeNewHeads";
5979
+ const params = {
5980
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
5981
+ };
5982
+ const subId = await this.sendReceive(method, params);
5983
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
5984
+ this.activeSubscriptions.set(subId, subscription);
5985
+ return subscription;
5846
5986
  }
5847
5987
  /**
5848
- * subscribe to 'starknet events'
5988
+ * Subscribes to events matching a given filter.
5989
+ * @param {BigNumberish} [fromAddress] - The contract address to filter by.
5990
+ * @param {string[][]} [keys] - The event keys to filter by.
5991
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block to start receiving notifications from. Defaults to 'latest'.
5992
+ * @returns {Promise<Subscription<EMITTED_EVENT>>} A Promise that resolves with a `Subscription` object for the specified events.
5849
5993
  */
5850
5994
  async subscribeEvents(fromAddress, keys, blockIdentifier) {
5851
- if (this.subscriptions.get(WSSubscriptions.EVENTS)) return false;
5852
- const subId = await this.subscribeEventsUnmanaged(fromAddress, keys, blockIdentifier);
5853
- this.subscriptions.set(WSSubscriptions.EVENTS, subId);
5854
- return subId;
5855
- }
5856
- /**
5857
- * Unsubscribe 'starknet events' subscription
5858
- */
5859
- unsubscribeEvents() {
5860
- const subId = this.subscriptions.get(WSSubscriptions.EVENTS);
5861
- if (!subId) throw Error("There is no subscription ID for this event");
5862
- return this.unsubscribe(subId, WSSubscriptions.EVENTS);
5863
- }
5864
- /**
5865
- * subscribe to transaction status
5866
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
5867
- */
5868
- subscribeTransactionStatusUnmanaged(transactionHash, blockIdentifier) {
5869
- const transaction_hash = toHex(transactionHash);
5870
- const block_id = blockIdentifier ? new Block(blockIdentifier).identifier : void 0;
5871
- return this.sendReceive("starknet_subscribeTransactionStatus", {
5872
- transaction_hash,
5873
- ...{ block_id }
5874
- });
5995
+ const method = "starknet_subscribeEvents";
5996
+ const params = {
5997
+ from_address: fromAddress !== void 0 ? toHex(fromAddress) : void 0,
5998
+ keys,
5999
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
6000
+ };
6001
+ const subId = await this.sendReceive(method, params);
6002
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
6003
+ this.activeSubscriptions.set(subId, subscription);
6004
+ return subscription;
5875
6005
  }
5876
6006
  /**
5877
- * subscribe to transaction status
6007
+ * Subscribes to status updates for a specific transaction.
6008
+ * @param {BigNumberish} transactionHash - The hash of the transaction to monitor.
6009
+ * @param {SubscriptionBlockIdentifier} [blockIdentifier] - The block context. Not typically required.
6010
+ * @returns {Promise<Subscription<NEW_TXN_STATUS>>} A Promise that resolves with a `Subscription` object for the transaction's status.
5878
6011
  */
5879
- async subscribeTransactionStatus(transactionHash) {
5880
- if (this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS)) return false;
5881
- const subId = await this.subscribeTransactionStatusUnmanaged(transactionHash);
5882
- this.subscriptions.set(WSSubscriptions.TRANSACTION_STATUS, subId);
5883
- return subId;
6012
+ async subscribeTransactionStatus(transactionHash, blockIdentifier) {
6013
+ const method = "starknet_subscribeTransactionStatus";
6014
+ const params = {
6015
+ transaction_hash: toHex(transactionHash),
6016
+ block_id: blockIdentifier ? new Block(blockIdentifier).identifier : void 0
6017
+ };
6018
+ const subId = await this.sendReceive(method, params);
6019
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
6020
+ this.activeSubscriptions.set(subId, subscription);
6021
+ return subscription;
5884
6022
  }
5885
6023
  /**
5886
- * unsubscribe 'transaction status' subscription
6024
+ * Subscribes to pending transactions.
6025
+ * @param {boolean} [transactionDetails] - If `true`, the full transaction details are included. Defaults to `false` (hash only).
6026
+ * @param {BigNumberish[]} [senderAddress] - An array of sender addresses to filter by.
6027
+ * @returns {Promise<Subscription<TXN_HASH | TXN_WITH_HASH>>} A Promise that resolves with a `Subscription` object for pending transactions.
5887
6028
  */
5888
- async unsubscribeTransactionStatus() {
5889
- const subId = this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS);
5890
- if (!subId) throw Error("There is no subscription ID for this event");
5891
- return this.unsubscribe(subId, WSSubscriptions.TRANSACTION_STATUS);
6029
+ async subscribePendingTransaction(transactionDetails, senderAddress) {
6030
+ const method = "starknet_subscribePendingTransactions";
6031
+ const params = {
6032
+ transaction_details: transactionDetails,
6033
+ sender_address: senderAddress && bigNumberishArrayToHexadecimalStringArray(senderAddress)
6034
+ };
6035
+ const subId = await this.sendReceive(method, params);
6036
+ const subscription = new Subscription(this, method, params, subId, this.maxBufferSize);
6037
+ this.activeSubscriptions.set(subId, subscription);
6038
+ return subscription;
5892
6039
  }
5893
6040
  /**
5894
- * subscribe to pending transactions (mempool)
5895
- * * you can subscribe to this event multiple times and you need to manage subscriptions manually
6041
+ * Internal method to remove subscription from active map.
6042
+ * @internal
5896
6043
  */
5897
- subscribePendingTransactionUnmanaged(transactionDetails, senderAddress) {
5898
- return this.sendReceive("starknet_subscribePendingTransactions", {
5899
- ...{ transaction_details: transactionDetails },
5900
- ...{
5901
- sender_address: senderAddress && bigNumberishArrayToHexadecimalStringArray(senderAddress)
5902
- }
5903
- });
6044
+ removeSubscription(id) {
6045
+ this.activeSubscriptions.delete(id);
5904
6046
  }
5905
6047
  /**
5906
- * subscribe to pending transactions (mempool)
6048
+ * Adds a listener for a given event.
6049
+ * @param event The event name.
6050
+ * @param listener The listener function to add.
5907
6051
  */
5908
- async subscribePendingTransaction(transactionDetails, senderAddress) {
5909
- if (this.subscriptions.get(WSSubscriptions.TRANSACTION_STATUS)) return false;
5910
- const subId = await this.subscribePendingTransactionUnmanaged(
5911
- transactionDetails,
5912
- senderAddress
5913
- );
5914
- this.subscriptions.set(WSSubscriptions.PENDING_TRANSACTION, subId);
5915
- return subId;
6052
+ on(event, listener) {
6053
+ this.events.on(event, listener);
5916
6054
  }
5917
6055
  /**
5918
- * unsubscribe 'pending transaction' subscription
6056
+ * Removes a listener for a given event.
6057
+ * @param event The event name.
6058
+ * @param listener The listener function to remove.
5919
6059
  */
5920
- async unsubscribePendingTransaction() {
5921
- const subId = this.subscriptions.get(WSSubscriptions.PENDING_TRANSACTION);
5922
- if (!subId) throw Error("There is no subscription ID for this event");
5923
- return this.unsubscribe(subId, WSSubscriptions.PENDING_TRANSACTION);
6060
+ off(event, listener) {
6061
+ this.events.off(event, listener);
5924
6062
  }
5925
6063
  };
5926
6064
 
@@ -10788,7 +10926,6 @@ function units(amount, simbol = "fri") {
10788
10926
  Contract,
10789
10927
  ContractFactory,
10790
10928
  ContractInterface,
10791
- CustomError,
10792
10929
  EDAMode,
10793
10930
  EDataAvailabilityMode,
10794
10931
  ETH_ADDRESS,
@@ -10825,6 +10962,8 @@ function units(amount, simbol = "fri") {
10825
10962
  RpcProvider,
10826
10963
  Signer,
10827
10964
  SignerInterface,
10965
+ Subscription,
10966
+ TimeoutError,
10828
10967
  TransactionExecutionStatus,
10829
10968
  TransactionFinalityStatus,
10830
10969
  TransactionType,
@@ -10841,9 +10980,9 @@ function units(amount, simbol = "fri") {
10841
10980
  UINT_512_MIN,
10842
10981
  Uint,
10843
10982
  ValidateType,
10844
- WSSubscriptions,
10845
10983
  WalletAccount,
10846
10984
  WebSocketChannel,
10985
+ WebSocketNotConnectedError,
10847
10986
  addAddressPadding,
10848
10987
  byteArray,
10849
10988
  cairo,
@@ -10857,8 +10996,6 @@ function units(amount, simbol = "fri") {
10857
10996
  eth,
10858
10997
  events,
10859
10998
  extractContractHashes,
10860
- fixProto,
10861
- fixStack,
10862
10999
  getCalldata,
10863
11000
  getChecksumAddress,
10864
11001
  getLedgerPathBuffer,