surfpool_core/rpc/ws.rs
1use std::{
2 collections::HashMap,
3 str::FromStr,
4 sync::{Arc, RwLock, atomic},
5};
6
7use crossbeam_channel::TryRecvError;
8use jsonrpc_core::{Error, ErrorCode, Result};
9use jsonrpc_derive::rpc;
10use jsonrpc_pubsub::{
11 SubscriptionId,
12 typed::{Sink, Subscriber},
13};
14use solana_account_decoder::{UiAccount, UiAccountEncoding};
15use solana_client::{
16 rpc_config::{RpcSignatureSubscribeConfig, RpcTransactionConfig, RpcTransactionLogsFilter},
17 rpc_filter::RpcFilterType,
18 rpc_response::{
19 ProcessedSignatureResult, ReceivedSignatureResult, RpcKeyedAccount, RpcLogsResponse,
20 RpcResponseContext, RpcSignatureResult,
21 },
22};
23use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
24use solana_pubkey::Pubkey;
25use solana_rpc_client_api::response::{Response as RpcResponse, SlotInfo, SlotUpdate};
26use solana_signature::Signature;
27use solana_transaction_status::{TransactionConfirmationStatus, UiTransactionEncoding};
28
29use super::{State, SurfnetRpcContext, SurfpoolWebsocketMeta};
30use crate::{
31 rpc::utils::MAX_SUPPORTED_TRANSACTION_VERSION,
32 surfnet::{GetTransactionResult, SignatureSubscriptionType},
33};
34
35/// Configuration for account subscription requests.
36///
37/// This struct defines the parameters that clients can specify when subscribing
38/// to account change notifications through WebSocket connections. It allows customization
39/// of both the commitment level for updates and the encoding format for account data.
40///
41/// ## Fields
42/// - `commitment`: Optional commitment configuration specifying when to send notifications
43/// (processed, confirmed, or finalized). Defaults to the node's default commitment level.
44/// - `encoding`: Optional encoding format for account data serialization (base58, base64, jsonParsed, etc.).
45/// Defaults to base58 encoding if not specified.
46///
47/// ## Usage
48/// Clients can provide this configuration to customize their subscription behavior:
49/// - Set commitment level to control notification timing based on confirmation status
50/// - Set encoding to specify the preferred format for receiving account data
51///
52/// ## Example Usage
53/// ```json
54/// {
55/// "commitment": "confirmed",
56/// "encoding": "base64"
57/// }
58/// ```
59#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct RpcAccountSubscribeConfig {
62 #[serde(flatten)]
63 pub commitment: Option<CommitmentConfig>,
64 pub encoding: Option<UiAccountEncoding>,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize, Default)]
68#[serde(rename_all = "camelCase")]
69pub struct RpcProgramSubscribeConfig {
70 #[serde(flatten)]
71 pub commitment: Option<CommitmentConfig>,
72 pub encoding: Option<UiAccountEncoding>,
73 pub filters: Option<Vec<RpcFilterType>>,
74}
75
76#[rpc]
77pub trait Rpc {
78 type Metadata;
79
80 /// Subscribe to signature status notifications via WebSocket.
81 ///
82 /// This method allows clients to subscribe to status updates for a specific transaction signature.
83 /// The subscriber will receive notifications when the transaction reaches the desired confirmation level
84 /// or when it's initially received by the network (if configured).
85 ///
86 /// ## Parameters
87 /// - `meta`: WebSocket metadata containing RPC context and connection information.
88 /// - `subscriber`: The subscription sink for sending signature status notifications to the client.
89 /// - `signature_str`: The transaction signature to monitor, as a base-58 encoded string.
90 /// - `config`: Optional configuration specifying commitment level and notification preferences.
91 ///
92 /// ## Returns
93 /// This method does not return a value directly. Instead, it establishes a WebSocket subscription
94 /// that will send `RpcResponse<RpcSignatureResult>` notifications to the subscriber when the
95 /// transaction status changes.
96 ///
97 /// ## Example WebSocket Request
98 /// ```json
99 /// {
100 /// "jsonrpc": "2.0",
101 /// "id": 1,
102 /// "method": "signatureSubscribe",
103 /// "params": [
104 /// "2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb",
105 /// {
106 /// "commitment": "finalized",
107 /// "enableReceivedNotification": false
108 /// }
109 /// ]
110 /// }
111 /// ```
112 ///
113 /// ## Example WebSocket Response (Subscription Confirmation)
114 /// ```json
115 /// {
116 /// "jsonrpc": "2.0",
117 /// "result": 0,
118 /// "id": 1
119 /// }
120 /// ```
121 ///
122 /// ## Example WebSocket Notification
123 /// ```json
124 /// {
125 /// "jsonrpc": "2.0",
126 /// "method": "signatureNotification",
127 /// "params": {
128 /// "result": {
129 /// "context": {
130 /// "slot": 5207624
131 /// },
132 /// "value": {
133 /// "err": null
134 /// }
135 /// },
136 /// "subscription": 0
137 /// }
138 /// }
139 /// ```
140 ///
141 /// ## Notes
142 /// - If the transaction already exists with the desired confirmation status, the subscriber
143 /// will be notified immediately and the subscription will complete.
144 /// - The subscription automatically terminates after sending the first matching notification.
145 /// - Invalid signature formats will cause the subscription to be rejected with an error.
146 /// - Each subscription runs in its own async task for optimal performance.
147 ///
148 /// ## See Also
149 /// - `signatureUnsubscribe`: Remove an active signature subscription
150 /// - `getSignatureStatuses`: Get current status of multiple signatures
151 #[pubsub(
152 subscription = "signatureNotification",
153 subscribe,
154 name = "signatureSubscribe"
155 )]
156 fn signature_subscribe(
157 &self,
158 meta: Self::Metadata,
159 subscriber: Subscriber<RpcResponse<RpcSignatureResult>>,
160 signature_str: String,
161 config: Option<RpcSignatureSubscribeConfig>,
162 );
163
164 /// Unsubscribe from signature status notifications.
165 ///
166 /// This method removes an active signature subscription, stopping further notifications
167 /// for the specified subscription ID.
168 ///
169 /// ## Parameters
170 /// - `meta`: Optional WebSocket metadata containing connection information.
171 /// - `subscription`: The subscription ID to remove, as returned by `signatureSubscribe`.
172 ///
173 /// ## Returns
174 /// A `Result<bool>` indicating whether the unsubscription was successful:
175 /// - `Ok(true)` if the subscription was successfully removed
176 /// - `Err(Error)` with `InvalidParams` if the subscription ID doesn't exist
177 ///
178 /// ## Example WebSocket Request
179 /// ```json
180 /// {
181 /// "jsonrpc": "2.0",
182 /// "id": 1,
183 /// "method": "signatureUnsubscribe",
184 /// "params": [0]
185 /// }
186 /// ```
187 ///
188 /// ## Example WebSocket Response
189 /// ```json
190 /// {
191 /// "jsonrpc": "2.0",
192 /// "result": true,
193 /// "id": 1
194 /// }
195 /// ```
196 ///
197 /// ## Notes
198 /// - Attempting to unsubscribe from a non-existent subscription will return an error.
199 /// - Successfully unsubscribed connections will no longer receive notifications.
200 /// - This method is thread-safe and can be called concurrently.
201 ///
202 /// ## See Also
203 /// - `signatureSubscribe`: Create a signature status subscription
204 #[pubsub(
205 subscription = "signatureNotification",
206 unsubscribe,
207 name = "signatureUnsubscribe"
208 )]
209 fn signature_unsubscribe(
210 &self,
211 meta: Option<Self::Metadata>,
212 subscription: SubscriptionId,
213 ) -> Result<bool>;
214
215 /// Subscribe to account change notifications via WebSocket.
216 ///
217 /// This method allows clients to subscribe to updates for a specific account.
218 /// The subscriber will receive notifications whenever the account's data, lamports balance,
219 /// ownership, or other properties change.
220 ///
221 /// ## Parameters
222 /// - `meta`: WebSocket metadata containing RPC context and connection information.
223 /// - `subscriber`: The subscription sink for sending account update notifications to the client.
224 /// - `pubkey_str`: The account public key to monitor, as a base-58 encoded string.
225 /// - `config`: Optional configuration specifying commitment level and encoding format for account data.
226 ///
227 /// ## Returns
228 /// This method does not return a value directly. Instead, it establishes a continuous WebSocket
229 /// subscription that will send `RpcResponse<UiAccount>` notifications to the subscriber whenever
230 /// the account state changes.
231 ///
232 /// ## Example WebSocket Request
233 /// ```json
234 /// {
235 /// "jsonrpc": "2.0",
236 /// "id": 1,
237 /// "method": "accountSubscribe",
238 /// "params": [
239 /// "CM78CPUeXjn8o3yroDHxUtKsZZgoy4GPkPPXfouKNH12",
240 /// {
241 /// "commitment": "finalized",
242 /// "encoding": "base64"
243 /// }
244 /// ]
245 /// }
246 /// ```
247 ///
248 /// ## Example WebSocket Response (Subscription Confirmation)
249 /// ```json
250 /// {
251 /// "jsonrpc": "2.0",
252 /// "result": 23784,
253 /// "id": 1
254 /// }
255 /// ```
256 ///
257 /// ## Example WebSocket Notification
258 /// ```json
259 /// {
260 /// "jsonrpc": "2.0",
261 /// "method": "accountNotification",
262 /// "params": {
263 /// "result": {
264 /// "context": {
265 /// "slot": 5208469
266 /// },
267 /// "value": {
268 /// "data": ["base64EncodedAccountData", "base64"],
269 /// "executable": false,
270 /// "lamports": 33594,
271 /// "owner": "11111111111111111111111111111112",
272 /// "rentEpoch": 636
273 /// }
274 /// },
275 /// "subscription": 23784
276 /// }
277 /// }
278 /// ```
279 ///
280 /// ## Notes
281 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
282 /// - Account notifications are sent whenever any aspect of the account changes.
283 /// - The encoding format specified in the config determines how account data is serialized.
284 /// - Invalid public key formats will cause the subscription to be rejected with an error.
285 /// - Each subscription runs in its own async task to ensure optimal performance.
286 ///
287 /// ## See Also
288 /// - `accountUnsubscribe`: Remove an active account subscription
289 /// - `getAccountInfo`: Get current account information
290 #[pubsub(
291 subscription = "accountNotification",
292 subscribe,
293 name = "accountSubscribe"
294 )]
295 fn account_subscribe(
296 &self,
297 meta: Self::Metadata,
298 subscriber: Subscriber<RpcResponse<UiAccount>>,
299 pubkey_str: String,
300 config: Option<RpcAccountSubscribeConfig>,
301 );
302
303 /// Unsubscribe from account change notifications.
304 ///
305 /// This method removes an active account subscription, stopping further notifications
306 /// for the specified subscription ID. The monitoring task will automatically terminate
307 /// when the subscription is removed.
308 ///
309 /// ## Parameters
310 /// - `meta`: Optional WebSocket metadata containing connection information.
311 /// - `subscription`: The subscription ID to remove, as returned by `accountSubscribe`.
312 ///
313 /// ## Returns
314 /// A `Result<bool>` indicating whether the unsubscription was successful:
315 /// - `Ok(true)` if the subscription was successfully removed
316 /// - `Err(Error)` with `InvalidParams` if the subscription ID doesn't exist
317 ///
318 /// ## Example WebSocket Request
319 /// ```json
320 /// {
321 /// "jsonrpc": "2.0",
322 /// "id": 1,
323 /// "method": "accountUnsubscribe",
324 /// "params": [23784]
325 /// }
326 /// ```
327 ///
328 /// ## Example WebSocket Response
329 /// ```json
330 /// {
331 /// "jsonrpc": "2.0",
332 /// "result": true,
333 /// "id": 1
334 /// }
335 /// ```
336 ///
337 /// ## Notes
338 /// - Attempting to unsubscribe from a non-existent subscription will return an error.
339 /// - Successfully unsubscribed connections will no longer receive account notifications.
340 /// - The monitoring task automatically detects subscription removal and terminates gracefully.
341 /// - This method is thread-safe and can be called concurrently.
342 ///
343 /// ## See Also
344 /// - `accountSubscribe`: Create an account change subscription
345 #[pubsub(
346 subscription = "accountNotification",
347 unsubscribe,
348 name = "accountUnsubscribe"
349 )]
350 fn account_unsubscribe(
351 &self,
352 meta: Option<Self::Metadata>,
353 subscription: SubscriptionId,
354 ) -> Result<bool>;
355
356 /// Subscribe to slot notifications.
357 ///
358 /// This method allows clients to subscribe to updates for a specific slot.
359 /// The subscriber will receive notifications whenever the slot changes.
360 ///
361 /// ## Parameters
362 /// - `meta`: WebSocket metadata containing RPC context and connection information.
363 /// - `subscriber`: The subscription sink for sending slot update notifications to the client.
364 ///
365 /// ## Returns
366 /// This method does not return a value directly. Instead, it establishes a continuous WebSocket
367 /// subscription that will send `SlotInfo` notifications to the subscriber whenever
368 /// the slot changes.
369 ///
370 /// ## Example WebSocket Request
371 /// ```json
372 /// {
373 /// "jsonrpc": "2.0",
374 /// "id": 1,
375 /// "method": "slotSubscribe",
376 /// "params": [
377 /// {
378 /// "commitment": "finalized"
379 /// }
380 /// ]
381 /// }
382 /// ```
383 ///
384 /// ## Example WebSocket Response (Subscription Confirmation)
385 /// ```json
386 /// {
387 /// "jsonrpc": "2.0",
388 /// "result": 5207624,
389 /// "id": 1
390 /// }
391 /// ```
392 ///
393 /// ## Example WebSocket Notification
394 /// ```json
395 /// {
396 /// "jsonrpc": "2.0",
397 /// "method": "slotNotification",
398 /// "params": {
399 /// "result": {
400 /// "slot": 5207624
401 /// },
402 /// "subscription": 5207624
403 /// }
404 /// }
405 /// ```
406 ///
407 /// ## Notes
408 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
409 /// - Slot notifications are sent whenever the slot changes.
410 /// - The subscription automatically terminates when the slot changes.
411 /// - Each subscription runs in its own async task for optimal performance.
412 ///
413 /// ## See Also
414 /// - `slotUnsubscribe`: Remove an active slot subscription
415 #[pubsub(subscription = "slotNotification", subscribe, name = "slotSubscribe")]
416 fn slot_subscribe(&self, meta: Self::Metadata, subscriber: Subscriber<SlotInfo>);
417
418 /// Unsubscribe from slot notifications.
419 ///
420 /// This method removes an active slot subscription, stopping further notifications
421 /// for the specified subscription ID.
422 ///
423 /// ## Parameters
424 /// - `meta`: Optional WebSocket metadata containing connection information.
425 /// - `subscription`: The subscription ID to remove, as returned by `slotSubscribe`.
426 ///
427 /// ## Returns
428 /// A `Result<bool>` indicating whether the unsubscription was successful:
429 /// - `Ok(true)` if the subscription was successfully removed
430 /// - `Err(Error)` with `InvalidParams` if the subscription ID doesn't exist
431 ///
432 /// ## Example WebSocket Request
433 /// ```json
434 /// {
435 /// "jsonrpc": "2.0",
436 /// "id": 1,
437 /// "method": "slotUnsubscribe",
438 /// "params": [0]
439 /// }
440 /// ```
441 ///
442 /// ## Example WebSocket Response
443 /// ```json
444 /// {
445 /// "jsonrpc": "2.0",
446 /// "result": true,
447 /// "id": 1
448 /// }
449 /// ```
450 ///
451 /// ## Notes
452 /// - Attempting to unsubscribe from a non-existent subscription will return an error.
453 /// - Successfully unsubscribed connections will no longer receive notifications.
454 /// - This method is thread-safe and can be called concurrently.
455 ///
456 /// ## See Also
457 /// - `slotSubscribe`: Create a slot subscription
458 #[pubsub(
459 subscription = "slotNotification",
460 unsubscribe,
461 name = "slotUnsubscribe"
462 )]
463 fn slot_unsubscribe(
464 &self,
465 meta: Option<Self::Metadata>,
466 subscription: SubscriptionId,
467 ) -> Result<bool>;
468
469 /// Subscribe to logs notifications.
470 ///
471 /// This method allows clients to subscribe to transaction log messages
472 /// emitted during transaction execution. It supports filtering by signature,
473 /// account mentions, or all transactions.
474 ///
475 /// ## Parameters
476 /// - `meta`: WebSocket metadata containing RPC context and connection information.
477 /// - `subscriber`: The subscription sink for sending log notifications to the client.
478 /// - `mentions`: Optional filter for the subscription: can be a specific signature, account, or `"all"`.
479 /// - `commitment`: Optional commitment level for filtering logs by block finality.
480 ///
481 /// ## Returns
482 /// This method establishes a continuous WebSocket subscription that streams
483 /// `RpcLogsResponse` notifications as new transactions are processed.
484 ///
485 /// ## Example WebSocket Request
486 /// ```json
487 /// {
488 /// "jsonrpc": "2.0",
489 /// "id": 1,
490 /// "method": "logsSubscribe",
491 /// "params": [
492 /// {
493 /// "mentions": ["11111111111111111111111111111111"]
494 /// },
495 /// {
496 /// "commitment": "finalized"
497 /// }
498 /// ]
499 /// }
500 /// ```
501 ///
502 /// ## Example WebSocket Response (Subscription Confirmation)
503 /// ```json
504 /// {
505 /// "jsonrpc": "2.0",
506 /// "result": 42,
507 /// "id": 1
508 /// }
509 /// ```
510 ///
511 /// ## Example WebSocket Notification
512 /// ```json
513 /// {
514 /// "jsonrpc": "2.0",
515 /// "method": "logsNotification",
516 /// "params": {
517 /// "result": {
518 /// "signature": "3s6n...",
519 /// "err": null,
520 /// "logs": ["Program 111111... invoke [1]", "Program 111111... success"]
521 /// },
522 /// "subscription": 42
523 /// }
524 /// }
525 /// ```
526 ///
527 /// ## Notes
528 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
529 /// - Each log subscription runs independently and supports filtering.
530 /// - Log messages may be truncated depending on cluster configuration.
531 ///
532 /// ## See Also
533 /// - `logsUnsubscribe`: Remove an active logs subscription.
534 #[pubsub(subscription = "logsNotification", subscribe, name = "logsSubscribe")]
535 fn logs_subscribe(
536 &self,
537 meta: Self::Metadata,
538 subscriber: Subscriber<RpcResponse<RpcLogsResponse>>,
539 mentions: Option<RpcTransactionLogsFilter>,
540 commitment: Option<CommitmentConfig>,
541 );
542
543 /// Unsubscribe from logs notifications.
544 ///
545 /// This method removes an active logs subscription, stopping further notifications
546 /// for the specified subscription ID.
547 ///
548 /// ## Parameters
549 /// - `meta`: Optional WebSocket metadata containing connection information.
550 /// - `subscription`: The subscription ID to remove, as returned by `logsSubscribe`.
551 ///
552 /// ## Returns
553 /// A `Result<bool>` indicating whether the unsubscription was successful:
554 /// - `Ok(true)` if the subscription was successfully removed.
555 /// - `Err(Error)` with `InvalidParams` if the subscription ID is not recognized.
556 ///
557 /// ## Example WebSocket Request
558 /// ```json
559 /// {
560 /// "jsonrpc": "2.0",
561 /// "id": 1,
562 /// "method": "logsUnsubscribe",
563 /// "params": [42]
564 /// }
565 /// ```
566 ///
567 /// ## Example WebSocket Response
568 /// ```json
569 /// {
570 /// "jsonrpc": "2.0",
571 /// "result": true,
572 /// "id": 1
573 /// }
574 /// ```
575 ///
576 /// ## Notes
577 /// - Unsubscribing from a non-existent subscription ID returns an error.
578 /// - Successfully unsubscribed clients will no longer receive logs notifications.
579 /// - This method is thread-safe and may be called concurrently.
580 ///
581 /// ## See Also
582 /// - `logsSubscribe`: Create a logs subscription.
583 #[pubsub(
584 subscription = "logsNotification",
585 unsubscribe,
586 name = "logsUnsubscribe"
587 )]
588 fn logs_unsubscribe(
589 &self,
590 meta: Option<Self::Metadata>,
591 subscription: SubscriptionId,
592 ) -> Result<bool>;
593
594 #[pubsub(subscription = "rootNotification", subscribe, name = "rootSubscribe")]
595 fn root_subscribe(&self, meta: Self::Metadata, subscriber: Subscriber<RpcResponse<()>>);
596
597 #[pubsub(
598 subscription = "rootNotification",
599 unsubscribe,
600 name = "rootUnsubscribe"
601 )]
602 fn root_unsubscribe(
603 &self,
604 meta: Option<Self::Metadata>,
605 subscription: SubscriptionId,
606 ) -> Result<bool>;
607
608 /// Subscribe to notifications for all accounts owned by a specific program via WebSocket.
609 ///
610 /// This method allows clients to subscribe to updates for any account whose `owner`
611 /// matches the given program ID. Notifications are sent whenever an account owned by
612 /// the program is created, updated, or deleted.
613 ///
614 /// ## Parameters
615 /// - `meta`: WebSocket metadata containing RPC context and connection information.
616 /// - `subscriber`: The subscription sink for sending program account notifications to the client.
617 /// - `pubkey_str`: The program public key to monitor, as a base-58 encoded string.
618 /// - `config`: Optional configuration specifying commitment level, encoding format, and filters.
619 ///
620 /// ## Returns
621 /// This method does not return a value directly. Instead, it establishes a continuous WebSocket
622 /// subscription that will send `RpcResponse<RpcKeyedAccount>` notifications to the subscriber
623 /// whenever an account owned by the program changes.
624 ///
625 /// ## Filters
626 /// The optional config may include filters to narrow which account updates trigger notifications:
627 /// - `dataSize`: Only notify for accounts with a specific data length (in bytes).
628 /// - `memcmp`: Only notify for accounts whose data matches specific bytes at a given offset.
629 ///
630 /// ## Example WebSocket Request
631 /// ```json
632 /// {
633 /// "jsonrpc": "2.0",
634 /// "id": 1,
635 /// "method": "programSubscribe",
636 /// "params": [
637 /// "11111111111111111111111111111111",
638 /// {
639 /// "encoding": "base64",
640 /// "filters": [
641 /// { "dataSize": 80 }
642 /// ]
643 /// }
644 /// ]
645 /// }
646 /// ```
647 ///
648 /// ## Example WebSocket Response (Subscription Confirmation)
649 /// ```json
650 /// {
651 /// "jsonrpc": "2.0",
652 /// "result": 24040,
653 /// "id": 1
654 /// }
655 /// ```
656 ///
657 /// ## Example WebSocket Notification
658 /// ```json
659 /// {
660 /// "jsonrpc": "2.0",
661 /// "method": "programNotification",
662 /// "params": {
663 /// "result": {
664 /// "context": { "slot": 5208469 },
665 /// "value": {
666 /// "pubkey": "H4vnBqifaSACnKa7acsxstsY1iV1bvJNxsCY7enrd1hq",
667 /// "account": {
668 /// "data": ["base64data", "base64"],
669 /// "executable": false,
670 /// "lamports": 33594,
671 /// "owner": "11111111111111111111111111111111",
672 /// "rentEpoch": 636,
673 /// "space": 36
674 /// }
675 /// }
676 /// },
677 /// "subscription": 24040
678 /// }
679 /// }
680 /// ```
681 ///
682 /// ## Notes
683 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
684 /// - Notifications include both the account pubkey and the full account data.
685 /// - Invalid public key formats will cause the subscription to be rejected with an error.
686 /// - Each subscription runs in its own async task for optimal performance.
687 ///
688 /// ## See Also
689 /// - `programUnsubscribe`: Remove an active program subscription
690 /// - `getProgramAccounts`: Get current accounts for a program
691 #[pubsub(
692 subscription = "programNotification",
693 subscribe,
694 name = "programSubscribe"
695 )]
696 fn program_subscribe(
697 &self,
698 meta: Self::Metadata,
699 subscriber: Subscriber<RpcResponse<RpcKeyedAccount>>,
700 pubkey_str: String,
701 config: Option<RpcProgramSubscribeConfig>,
702 );
703
704 /// Unsubscribe from program account change notifications.
705 ///
706 /// This method removes an active program subscription, stopping further notifications
707 /// for the specified subscription ID. The monitoring task will automatically terminate
708 /// when the subscription is removed.
709 ///
710 /// ## Parameters
711 /// - `meta`: Optional WebSocket metadata containing connection information.
712 /// - `subscription`: The subscription ID to remove, as returned by `programSubscribe`.
713 ///
714 /// ## Returns
715 /// A `Result<bool>` indicating whether the unsubscription was successful:
716 /// - `Ok(true)` if the subscription was successfully removed
717 /// - `Err(Error)` with `InternalError` if the subscription map lock could not be acquired
718 ///
719 /// ## Example WebSocket Request
720 /// ```json
721 /// {
722 /// "jsonrpc": "2.0",
723 /// "id": 1,
724 /// "method": "programUnsubscribe",
725 /// "params": [24040]
726 /// }
727 /// ```
728 ///
729 /// ## Example WebSocket Response
730 /// ```json
731 /// {
732 /// "jsonrpc": "2.0",
733 /// "result": true,
734 /// "id": 1
735 /// }
736 /// ```
737 ///
738 /// ## Notes
739 /// - Successfully unsubscribed connections will no longer receive program account notifications.
740 /// - The monitoring task automatically detects subscription removal and terminates gracefully.
741 /// - This method is thread-safe and can be called concurrently.
742 ///
743 /// ## See Also
744 /// - `programSubscribe`: Create a program account change subscription
745 #[pubsub(
746 subscription = "programNotification",
747 unsubscribe,
748 name = "programUnsubscribe"
749 )]
750 fn program_unsubscribe(
751 &self,
752 meta: Option<Self::Metadata>,
753 subscription: SubscriptionId,
754 ) -> Result<bool>;
755
756 /// Subscribe to tagged slot lifecycle notifications via WebSocket.
757 ///
758 /// This method streams [`SlotUpdate`] notifications produced by the
759 /// underlying surfnet SVM whenever a slot advances through its
760 /// lifecycle. The notification payload follows Solana's reference
761 /// [`slotsUpdatesSubscribe`](https://solana.com/docs/rpc/websocket/slotsupdatessubscribe)
762 /// wire format: a tagged enum serialized with `type` discriminator and
763 /// camelCase variants (e.g. `"createdBank"`, `"frozen"`,
764 /// `"optimisticConfirmation"`, `"root"`).
765 ///
766 /// ## Parameters
767 /// - `meta`: WebSocket metadata containing RPC context and connection information.
768 /// - `subscriber`: The subscription sink for sending slot update notifications to the client.
769 ///
770 /// ## Returns
771 /// This method does not return a value directly. Instead, it establishes a continuous WebSocket
772 /// subscription that streams `SlotUpdate` notifications to the subscriber whenever the slot
773 /// advances through its lifecycle.
774 ///
775 /// ## Variants Emitted by Surfpool
776 /// - `createdBank` – emitted when the simulator opens a new slot.
777 /// - `frozen` – emitted once the previous slot has been confirmed,
778 /// carrying execution `stats`.
779 /// - `optimisticConfirmation` – emitted alongside Geyser's `Confirmed`
780 /// slot-status update for the freshly confirmed slot.
781 /// - `root` – emitted alongside Geyser's `Rooted` slot-status update.
782 ///
783 /// Surfpool has no gossip/shred layer, so the `firstShredReceived`,
784 /// `completed`, and `dead` variants documented in the Solana reference
785 /// are not produced by this implementation.
786 ///
787 /// ## Example WebSocket Request
788 /// ```json
789 /// {
790 /// "jsonrpc": "2.0",
791 /// "id": 1,
792 /// "method": "slotsUpdatesSubscribe"
793 /// }
794 /// ```
795 ///
796 /// ## Example WebSocket Response (Subscription Confirmation)
797 /// ```json
798 /// {
799 /// "jsonrpc": "2.0",
800 /// "result": 0,
801 /// "id": 1
802 /// }
803 /// ```
804 ///
805 /// ## Example WebSocket Notification (`frozen` variant)
806 /// ```json
807 /// {
808 /// "jsonrpc": "2.0",
809 /// "method": "slotsUpdatesNotification",
810 /// "params": {
811 /// "result": {
812 /// "type": "frozen",
813 /// "slot": 356,
814 /// "timestamp": 1774643712834,
815 /// "stats": {
816 /// "numTransactionEntries": 1,
817 /// "numSuccessfulTransactions": 1,
818 /// "numFailedTransactions": 0,
819 /// "maxTransactionsPerEntry": 1
820 /// }
821 /// },
822 /// "subscription": 4
823 /// }
824 /// }
825 /// ```
826 ///
827 /// ## Notes
828 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
829 /// - Notifications are delivered without an `RpcResponse` wrapper – the
830 /// tagged `SlotUpdate` value appears directly under `params.result`.
831 /// - The `timestamp` field is a wall-clock millisecond Unix timestamp.
832 /// - Each subscription runs in its own async task for optimal performance.
833 ///
834 /// ## See Also
835 /// - [`slotsUpdatesUnsubscribe`]: Remove an active subscription
836 /// - [`slotSubscribe`]: Subscribe to the simpler `SlotInfo` stream
837 #[pubsub(
838 subscription = "slotsUpdatesNotification",
839 subscribe,
840 name = "slotsUpdatesSubscribe"
841 )]
842 fn slots_updates_subscribe(
843 &self,
844 meta: Self::Metadata,
845 subscriber: Subscriber<Arc<SlotUpdate>>,
846 );
847
848 /// Unsubscribe from tagged slot lifecycle notifications.
849 ///
850 /// This method removes an active `slotsUpdatesSubscribe` subscription,
851 /// stopping further notifications for the specified subscription ID. The
852 /// background monitoring task will detect the removal and terminate
853 /// gracefully.
854 ///
855 /// ## Parameters
856 /// - `meta`: Optional WebSocket metadata containing connection information.
857 /// - `subscription`: The subscription ID to remove, as returned by `slotsUpdatesSubscribe`.
858 ///
859 /// ## Returns
860 /// A `Result<bool>` indicating whether the unsubscription was successful:
861 /// - `Ok(true)` if the subscription was successfully removed
862 /// - `Err(Error)` with `InternalError` if the subscription map lock could not be acquired
863 ///
864 /// ## Example WebSocket Request
865 /// ```json
866 /// {
867 /// "jsonrpc": "2.0",
868 /// "id": 1,
869 /// "method": "slotsUpdatesUnsubscribe",
870 /// "params": [0]
871 /// }
872 /// ```
873 ///
874 /// ## Example WebSocket Response
875 /// ```json
876 /// {
877 /// "jsonrpc": "2.0",
878 /// "result": true,
879 /// "id": 1
880 /// }
881 /// ```
882 ///
883 /// ## See Also
884 /// - `slotsUpdatesSubscribe`: Create a slots-updates subscription.
885 #[pubsub(
886 subscription = "slotsUpdatesNotification",
887 unsubscribe,
888 name = "slotsUpdatesUnsubscribe"
889 )]
890 fn slots_updates_unsubscribe(
891 &self,
892 meta: Option<Self::Metadata>,
893 subscription: SubscriptionId,
894 ) -> Result<bool>;
895
896 #[pubsub(subscription = "blockNotification", subscribe, name = "blockSubscribe")]
897 fn block_subscribe(&self, meta: Self::Metadata, subscriber: Subscriber<RpcResponse<()>>);
898
899 #[pubsub(
900 subscription = "blockNotification",
901 unsubscribe,
902 name = "blockUnsubscribe"
903 )]
904 fn block_unsubscribe(
905 &self,
906 meta: Option<Self::Metadata>,
907 subscription: SubscriptionId,
908 ) -> Result<bool>;
909
910 #[pubsub(subscription = "voteNotification", subscribe, name = "voteSubscribe")]
911 fn vote_subscribe(&self, meta: Self::Metadata, subscriber: Subscriber<RpcResponse<()>>);
912
913 #[pubsub(
914 subscription = "voteNotification",
915 unsubscribe,
916 name = "voteUnsubscribe"
917 )]
918 fn vote_unsubscribe(
919 &self,
920 meta: Option<Self::Metadata>,
921 subscription: SubscriptionId,
922 ) -> Result<bool>;
923
924 /// Subscribe to snapshot import notifications via WebSocket.
925 ///
926 /// This method allows clients to subscribe to real-time updates about snapshot import operations
927 /// from a specific snapshot URL. The subscriber will receive notifications when the snapshot
928 /// is being imported, including progress updates and completion status.
929 ///
930 /// ## Parameters
931 /// - `meta`: WebSocket metadata containing RPC context and connection information.
932 /// - `subscriber`: The subscription sink for sending snapshot import notifications to the client.
933 /// - `snapshot_url`: The URL of the snapshot to import and monitor.
934 ///
935 /// ## Returns
936 /// This method does not return a value directly. Instead, it establishes a continuous WebSocket
937 /// subscription that will send `SnapshotImportNotification` notifications to the subscriber whenever
938 /// the snapshot import operation status changes.
939 ///
940 /// ## Example WebSocket Request
941 /// ```json
942 /// {
943 /// "jsonrpc": "2.0",
944 /// "id": 1,
945 /// "method": "snapshotSubscribe",
946 /// "params": ["https://example.com/snapshot.json"]
947 /// }
948 /// ```
949 ///
950 /// ## Example WebSocket Response (Subscription Confirmation)
951 /// ```json
952 /// {
953 /// "jsonrpc": "2.0",
954 /// "result": 12345,
955 /// "id": 1
956 /// }
957 /// ```
958 ///
959 /// ## Example WebSocket Notification
960 /// ```json
961 /// {
962 /// "jsonrpc": "2.0",
963 /// "method": "snapshotNotification",
964 /// "params": {
965 /// "result": {
966 /// "snapshotId": "snapshot_20240107_123456",
967 /// "status": "InProgress",
968 /// "accountsLoaded": 1500,
969 /// "totalAccounts": 3000,
970 /// "error": null
971 /// },
972 /// "subscription": 12345
973 /// }
974 /// }
975 /// ```
976 ///
977 /// ## Notes
978 /// - The subscription remains active until explicitly unsubscribed or the connection is closed.
979 /// - Multiple clients can subscribe to different snapshot notifications simultaneously.
980 /// - The snapshot URL must be accessible and contain a valid snapshot format.
981 /// - Each subscription runs in its own async task for optimal performance.
982 ///
983 /// ## See Also
984 /// - `snapshotUnsubscribe`: Remove an active snapshot subscription
985 #[pubsub(
986 subscription = "snapshotNotification",
987 subscribe,
988 name = "snapshotSubscribe"
989 )]
990 fn snapshot_subscribe(
991 &self,
992 meta: Self::Metadata,
993 subscriber: Subscriber<crate::surfnet::SnapshotImportNotification>,
994 snapshot_url: String,
995 );
996
997 /// Unsubscribe from snapshot import notifications.
998 ///
999 /// This method removes an active snapshot subscription, stopping further notifications
1000 /// for the specified subscription ID.
1001 ///
1002 /// ## Parameters
1003 /// - `meta`: Optional WebSocket metadata containing connection information.
1004 /// - `subscription`: The subscription ID to remove, as returned by `snapshotSubscribe`.
1005 ///
1006 /// ## Returns
1007 /// A `Result<bool>` indicating whether the unsubscription was successful:
1008 /// - `Ok(true)` if the subscription was successfully removed
1009 /// - `Err(Error)` with `InvalidParams` if the subscription ID doesn't exist
1010 ///
1011 /// ## Example WebSocket Request
1012 /// ```json
1013 /// {
1014 /// "jsonrpc": "2.0",
1015 /// "id": 1,
1016 /// "method": "snapshotUnsubscribe",
1017 /// "params": [12345]
1018 /// }
1019 /// ```
1020 ///
1021 /// ## Example WebSocket Response
1022 /// ```json
1023 /// {
1024 /// "jsonrpc": "2.0",
1025 /// "result": true,
1026 /// "id": 1
1027 /// }
1028 /// ```
1029 ///
1030 /// ## Notes
1031 /// - Attempting to unsubscribe from a non-existent subscription will return an error.
1032 /// - Successfully unsubscribed connections will no longer receive snapshot notifications.
1033 /// - This method is thread-safe and can be called concurrently.
1034 ///
1035 /// ## See Also
1036 /// - `snapshotSubscribe`: Create a snapshot import subscription
1037 #[pubsub(
1038 subscription = "snapshotNotification",
1039 unsubscribe,
1040 name = "snapshotUnsubscribe"
1041 )]
1042 fn snapshot_unsubscribe(
1043 &self,
1044 meta: Option<Self::Metadata>,
1045 subscription: SubscriptionId,
1046 ) -> Result<bool>;
1047}
1048
1049/// WebSocket RPC server implementation for Surfpool.
1050///
1051/// This struct manages WebSocket subscriptions for both signature status updates
1052/// and account change notifications in the Surfpool environment. It provides a complete
1053/// WebSocket RPC interface that allows clients to subscribe to real-time updates
1054/// from the Solana Virtual Machine (SVM) and handles the lifecycle of WebSocket connections.
1055///
1056/// ## Fields
1057/// - `uid`: Atomic counter for generating unique subscription IDs across all subscription types.
1058/// - `signature_subscription_map`: Thread-safe HashMap containing active signature subscriptions, mapping subscription IDs to their notification sinks.
1059/// - `account_subscription_map`: Thread-safe HashMap containing active account subscriptions, mapping subscription IDs to their notification sinks.
1060/// - `program_subscription_map`: Thread-safe HashMap containing active program subscriptions, mapping subscription IDs to their notification sinks.
1061/// - `slot_subscription_map`: Thread-safe HashMap containing active slot subscriptions, mapping subscription IDs to their notification sinks.
1062/// - `slots_updates_subscription_map`: Thread-safe HashMap containing active fine-grained slot-lifecycle subscriptions (`slotsUpdatesSubscribe`), mapping subscription IDs to their `SlotUpdate` notification sinks.
1063/// - `logs_subscription_map`: Thread-safe HashMap containing active logs subscriptions, mapping subscription IDs to their notification sinks.
1064/// - `snapshot_subscription_map`: Thread-safe HashMap containing active snapshot-import subscriptions, mapping subscription IDs to their notification sinks.
1065/// - `tokio_handle`: Runtime handle for spawning asynchronous subscription monitoring tasks.
1066///
1067/// ## Features
1068/// - **Concurrent Subscriptions**: Supports multiple simultaneous subscriptions without blocking.
1069/// - **Thread Safety**: All subscription management operations are thread-safe using RwLock.
1070/// - **Automatic Cleanup**: Subscriptions are automatically cleaned up when completed or unsubscribed.
1071/// - **Efficient Monitoring**: Each subscription runs in its own async task for optimal performance.
1072/// - **Real-time Updates**: Provides immediate notifications when monitored conditions are met.
1073///
1074/// ## Usage
1075/// This struct implements the `Rpc` trait and is typically used as part of a larger
1076/// WebSocket server infrastructure to provide real-time blockchain data to clients.
1077///
1078/// ## Notes
1079/// - Each subscription is assigned a unique numeric ID for tracking and management.
1080/// - The struct maintains separate maps for different subscription types to optimize performance.
1081/// - All async operations are managed through the provided Tokio runtime handle.
1082///
1083/// ## See Also
1084/// - `Rpc`: The trait interface this struct implements
1085/// - `RpcAccountSubscribeConfig`: Configuration options for account subscriptions
1086pub struct SurfpoolWsRpc {
1087 pub uid: atomic::AtomicUsize,
1088 pub signature_subscription_map:
1089 Arc<RwLock<HashMap<SubscriptionId, Sink<RpcResponse<RpcSignatureResult>>>>>,
1090 pub account_subscription_map:
1091 Arc<RwLock<HashMap<SubscriptionId, Sink<RpcResponse<UiAccount>>>>>,
1092 pub program_subscription_map:
1093 Arc<RwLock<HashMap<SubscriptionId, Sink<RpcResponse<RpcKeyedAccount>>>>>,
1094 pub slot_subscription_map: Arc<RwLock<HashMap<SubscriptionId, Sink<SlotInfo>>>>,
1095 pub slots_updates_subscription_map: Arc<RwLock<HashMap<SubscriptionId, Sink<Arc<SlotUpdate>>>>>,
1096 pub logs_subscription_map:
1097 Arc<RwLock<HashMap<SubscriptionId, Sink<RpcResponse<RpcLogsResponse>>>>>,
1098 pub snapshot_subscription_map:
1099 Arc<RwLock<HashMap<SubscriptionId, Sink<crate::surfnet::SnapshotImportNotification>>>>,
1100 pub tokio_handle: tokio::runtime::Handle,
1101}
1102
1103impl Rpc for SurfpoolWsRpc {
1104 type Metadata = Option<SurfpoolWebsocketMeta>;
1105
1106 /// Implementation of signature subscription for WebSocket clients.
1107 ///
1108 /// This method handles the complete lifecycle of signature subscriptions:
1109 /// 1. Validates the provided signature string format
1110 /// 2. Determines the subscription type (received vs commitment-based)
1111 /// 3. Checks if the transaction already exists in the desired state
1112 /// 4. If found and confirmed, immediately notifies the subscriber
1113 /// 5. Otherwise, sets up a continuous monitoring loop
1114 /// 6. Spawns an async task to handle ongoing subscription management
1115 ///
1116 /// # Error Handling
1117 /// - Rejects subscription with `InvalidParams` for malformed signatures
1118 /// - Handles RPC context retrieval failures
1119 /// - Manages subscription cleanup on completion or failure
1120 ///
1121 /// # Concurrency
1122 /// Each subscription runs in its own async task, allowing multiple
1123 /// concurrent subscriptions without blocking each other.
1124 fn signature_subscribe(
1125 &self,
1126 meta: Self::Metadata,
1127 subscriber: Subscriber<RpcResponse<RpcSignatureResult>>,
1128 signature_str: String,
1129 config: Option<RpcSignatureSubscribeConfig>,
1130 ) {
1131 let _ = meta
1132 .as_ref()
1133 .map(|m| m.log_debug("Websocket 'signature_subscribe' connection established"));
1134
1135 let signature = match Signature::from_str(&signature_str) {
1136 Ok(sig) => sig,
1137 Err(_) => {
1138 let error = Error {
1139 code: ErrorCode::InvalidParams,
1140 message: "Invalid signature format.".into(),
1141 data: None,
1142 };
1143 if let Err(e) = subscriber.reject(error.clone()) {
1144 log::error!("Failed to reject subscriber: {:?}", e);
1145 }
1146 return;
1147 }
1148 };
1149 let config = config.unwrap_or_default();
1150 let rpc_transaction_config = RpcTransactionConfig {
1151 encoding: Some(UiTransactionEncoding::Json),
1152 commitment: config.commitment,
1153 max_supported_transaction_version: Some(MAX_SUPPORTED_TRANSACTION_VERSION),
1154 };
1155
1156 let subscription_type = if config.enable_received_notification.unwrap_or(false) {
1157 SignatureSubscriptionType::Received
1158 } else {
1159 SignatureSubscriptionType::Commitment(config.commitment.unwrap_or_default().commitment)
1160 };
1161
1162 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1163 let sub_id = SubscriptionId::Number(id as u64);
1164 let sink = match subscriber.assign_id(sub_id.clone()) {
1165 Ok(sink) => sink,
1166 Err(e) => {
1167 log::error!("Failed to assign subscription ID: {:?}", e);
1168 return;
1169 }
1170 };
1171 let active = Arc::clone(&self.signature_subscription_map);
1172 let meta = meta.clone();
1173 self.tokio_handle.spawn(async move {
1174 if let Ok(mut guard) = active.write() {
1175 guard.insert(sub_id.clone(), sink);
1176 } else {
1177 log::error!("Failed to acquire write lock on signature_subscription_map");
1178 return;
1179 }
1180
1181 let SurfnetRpcContext {
1182 svm_locker,
1183 remote_ctx,
1184 } = match meta.get_rpc_context(()) {
1185 Ok(res) => res,
1186 Err(e) => {
1187 log::error!("Failed to get RPC context: {:?}", e);
1188 if let Ok(mut guard) = active.write() {
1189 if let Some(sink) = guard.remove(&sub_id) {
1190 if let Err(e) = sink.notify(Err(e.into())) {
1191 log::error!("Failed to notify client about RPC context error: {e}");
1192 }
1193 }
1194 }
1195 return;
1196 }
1197 };
1198 // get the signature from the SVM to see if it's already been processed
1199 let tx_result = match svm_locker
1200 .get_transaction(
1201 &remote_ctx.map(|(r, _)| r),
1202 &signature,
1203 rpc_transaction_config,
1204 )
1205 .await
1206 {
1207 Ok(res) => res,
1208 Err(e) => {
1209 if let Ok(mut guard) = active.write() {
1210 if let Some(sink) = guard.remove(&sub_id) {
1211 let _ = sink.notify(Err(e.into()));
1212 }
1213 }
1214 return;
1215 }
1216 };
1217
1218 // if we already had the transaction, check if its confirmation status matches the desired status set by the subscription
1219 // if so, notify the user and complete the subscription
1220 // otherwise, subscribe to the transaction updates
1221 if let GetTransactionResult::FoundTransaction(_, _, tx) = tx_result {
1222 match (&subscription_type, tx.confirmation_status) {
1223 (&SignatureSubscriptionType::Received, _)
1224 | (
1225 &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed),
1226 Some(TransactionConfirmationStatus::Processed),
1227 )
1228 | (
1229 &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed),
1230 Some(TransactionConfirmationStatus::Confirmed),
1231 )
1232 | (
1233 &SignatureSubscriptionType::Commitment(CommitmentLevel::Processed),
1234 Some(TransactionConfirmationStatus::Finalized),
1235 )
1236 | (
1237 &SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed),
1238 Some(TransactionConfirmationStatus::Confirmed),
1239 )
1240 | (
1241 &SignatureSubscriptionType::Commitment(CommitmentLevel::Confirmed),
1242 Some(TransactionConfirmationStatus::Finalized),
1243 )
1244 | (
1245 &SignatureSubscriptionType::Commitment(CommitmentLevel::Finalized),
1246 Some(TransactionConfirmationStatus::Finalized),
1247 ) => {
1248 if let Ok(mut guard) = active.write() {
1249 if let Some(sink) = guard.remove(&sub_id) {
1250 let _ = sink.notify(Ok(RpcResponse {
1251 context: RpcResponseContext::new(tx.slot),
1252 value: RpcSignatureResult::ProcessedSignature(
1253 ProcessedSignatureResult {
1254 err: tx.err.map(|e| e.into()),
1255 },
1256 ),
1257 }));
1258 }
1259 }
1260 return;
1261 }
1262 _ => {}
1263 }
1264 }
1265
1266 // update our surfnet SVM to subscribe to the signature updates
1267 let rx =
1268 svm_locker.subscribe_for_signature_updates(&signature, subscription_type.clone());
1269
1270 loop {
1271 let (slot, some_err) = match rx.try_recv() {
1272 Ok(msg) => msg,
1273 Err(e) => {
1274 match e {
1275 TryRecvError::Empty => {
1276 // no update yet, continue
1277 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1278 continue;
1279 }
1280 TryRecvError::Disconnected => {
1281 warn!(
1282 "Signature subscription channel closed for sub id {:?}",
1283 sub_id
1284 );
1285 // channel closed, exit loop
1286 break;
1287 }
1288 }
1289 }
1290 };
1291
1292 let Ok(mut guard) = active.write() else {
1293 log::error!("Failed to acquire read lock on signature_subscription_map");
1294 break;
1295 };
1296
1297 let Some(sink) = guard.remove(&sub_id) else {
1298 log::error!("Failed to get sink for subscription ID");
1299 break;
1300 };
1301
1302 let res = match subscription_type {
1303 SignatureSubscriptionType::Received => sink.notify(Ok(RpcResponse {
1304 context: RpcResponseContext::new(slot),
1305 value: RpcSignatureResult::ReceivedSignature(
1306 ReceivedSignatureResult::ReceivedSignature,
1307 ),
1308 })),
1309 SignatureSubscriptionType::Commitment(_) => sink.notify(Ok(RpcResponse {
1310 context: RpcResponseContext::new(slot),
1311 value: RpcSignatureResult::ProcessedSignature(ProcessedSignatureResult {
1312 err: some_err.map(|e| e.into()),
1313 }),
1314 })),
1315 };
1316
1317 if guard.is_empty() {
1318 break;
1319 }
1320
1321 if let Err(e) = res {
1322 log::error!("Failed to notify client about account update: {e}");
1323 break;
1324 }
1325 }
1326 });
1327 }
1328
1329 /// Implementation of signature unsubscription for WebSocket clients.
1330 ///
1331 /// This method removes an active signature subscription from the internal
1332 /// tracking maps, effectively stopping further notifications for that subscription.
1333 ///
1334 /// # Implementation Details
1335 /// - Attempts to remove the subscription from the active subscriptions map
1336 /// - Returns success if the subscription existed and was removed
1337 /// - Returns an error if the subscription ID was not found
1338 ///
1339 /// # Thread Safety
1340 /// Uses write locks to ensure thread-safe removal from the subscription map.
1341 fn signature_unsubscribe(
1342 &self,
1343 _meta: Option<Self::Metadata>,
1344 subscription: SubscriptionId,
1345 ) -> Result<bool> {
1346 if let Ok(mut guard) = self.signature_subscription_map.write() {
1347 guard.remove(&subscription);
1348 } else {
1349 log::error!("Failed to acquire write lock on signature_subscription_map");
1350 return Err(Error {
1351 code: ErrorCode::InternalError,
1352 message: "Internal error.".into(),
1353 data: None,
1354 });
1355 };
1356 Ok(true)
1357 }
1358
1359 /// Implementation of account subscription for WebSocket clients.
1360 ///
1361 /// This method handles the complete lifecycle of account subscriptions:
1362 /// 1. Validates the provided public key string format
1363 /// 2. Parses the subscription configuration (commitment and encoding)
1364 /// 3. Generates a unique subscription ID and assigns it to the subscriber
1365 /// 4. Spawns an async task to continuously monitor account changes
1366 /// 5. Sends notifications whenever the account state changes
1367 ///
1368 /// # Monitoring Loop
1369 /// The spawned task runs a continuous loop that:
1370 /// - Checks if the subscription is still active (not unsubscribed)
1371 /// - Polls for account updates from the SVM
1372 /// - Sends notifications to the subscriber when changes occur
1373 /// - Automatically terminates when the subscription is removed
1374 ///
1375 /// # Error Handling
1376 /// - Rejects subscription with `InvalidParams` for malformed public keys
1377 /// - Handles encoding configuration for account data serialization
1378 /// - Manages subscription cleanup through the monitoring loop
1379 ///
1380 /// # Performance
1381 /// Uses efficient polling with minimal CPU overhead and automatic
1382 /// cleanup when subscriptions are no longer needed.
1383 fn account_subscribe(
1384 &self,
1385 meta: Self::Metadata,
1386 subscriber: Subscriber<RpcResponse<UiAccount>>,
1387 pubkey_str: String,
1388 config: Option<RpcAccountSubscribeConfig>,
1389 ) {
1390 let _ = meta
1391 .as_ref()
1392 .map(|m| m.log_debug("Websocket 'account_subscribe' connection established"));
1393
1394 let pubkey = match Pubkey::from_str(&pubkey_str) {
1395 Ok(pk) => pk,
1396 Err(_) => {
1397 let error = Error {
1398 code: ErrorCode::InvalidParams,
1399 message: "Invalid pubkey format.".into(),
1400 data: None,
1401 };
1402 if subscriber.reject(error.clone()).is_err() {
1403 log::error!("Failed to reject subscriber for invalid pubkey format.");
1404 }
1405 return;
1406 }
1407 };
1408
1409 let config = config.unwrap_or(RpcAccountSubscribeConfig {
1410 commitment: None,
1411 encoding: None,
1412 });
1413
1414 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1415 let sub_id = SubscriptionId::Number(id as u64);
1416 let sink = match subscriber.assign_id(sub_id.clone()) {
1417 Ok(sink) => sink,
1418 Err(e) => {
1419 log::error!("Failed to assign subscription ID: {:?}", e);
1420 return;
1421 }
1422 };
1423
1424 let account_active = Arc::clone(&self.account_subscription_map);
1425 let meta = meta.clone();
1426 let svm_locker = match meta.get_svm_locker() {
1427 Ok(locker) => locker,
1428 Err(e) => {
1429 log::error!("Failed to get SVM locker for account subscription: {e}");
1430 if let Err(e) = sink.notify(Err(e.into())) {
1431 log::error!(
1432 "Failed to send error notification to client for SVM locker failure: {e}"
1433 );
1434 }
1435 return;
1436 }
1437 };
1438 let slot = svm_locker.with_svm_reader(|svm| svm.get_latest_absolute_slot());
1439
1440 self.tokio_handle.spawn(async move {
1441 if let Ok(mut guard) = account_active.write() {
1442 guard.insert(sub_id.clone(), sink);
1443 } else {
1444 log::error!("Failed to acquire write lock on account_subscription_map");
1445 return;
1446 }
1447
1448 // subscribe to account updates
1449 let rx = svm_locker.subscribe_for_account_updates(&pubkey, config.encoding);
1450
1451 loop {
1452 // if the subscription has been removed, break the loop
1453 if let Ok(guard) = account_active.read() {
1454 if guard.get(&sub_id).is_none() {
1455 break;
1456 }
1457 } else {
1458 log::error!("Failed to acquire read lock on account_subscription_map");
1459 break;
1460 }
1461
1462 let Ok(ui_account) = rx.try_recv() else {
1463 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1464 continue;
1465 };
1466
1467 let Ok(guard) = account_active.read() else {
1468 log::error!("Failed to acquire read lock on account_subscription_map");
1469 break;
1470 };
1471
1472 let Some(sink) = guard.get(&sub_id) else {
1473 log::error!("Failed to get sink for subscription ID");
1474 break;
1475 };
1476
1477 if let Err(e) = sink.notify(Ok(RpcResponse {
1478 context: RpcResponseContext::new(slot),
1479 value: ui_account,
1480 })) {
1481 log::error!("Failed to notify client about account update: {e}");
1482 break;
1483 }
1484 }
1485 });
1486 }
1487
1488 /// Implementation of account unsubscription for WebSocket clients.
1489 ///
1490 /// This method removes an active account subscription from the internal
1491 /// tracking maps, effectively stopping further notifications for that subscription.
1492 /// The monitoring loop in the corresponding subscription task will detect this
1493 /// removal and automatically terminate.
1494 ///
1495 /// # Implementation Details
1496 /// - Attempts to remove the subscription from the account subscriptions map
1497 /// - Returns success if the subscription existed and was removed
1498 /// - Returns an error if the subscription ID was not found
1499 /// - The removal triggers automatic cleanup of the monitoring task
1500 ///
1501 /// # Thread Safety
1502 /// Uses write locks to ensure thread-safe removal from the subscription map.
1503 /// The monitoring task uses read locks to check subscription status, creating
1504 /// a clean synchronization pattern.
1505 fn account_unsubscribe(
1506 &self,
1507 _meta: Option<Self::Metadata>,
1508 subscription: SubscriptionId,
1509 ) -> Result<bool> {
1510 if let Ok(mut guard) = self.account_subscription_map.write() {
1511 guard.remove(&subscription)
1512 } else {
1513 log::error!("Failed to acquire write lock on account_subscription_map");
1514 return Err(Error {
1515 code: ErrorCode::InternalError,
1516 message: "Internal error.".into(),
1517 data: None,
1518 });
1519 };
1520 Ok(true)
1521 }
1522
1523 fn slot_subscribe(&self, meta: Self::Metadata, subscriber: Subscriber<SlotInfo>) {
1524 let _ = meta
1525 .as_ref()
1526 .map(|m| m.log_debug("Websocket 'slot_subscribe' connection established"));
1527
1528 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1529 let sub_id = SubscriptionId::Number(id as u64);
1530 let sink = match subscriber.assign_id(sub_id.clone()) {
1531 Ok(sink) => sink,
1532 Err(e) => {
1533 log::error!("Failed to assign subscription ID: {:?}", e);
1534 return;
1535 }
1536 };
1537
1538 let slot_active = Arc::clone(&self.slot_subscription_map);
1539 let meta = meta.clone();
1540
1541 let svm_locker = match meta.get_svm_locker() {
1542 Ok(locker) => locker,
1543 Err(e) => {
1544 log::error!("Failed to get SVM locker for slot subscription: {e}");
1545 if let Err(e) = sink.notify(Err(e.into())) {
1546 log::error!(
1547 "Failed to send error notification to client for SVM locker failure: {e}"
1548 );
1549 }
1550 return;
1551 }
1552 };
1553
1554 self.tokio_handle.spawn(async move {
1555 if let Ok(mut guard) = slot_active.write() {
1556 guard.insert(sub_id.clone(), sink);
1557 } else {
1558 log::error!("Failed to acquire write lock on slot_subscription_map");
1559 return;
1560 }
1561
1562 let rx = svm_locker.subscribe_for_slot_updates();
1563
1564 loop {
1565 // if the subscription has been removed, break the loop
1566 if let Ok(guard) = slot_active.read() {
1567 if guard.get(&sub_id).is_none() {
1568 break;
1569 }
1570 } else {
1571 log::error!("Failed to acquire read lock on slot_subscription_map");
1572 break;
1573 }
1574
1575 let Ok(slot_info) = rx.try_recv() else {
1576 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1577 continue;
1578 };
1579
1580 let Ok(guard) = slot_active.read() else {
1581 log::error!("Failed to acquire read lock on slots_subscription_map");
1582 break;
1583 };
1584
1585 let Some(sink) = guard.get(&sub_id) else {
1586 log::error!("Failed to get sink for subscription ID");
1587 break;
1588 };
1589
1590 if let Err(e) = sink.notify(Ok(slot_info)) {
1591 log::error!("Failed to notify client about slots update: {e}");
1592 break;
1593 }
1594 }
1595 });
1596 }
1597
1598 fn slot_unsubscribe(
1599 &self,
1600 _meta: Option<Self::Metadata>,
1601 subscription: SubscriptionId,
1602 ) -> Result<bool> {
1603 if let Ok(mut guard) = self.slot_subscription_map.write() {
1604 guard.remove(&subscription)
1605 } else {
1606 log::error!("Failed to acquire write lock on slot_subscription_map");
1607 return Err(Error {
1608 code: ErrorCode::InternalError,
1609 message: "Internal error.".into(),
1610 data: None,
1611 });
1612 };
1613 Ok(true)
1614 }
1615
1616 fn logs_subscribe(
1617 &self,
1618 meta: Self::Metadata,
1619 subscriber: Subscriber<RpcResponse<RpcLogsResponse>>,
1620 mentions: Option<RpcTransactionLogsFilter>,
1621 commitment: Option<CommitmentConfig>,
1622 ) {
1623 let _ = meta
1624 .as_ref()
1625 .map(|m| m.log_debug("Websocket 'logs_subscribe' connection established"));
1626
1627 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1628 let sub_id = SubscriptionId::Number(id as u64);
1629 let sink = match subscriber.assign_id(sub_id.clone()) {
1630 Ok(sink) => sink,
1631 Err(e) => {
1632 log::error!("Failed to assign subscription ID: {:?}", e);
1633 return;
1634 }
1635 };
1636
1637 let mentions = mentions.unwrap_or(RpcTransactionLogsFilter::All);
1638 let commitment = commitment.unwrap_or_default().commitment;
1639
1640 let logs_active = Arc::clone(&self.logs_subscription_map);
1641 let meta = meta.clone();
1642
1643 let svm_locker = match meta.get_svm_locker() {
1644 Ok(locker) => locker,
1645 Err(e) => {
1646 log::error!("Failed to get SVM locker for slot subscription: {e}");
1647 if let Err(e) = sink.notify(Err(e.into())) {
1648 log::error!(
1649 "Failed to send error notification to client for SVM locker failure: {e}"
1650 );
1651 }
1652 return;
1653 }
1654 };
1655
1656 self.tokio_handle.spawn(async move {
1657 if let Ok(mut guard) = logs_active.write() {
1658 guard.insert(sub_id.clone(), sink);
1659 } else {
1660 log::error!("Failed to acquire write lock on slot_subscription_map");
1661 return;
1662 }
1663
1664 let rx = svm_locker.subscribe_for_logs_updates(&commitment, &mentions);
1665
1666 loop {
1667 // if the subscription has been removed, break the loop
1668 if let Ok(guard) = logs_active.read() {
1669 if guard.get(&sub_id).is_none() {
1670 break;
1671 }
1672 } else {
1673 log::error!("Failed to acquire read lock on slot_subscription_map");
1674 break;
1675 }
1676
1677 let Ok((slot, value)) = rx.try_recv() else {
1678 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1679 continue;
1680 };
1681
1682 let Ok(guard) = logs_active.read() else {
1683 log::error!("Failed to acquire read lock on logs_subscription_map");
1684 break;
1685 };
1686
1687 let Some(sink) = guard.get(&sub_id) else {
1688 log::error!("Failed to get sink for subscription ID");
1689 break;
1690 };
1691
1692 if let Err(e) = sink.notify(Ok(RpcResponse {
1693 context: RpcResponseContext::new(slot),
1694 value,
1695 })) {
1696 log::error!("Failed to notify client about logs update: {e}");
1697 break;
1698 }
1699 }
1700 });
1701 }
1702
1703 fn logs_unsubscribe(
1704 &self,
1705 _meta: Option<Self::Metadata>,
1706 subscription: SubscriptionId,
1707 ) -> Result<bool> {
1708 if let Ok(mut guard) = self.logs_subscription_map.write() {
1709 guard.remove(&subscription);
1710 } else {
1711 log::error!("Failed to acquire write lock on logs_subscription_map");
1712 return Err(Error {
1713 code: ErrorCode::InternalError,
1714 message: "Internal error.".into(),
1715 data: None,
1716 });
1717 };
1718 Ok(true)
1719 }
1720
1721 fn root_subscribe(&self, meta: Self::Metadata, _subscriber: Subscriber<RpcResponse<()>>) {
1722 let _ = meta
1723 .as_ref()
1724 .map(|m| m.log_warn("Websocket method 'root_subscribe' is uninmplemented"));
1725 }
1726
1727 fn root_unsubscribe(
1728 &self,
1729 _meta: Option<Self::Metadata>,
1730 _subscription: SubscriptionId,
1731 ) -> Result<bool> {
1732 Ok(true)
1733 }
1734
1735 /// Implementation of program subscription for WebSocket clients.
1736 ///
1737 /// This method handles the complete lifecycle of program subscriptions:
1738 /// 1. Validates the provided program public key string format
1739 /// 2. Parses the subscription configuration (commitment, encoding, and filters)
1740 /// 3. Generates a unique subscription ID and assigns it to the subscriber
1741 /// 4. Spawns an async task to continuously monitor account changes for the program
1742 /// 5. Sends notifications whenever an account owned by the program changes and matches filters
1743 ///
1744 /// # Monitoring Loop
1745 /// The spawned task runs a continuous loop that:
1746 /// - Checks if the subscription is still active (not unsubscribed)
1747 /// - Polls for program account updates from the SVM
1748 /// - Applies configured filters (dataSize, memcmp) before notifying
1749 /// - Sends `RpcKeyedAccount` notifications (including account pubkey) to the subscriber
1750 /// - Automatically terminates when the subscription is removed
1751 ///
1752 /// # Error Handling
1753 /// - Rejects subscription with `InvalidParams` for malformed public keys
1754 /// - Handles encoding configuration for account data serialization
1755 /// - Manages subscription cleanup through the monitoring loop
1756 ///
1757 /// # Performance
1758 /// Uses efficient polling with minimal CPU overhead and automatic
1759 /// cleanup when subscriptions are no longer needed.
1760 fn program_subscribe(
1761 &self,
1762 meta: Self::Metadata,
1763 subscriber: Subscriber<RpcResponse<RpcKeyedAccount>>,
1764 pubkey_str: String,
1765 config: Option<RpcProgramSubscribeConfig>,
1766 ) {
1767 let _ = meta
1768 .as_ref()
1769 .map(|m| m.log_debug("Websocket 'program_subscribe' connection established"));
1770
1771 let program_id = match Pubkey::from_str(&pubkey_str) {
1772 Ok(pk) => pk,
1773 Err(_) => {
1774 let error = Error {
1775 code: ErrorCode::InvalidParams,
1776 message: "Invalid pubkey format.".into(),
1777 data: None,
1778 };
1779 if subscriber.reject(error.clone()).is_err() {
1780 log::error!("Failed to reject subscriber for invalid pubkey format.");
1781 }
1782 return;
1783 }
1784 };
1785
1786 let config = config.unwrap_or_default();
1787
1788 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1789 let sub_id = SubscriptionId::Number(id as u64);
1790 let sink = match subscriber.assign_id(sub_id.clone()) {
1791 Ok(sink) => sink,
1792 Err(e) => {
1793 log::error!("Failed to assign subscription ID: {:?}", e);
1794 return;
1795 }
1796 };
1797
1798 let program_active = Arc::clone(&self.program_subscription_map);
1799 let meta = meta.clone();
1800 let svm_locker = match meta.get_svm_locker() {
1801 Ok(locker) => locker,
1802 Err(e) => {
1803 log::error!("Failed to get SVM locker for program subscription: {e}");
1804 if let Err(e) = sink.notify(Err(e.into())) {
1805 log::error!(
1806 "Failed to send error notification to client for SVM locker failure: {e}"
1807 );
1808 }
1809 return;
1810 }
1811 };
1812 let slot = svm_locker.with_svm_reader(|svm| svm.get_latest_absolute_slot());
1813
1814 self.tokio_handle.spawn(async move {
1815 if let Ok(mut guard) = program_active.write() {
1816 guard.insert(sub_id.clone(), sink);
1817 } else {
1818 log::error!("Failed to acquire write lock on program_subscription_map");
1819 return;
1820 }
1821
1822 let rx = svm_locker.subscribe_for_program_updates(
1823 &program_id,
1824 config.encoding,
1825 config.filters,
1826 );
1827
1828 loop {
1829 // if the subscription has been removed, break the loop
1830 if let Ok(guard) = program_active.read() {
1831 if guard.get(&sub_id).is_none() {
1832 break;
1833 }
1834 } else {
1835 log::error!("Failed to acquire read lock on program_subscription_map");
1836 break;
1837 }
1838
1839 let Ok(keyed_account) = rx.try_recv() else {
1840 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1841 continue;
1842 };
1843
1844 let Ok(guard) = program_active.read() else {
1845 log::error!("Failed to acquire read lock on program_subscription_map");
1846 break;
1847 };
1848
1849 let Some(sink) = guard.get(&sub_id) else {
1850 log::error!("Failed to get sink for subscription ID");
1851 break;
1852 };
1853
1854 if let Err(e) = sink.notify(Ok(RpcResponse {
1855 context: RpcResponseContext::new(slot),
1856 value: keyed_account,
1857 })) {
1858 log::error!("Failed to notify client about program account update: {e}");
1859 break;
1860 }
1861 }
1862 });
1863 }
1864
1865 /// Implementation of program unsubscription for WebSocket clients.
1866 ///
1867 /// This method removes an active program subscription from the internal
1868 /// tracking maps, effectively stopping further notifications for that subscription.
1869 /// The monitoring loop in the corresponding subscription task will detect this
1870 /// removal and automatically terminate.
1871 ///
1872 /// # Implementation Details
1873 /// - Attempts to remove the subscription from the program subscriptions map
1874 /// - Returns success if the subscription existed and was removed
1875 /// - Returns an error if the lock could not be acquired
1876 /// - The removal triggers automatic cleanup of the monitoring task
1877 ///
1878 /// # Thread Safety
1879 /// Uses write locks to ensure thread-safe removal from the subscription map.
1880 /// The monitoring task uses read locks to check subscription status, creating
1881 /// a clean synchronization pattern.
1882 fn program_unsubscribe(
1883 &self,
1884 _meta: Option<Self::Metadata>,
1885 subscription: SubscriptionId,
1886 ) -> Result<bool> {
1887 if let Ok(mut guard) = self.program_subscription_map.write() {
1888 guard.remove(&subscription);
1889 } else {
1890 log::error!("Failed to acquire write lock on program_subscription_map");
1891 return Err(Error {
1892 code: ErrorCode::InternalError,
1893 message: "Internal error.".into(),
1894 data: None,
1895 });
1896 };
1897 Ok(true)
1898 }
1899
1900 fn slots_updates_subscribe(
1901 &self,
1902 meta: Self::Metadata,
1903 subscriber: Subscriber<Arc<SlotUpdate>>,
1904 ) {
1905 let _ = meta
1906 .as_ref()
1907 .map(|m| m.log_debug("Websocket 'slots_updates_subscribe' connection established"));
1908
1909 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
1910 let sub_id = SubscriptionId::Number(id as u64);
1911 let sink = match subscriber.assign_id(sub_id.clone()) {
1912 Ok(sink) => sink,
1913 Err(e) => {
1914 log::error!("Failed to assign subscription ID: {:?}", e);
1915 return;
1916 }
1917 };
1918
1919 let slots_updates_active = Arc::clone(&self.slots_updates_subscription_map);
1920 let meta = meta.clone();
1921
1922 let svm_locker = match meta.get_svm_locker() {
1923 Ok(locker) => locker,
1924 Err(e) => {
1925 log::error!("Failed to get SVM locker for slots updates subscription: {e}");
1926 if let Err(e) = sink.notify(Err(e.into())) {
1927 log::error!(
1928 "Failed to send error notification to client for SVM locker failure: {e}"
1929 );
1930 }
1931 return;
1932 }
1933 };
1934
1935 self.tokio_handle.spawn(async move {
1936 if let Ok(mut guard) = slots_updates_active.write() {
1937 guard.insert(sub_id.clone(), sink);
1938 } else {
1939 log::error!("Failed to acquire write lock on slots_updates_subscription_map");
1940 return;
1941 }
1942
1943 let rx = svm_locker.subscribe_for_slots_updates();
1944
1945 loop {
1946 // if the subscription has been removed, break the loop
1947 if let Ok(guard) = slots_updates_active.read() {
1948 if guard.get(&sub_id).is_none() {
1949 break;
1950 }
1951 } else {
1952 log::error!("Failed to acquire read lock on slots_updates_subscription_map");
1953 break;
1954 }
1955
1956 let Ok(slot_update) = rx.try_recv() else {
1957 tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
1958 continue;
1959 };
1960
1961 let Ok(guard) = slots_updates_active.read() else {
1962 log::error!("Failed to acquire read lock on slots_updates_subscription_map");
1963 break;
1964 };
1965
1966 let Some(sink) = guard.get(&sub_id) else {
1967 // The subscription was removed by `slots_updates_unsubscribe`
1968 // between the loop-top check and re-acquiring the read lock
1969 // here. This is the normal clean-exit path, not an error.
1970 break;
1971 };
1972
1973 if let Err(e) = sink.notify(Ok(slot_update)) {
1974 log::error!("Failed to notify client about slots update event: {e}");
1975 break;
1976 }
1977 }
1978 });
1979 }
1980
1981 fn slots_updates_unsubscribe(
1982 &self,
1983 _meta: Option<Self::Metadata>,
1984 subscription: SubscriptionId,
1985 ) -> Result<bool> {
1986 if let Ok(mut guard) = self.slots_updates_subscription_map.write() {
1987 guard.remove(&subscription);
1988 } else {
1989 log::error!("Failed to acquire write lock on slots_updates_subscription_map");
1990 return Err(Error {
1991 code: ErrorCode::InternalError,
1992 message: "Internal error.".into(),
1993 data: None,
1994 });
1995 };
1996 Ok(true)
1997 }
1998
1999 fn block_subscribe(&self, meta: Self::Metadata, _subscriber: Subscriber<RpcResponse<()>>) {
2000 let _ = meta
2001 .as_ref()
2002 .map(|m| m.log_warn("Websocket method 'block_subscribe' is uninmplemented"));
2003 }
2004
2005 fn block_unsubscribe(
2006 &self,
2007 _meta: Option<Self::Metadata>,
2008 _subscription: SubscriptionId,
2009 ) -> Result<bool> {
2010 Ok(true)
2011 }
2012
2013 fn vote_subscribe(&self, meta: Self::Metadata, _subscriber: Subscriber<RpcResponse<()>>) {
2014 let _ = meta
2015 .as_ref()
2016 .map(|m| m.log_warn("Websocket method 'vote_subscribe' is uninmplemented"));
2017 }
2018
2019 fn vote_unsubscribe(
2020 &self,
2021 _meta: Option<Self::Metadata>,
2022 _subscription: SubscriptionId,
2023 ) -> Result<bool> {
2024 Ok(true)
2025 }
2026
2027 fn snapshot_subscribe(
2028 &self,
2029 meta: Self::Metadata,
2030 subscriber: Subscriber<crate::surfnet::SnapshotImportNotification>,
2031 snapshot_url: String,
2032 ) {
2033 let _ = meta
2034 .as_ref()
2035 .map(|m| m.log_debug("Websocket 'snapshot_subscribe' connection established"));
2036
2037 // Validate snapshot URL format
2038 if snapshot_url.is_empty() {
2039 let error = Error {
2040 code: ErrorCode::InvalidParams,
2041 message: "Invalid snapshot URL: URL cannot be empty.".into(),
2042 data: None,
2043 };
2044 if let Err(e) = subscriber.reject(error.clone()) {
2045 log::error!("Failed to reject subscriber: {:?}", e);
2046 }
2047 return;
2048 }
2049
2050 let id = self.uid.fetch_add(1, atomic::Ordering::SeqCst);
2051 let sub_id = SubscriptionId::Number(id as u64);
2052 let sink = match subscriber.assign_id(sub_id.clone()) {
2053 Ok(sink) => sink,
2054 Err(e) => {
2055 log::error!("Failed to assign subscription ID: {:?}", e);
2056 return;
2057 }
2058 };
2059
2060 let snapshot_active = Arc::clone(&self.snapshot_subscription_map);
2061 let meta = meta.clone();
2062
2063 let svm_locker = match meta.get_svm_locker() {
2064 Ok(locker) => locker,
2065 Err(e) => {
2066 log::error!("Failed to get SVM locker for snapshot subscription: {e}");
2067 if let Err(e) = sink.notify(Err(e.into())) {
2068 log::error!(
2069 "Failed to send error notification to client for SVM locker failure: {e}"
2070 );
2071 }
2072 return;
2073 }
2074 };
2075
2076 self.tokio_handle.spawn(async move {
2077 if let Ok(mut guard) = snapshot_active.write() {
2078 guard.insert(sub_id.clone(), sink);
2079 } else {
2080 log::error!("Failed to acquire write lock on snapshot_subscription_map");
2081 return;
2082 }
2083
2084 // Generate a unique snapshot ID for this import operation
2085 let snapshot_id = format!(
2086 "snapshot_{}",
2087 chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0)
2088 );
2089
2090 // Subscribe to snapshot import updates
2091 // The locker will send the Started notification through the channel
2092 let rx = svm_locker.subscribe_for_snapshot_import_updates(&snapshot_url, &snapshot_id);
2093
2094 loop {
2095 // if the subscription has been removed, break the loop
2096 if let Ok(guard) = snapshot_active.read() {
2097 if guard.get(&sub_id).is_none() {
2098 break;
2099 }
2100 } else {
2101 log::error!("Failed to acquire read lock on snapshot_subscription_map");
2102 break;
2103 }
2104
2105 let Ok(notification) = rx.try_recv() else {
2106 tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
2107 continue;
2108 };
2109
2110 let Ok(guard) = snapshot_active.read() else {
2111 log::error!("Failed to acquire read lock on snapshot_subscription_map");
2112 break;
2113 };
2114
2115 let Some(sink) = guard.get(&sub_id) else {
2116 log::error!("Failed to get sink for subscription ID");
2117 break;
2118 };
2119
2120 if let Err(e) = sink.notify(Ok(notification)) {
2121 log::error!("Failed to notify client about snapshot import update: {e}");
2122 break;
2123 }
2124
2125 // If the import is completed or failed, we can end the subscription
2126 if let Ok(guard) = snapshot_active.read() {
2127 if let Some(_sink) = guard.get(&sub_id) {
2128 // Check if this was the final notification
2129 // We'll determine this by checking the status in the last notification
2130 // For now, we'll keep the subscription alive in case of multiple imports
2131 }
2132 }
2133 }
2134 });
2135 }
2136
2137 fn snapshot_unsubscribe(
2138 &self,
2139 _meta: Option<Self::Metadata>,
2140 subscription: SubscriptionId,
2141 ) -> Result<bool> {
2142 if let Ok(mut guard) = self.snapshot_subscription_map.write() {
2143 guard.remove(&subscription);
2144 } else {
2145 log::error!("Failed to acquire write lock on snapshot_subscription_map");
2146 return Err(Error {
2147 code: ErrorCode::InternalError,
2148 message: "Internal error.".into(),
2149 data: None,
2150 });
2151 };
2152 Ok(true)
2153 }
2154}