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, consts::NAUTILUS_USER_AGENT, env::get_or_env_var_opt,
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 types::{Price, Quantity},
45};
46use nautilus_network::{
47 http::USER_AGENT,
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: String,
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<String>,
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,
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,
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
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,
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 config = WebSocketConfig {
558 url: self.url.clone(),
559 headers: vec![(USER_AGENT.to_string(), NAUTILUS_USER_AGENT.to_string())],
560 heartbeat_interval_secs: self.heartbeat_interval,
561 heartbeat_payload: None, connect_timeout_ms: Some(5_000),
563 reconnect_delay_initial_ms: None,
564 reconnect_delay_max_ms: None,
565 reconnect_backoff_factor: None,
566 reconnect_jitter_ms: None,
567 reconnect_max_attempts: None,
568 heartbeat_timeout_secs: None,
569 idle_timeout_ms: None,
570 backend: self.transport_backend,
571 proxy_url: self.proxy_url.clone(),
572 };
573
574 let keyed_quotas = vec![
576 (
577 DERIBIT_WS_SUBSCRIPTION_KEY.to_string(),
578 *DERIBIT_WS_SUBSCRIPTION_QUOTA,
579 ),
580 (DERIBIT_WS_ORDER_KEY.to_string(), *DERIBIT_WS_ORDER_QUOTA),
581 ];
582
583 let ws_client = WebSocketClient::builder()
585 .config(config)
586 .message_handler(message_handler)
587 .keyed_quotas(keyed_quotas)
588 .default_quota(*DERIBIT_WS_SUBSCRIPTION_QUOTA)
589 .maybe_state_sink(self.socket_control.as_ref().map(SocketControl::sink))
590 .connect()
591 .await?;
592
593 self.connection_mode
595 .store(ws_client.connection_mode_atomic());
596 let reconnect_handle = ws_client.reconnect_handle();
597
598 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel();
600 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel();
601
602 self.out_rx = Some(Arc::new(out_rx));
603
604 self.subscribe_errors.lock().clear();
605
606 let mut handler = DeribitWsFeedHandler::new(
608 self.signal.clone(),
609 cmd_rx,
610 raw_rx,
611 out_tx,
612 self.auth_tracker.clone(),
613 self.subscriptions_state.clone(),
614 self.option_greeks_subs.clone(),
615 self.mark_price_subs.clone(),
616 self.index_price_subs.clone(),
617 self.account_id,
618 self.bars_timestamp_on_close,
619 self.subscribe_errors.clone(),
620 );
621
622 if let Some(control) = &self.socket_control {
623 control.register(move || reconnect_handle.request_reconnect());
624 }
625
626 {
629 let mut command_sender = self.cmd_tx.write();
630 let _ = cmd_tx.send(HandlerCommand::SetClient(ws_client));
631
632 let instruments: Vec<InstrumentAny> =
633 self.instruments_cache.load().values().cloned().collect();
634
635 if !instruments.is_empty() {
636 log::debug!(
637 "Sending {} cached instruments to handler",
638 instruments.len()
639 );
640 let _ = cmd_tx.send(HandlerCommand::InitializeInstruments(instruments));
641 }
642
643 if let Some(interval) = self.heartbeat_interval {
644 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
645 }
646
647 *command_sender = cmd_tx.clone();
648 }
649
650 let subscriptions_state = self.subscriptions_state.clone();
652 let credential = self.credential.clone();
653 let auth_tracker = self.auth_tracker.clone();
654 let auth_state = self.auth_state.clone();
655 let heartbeat_interval = self.heartbeat_interval;
656
657 let handler_task = async move {
658 const MAX_REAUTH_ATTEMPTS: u32 = 3;
659
660 let mut pending_reauth = false;
661 let mut reauth_attempts: u32 = 0;
662
663 let mut refresh_cancel = CancellationToken::new();
664 let mut retry_cancel = CancellationToken::new();
665 let mut lifecycle_futures = FuturesUnordered::new();
666
667 loop {
668 let message = {
669 let next = handler.next();
672 tokio::pin!(next);
673
674 loop {
675 tokio::select! {
676 message = &mut next => break message,
677 _ = lifecycle_futures.next(), if !lifecycle_futures.is_empty() => {}
678 }
679 }
680 };
681
682 match message {
683 Some(msg) => match msg {
684 NautilusWsMessage::Reconnected => {
685 log::info!("Reconnected to WebSocket");
686
687 refresh_cancel.cancel();
689 refresh_cancel = CancellationToken::new();
690 retry_cancel.cancel();
691 retry_cancel = CancellationToken::new();
692
693 if let Some(interval) = heartbeat_interval {
697 let _ = cmd_tx.send(HandlerCommand::SetHeartbeat { interval });
698 }
699
700 let channels = subscriptions_state.reset_after_reconnect();
701
702 if let Some(cred) = &credential {
704 log::info!("Re-authenticating after reconnection...");
705
706 let _rx = auth_tracker.begin();
707 pending_reauth = true;
708 reauth_attempts = 1;
709
710 let previous_scope =
711 auth_state.read().await.as_ref().map(|s| s.scope.clone());
712
713 send_auth_request(cred, previous_scope, &cmd_tx);
714 } else {
715 if !channels.is_empty() {
717 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
718 }
719 }
720 }
721 NautilusWsMessage::Authenticated(result) => {
722 let timestamp = get_atomic_clock_realtime().get_time_ms();
723 let new_auth_state = AuthState::from_auth_result(&result, timestamp);
724 *auth_state.write().await = Some(new_auth_state);
725
726 refresh_cancel.cancel();
727 refresh_cancel = CancellationToken::new();
728 retry_cancel.cancel();
729 retry_cancel = CancellationToken::new();
730
731 lifecycle_futures.push(
732 refresh_token_after_delay(
733 result.expires_in,
734 result.refresh_token.clone(),
735 cmd_tx.clone(),
736 refresh_cancel.clone(),
737 )
738 .boxed(),
739 );
740
741 if pending_reauth {
742 pending_reauth = false;
743 reauth_attempts = 0;
744 log::info!(
745 "Re-authentication successful (scope: {}), resubscribing to channels",
746 result.scope
747 );
748
749 let channels = subscriptions_state.all_topics();
750
751 if !channels.is_empty() {
752 let _ = cmd_tx.send(HandlerCommand::Subscribe { channels });
753 }
754 } else {
755 log::debug!(
756 "Auth state stored: scope={}, expires_in={}s",
757 result.scope,
758 result.expires_in
759 );
760 }
761 }
762 NautilusWsMessage::AuthenticationFailed(reason) => {
763 if pending_reauth && reauth_attempts < MAX_REAUTH_ATTEMPTS {
764 let delay_secs = 1u64 << reauth_attempts; log::warn!(
766 "Re-authentication attempt {reauth_attempts}/{MAX_REAUTH_ATTEMPTS} \
767 failed: {reason} - retrying in {delay_secs}s",
768 );
769 reauth_attempts += 1;
770
771 if let Some(cred) = &credential {
774 let cred = cred.clone();
775 let auth_state = auth_state.clone();
776 let auth_tracker = auth_tracker.clone();
777 let cmd_tx = cmd_tx.clone();
778 let cancel = retry_cancel.clone();
779
780 lifecycle_futures.push(
781 async move {
782 tokio::select! {
783 () = tokio::time::sleep(Duration::from_secs(delay_secs)) => {}
784 () = cancel.cancelled() => return,
785 }
786 let _rx = auth_tracker.begin();
787 let previous_scope = auth_state
788 .read()
789 .await
790 .as_ref()
791 .map(|s| s.scope.clone());
792 send_auth_request(&cred, previous_scope, &cmd_tx);
793 }
794 .boxed(),
795 );
796 }
797 } else if pending_reauth {
798 pending_reauth = false;
799 reauth_attempts = 0;
800 log::error!(
801 "Re-authentication failed after {MAX_REAUTH_ATTEMPTS} \
802 attempts: {reason} \
803 - resubscribing to public channels only"
804 );
805
806 let all = subscriptions_state.all_topics();
807 let mut public_channels = Vec::new();
808
809 for ch in &all {
810 if DeribitWsChannel::requires_auth(ch) {
811 subscriptions_state.mark_unsubscribe(ch);
814 subscriptions_state.confirm_unsubscribe(ch);
815 subscriptions_state.remove_reference(ch);
816 } else {
817 public_channels.push(ch.clone());
818 }
819 }
820
821 if !public_channels.is_empty() {
822 let _ = cmd_tx.send(HandlerCommand::Subscribe {
823 channels: public_channels,
824 });
825 }
826 } else {
827 log::error!("Authentication failed: {reason}");
828 }
829 }
830 _ => {}
831 },
832 None => {
833 log::debug!("Handler returned None, stopping task");
834 break;
835 }
836 }
837 }
838 };
839
840 if let Err(e) = handler_spawner.spawn(handler_task) {
841 if let Some(control) = &self.socket_control {
842 control.deregister();
843 }
844 self.out_rx = None;
845 anyhow::bail!("failed to register WebSocket handler task: {e}");
846 }
847 log::debug!("Connected to WebSocket");
848
849 Ok(())
850 }
851
852 pub async fn close(&self) -> DeribitWsResult<()> {
858 self.begin_shutdown();
859 let connect_lock = Arc::clone(&self.connect_lock);
860 let _connect_guard = connect_lock.lock().await;
861 self.close_locked().await
862 }
863
864 async fn close_locked(&self) -> DeribitWsResult<()> {
865 log::debug!("Closing WebSocket connection");
866 self.begin_shutdown();
867
868 let _ = self.command_sender().send(HandlerCommand::Disconnect);
869
870 self.finish_handler().await?;
871
872 self.auth_tracker.invalidate();
873
874 if let Some(control) = &self.socket_control {
875 control.deregister();
876 }
877 Ok(())
878 }
879
880 async fn finish_handler(&self) -> DeribitWsResult<()> {
881 self.handler_tasks
882 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
883 .await
884 .map_err(|e| {
885 DeribitWsError::ClientError(format!("WebSocket handler shutdown failed: {e}"))
886 })
887 }
888
889 pub fn stream(&mut self) -> DeribitWsResult<impl Stream<Item = NautilusWsMessage> + 'static> {
895 let rx = self.out_rx.take().ok_or_else(|| {
896 DeribitWsError::ClientError(
897 "Stream receiver already taken or not connected".to_string(),
898 )
899 })?;
900 let mut rx = Arc::try_unwrap(rx).map_err(|_| {
901 DeribitWsError::ClientError(
902 "Cannot take stream ownership - other references exist".to_string(),
903 )
904 })?;
905
906 Ok(async_stream::stream! {
907 while let Some(msg) = rx.recv().await {
908 yield msg;
909 }
910 })
911 }
912
913 #[must_use]
915 pub fn has_credentials(&self) -> bool {
916 self.credential.is_some()
917 }
918
919 #[must_use]
921 pub fn is_authenticated(&self) -> bool {
922 self.auth_tracker.is_authenticated()
923 }
924
925 pub async fn authenticate(&self, session_name: Option<&str>) -> DeribitWsResult<()> {
944 let credential = self.credential.as_ref().ok_or_else(|| {
945 DeribitWsError::Authentication("API credentials not configured".to_string())
946 })?;
947
948 let scope = session_name.map(|name| format!("session:{name}"));
950
951 log::debug!("Authenticating WebSocket...");
952
953 let rx = self.auth_tracker.begin();
954
955 let cmd_tx = self.command_sender().clone();
957 send_auth_request(credential, scope, &cmd_tx);
958
959 match self
961 .auth_tracker
962 .wait_for_result::<DeribitWsError>(Duration::from_secs(self.auth_timeout_secs), rx)
963 .await
964 {
965 Ok(()) => {
966 log::debug!("WebSocket authenticated successfully");
967 Ok(())
968 }
969 Err(e) => {
970 log::error!("WebSocket authentication failed: error={e}");
971 Err(e)
972 }
973 }
974 }
975
976 pub async fn authenticate_session(&self, session_name: &str) -> DeribitWsResult<()> {
985 self.authenticate(Some(session_name)).await
986 }
987
988 pub async fn auth_state(&self) -> Option<AuthState> {
992 self.auth_state.read().await.clone()
993 }
994
995 pub async fn access_token(&self) -> Option<String> {
997 self.auth_state
998 .read()
999 .await
1000 .as_ref()
1001 .map(|s| s.access_token.clone())
1002 }
1003
1004 pub fn set_account_id(&mut self, account_id: AccountId) {
1006 self.account_id = Some(account_id);
1007 }
1008
1009 pub fn set_bars_timestamp_on_close(&mut self, value: bool) {
1013 self.bars_timestamp_on_close = value;
1014 }
1015
1016 async fn send_subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1017 let mut channels_to_subscribe = Vec::new();
1018
1019 for channel in channels {
1020 if self.subscriptions_state.add_reference(&channel) {
1021 self.subscriptions_state.mark_subscribe(&channel);
1022 channels_to_subscribe.push(channel);
1023 } else {
1024 log::debug!("Already subscribed to {channel}, skipping duplicate subscription");
1025 }
1026 }
1027
1028 if channels_to_subscribe.is_empty() {
1029 return Ok(());
1030 }
1031
1032 if let Err(e) = self.command_sender().send(HandlerCommand::Subscribe {
1033 channels: channels_to_subscribe.clone(),
1034 }) {
1035 for channel in &channels_to_subscribe {
1037 self.subscriptions_state.remove_reference(channel);
1038 self.subscriptions_state.mark_unsubscribe(channel);
1039 self.subscriptions_state.confirm_unsubscribe(channel);
1040 }
1041 return Err(DeribitWsError::Send(e.to_string()));
1042 }
1043
1044 log::debug!(
1045 "Sent subscribe for {} channels",
1046 channels_to_subscribe.len()
1047 );
1048 Ok(())
1049 }
1050
1051 async fn send_unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1052 let mut channels_to_unsubscribe = Vec::new();
1053
1054 for channel in channels {
1055 if self.subscriptions_state.remove_reference(&channel) {
1056 self.subscriptions_state.mark_unsubscribe(&channel);
1057 channels_to_unsubscribe.push(channel);
1058 } else {
1059 log::debug!("Still has references to {channel}, skipping unsubscription");
1060 }
1061 }
1062
1063 if channels_to_unsubscribe.is_empty() {
1064 return Ok(());
1065 }
1066
1067 if let Err(e) = self.command_sender().send(HandlerCommand::Unsubscribe {
1068 channels: channels_to_unsubscribe.clone(),
1069 }) {
1070 for channel in &channels_to_unsubscribe {
1077 self.subscriptions_state.confirm_unsubscribe(channel);
1078 self.subscriptions_state.add_reference(channel);
1079 self.subscriptions_state.mark_subscribe(channel);
1080 self.subscriptions_state.confirm_subscribe(channel);
1081 }
1082 return Err(DeribitWsError::Send(e.to_string()));
1083 }
1084
1085 log::debug!(
1086 "Sent unsubscribe for {} channels",
1087 channels_to_unsubscribe.len()
1088 );
1089 Ok(())
1090 }
1091
1092 pub async fn subscribe_trades(
1103 &self,
1104 instrument_id: InstrumentId,
1105 interval: Option<DeribitUpdateInterval>,
1106 ) -> DeribitWsResult<()> {
1107 let interval = interval.unwrap_or_default();
1108 self.check_auth_requirement(interval)?;
1109 let channel =
1110 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1111 self.send_subscribe(vec![channel]).await
1112 }
1113
1114 pub async fn unsubscribe_trades(
1120 &self,
1121 instrument_id: InstrumentId,
1122 interval: Option<DeribitUpdateInterval>,
1123 ) -> DeribitWsResult<()> {
1124 let interval = interval.unwrap_or_default();
1125 let channel =
1126 DeribitWsChannel::Trades.format_channel(instrument_id.symbol.as_str(), Some(interval));
1127 self.send_unsubscribe(vec![channel]).await
1128 }
1129
1130 pub async fn subscribe_book(
1141 &self,
1142 instrument_id: InstrumentId,
1143 interval: Option<DeribitUpdateInterval>,
1144 ) -> DeribitWsResult<()> {
1145 let interval = interval.unwrap_or_default();
1146 self.check_auth_requirement(interval)?;
1147 let channel =
1148 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1149 self.send_subscribe(vec![channel]).await
1150 }
1151
1152 pub async fn unsubscribe_book(
1158 &self,
1159 instrument_id: InstrumentId,
1160 interval: Option<DeribitUpdateInterval>,
1161 ) -> DeribitWsResult<()> {
1162 let interval = interval.unwrap_or_default();
1163 let channel =
1164 DeribitWsChannel::Book.format_channel(instrument_id.symbol.as_str(), Some(interval));
1165 self.send_unsubscribe(vec![channel]).await
1166 }
1167
1168 pub async fn subscribe_book_grouped(
1178 &self,
1179 instrument_id: InstrumentId,
1180 group: &str,
1181 depth: u32,
1182 interval: Option<DeribitUpdateInterval>,
1183 ) -> DeribitWsResult<()> {
1184 let interval = match interval {
1186 Some(DeribitUpdateInterval::Raw) | None => DeribitUpdateInterval::Ms100,
1187 Some(i) => i,
1188 };
1189
1190 let normalized_depth = if depth < 5 {
1191 1
1192 } else if depth < 15 {
1193 10
1194 } else {
1195 20
1196 };
1197
1198 let channel = format!(
1199 "book.{}.{}.{}.{}",
1200 instrument_id.symbol,
1201 group,
1202 normalized_depth,
1203 interval.as_str()
1204 );
1205 log::debug!("Subscribing to grouped book channel: {channel}");
1206 self.send_subscribe(vec![channel]).await
1207 }
1208
1209 pub async fn unsubscribe_book_grouped(
1217 &self,
1218 instrument_id: InstrumentId,
1219 group: &str,
1220 depth: u32,
1221 interval: Option<DeribitUpdateInterval>,
1222 ) -> DeribitWsResult<()> {
1223 let interval = match interval {
1225 Some(DeribitUpdateInterval::Raw) | None => DeribitUpdateInterval::Ms100,
1226 Some(i) => i,
1227 };
1228
1229 let normalized_depth = if depth < 5 {
1230 1
1231 } else if depth < 15 {
1232 10
1233 } else {
1234 20
1235 };
1236
1237 let channel = format!(
1238 "book.{}.{}.{}.{}",
1239 instrument_id.symbol,
1240 group,
1241 normalized_depth,
1242 interval.as_str()
1243 );
1244 self.send_unsubscribe(vec![channel]).await
1245 }
1246
1247 pub async fn subscribe_ticker(
1258 &self,
1259 instrument_id: InstrumentId,
1260 interval: Option<DeribitUpdateInterval>,
1261 ) -> DeribitWsResult<()> {
1262 let interval = interval.unwrap_or_default();
1263 self.check_auth_requirement(interval)?;
1264 let channel =
1265 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1266 self.send_subscribe(vec![channel]).await
1267 }
1268
1269 pub async fn unsubscribe_ticker(
1275 &self,
1276 instrument_id: InstrumentId,
1277 interval: Option<DeribitUpdateInterval>,
1278 ) -> DeribitWsResult<()> {
1279 let interval = interval.unwrap_or_default();
1280 let channel =
1281 DeribitWsChannel::Ticker.format_channel(instrument_id.symbol.as_str(), Some(interval));
1282 self.send_unsubscribe(vec![channel]).await
1283 }
1284
1285 pub async fn subscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1293 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1294 self.send_subscribe(vec![channel]).await
1295 }
1296
1297 pub async fn unsubscribe_quotes(&self, instrument_id: InstrumentId) -> DeribitWsResult<()> {
1303 let channel = DeribitWsChannel::Quote.format_channel(instrument_id.symbol.as_str(), None);
1304 self.send_unsubscribe(vec![channel]).await
1305 }
1306
1307 pub async fn subscribe_instrument_status(
1315 &self,
1316 kind: &str,
1317 currency: &str,
1318 ) -> DeribitWsResult<()> {
1319 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1320 self.send_subscribe(vec![channel]).await
1321 }
1322
1323 pub async fn unsubscribe_instrument_status(
1329 &self,
1330 kind: &str,
1331 currency: &str,
1332 ) -> DeribitWsResult<()> {
1333 let channel = DeribitWsChannel::format_instrument_state_channel(kind, currency);
1334 self.send_unsubscribe(vec![channel]).await
1335 }
1336
1337 pub async fn subscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1345 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1346 self.send_subscribe(vec![channel]).await
1347 }
1348
1349 pub async fn unsubscribe_volatility_index(&self, index_name: &str) -> DeribitWsResult<()> {
1355 let channel = DeribitWsChannel::VolatilityIndex.format_channel(index_name, None);
1356 self.send_unsubscribe(vec![channel]).await
1357 }
1358
1359 pub async fn subscribe_perpetual_interests_rates_updates(
1367 &self,
1368 instrument_id: InstrumentId,
1369 interval: Option<DeribitUpdateInterval>,
1370 ) -> DeribitWsResult<()> {
1371 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1372 let channel = DeribitWsChannel::Perpetual
1373 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1374
1375 self.send_subscribe(vec![channel]).await
1376 }
1377
1378 pub async fn unsubscribe_perpetual_interest_rates_updates(
1384 &self,
1385 instrument_id: InstrumentId,
1386 interval: Option<DeribitUpdateInterval>,
1387 ) -> DeribitWsResult<()> {
1388 let interval = interval.unwrap_or(DeribitUpdateInterval::Ms100);
1389 let channel = DeribitWsChannel::Perpetual
1390 .format_channel(instrument_id.symbol.as_str(), Some(interval));
1391
1392 self.send_unsubscribe(vec![channel]).await
1393 }
1394
1395 pub async fn subscribe_chart(
1407 &self,
1408 instrument_id: InstrumentId,
1409 resolution: &str,
1410 ) -> DeribitWsResult<()> {
1411 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1413 self.send_subscribe(vec![channel]).await
1414 }
1415
1416 pub async fn unsubscribe_chart(
1422 &self,
1423 instrument_id: InstrumentId,
1424 resolution: &str,
1425 ) -> DeribitWsResult<()> {
1426 let channel = format!("chart.trades.{}.{}", instrument_id.symbol, resolution);
1427 self.send_unsubscribe(vec![channel]).await
1428 }
1429
1430 pub async fn subscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1439 let resolution = bar_spec_to_resolution(&bar_type);
1440 self.subscribe_chart(bar_type.instrument_id(), &resolution)
1441 .await
1442 }
1443
1444 pub async fn unsubscribe_bars(&self, bar_type: BarType) -> DeribitWsResult<()> {
1450 let resolution = bar_spec_to_resolution(&bar_type);
1451 self.unsubscribe_chart(bar_type.instrument_id(), &resolution)
1452 .await
1453 }
1454
1455 fn check_auth_requirement(&self, interval: DeribitUpdateInterval) -> DeribitWsResult<()> {
1461 if interval.requires_auth() && !self.is_authenticated() {
1462 return Err(DeribitWsError::Authentication(
1463 "Raw streams require authentication. Call authenticate() first.".to_string(),
1464 ));
1465 }
1466 Ok(())
1467 }
1468
1469 pub async fn subscribe_user_orders(&self) -> DeribitWsResult<()> {
1477 if !self.is_authenticated() {
1478 return Err(DeribitWsError::Authentication(
1479 "User orders subscription requires authentication".to_string(),
1480 ));
1481 }
1482 self.send_subscribe(vec!["user.orders.any.any.raw".to_string()])
1483 .await
1484 }
1485
1486 pub async fn unsubscribe_user_orders(&self) -> DeribitWsResult<()> {
1492 self.send_unsubscribe(vec!["user.orders.any.any.raw".to_string()])
1493 .await
1494 }
1495
1496 pub async fn subscribe_user_trades(&self) -> DeribitWsResult<()> {
1504 if !self.is_authenticated() {
1505 return Err(DeribitWsError::Authentication(
1506 "User trades subscription requires authentication".to_string(),
1507 ));
1508 }
1509 self.send_subscribe(vec!["user.trades.any.any.raw".to_string()])
1510 .await
1511 }
1512
1513 pub async fn unsubscribe_user_trades(&self) -> DeribitWsResult<()> {
1519 self.send_unsubscribe(vec!["user.trades.any.any.raw".to_string()])
1520 .await
1521 }
1522
1523 pub async fn subscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1533 if !self.is_authenticated() {
1534 return Err(DeribitWsError::Authentication(
1535 "User portfolio subscription requires authentication".to_string(),
1536 ));
1537 }
1538 self.send_subscribe(vec!["user.portfolio.any".to_string()])
1539 .await
1540 }
1541
1542 pub async fn unsubscribe_user_portfolio(&self) -> DeribitWsResult<()> {
1548 self.send_unsubscribe(vec!["user.portfolio.any".to_string()])
1549 .await
1550 }
1551
1552 pub async fn subscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1558 self.send_subscribe(channels).await
1559 }
1560
1561 pub async fn unsubscribe(&self, channels: Vec<String>) -> DeribitWsResult<()> {
1567 self.send_unsubscribe(channels).await
1568 }
1569
1570 pub async fn submit_order(
1581 &self,
1582 order_side: OrderSide,
1583 params: DeribitOrderParams,
1584 client_order_id: ClientOrderId,
1585 trader_id: TraderId,
1586 strategy_id: StrategyId,
1587 instrument_id: InstrumentId,
1588 ) -> DeribitWsResult<()> {
1589 if !self.is_authenticated() {
1590 return Err(DeribitWsError::Authentication(
1591 "Submit order requires authentication. Call authenticate_session() first."
1592 .to_string(),
1593 ));
1594 }
1595
1596 log::debug!(
1597 "Sending {} order: instrument={}, amount={}, price={:?}, client_order_id={}",
1598 order_side,
1599 params.instrument_name,
1600 params.amount,
1601 params.price,
1602 client_order_id
1603 );
1604
1605 let cmd = match order_side {
1606 OrderSide::Buy => HandlerCommand::Buy {
1607 params,
1608 client_order_id,
1609 trader_id,
1610 strategy_id,
1611 instrument_id,
1612 },
1613 OrderSide::Sell => HandlerCommand::Sell {
1614 params,
1615 client_order_id,
1616 trader_id,
1617 strategy_id,
1618 instrument_id,
1619 },
1620 };
1621
1622 self.command_sender()
1623 .send(cmd)
1624 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1625
1626 Ok(())
1627 }
1628
1629 #[expect(clippy::too_many_arguments)]
1640 pub async fn modify_order(
1641 &self,
1642 order_id: &str,
1643 quantity: Quantity,
1644 price: Price,
1645 client_order_id: ClientOrderId,
1646 trader_id: TraderId,
1647 strategy_id: StrategyId,
1648 instrument_id: InstrumentId,
1649 ) -> DeribitWsResult<()> {
1650 if !self.is_authenticated() {
1651 return Err(DeribitWsError::Authentication(
1652 "Modify order requires authentication. Call authenticate_session() first."
1653 .to_string(),
1654 ));
1655 }
1656
1657 let params = DeribitEditParams {
1658 order_id: order_id.to_string(),
1659 amount: quantity.as_decimal(),
1660 price: Some(price.as_decimal()),
1661 post_only: None,
1662 reject_post_only: None,
1663 reduce_only: None,
1664 trigger_price: None,
1665 };
1666
1667 log::debug!(
1668 "Sending modify order: order_id={order_id}, quantity={quantity}, price={price}, client_order_id={client_order_id}"
1669 );
1670
1671 self.command_sender()
1672 .send(HandlerCommand::Edit {
1673 params,
1674 client_order_id,
1675 trader_id,
1676 strategy_id,
1677 instrument_id,
1678 })
1679 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1680
1681 Ok(())
1682 }
1683
1684 pub async fn cancel_order(
1695 &self,
1696 order_id: &str,
1697 client_order_id: ClientOrderId,
1698 trader_id: TraderId,
1699 strategy_id: StrategyId,
1700 instrument_id: InstrumentId,
1701 ) -> DeribitWsResult<()> {
1702 if !self.is_authenticated() {
1703 return Err(DeribitWsError::Authentication(
1704 "Cancel order requires authentication. Call authenticate_session() first."
1705 .to_string(),
1706 ));
1707 }
1708
1709 let params = DeribitCancelParams {
1710 order_id: order_id.to_string(),
1711 };
1712
1713 log::debug!("Sending cancel order: order_id={order_id}, client_order_id={client_order_id}");
1714
1715 self.command_sender()
1716 .send(HandlerCommand::Cancel {
1717 params,
1718 client_order_id,
1719 trader_id,
1720 strategy_id,
1721 instrument_id,
1722 })
1723 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1724
1725 Ok(())
1726 }
1727
1728 pub async fn cancel_all_orders(
1739 &self,
1740 instrument_id: InstrumentId,
1741 order_type: Option<String>,
1742 ) -> DeribitWsResult<()> {
1743 if !self.is_authenticated() {
1744 return Err(DeribitWsError::Authentication(
1745 "Cancel all orders requires authentication. Call authenticate_session() first."
1746 .to_string(),
1747 ));
1748 }
1749
1750 let instrument_name = instrument_id.symbol.to_string();
1751 let params = DeribitCancelAllByInstrumentParams {
1752 instrument_name: instrument_name.clone(),
1753 order_type,
1754 };
1755
1756 log::debug!("Sending cancel_all_orders: instrument={instrument_name}");
1757
1758 self.command_sender()
1759 .send(HandlerCommand::CancelAllByInstrument {
1760 params,
1761 instrument_id,
1762 })
1763 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1764
1765 Ok(())
1766 }
1767
1768 pub async fn query_order(
1779 &self,
1780 order_id: &str,
1781 client_order_id: ClientOrderId,
1782 trader_id: TraderId,
1783 strategy_id: StrategyId,
1784 instrument_id: InstrumentId,
1785 ) -> DeribitWsResult<()> {
1786 if !self.is_authenticated() {
1787 return Err(DeribitWsError::Authentication(
1788 "Query order state requires authentication. Call authenticate_session() first."
1789 .to_string(),
1790 ));
1791 }
1792
1793 log::debug!("Sending query_order: order_id={order_id}, client_order_id={client_order_id}");
1794
1795 self.command_sender()
1796 .send(HandlerCommand::GetOrderState {
1797 order_id: order_id.to_string(),
1798 client_order_id,
1799 trader_id,
1800 strategy_id,
1801 instrument_id,
1802 })
1803 .map_err(|e| DeribitWsError::Send(e.to_string()))?;
1804
1805 Ok(())
1806 }
1807
1808 fn command_sender(&self) -> RwLockReadGuard<'_, CommandSender> {
1809 self.cmd_tx.read()
1810 }
1811}
1812
1813#[cfg(test)]
1814mod tests {
1815 use rstest::rstest;
1816
1817 use super::*;
1818
1819 struct DropSignal(Arc<AtomicBool>);
1820
1821 impl Drop for DropSignal {
1822 fn drop(&mut self) {
1823 self.0.store(true, Ordering::Release);
1824 }
1825 }
1826
1827 #[tokio::test]
1828 async fn test_last_client_owner_drop_aborts_handler_task() {
1829 let client = DeribitWebSocketClient::new_unauthenticated(
1830 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
1831 30,
1832 DeribitEnvironment::Testnet,
1833 )
1834 .unwrap();
1835 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1836 let dropped = Arc::new(AtomicBool::new(false));
1837 let drop_signal = DropSignal(Arc::clone(&dropped));
1838 client
1839 .handler_tasks
1840 .spawn(async move {
1841 let _drop_signal = drop_signal;
1842 started_tx.send(()).expect("started receiver");
1843 std::future::pending::<()>().await;
1844 })
1845 .expect("handler task should register");
1846 started_rx.await.expect("handler task started");
1847 let clone = client.clone();
1848
1849 drop(client);
1850 assert!(!dropped.load(Ordering::Acquire));
1851 drop(clone);
1852
1853 tokio::time::timeout(Duration::from_secs(1), async {
1854 while !dropped.load(Ordering::Acquire) {
1855 tokio::task::yield_now().await;
1856 }
1857 })
1858 .await
1859 .expect("handler task aborted");
1860 }
1861
1862 #[rstest]
1863 #[tokio::test]
1864 async fn test_unsubscribe_send_failure_restores_subscription() {
1865 let client = DeribitWebSocketClient::new_unauthenticated(
1866 Some("ws://127.0.0.1:0/ws/api/v2".to_string()),
1867 30,
1868 DeribitEnvironment::Testnet,
1869 )
1870 .unwrap();
1871 let channel = "trades.BTC-PERPETUAL.raw";
1872 client.subscriptions_state.add_reference(channel);
1873 client.subscriptions_state.mark_subscribe(channel);
1874 client.subscriptions_state.confirm_subscribe(channel);
1875
1876 let error = client
1877 .send_unsubscribe(vec![channel.to_string()])
1878 .await
1879 .unwrap_err();
1880
1881 assert!(matches!(error, DeribitWsError::Send(_)));
1882 assert_eq!(client.subscriptions_state.get_reference_count(channel), 1);
1883 assert_eq!(client.subscriptions_state.len(), 1);
1884 assert_eq!(client.subscriptions_state.all_topics(), [channel]);
1885 assert!(
1886 client
1887 .subscriptions_state
1888 .pending_subscribe_topics()
1889 .is_empty()
1890 );
1891 assert!(
1892 client
1893 .subscriptions_state
1894 .pending_unsubscribe_topics()
1895 .is_empty()
1896 );
1897 }
1898}