1use std::{future::Future, time::Duration};
2
3use futures::{SinkExt, StreamExt};
4use serde::Deserialize;
5use simulator_api::EncodedBinary;
6use solana_client::{
7 nonblocking::pubsub_client::PubsubClient,
8 rpc_response::{Response, RpcLogsResponse},
9};
10use solana_commitment_config::CommitmentConfig;
11use solana_rpc_client_api::config::{RpcTransactionLogsConfig, RpcTransactionLogsFilter};
12use thiserror::Error;
13use tokio::{
14 sync::{oneshot, watch},
15 task::JoinHandle,
16};
17use tokio_tungstenite::tungstenite::Message;
18
19use crate::urls::{UrlError, http_to_ws_url};
20
21#[derive(Debug, Error)]
23pub enum SubscriptionError {
24 #[error(transparent)]
25 InvalidUrl(#[from] UrlError),
26
27 #[error("pubsub connect to {url} failed: {source}")]
28 Connect {
29 url: String,
30 #[source]
31 source: Box<dyn std::error::Error + Send + Sync>,
32 },
33
34 #[error("logs_subscribe failed: {source}")]
35 Subscribe {
36 #[source]
37 source: Box<dyn std::error::Error + Send + Sync>,
38 },
39
40 #[error("subscription task exited unexpectedly before signaling ready")]
41 TaskDropped,
42
43 #[error("session has no rpc_endpoint (was the session created?)")]
44 NoRpcEndpoint,
45}
46
47#[derive(Debug, Error)]
48pub enum SubscriptionRuntimeError {
49 #[error("{kind} subscription for {target} closed unexpectedly")]
50 Closed { kind: &'static str, target: String },
51
52 #[error("{kind} subscription callback worker for {target} failed: {source}")]
53 CallbackWorker {
54 kind: &'static str,
55 target: String,
56 #[source]
57 source: tokio::task::JoinError,
58 },
59}
60
61const SUBSCRIPTION_DRAIN_IDLE_TIMEOUT: Duration = Duration::from_millis(250);
62const SUBSCRIPTION_DRAIN_MAX_DURATION: Duration = Duration::from_secs(5);
63
64type SubscriptionTaskHandle = JoinHandle<Result<(), SubscriptionRuntimeError>>;
65type AccountDiffWs =
66 tokio_tungstenite::WebSocketStream<tokio_tungstenite::MaybeTlsStream<tokio::net::TcpStream>>;
67
68pub struct SubscriptionHandle {
70 pub join_handle: SubscriptionTaskHandle,
71 pub stop: watch::Sender<bool>,
72}
73
74impl From<LogSubscriptionHandle> for SubscriptionHandle {
75 fn from(h: LogSubscriptionHandle) -> Self {
76 Self {
77 join_handle: h.join_handle,
78 stop: h.stop,
79 }
80 }
81}
82
83impl From<AccountDiffSubscriptionHandle> for SubscriptionHandle {
84 fn from(h: AccountDiffSubscriptionHandle) -> Self {
85 Self {
86 join_handle: h.join_handle,
87 stop: h.stop,
88 }
89 }
90}
91
92impl From<ActionSubscriptionHandle> for SubscriptionHandle {
93 fn from(h: ActionSubscriptionHandle) -> Self {
94 Self {
95 join_handle: h.join_handle,
96 stop: h.stop,
97 }
98 }
99}
100
101pub struct LogSubscriptionHandle {
103 pub join_handle: SubscriptionTaskHandle,
108
109 pub stop: watch::Sender<bool>,
112}
113
114pub async fn subscribe_program_logs<F, Fut>(
152 rpc_endpoint: &str,
153 program_id: &str,
154 commitment: CommitmentConfig,
155 on_notification: F,
156) -> Result<LogSubscriptionHandle, SubscriptionError>
157where
158 F: Fn(Response<RpcLogsResponse>) -> Fut + Send + Sync + 'static,
159 Fut: Future<Output = ()> + Send + 'static,
160{
161 let ws_url = http_to_ws_url(rpc_endpoint)?;
162 let program_id = program_id.to_string();
163
164 let (ready_tx, ready_rx) = oneshot::channel::<Result<(), SubscriptionError>>();
165 let (stop_tx, mut stop_rx) = watch::channel(false);
166
167 let join_handle = tokio::spawn(async move {
171 let client = match PubsubClient::new(&ws_url).await {
172 Ok(c) => c,
173 Err(e) => {
174 let _ = ready_tx.send(Err(SubscriptionError::Connect {
175 url: ws_url,
176 source: Box::new(e),
177 }));
178 return Ok(());
179 }
180 };
181
182 let (mut stream, _unsubscribe) = match client
183 .logs_subscribe(
184 RpcTransactionLogsFilter::Mentions(vec![program_id.clone()]),
185 RpcTransactionLogsConfig {
186 commitment: Some(commitment),
187 },
188 )
189 .await
190 {
191 Ok(s) => s,
192 Err(e) => {
193 let _ = ready_tx.send(Err(SubscriptionError::Subscribe {
194 source: Box::new(e),
195 }));
196 return Ok(());
197 }
198 };
199
200 let _ = ready_tx.send(Ok(()));
201
202 let mut tasks: Vec<JoinHandle<()>> = Vec::new();
203 let kind = "program logs";
204
205 loop {
206 if *stop_rx.borrow() {
207 let drain_deadline = tokio::time::Instant::now() + SUBSCRIPTION_DRAIN_MAX_DURATION;
208 while let Ok(Ok(Some(notification))) = tokio::time::timeout_at(
209 drain_deadline,
210 tokio::time::timeout(SUBSCRIPTION_DRAIN_IDLE_TIMEOUT, stream.next()),
211 )
212 .await
213 {
214 tasks.push(tokio::spawn(on_notification(notification)));
215 }
216 break;
217 }
218
219 let notification = tokio::select! {
220 n = stream.next() => n,
221 _ = stop_rx.changed() => continue,
222 };
223
224 match notification {
225 Some(n) => tasks.push(tokio::spawn(on_notification(n))),
226 None => return Err(subscription_runtime_closed(kind, &program_id)),
227 }
228 }
229
230 for task in tasks {
232 if let Err(source) = task.await {
233 return Err(callback_worker_failed(kind, &program_id, source));
234 }
235 }
236
237 Ok(())
238 });
239
240 match ready_rx.await {
241 Ok(Ok(())) => Ok(LogSubscriptionHandle {
242 join_handle,
243 stop: stop_tx,
244 }),
245 Ok(Err(e)) => {
246 join_handle.abort();
247 Err(e)
248 }
249 Err(_) => {
250 join_handle.abort();
251 Err(SubscriptionError::TaskDropped)
252 }
253 }
254}
255
256#[derive(Debug, Clone, Deserialize)]
260pub struct AccountDiffContext {
261 pub slot: u64,
262}
263
264#[derive(Debug, Clone, Deserialize)]
266pub struct AccountDiffNotification {
267 pub context: AccountDiffContext,
268 pub account: Option<String>,
270 pub signature: Option<String>,
272 #[serde(default)]
274 pub tx_index: Option<u32>,
275 #[serde(default)]
277 pub block_time: Option<i64>,
278 pub pre: Option<serde_json::Value>,
280 pub post: Option<serde_json::Value>,
282}
283
284#[derive(Debug, Clone, Deserialize)]
285pub struct ActionResultContext {
286 pub slot: u64,
287}
288
289#[derive(Debug, Clone, Deserialize)]
291#[serde(rename_all = "camelCase")]
292pub struct ActionResultNotification {
293 pub context: ActionResultContext,
294 pub slot: u64,
295 #[serde(default)]
297 pub batch_index: Option<u32>,
298 pub action_index: u32,
300 #[serde(default)]
301 pub label: Option<String>,
302 pub committed: bool,
303 #[serde(default)]
306 pub transaction_outcomes: Vec<ActionTransactionOutcome>,
307 #[serde(default)]
310 pub accounts: Vec<Option<serde_json::Value>>,
311 #[serde(default)]
315 pub matched: Option<EncodedBinary>,
316}
317
318#[derive(Debug, Clone, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct ActionTransactionOutcome {
322 #[serde(default)]
324 pub err: Option<String>,
325 #[serde(default)]
326 pub logs: Vec<String>,
327 pub units_consumed: u64,
328 #[serde(default)]
329 pub fee: Option<u64>,
330 #[serde(default)]
332 pub return_data: Option<serde_json::Value>,
333}
334
335#[derive(Debug, Clone)]
337pub struct RoutedAccountDiffNotification {
338 pub account: String,
339 pub notification: AccountDiffNotification,
340}
341
342pub struct AccountDiffSubscriptionHandle {
346 pub join_handle: SubscriptionTaskHandle,
347 pub stop: watch::Sender<bool>,
348}
349
350fn subscription_runtime_closed(
351 kind: &'static str,
352 target: impl Into<String>,
353) -> SubscriptionRuntimeError {
354 SubscriptionRuntimeError::Closed {
355 kind,
356 target: target.into(),
357 }
358}
359
360fn callback_worker_failed(
361 kind: &'static str,
362 target: impl Into<String>,
363 source: tokio::task::JoinError,
364) -> SubscriptionRuntimeError {
365 SubscriptionRuntimeError::CallbackWorker {
366 kind,
367 target: target.into(),
368 source,
369 }
370}
371
372pub async fn subscribe_account_diffs<F, Fut>(
402 rpc_endpoint: &str,
403 account: &str,
404 on_notification: F,
405) -> Result<AccountDiffSubscriptionHandle, SubscriptionError>
406where
407 F: Fn(AccountDiffNotification) -> Fut + Send + Sync + 'static,
408 Fut: Future<Output = ()> + Send + 'static,
409{
410 subscribe_account_diffs_many(rpc_endpoint, [account.to_string()], move |notification| {
411 on_notification(notification.notification)
412 })
413 .await
414}
415
416pub async fn subscribe_account_diffs_many<F, Fut, I, S>(
422 rpc_endpoint: &str,
423 accounts: I,
424 on_notification: F,
425) -> Result<AccountDiffSubscriptionHandle, SubscriptionError>
426where
427 F: Fn(RoutedAccountDiffNotification) -> Fut + Send + Sync + 'static,
428 Fut: Future<Output = ()> + Send + 'static,
429 I: IntoIterator<Item = S>,
430 S: Into<String>,
431{
432 let ws_url = http_to_ws_url(rpc_endpoint)?;
433 let accounts = dedup_accounts(accounts);
434 if accounts.is_empty() {
435 let (stop_tx, stop_rx) = watch::channel(false);
436 return Ok(AccountDiffSubscriptionHandle {
437 join_handle: tokio::spawn(async move {
438 let _ = stop_rx;
439 Ok(())
440 }),
441 stop: stop_tx,
442 });
443 }
444
445 let (ready_tx, ready_rx) = oneshot::channel::<Result<(), SubscriptionError>>();
446 let (stop_tx, mut stop_rx) = watch::channel(false);
447 let target = format!("{} accounts", accounts.len());
448
449 let join_handle = tokio::spawn(async move {
450 let (notification_tx, mut notification_rx) = tokio::sync::mpsc::unbounded_channel();
451 let callback_handle = tokio::spawn(async move {
452 while let Some(notification) = notification_rx.recv().await {
453 on_notification(notification).await;
454 }
455 });
456
457 let (mut ws, _) = match tokio_tungstenite::connect_async(&ws_url).await {
458 Ok(connection) => connection,
459 Err(e) => {
460 let _ = ready_tx.send(Err(SubscriptionError::Connect {
461 url: ws_url,
462 source: Box::new(e),
463 }));
464 return Ok(());
465 }
466 };
467
468 let subscriptions =
469 match send_account_diff_subscribe_many(&mut ws, &accounts, ¬ification_tx).await {
470 Ok(subscriptions) => subscriptions,
471 Err(error) => {
472 let _ = ready_tx.send(Err(error));
473 return Ok(());
474 }
475 };
476
477 let _ = ready_tx.send(Ok(()));
478
479 if let Err(error) =
480 drive_account_diff_stream_many(&mut ws, &subscriptions, ¬ification_tx, &mut stop_rx)
481 .await
482 {
483 drop(notification_tx);
484 if let Err(source) = callback_handle.await {
485 return Err(callback_worker_failed("account diff", target, source));
486 }
487 return Err(error);
488 }
489
490 drop(notification_tx);
491 if let Err(source) = callback_handle.await {
492 return Err(callback_worker_failed("account diff", target, source));
493 }
494
495 Ok(())
496 });
497
498 match ready_rx.await {
499 Ok(Ok(())) => Ok(AccountDiffSubscriptionHandle {
500 join_handle,
501 stop: stop_tx,
502 }),
503 Ok(Err(e)) => {
504 join_handle.abort();
505 Err(e)
506 }
507 Err(_) => {
508 join_handle.abort();
509 Err(SubscriptionError::TaskDropped)
510 }
511 }
512}
513
514#[derive(Deserialize)]
515struct AccountDiffMessage {
516 method: String,
517 params: AccountDiffParams,
518}
519
520#[derive(Deserialize)]
521struct AccountDiffParams {
522 subscription: u64,
523 result: AccountDiffNotification,
524}
525
526async fn send_account_diff_subscribe_many(
527 ws: &mut AccountDiffWs,
528 accounts: &[String],
529 notification_tx: &tokio::sync::mpsc::UnboundedSender<RoutedAccountDiffNotification>,
530) -> Result<std::collections::HashMap<u64, String>, SubscriptionError> {
531 #[derive(Deserialize)]
532 struct SubscriptionConfirmation {
533 id: u64,
534 result: Option<u64>,
535 }
536
537 let mut pending: std::collections::HashMap<u64, String> = std::collections::HashMap::new();
538 let mut subscriptions = std::collections::HashMap::with_capacity(accounts.len());
539
540 for (index, account) in accounts.iter().enumerate() {
541 let request_id = (index + 1) as u64;
542 let req = serde_json::json!({
543 "jsonrpc": "2.0",
544 "id": request_id,
545 "method": "accountDiffSubscribe",
546 "params": [account]
547 });
548 ws.send(Message::Text(req.to_string()))
549 .await
550 .map_err(|source| SubscriptionError::Subscribe {
551 source: Box::new(source),
552 })?;
553 pending.insert(request_id, account.clone());
554 }
555
556 while !pending.is_empty() {
557 match ws.next().await {
558 Some(Ok(Message::Text(text))) => {
559 if let Ok(confirmation) = serde_json::from_str::<SubscriptionConfirmation>(&text) {
560 let Some(account) = pending.remove(&confirmation.id) else {
561 continue;
562 };
563 let Some(subscription_id) = confirmation.result else {
564 return Err(SubscriptionError::TaskDropped);
565 };
566 subscriptions.insert(subscription_id, account);
567 continue;
568 }
569
570 if let Some(notification) =
571 parse_routed_account_diff_notification(&text, &subscriptions)
572 {
573 let _ = notification_tx.send(notification);
574 }
575 }
576 Some(Ok(_)) => {}
577 _ => return Err(SubscriptionError::TaskDropped),
578 }
579 }
580
581 Ok(subscriptions)
582}
583
584async fn drive_account_diff_stream_many(
585 ws: &mut AccountDiffWs,
586 subscriptions: &std::collections::HashMap<u64, String>,
587 notification_tx: &tokio::sync::mpsc::UnboundedSender<RoutedAccountDiffNotification>,
588 stop_rx: &mut watch::Receiver<bool>,
589) -> Result<(), SubscriptionRuntimeError> {
590 loop {
591 if *stop_rx.borrow() {
592 let drain_deadline = tokio::time::Instant::now() + SUBSCRIPTION_DRAIN_MAX_DURATION;
593 loop {
594 match tokio::time::timeout_at(
595 drain_deadline,
596 tokio::time::timeout(SUBSCRIPTION_DRAIN_IDLE_TIMEOUT, ws.next()),
597 )
598 .await
599 {
600 Ok(Ok(Some(Ok(Message::Text(text))))) => {
601 if let Some(notification) =
602 parse_routed_account_diff_notification(&text, subscriptions)
603 {
604 let _ = notification_tx.send(notification);
605 }
606 }
607 _ => return Ok(()),
608 }
609 }
610 }
611
612 let msg = tokio::select! {
613 m = ws.next() => m,
614 _ = stop_rx.changed() => continue,
615 };
616
617 match msg {
618 Some(Ok(Message::Text(text))) => {
619 if let Some(notification) =
620 parse_routed_account_diff_notification(&text, subscriptions)
621 {
622 let _ = notification_tx.send(notification);
623 }
624 }
625 Some(Ok(_)) => {}
626 _ => {
627 return Err(subscription_runtime_closed(
628 "account diff",
629 format!("{} accounts", subscriptions.len()),
630 ));
631 }
632 }
633 }
634}
635
636fn parse_account_diff_message(text: &str) -> Option<AccountDiffMessage> {
637 let msg: AccountDiffMessage = serde_json::from_str(text).ok()?;
638 (msg.method == "accountDiffNotification").then_some(msg)
639}
640
641fn parse_routed_account_diff_notification(
642 text: &str,
643 subscriptions: &std::collections::HashMap<u64, String>,
644) -> Option<RoutedAccountDiffNotification> {
645 let msg = parse_account_diff_message(text)?;
646 let account = subscriptions.get(&msg.params.subscription)?.clone();
647 Some(RoutedAccountDiffNotification {
648 account,
649 notification: msg.params.result,
650 })
651}
652
653fn dedup_accounts<I, S>(accounts: I) -> Vec<String>
654where
655 I: IntoIterator<Item = S>,
656 S: Into<String>,
657{
658 let mut unique = std::collections::BTreeSet::new();
659 accounts
660 .into_iter()
661 .map(Into::into)
662 .filter(|account| unique.insert(account.clone()))
663 .collect()
664}
665
666pub async fn subscribe_program_diffs<F, Fut>(
699 rpc_endpoint: &str,
700 program_id: &str,
701 on_notification: F,
702) -> Result<AccountDiffSubscriptionHandle, SubscriptionError>
703where
704 F: Fn(AccountDiffNotification) -> Fut + Send + Sync + 'static,
705 Fut: Future<Output = ()> + Send + 'static,
706{
707 let ws_url = http_to_ws_url(rpc_endpoint)?;
708 let program_id = program_id.to_string();
709
710 let (ready_tx, ready_rx) = oneshot::channel::<Result<(), SubscriptionError>>();
711 let (stop_tx, mut stop_rx) = watch::channel(false);
712
713 let join_handle = tokio::spawn(async move {
714 let (notification_tx, mut notification_rx) = tokio::sync::mpsc::unbounded_channel();
715 let callback_handle = tokio::spawn(async move {
716 while let Some(notification) = notification_rx.recv().await {
717 on_notification(notification).await;
718 }
719 });
720
721 let (mut ws, _) = match tokio_tungstenite::connect_async(&ws_url).await {
722 Ok(connection) => connection,
723 Err(e) => {
724 let _ = ready_tx.send(Err(SubscriptionError::Connect {
725 url: ws_url,
726 source: Box::new(e),
727 }));
728 return Ok(());
729 }
730 };
731
732 if let Err(error) = send_program_diff_subscribe(&mut ws, &program_id).await {
733 let _ = ready_tx.send(Err(error));
734 return Ok(());
735 }
736
737 let _ = ready_tx.send(Ok(()));
738
739 if let Err(error) =
740 drive_program_diff_stream(&mut ws, ¬ification_tx, &mut stop_rx, &program_id).await
741 {
742 drop(notification_tx);
743 if let Err(source) = callback_handle.await {
744 return Err(callback_worker_failed(
745 "program account diff",
746 &program_id,
747 source,
748 ));
749 }
750 return Err(error);
751 }
752
753 drop(notification_tx);
754 if let Err(source) = callback_handle.await {
755 return Err(callback_worker_failed(
756 "program account diff",
757 &program_id,
758 source,
759 ));
760 }
761
762 Ok(())
763 });
764
765 match ready_rx.await {
766 Ok(Ok(())) => Ok(AccountDiffSubscriptionHandle {
767 join_handle,
768 stop: stop_tx,
769 }),
770 Ok(Err(e)) => {
771 join_handle.abort();
772 Err(e)
773 }
774 Err(_) => {
775 join_handle.abort();
776 Err(SubscriptionError::TaskDropped)
777 }
778 }
779}
780
781async fn send_program_diff_subscribe(
782 ws: &mut AccountDiffWs,
783 program_id: &str,
784) -> Result<(), SubscriptionError> {
785 #[derive(Deserialize)]
786 struct SubscriptionConfirmation {
787 result: Option<u64>,
788 }
789
790 let req = serde_json::json!({
791 "jsonrpc": "2.0",
792 "id": 1,
793 "method": "accountDiffSubscribe",
794 "params": [program_id, {"address_type": "program"}]
795 });
796 ws.send(Message::Text(req.to_string()))
797 .await
798 .map_err(|source| SubscriptionError::Subscribe {
799 source: Box::new(source),
800 })?;
801
802 loop {
803 match ws.next().await {
804 Some(Ok(Message::Text(text))) => {
805 match serde_json::from_str::<SubscriptionConfirmation>(&text) {
806 Ok(SubscriptionConfirmation { result: Some(_) }) => return Ok(()),
807 Ok(_) => continue,
808 Err(source) => {
809 return Err(SubscriptionError::Subscribe {
810 source: Box::new(source),
811 });
812 }
813 }
814 }
815 Some(Ok(_)) => continue,
816 _ => return Err(SubscriptionError::TaskDropped),
817 }
818 }
819}
820
821async fn drive_program_diff_stream(
822 ws: &mut AccountDiffWs,
823 notification_tx: &tokio::sync::mpsc::UnboundedSender<AccountDiffNotification>,
824 stop_rx: &mut watch::Receiver<bool>,
825 program_id: &str,
826) -> Result<(), SubscriptionRuntimeError> {
827 loop {
828 if *stop_rx.borrow() {
829 let drain_deadline = tokio::time::Instant::now() + SUBSCRIPTION_DRAIN_MAX_DURATION;
830 loop {
831 match tokio::time::timeout_at(
832 drain_deadline,
833 tokio::time::timeout(SUBSCRIPTION_DRAIN_IDLE_TIMEOUT, ws.next()),
834 )
835 .await
836 {
837 Ok(Ok(Some(Ok(Message::Text(text))))) => {
838 if let Some(msg) = parse_account_diff_message(&text) {
839 let _ = notification_tx.send(msg.params.result);
840 }
841 }
842 _ => return Ok(()),
843 }
844 }
845 }
846
847 let msg = tokio::select! {
848 m = ws.next() => m,
849 _ = stop_rx.changed() => continue,
850 };
851
852 match msg {
853 Some(Ok(Message::Text(text))) => {
854 if let Some(msg) = parse_account_diff_message(&text) {
855 let _ = notification_tx.send(msg.params.result);
856 }
857 }
858 Some(Ok(_)) => {}
859 _ => {
860 return Err(subscription_runtime_closed(
861 "program account diff",
862 program_id,
863 ));
864 }
865 }
866 }
867}
868
869pub struct ActionSubscriptionHandle {
875 pub join_handle: SubscriptionTaskHandle,
876 pub stop: watch::Sender<bool>,
877}
878
879#[derive(Deserialize)]
880struct ActionMessage {
881 method: String,
882 params: ActionParams,
883}
884
885#[derive(Deserialize)]
886struct ActionParams {
887 #[allow(dead_code)]
888 subscription: u64,
889 result: ActionResultNotification,
890}
891
892fn parse_action_message(text: &str) -> Option<ActionResultNotification> {
893 let msg: ActionMessage = serde_json::from_str(text).ok()?;
894 (msg.method == "actionNotification").then_some(msg.params.result)
895}
896
897pub async fn subscribe_actions<F, Fut>(
927 rpc_endpoint: &str,
928 on_notification: F,
929) -> Result<ActionSubscriptionHandle, SubscriptionError>
930where
931 F: Fn(ActionResultNotification) -> Fut + Send + Sync + 'static,
932 Fut: Future<Output = ()> + Send + 'static,
933{
934 let ws_url = http_to_ws_url(rpc_endpoint)?;
935
936 let (ready_tx, ready_rx) = oneshot::channel::<Result<(), SubscriptionError>>();
937 let (stop_tx, mut stop_rx) = watch::channel(false);
938
939 let join_handle = tokio::spawn(async move {
940 let (notification_tx, mut notification_rx) = tokio::sync::mpsc::unbounded_channel();
941 let callback_handle = tokio::spawn(async move {
942 while let Some(notification) = notification_rx.recv().await {
943 on_notification(notification).await;
944 }
945 });
946
947 let (mut ws, _) = match tokio_tungstenite::connect_async(&ws_url).await {
948 Ok(connection) => connection,
949 Err(e) => {
950 let _ = ready_tx.send(Err(SubscriptionError::Connect {
951 url: ws_url,
952 source: Box::new(e),
953 }));
954 return Ok(());
955 }
956 };
957
958 if let Err(error) = send_action_subscribe(&mut ws).await {
959 let _ = ready_tx.send(Err(error));
960 return Ok(());
961 }
962
963 let _ = ready_tx.send(Ok(()));
964
965 if let Err(error) = drive_action_stream(&mut ws, ¬ification_tx, &mut stop_rx).await {
966 drop(notification_tx);
967 if let Err(source) = callback_handle.await {
968 return Err(callback_worker_failed("action", "actions", source));
969 }
970 return Err(error);
971 }
972
973 drop(notification_tx);
974 if let Err(source) = callback_handle.await {
975 return Err(callback_worker_failed("action", "actions", source));
976 }
977
978 Ok(())
979 });
980
981 match ready_rx.await {
982 Ok(Ok(())) => Ok(ActionSubscriptionHandle {
983 join_handle,
984 stop: stop_tx,
985 }),
986 Ok(Err(e)) => {
987 join_handle.abort();
988 Err(e)
989 }
990 Err(_) => {
991 join_handle.abort();
992 Err(SubscriptionError::TaskDropped)
993 }
994 }
995}
996
997async fn send_action_subscribe(ws: &mut AccountDiffWs) -> Result<(), SubscriptionError> {
998 #[derive(Deserialize)]
999 struct SubscriptionConfirmation {
1000 result: Option<u64>,
1001 }
1002
1003 let req = serde_json::json!({
1005 "jsonrpc": "2.0",
1006 "id": 1,
1007 "method": "actionSubscribe",
1008 "params": [serde_json::Value::Null, {}]
1009 });
1010
1011 ws.send(Message::Text(req.to_string()))
1012 .await
1013 .map_err(|source| SubscriptionError::Subscribe {
1014 source: Box::new(source),
1015 })?;
1016
1017 loop {
1018 match ws.next().await {
1019 Some(Ok(Message::Text(text))) => {
1020 match serde_json::from_str::<SubscriptionConfirmation>(&text) {
1021 Ok(SubscriptionConfirmation { result: Some(_) }) => return Ok(()),
1022 Ok(_) => continue,
1023 Err(source) => {
1024 return Err(SubscriptionError::Subscribe {
1025 source: Box::new(source),
1026 });
1027 }
1028 }
1029 }
1030 Some(Ok(_)) => continue,
1031 _ => return Err(SubscriptionError::TaskDropped),
1032 }
1033 }
1034}
1035
1036async fn drive_action_stream(
1037 ws: &mut AccountDiffWs,
1038 notification_tx: &tokio::sync::mpsc::UnboundedSender<ActionResultNotification>,
1039 stop_rx: &mut watch::Receiver<bool>,
1040) -> Result<(), SubscriptionRuntimeError> {
1041 loop {
1042 if *stop_rx.borrow() {
1043 let drain_deadline = tokio::time::Instant::now() + SUBSCRIPTION_DRAIN_MAX_DURATION;
1044 loop {
1045 match tokio::time::timeout_at(
1046 drain_deadline,
1047 tokio::time::timeout(SUBSCRIPTION_DRAIN_IDLE_TIMEOUT, ws.next()),
1048 )
1049 .await
1050 {
1051 Ok(Ok(Some(Ok(Message::Text(text))))) => {
1052 if let Some(result) = parse_action_message(&text) {
1053 let _ = notification_tx.send(result);
1054 }
1055 }
1056 _ => return Ok(()),
1057 }
1058 }
1059 }
1060
1061 let msg = tokio::select! {
1062 m = ws.next() => m,
1063 _ = stop_rx.changed() => continue,
1064 };
1065
1066 match msg {
1067 Some(Ok(Message::Text(text))) => {
1068 if let Some(result) = parse_action_message(&text) {
1069 let _ = notification_tx.send(result);
1070 }
1071 }
1072 Some(Ok(_)) => {}
1073 _ => return Err(subscription_runtime_closed("action", "actions")),
1074 }
1075 }
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080 use super::*;
1081
1082 #[test]
1083 fn parse_account_diff_notification_ignores_other_messages() {
1084 let confirmation = r#"{"jsonrpc":"2.0","result":1,"id":1}"#;
1085 assert!(parse_account_diff_message(confirmation).is_none());
1086 }
1087
1088 #[test]
1089 fn parse_account_diff_notification_extracts_payload() {
1090 let text = r#"{
1091 "jsonrpc":"2.0",
1092 "method":"accountDiffNotification",
1093 "params":{
1094 "subscription":7,
1095 "result":{
1096 "context":{"slot":123},
1097 "signature":"sig",
1098 "pre":{"a":1},
1099 "post":{"a":2}
1100 }
1101 }
1102 }"#;
1103
1104 let notification = parse_account_diff_message(text)
1105 .expect("notification")
1106 .params
1107 .result;
1108 assert_eq!(notification.context.slot, 123);
1109 assert_eq!(notification.signature.as_deref(), Some("sig"));
1110 assert_eq!(notification.pre, Some(serde_json::json!({"a": 1})));
1111 assert_eq!(notification.post, Some(serde_json::json!({"a": 2})));
1112 }
1113
1114 #[test]
1115 fn parse_routed_account_diff_notification_extracts_subscription_account() {
1116 let text = r#"{
1117 "jsonrpc":"2.0",
1118 "method":"accountDiffNotification",
1119 "params":{
1120 "subscription":42,
1121 "result":{
1122 "context":{"slot":456},
1123 "signature":"sig",
1124 "pre":null,
1125 "post":{"a":2}
1126 }
1127 }
1128 }"#;
1129 let subscriptions = std::collections::HashMap::from([(42_u64, "acct".to_string())]);
1130
1131 let notification =
1132 parse_routed_account_diff_notification(text, &subscriptions).expect("notification");
1133 assert_eq!(notification.account, "acct");
1134 assert_eq!(notification.notification.context.slot, 456);
1135 }
1136
1137 #[test]
1138 fn dedup_accounts_preserves_first_seen_order() {
1139 let accounts = dedup_accounts([
1140 "b".to_string(),
1141 "a".to_string(),
1142 "b".to_string(),
1143 "c".to_string(),
1144 ]);
1145 assert_eq!(accounts, vec!["b", "a", "c"]);
1146 }
1147}