1use std::{
23 fmt::Debug,
24 sync::{
25 Arc,
26 atomic::{AtomicBool, AtomicU8, Ordering},
27 },
28 time::Duration,
29};
30
31use arc_swap::ArcSwap;
32use futures_util::{FutureExt, Stream, StreamExt, stream::FuturesUnordered};
33use nautilus_common::{enums::LogColor, log_debug};
34use nautilus_core::{
35 AtomicMap, AtomicSet, env::get_or_env_var_opt, string::secret::SecretString,
36 time::get_atomic_clock_realtime,
37};
38use nautilus_live::{SocketControl, task::TaskGroup};
39use nautilus_model::{
40 data::BarType,
41 enums::OrderSide,
42 identifiers::{AccountId, ClientOrderId, InstrumentId, StrategyId, TraderId},
43 instruments::{Instrument, InstrumentAny},
44};
45use nautilus_network::{
46 ConnectionUrl,
47 http::create_standard_nautilus_headers,
48 mode::ConnectionMode,
49 websocket::{
50 AuthTracker, SubscriptionState, TransportBackend, WebSocketClient, WebSocketConfig,
51 channel_message_handler,
52 },
53};
54use parking_lot::{Mutex, RwLock, RwLockReadGuard};
55use tokio_util::sync::CancellationToken;
56use ustr::Ustr;
57
58use super::{
59 auth::{AuthState, refresh_token_after_delay, send_auth_request},
60 enums::{DeribitUpdateInterval, DeribitWsChannel},
61 error::{DeribitWsError, DeribitWsResult},
62 handler::{DeribitWsFeedHandler, HandlerCommand},
63 messages::{
64 DeribitCancelAllByInstrumentParams, DeribitCancelParams, DeribitEditParams,
65 DeribitOrderParams, NautilusWsMessage,
66 },
67};
68use crate::common::{
69 consts::{
70 DERIBIT_TESTNET_WS_URL, DERIBIT_WS_HEARTBEAT_SECS, DERIBIT_WS_ORDER_KEY,
71 DERIBIT_WS_ORDER_QUOTA, DERIBIT_WS_SUBSCRIPTION_KEY, DERIBIT_WS_SUBSCRIPTION_QUOTA,
72 DERIBIT_WS_URL,
73 },
74 credential::{Credential, credential_env_vars},
75 enums::DeribitEnvironment,
76 parse::bar_spec_to_resolution,
77};
78
79const AUTHENTICATION_TIMEOUT_SECS: u64 = 30;
81
82type CommandSender = tokio::sync::mpsc::UnboundedSender<HandlerCommand>;
83
84#[derive(Clone)]
86pub struct DeribitWebSocketClient {
87 url: ConnectionUrl,
88 environment: DeribitEnvironment,
89 heartbeat_interval: Option<u64>,
90 auth_timeout_secs: u64,
91 credential: Option<Credential>,
92 auth_state: Arc<tokio::sync::RwLock<Option<AuthState>>>,
93 signal: Arc<AtomicBool>,
94 connection_mode: Arc<ArcSwap<AtomicU8>>,
95 auth_tracker: AuthTracker,
96 cmd_tx: Arc<RwLock<CommandSender>>,
97 out_rx: Option<Arc<tokio::sync::mpsc::UnboundedReceiver<NautilusWsMessage>>>,
98 handler_tasks: Arc<TaskGroup>,
99 connect_lock: Arc<tokio::sync::Mutex<()>>,
100 subscriptions_state: SubscriptionState,
101 instruments_cache: Arc<AtomicMap<Ustr, InstrumentAny>>,
102 option_greeks_subs: Arc<AtomicSet<InstrumentId>>,
103 mark_price_subs: Arc<AtomicSet<InstrumentId>>,
104 index_price_subs: Arc<AtomicSet<InstrumentId>>,
105 cancellation_token: CancellationToken,
106 account_id: Option<AccountId>,
107 bars_timestamp_on_close: bool,
108 subscribe_errors: Arc<Mutex<Vec<String>>>,
109 transport_backend: TransportBackend,
110 proxy_url: Option<SecretString>,
111 socket_control: Option<SocketControl>,
112}
113
114impl Debug for DeribitWebSocketClient {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct(stringify!(DeribitWebSocketClient))
117 .field("url", &self.url)
118 .field("environment", &self.environment)
119 .field("has_credentials", &self.credential.is_some())
120 .field("is_authenticated", &self.auth_tracker.is_authenticated())
121 .field(
122 "has_auth_state",
123 &self.auth_state.try_read().is_ok_and(|s| s.is_some()),
124 )
125 .field("heartbeat_interval", &self.heartbeat_interval)
126 .finish_non_exhaustive()
127 }
128}
129
130impl DeribitWebSocketClient {
131 #[expect(clippy::too_many_arguments)]
139 pub fn new(
140 url: Option<String>,
141 api_key: Option<String>,
142 api_secret: Option<String>,
143 heartbeat_interval: u64,
144 auth_timeout_secs: Option<u64>,
145 environment: DeribitEnvironment,
146 transport_backend: TransportBackend,
147 proxy_url: Option<String>,
148 ) -> anyhow::Result<Self> {
149 Self::new_inner(
150 url,
151 api_key,
152 api_secret,
153 heartbeat_interval,
154 auth_timeout_secs,
155 environment,
156 true,
157 transport_backend,
158 proxy_url,
159 )
160 }
161
162 #[expect(clippy::too_many_arguments)]
164 fn new_inner(
165 url: Option<String>,
166 api_key: Option<String>,
167 api_secret: Option<String>,
168 heartbeat_interval: u64,
169 auth_timeout_secs: Option<u64>,
170 environment: DeribitEnvironment,
171 env_fallback: bool,
172 transport_backend: TransportBackend,
173 proxy_url: Option<String>,
174 ) -> anyhow::Result<Self> {
175 let url = url.unwrap_or_else(|| match environment {
176 DeribitEnvironment::Testnet => DERIBIT_TESTNET_WS_URL.to_string(),
177 DeribitEnvironment::Mainnet => DERIBIT_WS_URL.to_string(),
178 });
179
180 let credential =
182 Credential::resolve_with_env_fallback(api_key, api_secret, environment, env_fallback)?;
183
184 if credential.is_some() {
185 log::debug!("Credentials loaded ({environment})");
186 } else {
187 log::debug!("No credentials configured - unauthenticated mode");
188 }
189
190 let signal = Arc::new(AtomicBool::new(false));
191 let subscriptions_state = SubscriptionState::new('.');
192
193 Ok(Self {
194 url: url.into(),
195 environment,
196 heartbeat_interval: Some(heartbeat_interval),
197 auth_timeout_secs: auth_timeout_secs.unwrap_or(AUTHENTICATION_TIMEOUT_SECS),
198 credential,
199 auth_state: Arc::new(tokio::sync::RwLock::new(None)),
200 signal,
201 connection_mode: Arc::new(ArcSwap::from_pointee(AtomicU8::new(
202 ConnectionMode::Closed.as_u8(),
203 ))),
204 auth_tracker: AuthTracker::new(),
205 cmd_tx: {
206 let (tx, _) = tokio::sync::mpsc::unbounded_channel();
207 Arc::new(RwLock::new(tx))
208 },
209 out_rx: None,
210 handler_tasks: Arc::new(TaskGroup::new()),
211 connect_lock: Arc::new(tokio::sync::Mutex::new(())),
212 subscriptions_state,
213 instruments_cache: Arc::new(AtomicMap::new()),
214 option_greeks_subs: Arc::new(AtomicSet::new()),
215 mark_price_subs: Arc::new(AtomicSet::new()),
216 index_price_subs: Arc::new(AtomicSet::new()),
217 cancellation_token: CancellationToken::new(),
218 account_id: None,
219 bars_timestamp_on_close: true,
220 subscribe_errors: Arc::new(Mutex::new(Vec::new())),
221 transport_backend,
222 proxy_url: proxy_url.map(SecretString::from),
223 socket_control: None,
224 })
225 }
226
227 pub(crate) fn begin_shutdown(&self) {
228 self.handler_tasks.begin_shutdown();
229 self.signal.store(true, Ordering::Relaxed);
230 }
231
232 #[must_use]
234 pub fn with_socket_control(mut self, control: SocketControl) -> Self {
235 self.socket_control = Some(control);
236 self
237 }
238
239 pub fn new_public(
247 environment: DeribitEnvironment,
248 proxy_url: Option<String>,
249 ) -> anyhow::Result<Self> {
250 Self::new_inner(
251 None,
252 None,
253 None,
254 DERIBIT_WS_HEARTBEAT_SECS,
255 None,
256 environment,
257 false,
258 TransportBackend::default(),
259 proxy_url,
260 )
261 }
262
263 pub fn new_unauthenticated(
272 url: Option<String>,
273 heartbeat_interval: u64,
274 environment: DeribitEnvironment,
275 ) -> anyhow::Result<Self> {
276 Self::new_inner(
277 url,
278 None,
279 None,
280 heartbeat_interval,
281 None,
282 environment,
283 false,
284 TransportBackend::default(),
285 None,
286 )
287 }
288
289 pub fn with_credentials(
301 environment: DeribitEnvironment,
302 api_key: Option<String>,
303 api_secret: Option<String>,
304 auth_timeout_secs: Option<u64>,
305 proxy_url: Option<String>,
306 ) -> anyhow::Result<Self> {
307 let (key_env, secret_env) = credential_env_vars(environment);
308
309 let api_key = get_or_env_var_opt(api_key, key_env)
310 .ok_or_else(|| anyhow::anyhow!("Missing environment variable: {key_env}"))?;
311 let api_secret = get_or_env_var_opt(api_secret, secret_env)
312 .ok_or_else(|| anyhow::anyhow!("Missing environment variable: {secret_env}"))?;
313
314 Self::new(
315 None,
316 Some(api_key),
317 Some(api_secret),
318 DERIBIT_WS_HEARTBEAT_SECS,
319 auth_timeout_secs,
320 environment,
321 TransportBackend::default(),
322 proxy_url,
323 )
324 }
325
326 fn connection_mode(&self) -> ConnectionMode {
328 let mode_u8 = self.connection_mode.load().load(Ordering::Relaxed);
329 ConnectionMode::from_u8(mode_u8)
330 }
331
332 #[must_use]
334 pub fn is_active(&self) -> bool {
335 self.connection_mode() == ConnectionMode::Active
336 }
337
338 #[must_use]
340 pub fn url(&self) -> &str {
341 self.url.expose()
342 }
343
344 #[must_use]
346 pub fn environment(&self) -> DeribitEnvironment {
347 self.environment
348 }
349
350 #[must_use]
352 pub fn is_closed(&self) -> bool {
353 let mode = self.connection_mode();
354 mode == ConnectionMode::Disconnect || mode == ConnectionMode::Closed
355 }
356
357 pub fn cancel_all_requests(&self) {
359 self.cancellation_token.cancel();
360 }
361
362 #[must_use]
364 pub fn cancellation_token(&self) -> &CancellationToken {
365 &self.cancellation_token
366 }
367
368 pub async fn wait_until_active(&self, timeout_secs: f64) -> DeribitWsResult<()> {
374 let timeout = tokio::time::Duration::from_secs_f64(timeout_secs);
375
376 tokio::time::timeout(timeout, async {
377 while !self.is_active() {
378 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
379 }
380 })
381 .await
382 .map_err(|_| {
383 DeribitWsError::Timeout(format!(
384 "WebSocket connection timeout after {timeout_secs} seconds"
385 ))
386 })?;
387
388 Ok(())
389 }
390
391 pub async fn wait_for_subscriptions_confirmed(&self, timeout_secs: f64) -> DeribitWsResult<()> {
397 let timeout = Duration::from_secs_f64(timeout_secs);
398
399 tokio::time::timeout(timeout, async {
400 loop {
401 let subscribe_error = {
403 let mut errors = self.subscribe_errors.lock();
404 if errors.is_empty() {
405 None
406 } else {
407 let msg = errors.join("; ");
408 errors.clear();
409 Some(msg)
410 }
411 };
412
413 if let Some(msg) = subscribe_error {
414 return Err(DeribitWsError::Subscribe(msg));
415 }
416
417 let pending = self.subscriptions_state.pending_subscribe_topics();
418 if pending.is_empty() {
419 return Ok(());
420 }
421 tokio::time::sleep(Duration::from_millis(10)).await;
422 }
423 })
424 .await
425 .map_err(|_| {
426 let pending = self.subscriptions_state.pending_subscribe_topics();
427 DeribitWsError::Timeout(format!(
428 "Subscription confirmation timeout after {timeout_secs}s, \
429 still pending: {pending:?}"
430 ))
431 })?
432 }
433
434 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
436 let tx = self.command_sender();
437
438 self.instruments_cache.rcu(|m| {
439 for inst in instruments {
440 m.insert(inst.raw_symbol().inner(), inst.clone());
441 }
442 });
443 log::debug!("Cached {} instruments", self.instruments_cache.len());
444
445 if self.is_active() {
448 for inst in instruments {
449 let _ = tx.send(HandlerCommand::UpdateInstrument(Box::new(inst.clone())));
450 }
451 }
452 }
453
454 pub fn cache_instrument(&self, instrument: InstrumentAny) {
456 let tx = self.command_sender();
457 let symbol = instrument.raw_symbol().inner();
458 self.instruments_cache.insert(symbol, instrument);
459
460 if self.is_active() {
462 let inst = self.instruments_cache.get_cloned(&symbol);
463
464 if let Some(inst) = inst {
465 let _ = tx.send(HandlerCommand::UpdateInstrument(Box::new(inst)));
466 }
467 }
468 }
469
470 pub fn set_option_greeks_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
472 self.option_greeks_subs = subs;
473 }
474
475 pub fn set_mark_price_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
477 self.mark_price_subs = subs;
478 }
479
480 pub fn set_index_price_subs(&mut self, subs: Arc<AtomicSet<InstrumentId>>) {
482 self.index_price_subs = subs;
483 }
484
485 pub fn add_mark_price_sub(&self, instrument_id: InstrumentId) {
487 self.mark_price_subs.insert(instrument_id);
488 }
489
490 pub fn remove_mark_price_sub(&self, instrument_id: &InstrumentId) {
492 self.mark_price_subs.remove(instrument_id);
493 }
494
495 pub fn add_index_price_sub(&self, instrument_id: InstrumentId) {
497 self.index_price_subs.insert(instrument_id);
498 }
499
500 pub fn remove_index_price_sub(&self, instrument_id: &InstrumentId) {
502 self.index_price_subs.remove(instrument_id);
503 }
504
505 pub fn add_option_greeks_sub(&self, instrument_id: InstrumentId) {
507 self.option_greeks_subs.insert(instrument_id);
508 }
509
510 pub fn remove_option_greeks_sub(&self, instrument_id: &InstrumentId) {
512 self.option_greeks_subs.remove(instrument_id);
513 }
514
515 pub async fn connect(&mut self) -> anyhow::Result<()> {
521 let connect_lock = Arc::clone(&self.connect_lock);
522 let _connect_guard = connect_lock.lock().await;
523
524 log_debug!(
525 "Connecting to WebSocket: {}",
526 self.url.origin(),
527 color = LogColor::Blue
528 );
529
530 if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
531 self.handler_tasks.begin_shutdown();
532 self.signal.store(true, Ordering::Relaxed);
533 self.finish_handler()
534 .await
535 .map_err(|e| anyhow::anyhow!("failed to stop prior WebSocket handler: {e}"))?;
536 self.handler_tasks.start_generation().map_err(|e| {
537 anyhow::anyhow!("failed to start WebSocket handler task generation: {e}")
538 })?;
539 }
540 let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
541 anyhow::anyhow!("failed to acquire WebSocket handler task spawner: {e}")
542 })?;
543
544 self.signal.store(false, Ordering::Relaxed);
547 self.subscriptions_state.clear();
548
549 let (message_handler, raw_rx) = channel_message_handler();
551
552 let headers = create_standard_nautilus_headers();
558
559 let config = WebSocketConfig {
560 url: self.url.expose().to_owned(),
561 headers,
562 heartbeat_interval_secs: self.heartbeat_interval,
563 heartbeat_payload: None, connect_timeout_ms: Some(5_000),
565 reconnect_delay_initial_ms: None,
566 reconnect_delay_max_ms: None,
567 reconnect_backoff_factor: None,
568 reconnect_jitter_ms: None,
569 reconnect_max_attempts: None,
570 heartbeat_timeout_secs: None,
571 idle_timeout_ms: None,
572 writer_capacity: None,
573 backend: self.transport_backend,
574 proxy_url: self
575 .proxy_url
576 .as_ref()
577 .map(|value| value.expose_secret().to_owned()),
578 max_message_size_bytes: None,
579 max_frame_size_bytes: None,
580 };
581
582 let keyed_quotas = vec![
584 (
585 DERIBIT_WS_SUBSCRIPTION_KEY.to_string(),
586 *DERIBIT_WS_SUBSCRIPTION_QUOTA,
587 ),
588 (DERIBIT_WS_ORDER_KEY.to_string(), *DERIBIT_WS_ORDER_QUOTA),
589 ];
590
591 let ws_client = WebSocketClient::builder()
593 .config(config)
594 .message_handler(message_handler)
595 .keyed_quotas(keyed_quotas)
596 .default_quota(*DERIBIT_WS_SUBSCRIPTION_QUOTA)
597 .maybe_state_sink(self.socket_control.as_ref().map(SocketControl::sink))
598 .connect()
599 .await?;
600
601 self.connection_mode
603 .store(ws_client.connection_mode_atomic());
604 let reconnect_handle = ws_client.reconnect_handle();
605
606 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
608 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel();
609
610 self.out_rx = Some(Arc::new(out_rx));
611
612 self.subscribe_errors.lock().clear();
613
614 let mut handler = DeribitWsFeedHandler::new(
616 self.signal.clone(),
617 cmd_rx,
618 raw_rx,
619 out_tx,
620 self.auth_tracker.clone(),
621 self.subscriptions_state.clone(),
622 self.option_greeks_subs.clone(),
623 self.mark_price_subs.clone(),
624 self.index_price_subs.clone(),
625 self.account_id,
626 self.bars_timestamp_on_close,
627 self.subscribe_errors.clone(),
628 );
629
630 if let Some(control) = &self.socket_control {
631 control.register(move || reconnect_handle.request_reconnect());
632 }
633
634 {
637 let mut command_sender = self.cmd_tx.write();
638 let _ = cmd_tx.send(HandlerCommand::SetClient(ws_client));
639
640 let instruments: Vec<InstrumentAny> =
641 self.instruments_cache.load().values().cloned().collect();
642
643 if !instruments.is_empty() {
644 log::debug!(
645 "Sending {} cached instruments to handler",
646 instruments.len()
647 );
648 let _ = cmd_tx.send(HandlerCommand::InitializeInstruments(instruments));
649 }
650
651 if let Some(interval) = self.heartbeat_interval {
652 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
653 }
654
655 *command_sender = cmd_tx.clone();
656 }
657
658 let subscriptions_state = self.subscriptions_state.clone();
660 let credential = self.credential.clone();
661 let auth_tracker = self.auth_tracker.clone();
662 let auth_state = self.auth_state.clone();
663 let heartbeat_interval = self.heartbeat_interval;
664
665 let handler_task = async move {
666 const MAX_REAUTH_ATTEMPTS: u32 = 3;
667
668 let mut pending_reauth = false;
669 let mut reauth_attempts: u32 = 0;
670
671 let mut refresh_cancel = CancellationToken::new();
672 let mut retry_cancel = CancellationToken::new();
673 let mut lifecycle_futures = FuturesUnordered::new();
674
675 loop {
676 let message = {
677 let next = handler.next();
680 tokio::pin!(next);
681
682 loop {
683 tokio::select! {
684 message = &mut next => break message,
685 _ = lifecycle_futures.next(), if !lifecycle_futures.is_empty() => {}
686 }
687 }
688 };
689
690 match message {
691 Some(msg) => match msg {
692 NautilusWsMessage::Reconnected => {
693 log::info!("Reconnected to WebSocket");
694
695 refresh_cancel.cancel();
697 refresh_cancel = CancellationToken::new();
698 retry_cancel.cancel();
699 retry_cancel = CancellationToken::new();
700
701 if let Some(interval) = heartbeat_interval {
705 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
706 }
707
708 let channels = subscriptions_state.reset_after_reconnect();
709
710 if let Some(cred) = &credential {
712 log::info!("Re-authenticating after reconnection...");
713
714 let _rx = auth_tracker.begin();
715 pending_reauth = true;
716 reauth_attempts = 1;
717
718 let previous_scope =
719 auth_state.read().await.as_ref().map(|s| s.scope.clone());
720
721 send_auth_request(cred, previous_scope, &cmd_tx);
722 } else {
723 if !channels.is_empty() {
725 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
726 }
727 }
728 }
729 NautilusWsMessage::Authenticated(result) => {
730 let result = *result;
731 let timestamp = get_atomic_clock_realtime().get_time_ms();
732 let new_auth_state = AuthState::from_auth_result(&result, timestamp);
733 *auth_state.write().await = Some(new_auth_state);
734
735 refresh_cancel.cancel();
736 refresh_cancel = CancellationToken::new();
737 retry_cancel.cancel();
738 retry_cancel = CancellationToken::new();
739
740 lifecycle_futures.push(
741 refresh_token_after_delay(
742 result.expires_in,
743 result.refresh_token.clone(),
744 cmd_tx.clone(),
745 refresh_cancel.clone(),
746 )
747 .boxed(),
748 );
749
750 if pending_reauth {
751 pending_reauth = false;
752 reauth_attempts = 0;
753 log::info!(
754 "Re-authentication successful (scope: {}), resubscribing to channels",
755 result.scope
756 );
757
758 let channels = subscriptions_state.all_topics();
759
760 if !channels.is_empty() {
761 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
762 }
763 } else {
764 log::debug!(
765 "Auth state stored: scope={}, expires_in={}s",
766 result.scope,
767 result.expires_in
768 );
769 }
770 }
771 NautilusWsMessage::AuthenticationFailed(reason) => {
772 if pending_reauth && reauth_attempts < MAX_REAUTH_ATTEMPTS {
773 let delay_secs = 1u64 << reauth_attempts; log::warn!(
775 "Re-authentication attempt {reauth_attempts}/{MAX_REAUTH_ATTEMPTS} \
776 failed: {reason} - retrying in {delay_secs}s",
777 );
778 reauth_attempts += 1;
779
780 if let Some(cred) = &credential {
783 let cred = cred.clone();
784 let auth_state = auth_state.clone();
785 let auth_tracker = auth_tracker.clone();
786 let cmd_tx = cmd_tx.clone();
787 let cancel = retry_cancel.clone();
788
789 lifecycle_futures.push(
790 async move {
791 tokio::select! {
792 () = tokio::time::sleep(Duration::from_secs(delay_secs)) => {}
793 () = cancel.cancelled() => return,
794 }
795 let _rx = auth_tracker.begin();
796 let previous_scope = auth_state
797 .read()
798 .await
799 .as_ref()
800 .map(|s| s.scope.clone());
801 send_auth_request(&cred, previous_scope, &cmd_tx);
802 }
803 .boxed(),
804 );
805 }
806 } else if pending_reauth {
807 pending_reauth = false;
808 reauth_attempts = 0;
809 log::error!(
810 "Re-authentication failed after {MAX_REAUTH_ATTEMPTS} \
811 attempts: {reason} \
812 - resubscribing to public channels only"
813 );
814
815 let all = subscriptions_state.all_topics();
816 let mut public_channels = Vec::new();
817
818 for ch in &all {
819 if DeribitWsChannel::requires_auth(ch) {
820 subscriptions_state.mark_unsubscribe(ch);
823 subscriptions_state.confirm_unsubscribe(ch);
824 subscriptions_state.remove_reference(ch);
825 } else {
826 public_channels.push(ch.clone());
827 }
828 }
829
830 if !public_channels.is_empty() {
831 let _ = cmd_tx.send(HandlerCommand::Subscribe {
832 channels: public_channels,
833 });
834 }
835 } else {
836 log::error!("Authentication failed: {reason}");
837 }
838 }
839 _ => {}
840 },
841 None => {
842 log::debug!("Handler returned None, stopping task");
843 break;
844 }
845 }
846 }
847 };
848
849 if let Err(e) = handler_spawner.spawn(handler_task) {
850 if let Some(control) = &self.socket_control {
851 control.deregister();
852 }
853 self.out_rx = None;
854 anyhow::bail!("failed to register WebSocket handler task: {e}");
855 }
856 log::debug!("Connected to WebSocket");
857
858 Ok(())
859 }
860
861 pub async fn close(&self) -> DeribitWsResult<()> {
867 self.begin_shutdown();
868 let connect_lock = Arc::clone(&self.connect_lock);
869 let _connect_guard = connect_lock.lock().await;
870 self.close_locked().await
871 }
872
873 async fn close_locked(&self) -> DeribitWsResult<()> {
874 log::debug!("Closing WebSocket connection");
875 self.begin_shutdown();
876
877 let _ = self.command_sender().send(HandlerCommand::Disconnect);
878
879 self.finish_handler().await?;
880
881 self.auth_tracker.invalidate();
882
883 if let Some(control) = &self.socket_control {
884 control.deregister();
885 }
886 Ok(())
887 }
888
889 async fn finish_handler(&self) -> DeribitWsResult<()> {
890 self.handler_tasks
891 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
892 .await
893 .map_err(|e| {
894 DeribitWsError::ClientError(format!("WebSocket handler shutdown failed: {e}"))
895 })
896 }
897
898 pub fn stream(&mut self) -> DeribitWsResult<impl Stream<Item = NautilusWsMessage> + 'static> {
904 let rx = self.out_rx.take().ok_or_else(|| {
905 DeribitWsError::ClientError(
906 "Stream receiver already taken or not connected".to_string(),
907 )
908 })?;
909 let mut rx = Arc::try_unwrap(rx).map_err(|_| {
910 DeribitWsError::ClientError(
911 "Cannot take stream ownership - other references exist".to_string(),
912 )
913 })?;
914
915 Ok(async_stream::stream! {
916 while let Some(msg) = rx.recv().await {
917 yield msg;
918 }
919 })
920 }
921
922 #[must_use]
924 pub fn has_credentials(&self) -> bool {
925 self.credential.is_some()
926 }
927
928 #[must_use]
930 pub fn is_authenticated(&self) -> bool {
931 self.auth_tracker.is_authenticated()
932 }
933
934 pub async fn authenticate(&self, session_name: Option<&str>) -> DeribitWsResult<()> {
953 let credential = self.credential.as_ref().ok_or_else(|| {
954 DeribitWsError::Authentication("API credentials not configured".to_string())
955 })?;
956
957 let scope = session_name.map(|name| format!("session:{name}"));
959
960 log::debug!("Authenticating WebSocket...");
961
962 let rx = self.auth_tracker.begin();
963
964 let cmd_tx = self.command_sender().clone();
966 send_auth_request(credential, scope, &cmd_tx);
967
968 match self
970 .auth_tracker
971 .wait_for_result::<DeribitWsError>(Duration::from_secs(self.auth_timeout_secs), rx)
972 .await
973 {
974 Ok(()) => {
975 log::debug!("WebSocket authenticated successfully");
976 Ok(())
977 }
978 Err(e) => {
979 log::error!("WebSocket authentication failed: error={e}");
980 Err(e)
981 }
982 }
983 }
984
985 pub async fn authenticate_session(&self, session_name: &str) -> DeribitWsResult<()> {
994 self.authenticate(Some(session_name)).await
995 }
996
997 pub async fn auth_state(&self) -> Option<AuthState> {
1001 self.auth_state.read().await.clone()
1002 }
1003
1004 pub async fn access_token(&self) -> Option<SecretString> {
1006 self.auth_state
1007 .read()
1008 .await
1009 .as_ref()
1010 .map(|s| s.access_token.clone())
1011 }
1012
1013 pub fn set_account_id(&mut self, account_id: AccountId) {
1015 self.account_id = Some(account_id);
1016 }
1017
1018 pub fn set_bars_timestamp_on_close(&mut self, value: bool) {
1022 self.bars_timestamp_on_close = value;
1023 }
1024
1025 async fn send_subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1026 let mut channels_to_subscribe = Vec::new();
1027
1028 for channel in channels {
1029 if self.subscriptions_state.add_reference(&channel) {
1030 self.subscriptions_state.mark_subscribe(&channel);
1031 channels_to_subscribe.push(channel);
1032 } else {
1033 log::debug!("Already subscribed to {channel}, skipping duplicate subscription");
1034 }
1035 }
1036
1037 if channels_to_subscribe.is_empty() {
1038 return Ok(());
1039 }
1040
1041 if let Err(e) = self.command_sender().send(HandlerCommand::Subscribe {
1042 channels: channels_to_subscribe.clone(),
1043 }) {
1044 for channel in &channels_to_subscribe {
1046 self.subscriptions_state.remove_reference(channel);
1047 self.subscriptions_state.mark_unsubscribe(channel);
1048 self.subscriptions_state.confirm_unsubscribe(channel);
1049 }
1050 return Err(DeribitWsError::Send(e.to_string()));
1051 }
1052
1053 log::debug!(
1054 "Sent subscribe for {} channels",
1055 channels_to_subscribe.len()
1056 );
1057 Ok(())
1058 }
1059
1060 async fn send_unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1061 let mut channels_to_unsubscribe = Vec::new();
1062
1063 for channel in channels {
1064 if self.subscriptions_state.remove_reference(&channel) {
1065 self.subscriptions_state.mark_unsubscribe(&channel);
1066 channels_to_unsubscribe.push(channel);
1067 } else {
1068 log::debug!("Still has references to {channel}, skipping unsubscription");
1069 }
1070 }
1071
1072 if channels_to_unsubscribe.is_empty() {
1073 return Ok(());
1074 }
1075
1076 if let Err(e) = self.command_sender().send(HandlerCommand::Unsubscribe {
1077 channels: channels_to_unsubscribe.clone(),
1078 }) {
1079 for channel in &channels_to_unsubscribe {
1086 self.subscriptions_state.confirm_unsubscribe(channel);
1087 self.subscriptions_state.add_reference(channel);
1088 self.subscriptions_state.mark_subscribe(channel);
1089 self.subscriptions_state.confirm_subscribe(channel);
1090 }
1091 return Err(DeribitWsError::Send(e.to_string()));
1092 }
1093
1094 log::debug!(
1095 "Sent unsubscribe for {} channels",
1096 channels_to_unsubscribe.len()
1097 );
1098 Ok(())
1099 }
1100
1101 pub async fn subscribe_trades(
1112 &self,
1113 instrument_id: InstrumentId,
1114 interval: Option<DeribitUpdateInterval>,
1115 ) -> DeribitWsResult<()> {
1116 let interval = interval.unwrap_or_default();
1117 self.check_auth_requirement(interval)?;
1118 let channel =
1119 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1120 self.send_subscribe(vec![channel]).await
1121 }
1122
1123 pub async fn unsubscribe_trades(
1129 &self,
1130 instrument_id: InstrumentId,
1131 interval: Option<DeribitUpdateInterval>,
1132 ) -> DeribitWsResult<()> {
1133 let interval = interval.unwrap_or_default();
1134 let channel =
1135 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1136 self.send_unsubscribe(vec![channel]).await
1137 }
1138
1139 pub async fn subscribe_book(
1150 &self,
1151 instrument_id: InstrumentId,
1152 interval: Option<DeribitUpdateInterval>,
1153 ) -> DeribitWsResult<()> {
1154 let interval = interval.unwrap_or_default();
1155 self.check_auth_requirement(interval)?;
1156 let channel =
1157 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1158 self.send_subscribe(vec![channel]).await
1159 }
1160
1161 pub async fn unsubscribe_book(
1167 &self,
1168 instrument_id: InstrumentId,
1169 interval: Option<DeribitUpdateInterval>,
1170 ) -> DeribitWsResult<()> {
1171 let interval = interval.unwrap_or_default();
1172 let channel =
1173 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1174 self.send_unsubscribe(vec![channel]).await
1175 }
1176
1177 pub async fn subscribe_book_grouped(
1187 &self,
1188 instrument_id: InstrumentId,
1189 group: &str,
1190 depth: u32,
1191 interval: Option<DeribitUpdateInterval>,
1192 ) -> DeribitWsResult<()> {
1193 let interval = match interval {
1195 Some(DeribitUpdateInterval::Raw) | None => DeribitUpdateInterval::Ms100,
1196 Some(i) => i,
1197 };
1198
1199 let normalized_depth = if depth < 5 {
1200 1
1201 } else if depth < 15 {
1202 10
1203 } else {
1204 20
1205 };
1206
1207 let channel = format!(
1208 "book.{}.{}.{}.{}",
1209 instrument_id.symbol,
1210 group,
1211 normalized_depth,
1212 interval.as_str()
1213 );
1214 log::debug!("Subscribing to grouped book channel: {channel}");
1215 self.send_subscribe(vec![channel]).await
1216 }
1217
1218 pub async fn unsubscribe_book_grouped(
1226 &self,
1227 instrument_id: InstrumentId,
1228 group: &str,
1229 depth: u32,
1230 interval: Option<DeribitUpdateInterval>,
1231 ) -> DeribitWsResult<()> {
1232 let interval = match interval {
1234 Some(DeribitUpdateInterval::Raw) | None => DeribitUpdateInterval::Ms100,
1235 Some(i) => i,
1236 };
1237
1238 let normalized_depth = if depth < 5 {
1239 1
1240 } else if depth < 15 {
1241 10
1242 } else {
1243 20
1244 };
1245
1246 let channel = format!(
1247 "book.{}.{}.{}.{}",
1248 instrument_id.symbol,
1249 group,
1250 normalized_depth,
1251 interval.as_str()
1252 );
1253 self.send_unsubscribe(vec![channel]).await
1254 }
1255
1256 pub async fn subscribe_ticker(
1267 &self,
1268 instrument_id: InstrumentId,
1269 interval: Option<DeribitUpdateInterval>,
1270 ) -> DeribitWsResult<()> {
1271 let interval = interval.unwrap_or_default();
1272 self.check_auth_requirement(interval)?;
1273 let channel =
1274 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1275 self.send_subscribe(vec![channel]).await
1276 }
1277
1278 pub async fn unsubscribe_ticker(
1284 &self,
1285 instrument_id: InstrumentId,
1286 interval: Option<DeribitUpdateInterval>,
1287 ) -> DeribitWsResult<()> {
1288 let interval = interval.unwrap_or_default();
1289 let channel =
1290 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1291 self.send_unsubscribe(vec![channel]).await
1292 }
1293
1294 pub async fn subscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1302 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1303 self.send_subscribe(vec![channel]).await
1304 }
1305
1306 pub async fn unsubscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1312 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1313 self.send_unsubscribe(vec![channel]).await
1314 }
1315
1316 pub async fn subscribe_instrument_status(
1324 &self,
1325 kind: &str,
1326 currency: &str,
1327 ) -> DeribitWsResult<()> {
1328 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1329 self.send_subscribe(vec![channel]).await
1330 }
1331
1332 pub async fn unsubscribe_instrument_status(
1338 &self,
1339 kind: &str,
1340 currency: &str,
1341 ) -> DeribitWsResult<()> {
1342 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1343 self.send_unsubscribe(vec![channel]).await
1344 }
1345
1346 pub async fn subscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1354 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1355 self.send_subscribe(vec![channel]).await
1356 }
1357
1358 pub async fn unsubscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1364 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1365 self.send_unsubscribe(vec![channel]).await
1366 }
1367
1368 pub async fn subscribe_perpetual_interests_rates_updates(
1376 &self,
1377 instrument_id: InstrumentId,
1378 interval: Option<DeribitUpdateInterval>,
1379 ) -> DeribitWsResult<()> {
1380 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1381 let channel = DeribitWsChannel::Perpetual
1382 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1383
1384 self.send_subscribe(vec![channel]).await
1385 }
1386
1387 pub async fn unsubscribe_perpetual_interest_rates_updates(
1393 &self,
1394 instrument_id: InstrumentId,
1395 interval: Option<DeribitUpdateInterval>,
1396 ) -> DeribitWsResult<()> {
1397 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1398 let channel = DeribitWsChannel::Perpetual
1399 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1400
1401 self.send_unsubscribe(vec![channel]).await
1402 }
1403
1404 pub async fn subscribe_chart(
1416 &self,
1417 instrument_id: InstrumentId,
1418 resolution: &str,
1419 ) -> DeribitWsResult<()> {
1420 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1422 self.send_subscribe(vec![channel]).await
1423 }
1424
1425 pub async fn unsubscribe_chart(
1431 &self,
1432 instrument_id: InstrumentId,
1433 resolution: &str,
1434 ) -> DeribitWsResult<()> {
1435 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1436 self.send_unsubscribe(vec![channel]).await
1437 }
1438
1439 pub async fn subscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1448 let resolution = bar_spec_to_resolution(&bar_type);
1449 self.subscribe_chart(bar_type.instrument_id(), &resolution)
1450 .await
1451 }
1452
1453 pub async fn unsubscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1459 let resolution = bar_spec_to_resolution(&bar_type);
1460 self.unsubscribe_chart(bar_type.instrument_id(), &resolution)
1461 .await
1462 }
1463
1464 fn check_auth_requirement(&self, interval: DeribitUpdateInterval) -> DeribitWsResult<()> {
1470 if interval.requires_auth() && !self.is_authenticated() {
1471 return Err(DeribitWsError::Authentication(
1472 "Raw streams require authentication. Call authenticate() first.".to_string(),
1473 ));
1474 }
1475 Ok(())
1476 }
1477
1478 pub async fn subscribe_user_orders(&self) -> DeribitWsResult<()> {
1486 if !self.is_authenticated() {
1487 return Err(DeribitWsError::Authentication(
1488 "User orders subscription requires authentication".to_string(),
1489 ));
1490 }
1491 self.send_subscribe(vec!["user.orders.any.any.raw".to_string()])
1492 .await
1493 }
1494
1495 pub async fn unsubscribe_user_orders(&self) -> DeribitWsResult<()> {
1501 self.send_unsubscribe(vec!["user.orders.any.any.raw".to_string()])
1502 .await
1503 }
1504
1505 pub async fn subscribe_user_trades(&self) -> DeribitWsResult<()> {
1513 if !self.is_authenticated() {
1514 return Err(DeribitWsError::Authentication(
1515 "User trades subscription requires authentication".to_string(),
1516 ));
1517 }
1518 self.send_subscribe(vec!["user.trades.any.any.raw".to_string()])
1519 .await
1520 }
1521
1522 pub async fn unsubscribe_user_trades(&self) -> DeribitWsResult<()> {
1528 self.send_unsubscribe(vec!["user.trades.any.any.raw".to_string()])
1529 .await
1530 }
1531
1532 pub async fn subscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1542 if !self.is_authenticated() {
1543 return Err(DeribitWsError::Authentication(
1544 "User portfolio subscription requires authentication".to_string(),
1545 ));
1546 }
1547 self.send_subscribe(vec!["user.portfolio.any".to_string()])
1548 .await
1549 }
1550
1551 pub async fn unsubscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1557 self.send_unsubscribe(vec!["user.portfolio.any".to_string()])
1558 .await
1559 }
1560
1561 pub async fn subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1567 self.send_subscribe(channels).await
1568 }
1569
1570 pub async fn unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1576 self.send_unsubscribe(channels).await
1577 }
1578
1579 pub async fn submit_order(
1590 &self,
1591 order_side: OrderSide,
1592 params: DeribitOrderParams,
1593 client_order_id: ClientOrderId,
1594 trader_id: TraderId,
1595 strategy_id: StrategyId,
1596 instrument_id: InstrumentId,
1597 ) -> DeribitWsResult<()> {
1598 if !self.is_authenticated() {
1599 return Err(DeribitWsError::Authentication(
1600 "Submit order requires authentication. Call authenticate_session() first."
1601 .to_string(),
1602 ));
1603 }
1604
1605 log::debug!(
1606 "Sending {} order: instrument={}, amount={}, price={:?}, client_order_id={}",
1607 order_side,
1608 params.instrument_name,
1609 params.amount,
1610 params.price,
1611 client_order_id
1612 );
1613
1614 let cmd = match order_side {
1615 OrderSide::Buy => HandlerCommand::Buy {
1616 params,
1617 client_order_id,
1618 trader_id,
1619 strategy_id,
1620 instrument_id,
1621 },
1622 OrderSide::Sell => HandlerCommand::Sell {
1623 params,
1624 client_order_id,
1625 trader_id,
1626 strategy_id,
1627 instrument_id,
1628 },
1629 };
1630
1631 self.command_sender()
1632 .send(cmd)
1633 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1634
1635 Ok(())
1636 }
1637
1638 pub async fn modify_order(
1649 &self,
1650 params: DeribitEditParams,
1651 client_order_id: ClientOrderId,
1652 trader_id: TraderId,
1653 strategy_id: StrategyId,
1654 instrument_id: InstrumentId,
1655 ) -> DeribitWsResult<()> {
1656 if !self.is_authenticated() {
1657 return Err(DeribitWsError::Authentication(
1658 "Modify order requires authentication. Call authenticate_session() first."
1659 .to_string(),
1660 ));
1661 }
1662
1663 log::debug!(
1664 "Sending modify order: order_id={}, amount={}, price={:?}, client_order_id={client_order_id}",
1665 params.order_id,
1666 params.amount,
1667 params.price,
1668 );
1669
1670 self.command_sender()
1671 .send(HandlerCommand::Edit {
1672 params,
1673 client_order_id,
1674 trader_id,
1675 strategy_id,
1676 instrument_id,
1677 })
1678 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1679
1680 Ok(())
1681 }
1682
1683 pub async fn cancel_order(
1694 &self,
1695 order_id: &str,
1696 client_order_id: ClientOrderId,
1697 trader_id: TraderId,
1698 strategy_id: StrategyId,
1699 instrument_id: InstrumentId,
1700 ) -> DeribitWsResult<()> {
1701 if !self.is_authenticated() {
1702 return Err(DeribitWsError::Authentication(
1703 "Cancel order requires authentication. Call authenticate_session() first."
1704 .to_string(),
1705 ));
1706 }
1707
1708 let params = DeribitCancelParams {
1709 order_id: order_id.to_string(),
1710 };
1711
1712 log::debug!("Sending cancel order: order_id={order_id}, client_order_id={client_order_id}");
1713
1714 self.command_sender()
1715 .send(HandlerCommand::Cancel {
1716 params,
1717 client_order_id,
1718 trader_id,
1719 strategy_id,
1720 instrument_id,
1721 })
1722 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1723
1724 Ok(())
1725 }
1726
1727 pub async fn cancel_all_orders(
1738 &self,
1739 instrument_id: InstrumentId,
1740 order_type: Option<String>,
1741 ) -> DeribitWsResult<()> {
1742 if !self.is_authenticated() {
1743 return Err(DeribitWsError::Authentication(
1744 "Cancel all orders requires authentication. Call authenticate_session() first."
1745 .to_string(),
1746 ));
1747 }
1748
1749 let instrument_name = instrument_id.symbol.to_string();
1750 let params = DeribitCancelAllByInstrumentParams {
1751 instrument_name: instrument_name.clone(),
1752 order_type,
1753 };
1754
1755 log::debug!("Sending cancel_all_orders: instrument={instrument_name}");
1756
1757 self.command_sender()
1758 .send(HandlerCommand::CancelAllByInstrument {
1759 params,
1760 instrument_id,
1761 })
1762 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1763
1764 Ok(())
1765 }
1766
1767 pub async fn query_order(
1778 &self,
1779 order_id: &str,
1780 client_order_id: ClientOrderId,
1781 trader_id: TraderId,
1782 strategy_id: StrategyId,
1783 instrument_id: InstrumentId,
1784 ) -> DeribitWsResult<()> {
1785 if !self.is_authenticated() {
1786 return Err(DeribitWsError::Authentication(
1787 "Query order state requires authentication. Call authenticate_session() first."
1788 .to_string(),
1789 ));
1790 }
1791
1792 log::debug!("Sending query_order: order_id={order_id}, client_order_id={client_order_id}");
1793
1794 self.command_sender()
1795 .send(HandlerCommand::GetOrderState {
1796 order_id: order_id.to_string(),
1797 client_order_id,
1798 trader_id,
1799 strategy_id,
1800 instrument_id,
1801 })
1802 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1803
1804 Ok(())
1805 }
1806
1807 fn command_sender(&self) -> RwLockReadGuard<'_, CommandSender> {
1808 self.cmd_tx.read()
1809 }
1810}
1811
1812#[cfg(test)]
1813mod tests {
1814 use rstest::rstest;
1815
1816 use super::*;
1817
1818 struct DropSignal(Arc<AtomicBool>);
1819
1820 impl Drop for DropSignal {
1821 fn drop(&mut self) {
1822 self.0.store(true, Ordering::Release);
1823 }
1824 }
1825
1826 #[tokio::test]
1827 async fn test_last_client_owner_drop_aborts_handler_task() {
1828 let client = DeribitWebSocketClient::new_unauthenticated(
1829 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
1830 30,
1831 DeribitEnvironment::Testnet,
1832 )
1833 .unwrap();
1834 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1835 let dropped = Arc::new(AtomicBool::new(false));
1836 let drop_signal = DropSignal(Arc::clone(&dropped));
1837 client
1838 .handler_tasks
1839 .spawn(async move {
1840 let _drop_signal = drop_signal;
1841 started_tx.send(()).expect("started receiver");
1842 std::future::pending::<()>().await;
1843 })
1844 .expect("handler task should register");
1845 started_rx.await.expect("handler task started");
1846 let clone = client.clone();
1847
1848 drop(client);
1849 assert!(!dropped.load(Ordering::Acquire));
1850 drop(clone);
1851
1852 tokio::time::timeout(Duration::from_secs(1), async {
1853 while !dropped.load(Ordering::Acquire) {
1854 tokio::task::yield_now().await;
1855 }
1856 })
1857 .await
1858 .expect("handler task aborted");
1859 }
1860
1861 #[rstest]
1862 #[tokio::test]
1863 async fn test_unsubscribe_send_failure_restores_subscription() {
1864 let client = DeribitWebSocketClient::new_unauthenticated(
1865 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
1866 30,
1867 DeribitEnvironment::Testnet,
1868 )
1869 .unwrap();
1870 let channel = "trades.BTC-PERPETUAL.raw";
1871 client.subscriptions_state.add_reference(channel);
1872 client.subscriptions_state.mark_subscribe(channel);
1873 client.subscriptions_state.confirm_subscribe(channel);
1874
1875 let error = client
1876 .send_unsubscribe(vec![channel.to_string()])
1877 .await
1878 .unwrap_err();
1879
1880 assert!(matches!(error, DeribitWsError::Send(_)));
1881 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
1882 assert_eq!(client.subscriptions_state.len(), 1);
1883 assert_eq!(client.subscriptions_state.all_topics(), [channel]);
1884 assert!(
1885 client
1886 .subscriptions_state
1887 .pending_subscribe_topics()
1888 .is_empty()
1889 );
1890 assert!(
1891 client
1892 .subscriptions_state
1893 .pending_unsubscribe_topics()
1894 .is_empty()
1895 );
1896 }
1897}