1use std::{
25 fmt::Debug,
26 sync::{
27 Arc,
28 atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering},
29 },
30 time::Duration,
31};
32
33use alloy::signers::local::PrivateKeySigner;
34use arc_swap::ArcSwap;
35use dashmap::DashMap;
36#[cfg(test)]
37use nautilus_common::live::get_runtime;
38use nautilus_core::UUID4;
39use nautilus_live::{
40 SocketControl,
41 task::{SharedTaskSlot, TaskJoinOutcome, TaskSlot, finish_task},
42};
43use nautilus_network::{
44 mode::ConnectionMode,
45 ratelimiter::clock::MonotonicClock,
46 websocket::{
47 AuthTracker, TransportBackend, WebSocketClient, WebSocketConfig, channel_message_handler,
48 },
49};
50use serde::{Serialize, de::DeserializeOwned};
51use serde_json::Value;
52use ustr::Ustr;
53
54use super::{
55 error::{DeriveWsError, Result},
56 handler::{
57 DeriveWsMessage, FeedHandler, HandlerCommand, orderbook_subscribe_params,
58 ticker_subscribe_params, trades_subscribe_params,
59 },
60 messages::{
61 DeriveWsChannel, WsLoginParams, WsLoginResult, WsSubscribeParams, WsSubscribeResult,
62 WsUnsubscribeParams, WsUnsubscribeResult, methods, orderbook_channel, ticker_channel,
63 trades_channel,
64 },
65};
66use crate::{
67 common::{
68 consts::{
69 RECONNECT_BACKOFF_FACTOR, RECONNECT_BASE_BACKOFF, RECONNECT_JITTER_MS,
70 RECONNECT_MAX_BACKOFF, RECONNECT_TIMEOUT, WS_HEARTBEAT_SECS, WS_HEARTBEAT_TIMEOUT,
71 WS_REQUEST_TIMEOUT,
72 },
73 enums::DeriveEnvironment,
74 rate_limit::{
75 DeriveRateLimiter, FixedWindowLimiter, FixedWindowLimits, RateClass,
76 rate_class_for_method,
77 },
78 urls,
79 },
80 http::{
81 models::{
82 DeriveCancelByInstrumentResult, DeriveCancelByLabelResult, DeriveEmptyResult,
83 DeriveOpenOrdersResult, DeriveOrder, DeriveOrderResult, DeriveReplaceOutcome,
84 DeriveReplaceResult,
85 },
86 query::{
87 DeriveCancelAllParams, DeriveCancelByInstrumentParams, DeriveCancelByLabelParams,
88 DeriveCancelParams, DeriveCancelTriggerOrderParams, DeriveGetTriggerOrdersParams,
89 DeriveOrderParams, DeriveReplaceParams, DeriveTriggerOrderParams,
90 },
91 },
92 signing::auth::build_ws_login,
93};
94
95#[derive(Clone)]
98pub struct DeriveWsCredentials {
99 pub wallet_address: String,
101 pub signer: PrivateKeySigner,
103}
104
105impl DeriveWsCredentials {
106 pub fn new(wallet_address: impl Into<String>, session_key_hex: &str) -> Result<Self> {
112 let signer: PrivateKeySigner = session_key_hex
113 .parse()
114 .map_err(|e| DeriveWsError::transport(format!("invalid session key: {e}")))?;
115 Ok(Self {
116 wallet_address: wallet_address.into(),
117 signer,
118 })
119 }
120}
121
122impl Debug for DeriveWsCredentials {
123 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124 f.debug_struct(stringify!(DeriveWsCredentials))
125 .field("wallet_address", &self.wallet_address)
126 .field("signer", &"***redacted***")
127 .finish()
128 }
129}
130
131type WsRateLimiter = DeriveRateLimiter;
134
135const MAX_SESSION_RECOVERY_ATTEMPTS: u32 = 3;
136const SUBSCRIPTION_ACCEPTED_STATUSES: &[&str] = &["ok"];
137const SUBSCRIPTION_REPLAY_ACCEPTED_STATUSES: &[&str] = &["ok", "already subscribed"];
138pub(super) const UNAUTHENTICATED_CONNECTION_EPOCH: u64 = u64::MAX;
139
140#[derive(Debug)]
146pub struct DeriveWebSocketClient {
147 url: String,
148 transport_backend: TransportBackend,
149 proxy_url: Option<String>,
150 connection_mode: Arc<ArcSwap<AtomicU8>>,
151 connection_epoch: Arc<ArcSwap<AtomicU64>>,
152 signal: Arc<AtomicBool>,
153 auth_tracker: AuthTracker,
154 authenticated_epoch: Arc<AtomicU64>,
155 credentials: Option<DeriveWsCredentials>,
156 next_id: Arc<AtomicU64>,
157 cmd_tx: Arc<tokio::sync::RwLock<tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>,
158 out_rx: Option<tokio::sync::mpsc::UnboundedReceiver<DeriveWsMessage>>,
159 subscriptions: Arc<DashMap<String, ()>>,
160 subscription_lock: Arc<tokio::sync::Mutex<()>>,
161 task_handle: TaskSlot<()>,
162 send_task: Arc<SharedTaskSlot<()>>,
163 shutdown_errors: Vec<String>,
164 request_timeout: Duration,
165 conn_id: Arc<ArcSwap<String>>,
166 rate_limiter: Arc<WsRateLimiter>,
167 socket_control: Option<SocketControl>,
168}
169
170#[derive(Clone, Debug)]
171pub(crate) struct DeriveWebSocketShutdownHandle {
172 signal: Arc<AtomicBool>,
173}
174
175impl DeriveWebSocketShutdownHandle {
176 pub(crate) fn begin_shutdown(&self) {
177 self.signal.store(true, Ordering::Release);
178 }
179}
180
181struct DeriveWebSocketSetupGuard {
182 shutdown: DeriveWebSocketShutdownHandle,
183 armed: bool,
184}
185
186impl DeriveWebSocketSetupGuard {
187 fn new(shutdown: DeriveWebSocketShutdownHandle) -> Self {
188 Self {
189 shutdown,
190 armed: true,
191 }
192 }
193
194 fn disarm(mut self) {
195 self.armed = false;
196 }
197}
198
199impl Drop for DeriveWebSocketSetupGuard {
200 fn drop(&mut self) {
201 if self.armed {
202 self.shutdown.begin_shutdown();
203 }
204 }
205}
206
207#[derive(Debug, Clone)]
209pub struct DeriveWebSocketSubscriptionHandle {
210 cmd_tx: Arc<tokio::sync::RwLock<tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>,
211 subscriptions: Arc<DashMap<String, ()>>,
212 subscription_lock: Arc<tokio::sync::Mutex<()>>,
213 request_timeout: Duration,
214 rate_limiter: Arc<WsRateLimiter>,
215}
216
217#[derive(Debug, Clone)]
226pub struct DeriveWsExecutionHandle {
227 cmd_tx: Arc<tokio::sync::RwLock<tokio::sync::mpsc::UnboundedSender<HandlerCommand>>>,
228 auth_tracker: AuthTracker,
229 request_timeout: Duration,
230 conn_id: Arc<ArcSwap<String>>,
231 rate_limiter: Arc<WsRateLimiter>,
232}
233
234#[derive(Debug)]
235pub(crate) struct MatchingRateLimitReservation {
236 method: &'static str,
237 instrument_name: Ustr,
238 window: u32,
239}
240
241impl DeriveWebSocketClient {
242 #[must_use]
245 pub fn new(
246 url: Option<String>,
247 environment: DeriveEnvironment,
248 transport_backend: TransportBackend,
249 proxy_url: Option<String>,
250 ) -> Self {
251 let url = url.unwrap_or_else(|| urls::ws_url(environment).to_string());
252 Self::build(
253 url,
254 transport_backend,
255 proxy_url,
256 None,
257 FixedWindowLimits::websocket(None, None),
258 )
259 }
260
261 #[must_use]
269 pub fn with_credentials(
270 url: Option<String>,
271 environment: DeriveEnvironment,
272 transport_backend: TransportBackend,
273 proxy_url: Option<String>,
274 credentials: DeriveWsCredentials,
275 max_matching_requests_per_second: Option<u32>,
276 max_per_instrument_matching_requests_per_second: Option<u32>,
277 ) -> Self {
278 let url = url.unwrap_or_else(|| urls::ws_url(environment).to_string());
279 let limits = FixedWindowLimits::websocket(
280 max_matching_requests_per_second,
281 max_per_instrument_matching_requests_per_second,
282 );
283 Self::build(url, transport_backend, proxy_url, Some(credentials), limits)
284 }
285
286 fn build(
287 url: String,
288 transport_backend: TransportBackend,
289 proxy_url: Option<String>,
290 credentials: Option<DeriveWsCredentials>,
291 limits: FixedWindowLimits,
292 ) -> Self {
293 let connection_mode = Arc::new(ArcSwap::new(Arc::new(AtomicU8::new(
294 ConnectionMode::Closed as u8,
295 ))));
296 let connection_epoch = Arc::new(ArcSwap::new(Arc::new(AtomicU64::new(0))));
297
298 let (placeholder_tx, _) = tokio::sync::mpsc::unbounded_channel();
300
301 let rate_limiter = Arc::new(FixedWindowLimiter::new(limits, MonotonicClock {}));
307 Self {
308 url,
309 transport_backend,
310 proxy_url,
311 connection_mode,
312 connection_epoch,
313 signal: Arc::new(AtomicBool::new(false)),
314 auth_tracker: AuthTracker::new(),
315 authenticated_epoch: Arc::new(AtomicU64::new(UNAUTHENTICATED_CONNECTION_EPOCH)),
316 credentials,
317 next_id: Arc::new(AtomicU64::new(1)),
318 cmd_tx: Arc::new(tokio::sync::RwLock::new(placeholder_tx)),
319 out_rx: None,
320 subscriptions: Arc::new(DashMap::new()),
321 subscription_lock: Arc::new(tokio::sync::Mutex::new(())),
322 task_handle: TaskSlot::new(),
323 send_task: Arc::new(SharedTaskSlot::new()),
324 shutdown_errors: Vec::new(),
325 request_timeout: WS_REQUEST_TIMEOUT,
326 conn_id: Arc::new(ArcSwap::from_pointee(UUID4::new().to_string())),
327 rate_limiter,
328 socket_control: None,
329 }
330 }
331
332 #[must_use]
334 pub fn with_socket_control(mut self, control: SocketControl) -> Self {
335 self.socket_control = Some(control);
336 self
337 }
338
339 #[must_use]
341 pub fn url(&self) -> &str {
342 &self.url
343 }
344
345 pub fn set_request_timeout(&mut self, timeout: Duration) {
349 self.request_timeout = timeout;
350 }
351
352 #[must_use]
355 pub fn is_authenticated(&self) -> bool {
356 self.auth_tracker.is_authenticated()
357 && self.is_active()
358 && self.authenticated_epoch.load(Ordering::Acquire)
359 == self.connection_epoch.load().load(Ordering::Acquire)
360 }
361
362 #[must_use]
364 pub fn is_active(&self) -> bool {
365 self.connection_mode.load().load(Ordering::Relaxed) == ConnectionMode::Active as u8
366 }
367
368 pub async fn connect(&mut self) -> Result<()> {
379 let auth_ok = self.credentials.is_none() || self.is_authenticated();
382 if self.is_active() && auth_ok && self.task_handle.is_some() {
383 log::warn!("Derive WebSocket already connected");
384 return Ok(());
385 }
386
387 if self.task_handle.is_some() || !self.send_task.is_empty() {
389 log::debug!("Tearing down stale Derive WebSocket state before connect");
390 self.teardown().await?;
391 }
392
393 self.signal.store(false, Ordering::Release);
394 let setup_guard = DeriveWebSocketSetupGuard::new(self.shutdown_handle());
395
396 self.authenticated_epoch
397 .store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
398
399 let (message_handler, raw_rx) = channel_message_handler();
400 let cfg = WebSocketConfig {
401 url: self.url.clone(),
402 headers: vec![],
403 heartbeat_interval_secs: Some(WS_HEARTBEAT_SECS),
404 heartbeat_payload: None,
405 connect_timeout_ms: Some(RECONNECT_TIMEOUT.as_millis() as u64),
406 reconnect_delay_initial_ms: Some(RECONNECT_BASE_BACKOFF.as_millis() as u64),
407 reconnect_delay_max_ms: Some(RECONNECT_MAX_BACKOFF.as_millis() as u64),
408 reconnect_backoff_factor: Some(RECONNECT_BACKOFF_FACTOR),
409 reconnect_jitter_ms: Some(RECONNECT_JITTER_MS),
410 reconnect_max_attempts: None,
411 heartbeat_timeout_secs: Some(WS_HEARTBEAT_TIMEOUT.as_secs()),
412 idle_timeout_ms: None,
413 backend: self.transport_backend,
414 proxy_url: self.proxy_url.clone(),
415 };
416 let client = WebSocketClient::builder()
420 .config(cfg)
421 .message_handler(message_handler)
422 .maybe_state_sink(self.socket_control.as_ref().map(SocketControl::sink))
423 .connect()
424 .await
425 .map_err(|e| DeriveWsError::transport(e.to_string()))?;
426
427 client.set_auth_tracker(self.auth_tracker.clone(), false);
431
432 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<HandlerCommand>();
433 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel::<DeriveWsMessage>();
434
435 *self.cmd_tx.write().await = cmd_tx.clone();
436 self.out_rx = Some(out_rx);
437 self.conn_id.store(Arc::new(UUID4::new().to_string()));
438
439 let connection_mode = client.connection_mode_atomic();
440 let connection_epoch = client.connection_epoch_atomic();
441 let reconnect_handle = client.reconnect_handle();
442 self.connection_mode.store(Arc::clone(&connection_mode));
443 self.connection_epoch.store(Arc::clone(&connection_epoch));
444 log::debug!("Derive WebSocket connected: {}", self.url);
445
446 if let Err(e) = cmd_tx.send(HandlerCommand::SetClient(client)) {
447 return Err(DeriveWsError::transport(format!(
448 "failed to send SetClient command: {e}",
449 )));
450 }
451
452 let signal = Arc::clone(&self.signal);
453 let auth_tracker = self.auth_tracker.clone();
454 let authenticated_epoch = Arc::clone(&self.authenticated_epoch);
455 let next_id = Arc::clone(&self.next_id);
456 let credentials = self.credentials.clone();
457 let subscriptions = Arc::clone(&self.subscriptions);
458 let subscription_lock = Arc::clone(&self.subscription_lock);
459 let conn_id = Arc::clone(&self.conn_id);
460 let cmd_tx_for_loop = cmd_tx.clone();
461 let rate_limiter = Arc::clone(&self.rate_limiter);
462 let request_timeout = self.request_timeout;
463 let recovery_connection_mode = Arc::clone(&connection_mode);
464 let recovery_connection_epoch = Arc::clone(&connection_epoch);
465 let send_task = Arc::clone(&self.send_task);
466
467 if let Err(e) = self.task_handle.spawn(async move {
468 let (recovery_tx, mut recovery_rx) = tokio::sync::mpsc::unbounded_channel::<u64>();
469 let recovery_out_tx = out_tx.clone();
470 let recovery_cmd_tx = cmd_tx_for_loop.clone();
471 let recovery_auth_tracker = auth_tracker.clone();
472 let recovery_authenticated_epoch = Arc::clone(&authenticated_epoch);
473 let recovery_subscriptions = Arc::clone(&subscriptions);
474 let recovery_subscription_lock = Arc::clone(&subscription_lock);
475 let recovery_rate_limiter = Arc::clone(&rate_limiter);
476 let recovery_credentials = credentials.clone();
477 let recovery_mode = Arc::clone(&recovery_connection_mode);
478 let recovery_epoch = Arc::clone(&recovery_connection_epoch);
479 let mut recovery_tasks = tokio::task::JoinSet::new();
480
481 recovery_tasks.spawn(async move {
482 while let Some(mut requested_epoch) = recovery_rx.recv().await {
483 while let Ok(epoch) = recovery_rx.try_recv() {
484 requested_epoch = requested_epoch.max(epoch);
485 }
486
487 loop {
488 match recover_session(
489 &recovery_rate_limiter,
490 &recovery_cmd_tx,
491 &recovery_auth_tracker,
492 &recovery_authenticated_epoch,
493 &recovery_mode,
494 &recovery_epoch,
495 recovery_credentials.as_ref(),
496 &recovery_subscriptions,
497 &recovery_subscription_lock,
498 request_timeout,
499 )
500 .await
501 {
502 Ok(recovered_epoch) => {
503 while let Ok(epoch) = recovery_rx.try_recv() {
504 requested_epoch = requested_epoch.max(epoch);
505 }
506
507 if requested_epoch > recovered_epoch
508 || !session_connection_is_active(
509 &recovery_mode,
510 &recovery_epoch,
511 recovered_epoch,
512 )
513 {
514 continue;
515 }
516
517 if recovery_out_tx.send(DeriveWsMessage::Reconnected).is_err() {
518 log::debug!("Derive outer receiver dropped during recovery");
519 }
520 break;
521 }
522 Err(e) => {
523 let mut retry = false;
524
525 while let Ok(epoch) = recovery_rx.try_recv() {
526 requested_epoch = requested_epoch.max(epoch);
527 retry = true;
528 }
529
530 if retry {
531 continue;
532 }
533 log::error!("Derive WebSocket session recovery failed: {e}");
534 let _ = recovery_out_tx
535 .send(DeriveWsMessage::SessionRecoveryFailed(e.to_string()));
536 let _ = recovery_cmd_tx.send(HandlerCommand::Disconnect);
537 return;
538 }
539 }
540 }
541 }
542 });
543
544 let mut handler = FeedHandler::new_with_send_task(
545 signal,
546 cmd_rx,
547 raw_rx,
548 next_id,
549 auth_tracker.clone(),
550 Arc::clone(&authenticated_epoch),
551 send_task,
552 );
553
554 loop {
555 match handler.next().await {
556 Some(DeriveWsMessage::Reconnected) => {
557 log::info!("Derive WebSocket re-establishing session after reconnect");
558 conn_id.store(Arc::new(UUID4::new().to_string()));
559 let epoch = recovery_connection_epoch.load(Ordering::Acquire);
560 if recovery_tx.send(epoch).is_err() {
561 log::error!("Derive WebSocket recovery task stopped unexpectedly");
562 let _ = cmd_tx_for_loop.send(HandlerCommand::Disconnect);
563 }
564 }
565 Some(msg) => {
566 if out_tx.send(msg).is_err() {
567 log::debug!("Derive outer receiver dropped, exiting stream loop");
568 break;
569 }
570 }
571 None => {
572 log::debug!("Derive handler task ended");
573 break;
574 }
575 }
576 }
577 }) {
578 let shutdown_result = self.teardown().await;
579 return Err(DeriveWsError::transport(match shutdown_result {
580 Ok(()) => format!("failed to start WebSocket handler task: {e}"),
581 Err(shutdown_error) => format!(
582 "failed to start WebSocket handler task: {e}; startup rollback failed: \
583 {shutdown_error}"
584 ),
585 }));
586 }
587
588 if let Some(control) = &self.socket_control {
589 control.register(move || reconnect_handle.request_reconnect());
590 }
591
592 if let Some(creds) = self.credentials.clone()
593 && let Err(e) = login_via_handler(
594 &self.rate_limiter,
595 &cmd_tx,
596 &self.auth_tracker,
597 &self.authenticated_epoch,
598 &connection_mode,
599 &connection_epoch,
600 &creds,
601 self.request_timeout,
602 )
603 .await
604 {
605 log::warn!("Derive WebSocket login failed; tearing down transport: {e}");
608 self.teardown().await?;
609 return Err(e);
610 }
611
612 setup_guard.disarm();
613 Ok(())
614 }
615
616 pub(crate) fn begin_shutdown(&self) {
617 self.signal.store(true, Ordering::Release);
618 }
619
620 pub(crate) fn shutdown_handle(&self) -> DeriveWebSocketShutdownHandle {
621 DeriveWebSocketShutdownHandle {
622 signal: Arc::clone(&self.signal),
623 }
624 }
625
626 async fn teardown(&mut self) -> Result<()> {
630 self.begin_shutdown();
631
632 if let Err(e) = self.cmd_tx.read().await.send(HandlerCommand::Disconnect) {
633 log::debug!(
634 "Failed to enqueue Disconnect command (handler may already be shut down): {e}",
635 );
636 }
637
638 match finish_task(
639 &mut self.task_handle,
640 Duration::from_secs(2),
641 Duration::from_secs(2),
642 )
643 .await
644 {
645 None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => {}
646 Some(TaskJoinOutcome::Failed(error)) => self
647 .shutdown_errors
648 .push(format!("WebSocket handler task failed: {error}")),
649 Some(TaskJoinOutcome::Incomplete) => self
650 .shutdown_errors
651 .push("WebSocket handler task did not stop after abort".to_string()),
652 }
653
654 if self.task_handle.is_some() {
655 if let Some(control) = &self.socket_control {
656 control.deregister();
657 }
658 return self.take_shutdown_result();
659 }
660
661 match self
662 .send_task
663 .finish(Duration::from_secs(1), Duration::from_secs(2))
664 .await
665 {
666 None | Some(TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted) => {}
667 Some(TaskJoinOutcome::Failed(error)) => self
668 .shutdown_errors
669 .push(format!("WebSocket send worker failed: {error}")),
670 Some(TaskJoinOutcome::Incomplete) => self
671 .shutdown_errors
672 .push("WebSocket send worker did not stop after abort".to_string()),
673 }
674
675 if !self.send_task.is_empty() {
676 if let Some(control) = &self.socket_control {
677 control.deregister();
678 }
679 return self.take_shutdown_result();
680 }
681
682 let (placeholder_tx, _) = tokio::sync::mpsc::unbounded_channel();
685 *self.cmd_tx.write().await = placeholder_tx;
686 self.out_rx = None;
687 self.connection_mode
688 .store(Arc::new(AtomicU8::new(ConnectionMode::Closed as u8)));
689 self.connection_epoch.store(Arc::new(AtomicU64::new(0)));
690 self.auth_tracker.invalidate();
691 self.authenticated_epoch
692 .store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
693 self.subscriptions.clear();
694 self.signal.store(false, Ordering::Relaxed);
695
696 if let Some(control) = &self.socket_control {
697 control.deregister();
698 }
699
700 self.take_shutdown_result()
701 }
702
703 fn take_shutdown_result(&mut self) -> Result<()> {
704 if self.shutdown_errors.is_empty() {
705 Ok(())
706 } else {
707 Err(DeriveWsError::transport(
708 std::mem::take(&mut self.shutdown_errors).join("; "),
709 ))
710 }
711 }
712
713 pub async fn disconnect(&mut self) -> Result<()> {
720 log::debug!("Disconnecting Derive WebSocket");
721 self.begin_shutdown();
722 self.teardown().await
723 }
724
725 pub async fn subscribe_ticker(&self, instrument_name: &str, interval: &str) -> Result<()> {
733 self.subscription_handle()
734 .subscribe_ticker(instrument_name, interval)
735 .await
736 }
737
738 pub async fn unsubscribe_ticker(&self, instrument_name: &str, interval: &str) -> Result<()> {
745 self.subscription_handle()
746 .unsubscribe_ticker(instrument_name, interval)
747 .await
748 }
749
750 pub async fn subscribe_orderbook(
757 &self,
758 instrument_name: &str,
759 group: &str,
760 depth: &str,
761 ) -> Result<()> {
762 self.subscription_handle()
763 .subscribe_orderbook(instrument_name, group, depth)
764 .await
765 }
766
767 pub async fn unsubscribe_orderbook(
774 &self,
775 instrument_name: &str,
776 group: &str,
777 depth: &str,
778 ) -> Result<()> {
779 self.subscription_handle()
780 .unsubscribe_orderbook(instrument_name, group, depth)
781 .await
782 }
783
784 pub async fn subscribe_trades(&self, instrument_type: &str, currency: &str) -> Result<()> {
791 self.subscription_handle()
792 .subscribe_trades(instrument_type, currency)
793 .await
794 }
795
796 pub async fn unsubscribe_trades(&self, instrument_type: &str, currency: &str) -> Result<()> {
803 self.subscription_handle()
804 .unsubscribe_trades(instrument_type, currency)
805 .await
806 }
807
808 pub async fn subscribe_channels<C>(&self, channels: Vec<C>) -> Result<()>
819 where
820 C: Into<DeriveWsChannel>,
821 {
822 self.subscription_handle()
823 .subscribe_channels(channels)
824 .await
825 }
826
827 pub async fn unsubscribe_channels<C>(&self, channels: Vec<C>) -> Result<()>
835 where
836 C: Into<DeriveWsChannel>,
837 {
838 self.subscription_handle()
839 .unsubscribe_channels(channels)
840 .await
841 }
842
843 pub async fn next_event(&mut self) -> Option<DeriveWsMessage> {
845 if let Some(rx) = self.out_rx.as_mut() {
846 rx.recv().await
847 } else {
848 None
849 }
850 }
851
852 #[must_use]
855 pub fn subscription_count(&self) -> usize {
856 self.subscriptions.len()
857 }
858
859 #[must_use]
861 pub fn subscription_handle(&self) -> DeriveWebSocketSubscriptionHandle {
862 DeriveWebSocketSubscriptionHandle {
863 cmd_tx: Arc::clone(&self.cmd_tx),
864 subscriptions: Arc::clone(&self.subscriptions),
865 subscription_lock: Arc::clone(&self.subscription_lock),
866 request_timeout: self.request_timeout,
867 rate_limiter: Arc::clone(&self.rate_limiter),
868 }
869 }
870
871 #[must_use]
878 pub fn execution_handle(&self) -> DeriveWsExecutionHandle {
879 DeriveWsExecutionHandle {
880 cmd_tx: Arc::clone(&self.cmd_tx),
881 auth_tracker: self.auth_tracker.clone(),
882 request_timeout: self.request_timeout,
883 conn_id: Arc::clone(&self.conn_id),
884 rate_limiter: Arc::clone(&self.rate_limiter),
885 }
886 }
887
888 pub fn take_event_receiver(
893 &mut self,
894 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<DeriveWsMessage>> {
895 self.out_rx.take()
896 }
897}
898
899impl Drop for DeriveWebSocketClient {
900 fn drop(&mut self) {
901 self.signal.store(true, Ordering::Relaxed);
902
903 if let Some(handle) = self.task_handle.as_ref() {
904 handle.abort();
905 }
906
907 if let Some(control) = &self.socket_control {
908 control.deregister();
909 }
910 }
911}
912
913impl DeriveWebSocketSubscriptionHandle {
914 pub(crate) fn has_subscription(&self, channel: &str) -> bool {
915 self.subscriptions.contains_key(channel)
916 }
917
918 pub(crate) fn forget_subscription(&self, channel: &str) {
919 self.subscriptions.remove(channel);
920 }
921
922 pub(crate) fn remember_subscription(&self, channel: &str) {
923 self.subscriptions.insert(channel.to_string(), ());
924 }
925
926 pub async fn subscribe_ticker(&self, instrument_name: &str, interval: &str) -> Result<()> {
933 let channel = ticker_channel(instrument_name, interval);
934 let params = ticker_subscribe_params(instrument_name, interval);
935 self.send_subscribe(channel, ¶ms).await
936 }
937
938 pub async fn unsubscribe_ticker(&self, instrument_name: &str, interval: &str) -> Result<()> {
945 let channel = ticker_channel(instrument_name, interval);
946 self.send_unsubscribe(channel).await
947 }
948
949 pub async fn subscribe_orderbook(
956 &self,
957 instrument_name: &str,
958 group: &str,
959 depth: &str,
960 ) -> Result<()> {
961 let channel = orderbook_channel(instrument_name, group, depth);
962 let params = orderbook_subscribe_params(instrument_name, group, depth);
963 self.send_subscribe(channel, ¶ms).await
964 }
965
966 pub async fn unsubscribe_orderbook(
973 &self,
974 instrument_name: &str,
975 group: &str,
976 depth: &str,
977 ) -> Result<()> {
978 let channel = orderbook_channel(instrument_name, group, depth);
979 self.send_unsubscribe(channel).await
980 }
981
982 pub async fn subscribe_trades(&self, instrument_type: &str, currency: &str) -> Result<()> {
989 let channel = trades_channel(instrument_type, currency);
990 let params = trades_subscribe_params(instrument_type, currency);
991 self.send_subscribe(channel, ¶ms).await
992 }
993
994 pub async fn unsubscribe_trades(&self, instrument_type: &str, currency: &str) -> Result<()> {
1001 let channel = trades_channel(instrument_type, currency);
1002 self.send_unsubscribe(channel).await
1003 }
1004
1005 pub async fn subscribe_channels<C>(&self, channels: Vec<C>) -> Result<()>
1012 where
1013 C: Into<DeriveWsChannel>,
1014 {
1015 let channels = channels.into_iter().map(Into::into).collect::<Vec<_>>();
1016 if channels.is_empty() {
1017 return Ok(());
1018 }
1019 let _guard = self.subscription_lock.lock().await;
1020 let params = WsSubscribeParams { channels };
1021 let cmd_tx = self.cmd_tx.read().await.clone();
1022 let result: WsSubscribeResult = send_request(
1023 &self.rate_limiter,
1024 &cmd_tx,
1025 methods::PUBLIC_SUBSCRIBE,
1026 ¶ms,
1027 self.request_timeout,
1028 )
1029 .await?;
1030
1031 let (confirmed, failure) =
1032 subscription_outcome(¶ms.channels, &result, SUBSCRIPTION_ACCEPTED_STATUSES);
1033
1034 for channel in confirmed {
1035 self.subscriptions.insert(channel, ());
1036 }
1037 failure.map_or(Ok(()), Err)
1038 }
1039
1040 pub async fn unsubscribe_channels<C>(&self, channels: Vec<C>) -> Result<()>
1048 where
1049 C: Into<DeriveWsChannel>,
1050 {
1051 let channels = channels.into_iter().map(Into::into).collect::<Vec<_>>();
1052 if channels.is_empty() {
1053 return Ok(());
1054 }
1055 let _guard = self.subscription_lock.lock().await;
1056 let topics = channel_topics(&channels);
1057 let params = WsUnsubscribeParams { channels };
1058 let cmd_tx = self.cmd_tx.read().await.clone();
1059 let _: WsUnsubscribeResult = send_request(
1060 &self.rate_limiter,
1061 &cmd_tx,
1062 methods::PUBLIC_UNSUBSCRIBE,
1063 ¶ms,
1064 self.request_timeout,
1065 )
1066 .await?;
1067
1068 for channel in topics {
1069 self.subscriptions.remove(&channel);
1070 }
1071
1072 Ok(())
1073 }
1074
1075 async fn send_subscribe(&self, channel: String, params: &WsSubscribeParams) -> Result<()> {
1076 let _guard = self.subscription_lock.lock().await;
1077 let cmd_tx = self.cmd_tx.read().await.clone();
1078 let result: WsSubscribeResult = send_request(
1079 &self.rate_limiter,
1080 &cmd_tx,
1081 methods::PUBLIC_SUBSCRIBE,
1082 params,
1083 self.request_timeout,
1084 )
1085 .await?;
1086
1087 let (confirmed, failure) =
1088 subscription_outcome(¶ms.channels, &result, SUBSCRIPTION_ACCEPTED_STATUSES);
1089
1090 if confirmed.iter().any(|topic| topic == &channel) {
1091 self.subscriptions.insert(channel, ());
1092 }
1093 failure.map_or(Ok(()), Err)
1094 }
1095
1096 async fn send_unsubscribe(&self, channel: String) -> Result<()> {
1097 let _guard = self.subscription_lock.lock().await;
1098 let params = WsUnsubscribeParams {
1099 channels: vec![DeriveWsChannel::from(channel.clone())],
1100 };
1101 let cmd_tx = self.cmd_tx.read().await.clone();
1102 let _: WsUnsubscribeResult = send_request(
1103 &self.rate_limiter,
1104 &cmd_tx,
1105 methods::PUBLIC_UNSUBSCRIBE,
1106 ¶ms,
1107 self.request_timeout,
1108 )
1109 .await?;
1110
1111 self.subscriptions.remove(&channel);
1112
1113 Ok(())
1114 }
1115}
1116
1117impl DeriveWsExecutionHandle {
1118 #[must_use]
1120 pub fn conn_id(&self) -> String {
1121 self.conn_id.load_full().as_ref().clone()
1122 }
1123
1124 pub async fn submit_order(&self, params: &DeriveOrderParams) -> Result<DeriveOrder> {
1136 let reservation = self
1137 .reserve_matching_request(methods::PRIVATE_ORDER, ¶ms.instrument_name)
1138 .await?;
1139 self.submit_order_after_rate_limit(params, reservation)
1140 .await
1141 }
1142
1143 pub(crate) async fn submit_order_after_rate_limit(
1144 &self,
1145 params: &DeriveOrderParams,
1146 reservation: MatchingRateLimitReservation,
1147 ) -> Result<DeriveOrder> {
1148 self.ensure_authenticated(methods::PRIVATE_ORDER)?;
1149 debug_assert_eq!(reservation.method, methods::PRIVATE_ORDER);
1150 self.refresh_matching_reservation(&reservation).await;
1151 let cmd_tx = self.cmd_tx.read().await.clone();
1152 let result: DeriveOrderResult = send_request_typed_after_rate_limit(
1153 &self.rate_limiter,
1154 &cmd_tx,
1155 methods::PRIVATE_ORDER,
1156 params,
1157 self.request_timeout,
1158 )
1159 .await?;
1160 Ok(result.order)
1161 }
1162
1163 pub async fn submit_trigger_order(
1171 &self,
1172 params: &DeriveTriggerOrderParams,
1173 ) -> Result<DeriveOrder> {
1174 let reservation = self
1175 .reserve_matching_request(
1176 methods::PRIVATE_TRIGGER_ORDER,
1177 ¶ms.order.instrument_name,
1178 )
1179 .await?;
1180 self.submit_trigger_order_after_rate_limit(params, reservation)
1181 .await
1182 }
1183
1184 pub(crate) async fn submit_trigger_order_after_rate_limit(
1185 &self,
1186 params: &DeriveTriggerOrderParams,
1187 reservation: MatchingRateLimitReservation,
1188 ) -> Result<DeriveOrder> {
1189 self.ensure_authenticated(methods::PRIVATE_TRIGGER_ORDER)?;
1190 debug_assert_eq!(reservation.method, methods::PRIVATE_TRIGGER_ORDER);
1191 self.refresh_matching_reservation(&reservation).await;
1192 let cmd_tx = self.cmd_tx.read().await.clone();
1193 let result: DeriveOrderResult = send_request_typed_after_rate_limit(
1194 &self.rate_limiter,
1195 &cmd_tx,
1196 methods::PRIVATE_TRIGGER_ORDER,
1197 params,
1198 self.request_timeout,
1199 )
1200 .await?;
1201 Ok(result.order)
1202 }
1203
1204 pub async fn modify_order(&self, params: &DeriveReplaceParams) -> Result<DeriveReplaceOutcome> {
1213 let reservation = self
1214 .reserve_matching_request(methods::PRIVATE_REPLACE, ¶ms.order.instrument_name)
1215 .await?;
1216 self.modify_order_after_rate_limit(params, reservation)
1217 .await
1218 }
1219
1220 pub(crate) async fn modify_order_after_rate_limit(
1221 &self,
1222 params: &DeriveReplaceParams,
1223 reservation: MatchingRateLimitReservation,
1224 ) -> Result<DeriveReplaceOutcome> {
1225 self.ensure_authenticated(methods::PRIVATE_REPLACE)?;
1226 debug_assert_eq!(reservation.method, methods::PRIVATE_REPLACE);
1227 self.refresh_matching_reservation(&reservation).await;
1228 let cmd_tx = self.cmd_tx.read().await.clone();
1229 let result: DeriveReplaceResult = send_request_typed_after_rate_limit(
1230 &self.rate_limiter,
1231 &cmd_tx,
1232 methods::PRIVATE_REPLACE,
1233 params,
1234 self.request_timeout,
1235 )
1236 .await?;
1237 result
1238 .into_outcome(¶ms.order_id_to_cancel, ¶ms.order.label)
1239 .map_err(|message| {
1240 DeriveWsError::Serde(<serde_json::Error as serde::de::Error>::custom(message))
1241 })
1242 }
1243
1244 pub async fn cancel_order(&self, params: &DeriveCancelParams) -> Result<()> {
1252 self.require_authenticated(methods::PRIVATE_CANCEL).await?;
1253 let cmd_tx = self.cmd_tx.read().await.clone();
1254 let _: DeriveEmptyResult = send_request_for_instrument(
1255 &self.rate_limiter,
1256 &cmd_tx,
1257 methods::PRIVATE_CANCEL,
1258 params,
1259 self.request_timeout,
1260 params.instrument_name,
1261 )
1262 .await?;
1263 Ok(())
1264 }
1265
1266 pub async fn cancel_by_instrument(
1274 &self,
1275 params: &DeriveCancelByInstrumentParams,
1276 ) -> Result<DeriveCancelByInstrumentResult> {
1277 self.require_authenticated(methods::PRIVATE_CANCEL_BY_INSTRUMENT)
1278 .await?;
1279 let cmd_tx = self.cmd_tx.read().await.clone();
1280 send_request_typed_for_instrument(
1281 &self.rate_limiter,
1282 &cmd_tx,
1283 methods::PRIVATE_CANCEL_BY_INSTRUMENT,
1284 params,
1285 self.request_timeout,
1286 params.instrument_name,
1287 )
1288 .await
1289 }
1290
1291 pub async fn cancel_trigger_order(
1299 &self,
1300 params: &DeriveCancelTriggerOrderParams,
1301 ) -> Result<DeriveOrder> {
1302 self.require_authenticated(methods::PRIVATE_CANCEL_TRIGGER_ORDER)
1303 .await?;
1304 let cmd_tx = self.cmd_tx.read().await.clone();
1305 send_request_typed(
1306 &self.rate_limiter,
1307 &cmd_tx,
1308 methods::PRIVATE_CANCEL_TRIGGER_ORDER,
1309 params,
1310 self.request_timeout,
1311 )
1312 .await
1313 }
1314
1315 pub async fn cancel_by_label(
1324 &self,
1325 params: &DeriveCancelByLabelParams,
1326 ) -> Result<DeriveCancelByLabelResult> {
1327 self.require_authenticated(methods::PRIVATE_CANCEL_BY_LABEL)
1328 .await?;
1329 let cmd_tx = self.cmd_tx.read().await.clone();
1330 send_request_typed(
1331 &self.rate_limiter,
1332 &cmd_tx,
1333 methods::PRIVATE_CANCEL_BY_LABEL,
1334 params,
1335 self.request_timeout,
1336 )
1337 .await
1338 }
1339
1340 pub async fn get_trigger_orders(
1349 &self,
1350 params: &DeriveGetTriggerOrdersParams,
1351 ) -> Result<DeriveOpenOrdersResult> {
1352 self.require_authenticated(methods::PRIVATE_GET_TRIGGER_ORDERS)
1353 .await?;
1354 let cmd_tx = self.cmd_tx.read().await.clone();
1355 send_request_typed(
1356 &self.rate_limiter,
1357 &cmd_tx,
1358 methods::PRIVATE_GET_TRIGGER_ORDERS,
1359 params,
1360 self.request_timeout,
1361 )
1362 .await
1363 }
1364
1365 pub async fn cancel_all_orders(&self, params: &DeriveCancelAllParams) -> Result<()> {
1374 self.require_authenticated(methods::PRIVATE_CANCEL_ALL)
1375 .await?;
1376 let cmd_tx = self.cmd_tx.read().await.clone();
1377 let _: DeriveEmptyResult = send_request(
1378 &self.rate_limiter,
1379 &cmd_tx,
1380 methods::PRIVATE_CANCEL_ALL,
1381 params,
1382 self.request_timeout,
1383 )
1384 .await?;
1385 Ok(())
1386 }
1387
1388 pub(crate) async fn reserve_matching_request(
1389 &self,
1390 operation: &'static str,
1391 instrument_name: &Ustr,
1392 ) -> Result<MatchingRateLimitReservation> {
1393 self.require_authenticated(operation).await?;
1394 debug_assert_eq!(rate_class_for_method(operation), RateClass::Matching);
1395 let window = self
1396 .rate_limiter
1397 .await_class_ready(RateClass::Matching, Some(instrument_name))
1398 .await;
1399 self.ensure_authenticated(operation)?;
1400 Ok(MatchingRateLimitReservation {
1401 method: operation,
1402 instrument_name: *instrument_name,
1403 window,
1404 })
1405 }
1406
1407 async fn refresh_matching_reservation(&self, reservation: &MatchingRateLimitReservation) {
1411 self.rate_limiter
1412 .ensure_window_current(
1413 RateClass::Matching,
1414 Some(&reservation.instrument_name),
1415 reservation.window,
1416 )
1417 .await;
1418 }
1419
1420 fn ensure_authenticated(&self, operation: &'static str) -> Result<()> {
1421 if self.auth_tracker.is_authenticated() {
1422 return Ok(());
1423 }
1424
1425 Err(DeriveWsError::Authentication {
1426 operation: operation.to_string(),
1427 reason: "WebSocket session is not authenticated".to_string(),
1428 })
1429 }
1430
1431 async fn require_authenticated(&self, operation: &'static str) -> Result<()> {
1432 if self
1433 .auth_tracker
1434 .wait_for_authenticated(self.request_timeout)
1435 .await
1436 {
1437 return Ok(());
1438 }
1439
1440 Err(DeriveWsError::Authentication {
1441 operation: operation.to_string(),
1442 reason: "WebSocket session is not authenticated".to_string(),
1443 })
1444 }
1445}
1446
1447#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1451enum RequestRateLimit {
1452 Await(Option<Ustr>),
1455 Reserved,
1458}
1459
1460async fn send_raw<P>(
1461 rate_limiter: &WsRateLimiter,
1462 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1463 method: &'static str,
1464 params: &P,
1465 timeout: Duration,
1466) -> Result<Value>
1467where
1468 P: Serialize + ?Sized,
1469{
1470 send_raw_with_rate_limit(
1471 rate_limiter,
1472 cmd_tx,
1473 method,
1474 params,
1475 timeout,
1476 RequestRateLimit::Await(None),
1477 None,
1478 )
1479 .await
1480}
1481
1482async fn send_raw_for_instrument<P>(
1485 rate_limiter: &WsRateLimiter,
1486 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1487 method: &'static str,
1488 params: &P,
1489 timeout: Duration,
1490 instrument_name: Ustr,
1491) -> Result<Value>
1492where
1493 P: Serialize + ?Sized,
1494{
1495 send_raw_with_rate_limit(
1496 rate_limiter,
1497 cmd_tx,
1498 method,
1499 params,
1500 timeout,
1501 RequestRateLimit::Await(Some(instrument_name)),
1502 None,
1503 )
1504 .await
1505}
1506
1507async fn send_raw_after_rate_limit<P>(
1508 rate_limiter: &WsRateLimiter,
1509 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1510 method: &'static str,
1511 params: &P,
1512 timeout: Duration,
1513) -> Result<Value>
1514where
1515 P: Serialize + ?Sized,
1516{
1517 send_raw_with_rate_limit(
1518 rate_limiter,
1519 cmd_tx,
1520 method,
1521 params,
1522 timeout,
1523 RequestRateLimit::Reserved,
1524 None,
1525 )
1526 .await
1527}
1528
1529async fn send_raw_with_rate_limit<P>(
1530 rate_limiter: &WsRateLimiter,
1531 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1532 method: &'static str,
1533 params: &P,
1534 timeout: Duration,
1535 rate_limit: RequestRateLimit,
1536 connection_epoch: Option<u64>,
1537) -> Result<Value>
1538where
1539 P: Serialize + ?Sized,
1540{
1541 let params = serde_json::to_value(params)?;
1542
1543 if let RequestRateLimit::Await(instrument_name) = rate_limit {
1544 rate_limiter
1545 .await_class_ready(rate_class_for_method(method), instrument_name.as_ref())
1546 .await;
1547 }
1548
1549 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
1550 cmd_tx
1551 .send(HandlerCommand::Request {
1552 method,
1553 params,
1554 connection_epoch,
1555 response_tx,
1556 })
1557 .map_err(|e| DeriveWsError::transport(format!("failed to enqueue `{method}`: {e}")))?;
1558
1559 match tokio::time::timeout(timeout, response_rx).await {
1563 Ok(Ok(outcome)) => outcome,
1564 Ok(Err(_)) => Err(DeriveWsError::RequestCancelled {
1565 method: method.to_owned(),
1566 }),
1567 Err(_) => Err(DeriveWsError::Timeout {
1568 method: method.to_owned(),
1569 }),
1570 }
1571}
1572
1573async fn send_request<P, R>(
1576 rate_limiter: &WsRateLimiter,
1577 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1578 method: &'static str,
1579 params: &P,
1580 timeout: Duration,
1581) -> Result<R>
1582where
1583 P: Serialize + ?Sized,
1584 R: Default + DeserializeOwned,
1585{
1586 let value = send_raw(rate_limiter, cmd_tx, method, params, timeout).await?;
1587 decode_default_result(value)
1588}
1589
1590async fn send_request_for_instrument<P, R>(
1593 rate_limiter: &WsRateLimiter,
1594 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1595 method: &'static str,
1596 params: &P,
1597 timeout: Duration,
1598 instrument_name: Ustr,
1599) -> Result<R>
1600where
1601 P: Serialize + ?Sized,
1602 R: Default + DeserializeOwned,
1603{
1604 let value = send_raw_for_instrument(
1605 rate_limiter,
1606 cmd_tx,
1607 method,
1608 params,
1609 timeout,
1610 instrument_name,
1611 )
1612 .await?;
1613 decode_default_result(value)
1614}
1615
1616async fn send_request_typed_for_instrument<P, R>(
1618 rate_limiter: &WsRateLimiter,
1619 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1620 method: &'static str,
1621 params: &P,
1622 timeout: Duration,
1623 instrument_name: Ustr,
1624) -> Result<R>
1625where
1626 P: Serialize + ?Sized,
1627 R: DeserializeOwned,
1628{
1629 let value = send_raw_for_instrument(
1630 rate_limiter,
1631 cmd_tx,
1632 method,
1633 params,
1634 timeout,
1635 instrument_name,
1636 )
1637 .await?;
1638 Ok(serde_json::from_value(value)?)
1639}
1640
1641fn decode_default_result<R>(value: Value) -> Result<R>
1642where
1643 R: Default + DeserializeOwned,
1644{
1645 if value.is_null() {
1646 Ok(R::default())
1647 } else {
1648 Ok(serde_json::from_value(value)?)
1649 }
1650}
1651
1652async fn send_request_on_connection<P, R>(
1653 rate_limiter: &WsRateLimiter,
1654 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1655 method: &'static str,
1656 params: &P,
1657 timeout: Duration,
1658 connection_epoch: u64,
1659) -> Result<R>
1660where
1661 P: Serialize + ?Sized,
1662 R: Default + DeserializeOwned,
1663{
1664 let value = send_raw_with_rate_limit(
1665 rate_limiter,
1666 cmd_tx,
1667 method,
1668 params,
1669 timeout,
1670 RequestRateLimit::Await(None),
1671 Some(connection_epoch),
1672 )
1673 .await?;
1674
1675 decode_default_result(value)
1676}
1677
1678async fn send_request_typed<P, R>(
1681 rate_limiter: &WsRateLimiter,
1682 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1683 method: &'static str,
1684 params: &P,
1685 timeout: Duration,
1686) -> Result<R>
1687where
1688 P: Serialize + ?Sized,
1689 R: DeserializeOwned,
1690{
1691 let value = send_raw(rate_limiter, cmd_tx, method, params, timeout).await?;
1692 Ok(serde_json::from_value(value)?)
1693}
1694
1695async fn send_request_typed_after_rate_limit<P, R>(
1696 rate_limiter: &WsRateLimiter,
1697 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1698 method: &'static str,
1699 params: &P,
1700 timeout: Duration,
1701) -> Result<R>
1702where
1703 P: Serialize + ?Sized,
1704 R: DeserializeOwned,
1705{
1706 let value = send_raw_after_rate_limit(rate_limiter, cmd_tx, method, params, timeout).await?;
1707 Ok(serde_json::from_value(value)?)
1708}
1709
1710fn channel_topics(channels: &[DeriveWsChannel]) -> Vec<String> {
1711 channels.iter().map(ToString::to_string).collect()
1712}
1713
1714#[expect(
1715 clippy::too_many_arguments,
1716 reason = "authentication state is passed explicitly for epoch fencing"
1717)]
1718async fn login_via_handler(
1719 rate_limiter: &WsRateLimiter,
1720 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1721 auth_tracker: &AuthTracker,
1722 authenticated_epoch: &AtomicU64,
1723 connection_mode: &AtomicU8,
1724 connection_epoch: &AtomicU64,
1725 creds: &DeriveWsCredentials,
1726 timeout: Duration,
1727) -> Result<()> {
1728 let _receiver = auth_tracker.begin();
1729 let expected_epoch = connection_epoch.load(Ordering::Acquire);
1730
1731 match send_login_request(rate_limiter, cmd_tx, creds, timeout, expected_epoch).await {
1732 Ok(())
1733 if complete_session_authentication(
1734 auth_tracker,
1735 authenticated_epoch,
1736 connection_mode,
1737 connection_epoch,
1738 expected_epoch,
1739 ) =>
1740 {
1741 log::debug!("Derive WebSocket authenticated");
1742
1743 Ok(())
1744 }
1745 Ok(()) => {
1746 let e = DeriveWsError::transport(
1747 "connection changed while completing WebSocket authentication",
1748 );
1749 authenticated_epoch.store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
1750 auth_tracker.fail(e.to_string());
1751 Err(e)
1752 }
1753 Err(e) => {
1754 authenticated_epoch.store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
1755 auth_tracker.fail(e.to_string());
1756 Err(e)
1757 }
1758 }
1759}
1760
1761async fn send_login_request(
1762 rate_limiter: &WsRateLimiter,
1763 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1764 creds: &DeriveWsCredentials,
1765 timeout: Duration,
1766 connection_epoch: u64,
1767) -> Result<()> {
1768 let login = build_ws_login(&creds.wallet_address, &creds.signer)?;
1769 let params = WsLoginParams {
1770 wallet: login.wallet,
1771 timestamp: login.timestamp,
1772 signature: login.signature,
1773 };
1774 let result = send_request_on_connection::<_, WsLoginResult>(
1775 rate_limiter,
1776 cmd_tx,
1777 methods::PUBLIC_LOGIN,
1778 ¶ms,
1779 timeout,
1780 connection_epoch,
1781 )
1782 .await?;
1783
1784 if matches!(result, WsLoginResult::Success { success: false }) {
1785 return Err(DeriveWsError::Authentication {
1786 operation: methods::PUBLIC_LOGIN.to_string(),
1787 reason: "venue returned an unsuccessful login result".to_string(),
1788 });
1789 }
1790
1791 Ok(())
1792}
1793
1794#[expect(
1795 clippy::too_many_arguments,
1796 reason = "recovery state is passed explicitly for epoch fencing"
1797)]
1798async fn recover_session(
1799 rate_limiter: &WsRateLimiter,
1800 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1801 auth_tracker: &AuthTracker,
1802 authenticated_epoch: &AtomicU64,
1803 connection_mode: &AtomicU8,
1804 connection_epoch: &AtomicU64,
1805 creds: Option<&DeriveWsCredentials>,
1806 subscriptions: &DashMap<String, ()>,
1807 subscription_lock: &tokio::sync::Mutex<()>,
1808 timeout: Duration,
1809) -> Result<u64> {
1810 let _guard = subscription_lock.lock().await;
1811 let _receiver = creds.map(|_| auth_tracker.begin());
1812
1813 for attempt in 1..=MAX_SESSION_RECOVERY_ATTEMPTS {
1814 let expected_epoch = wait_for_session_connection(connection_mode, connection_epoch).await?;
1815
1816 let result = async {
1817 if !session_connection_is_active(connection_mode, connection_epoch, expected_epoch) {
1818 return Err(DeriveWsError::transport(
1819 "connection changed before WebSocket session recovery",
1820 ));
1821 }
1822
1823 if let Some(creds) = creds {
1824 send_login_request(rate_limiter, cmd_tx, creds, timeout, expected_epoch).await?;
1825 }
1826 let channels: Vec<String> = subscriptions
1827 .iter()
1828 .map(|entry| entry.key().clone())
1829 .collect();
1830 subscribe_via_handler(rate_limiter, cmd_tx, channels, timeout, expected_epoch).await?;
1831
1832 if !session_connection_is_active(connection_mode, connection_epoch, expected_epoch) {
1833 return Err(DeriveWsError::transport(
1834 "connection changed during WebSocket session recovery",
1835 ));
1836 }
1837
1838 Ok(())
1839 }
1840 .await;
1841
1842 match result {
1843 Ok(()) => {
1844 if creds.is_some()
1845 && !complete_session_authentication(
1846 auth_tracker,
1847 authenticated_epoch,
1848 connection_mode,
1849 connection_epoch,
1850 expected_epoch,
1851 )
1852 {
1853 continue;
1854 }
1855
1856 if creds.is_some() {
1857 log::info!("Derive WebSocket session re-authenticated");
1858 }
1859
1860 return Ok(expected_epoch);
1861 }
1862 Err(e) if attempt < MAX_SESSION_RECOVERY_ATTEMPTS => {
1863 let multiplier = 1_u32 << (attempt - 1);
1864 let delay = RECONNECT_BASE_BACKOFF
1865 .saturating_mul(multiplier)
1866 .min(RECONNECT_MAX_BACKOFF);
1867 log::warn!(
1868 "Derive WebSocket session recovery attempt {attempt}/{MAX_SESSION_RECOVERY_ATTEMPTS} failed: {e}; retrying in {delay:?}",
1869 );
1870 tokio::time::sleep(delay).await;
1871 }
1872 Err(e) => {
1873 authenticated_epoch.store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
1874
1875 if creds.is_some() {
1876 auth_tracker.fail(e.to_string());
1877 }
1878 return Err(e);
1879 }
1880 }
1881 }
1882
1883 let e = DeriveWsError::transport("WebSocket session changed while recovery completed");
1884 authenticated_epoch.store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
1885
1886 if creds.is_some() {
1887 auth_tracker.fail(e.to_string());
1888 }
1889 Err(e)
1890}
1891
1892async fn wait_for_session_connection(
1893 connection_mode: &AtomicU8,
1894 connection_epoch: &AtomicU64,
1895) -> Result<u64> {
1896 loop {
1897 match ConnectionMode::from_atomic(connection_mode) {
1898 ConnectionMode::Active => return Ok(connection_epoch.load(Ordering::Acquire)),
1899 ConnectionMode::Reconnect => tokio::time::sleep(RECONNECT_BASE_BACKOFF).await,
1900 ConnectionMode::Disconnect | ConnectionMode::Closed => {
1901 return Err(DeriveWsError::transport(
1902 "WebSocket closed during session recovery",
1903 ));
1904 }
1905 }
1906 }
1907}
1908
1909fn complete_session_authentication(
1910 auth_tracker: &AuthTracker,
1911 authenticated_epoch: &AtomicU64,
1912 connection_mode: &AtomicU8,
1913 connection_epoch: &AtomicU64,
1914 expected_epoch: u64,
1915) -> bool {
1916 if !session_connection_is_active(connection_mode, connection_epoch, expected_epoch) {
1917 return false;
1918 }
1919
1920 authenticated_epoch.store(expected_epoch, Ordering::Release);
1921 auth_tracker.succeed();
1922
1923 if session_connection_is_active(connection_mode, connection_epoch, expected_epoch) {
1924 true
1925 } else {
1926 authenticated_epoch.store(UNAUTHENTICATED_CONNECTION_EPOCH, Ordering::Release);
1927 auth_tracker.invalidate();
1928 false
1929 }
1930}
1931
1932fn session_connection_is_active(
1933 connection_mode: &AtomicU8,
1934 connection_epoch: &AtomicU64,
1935 expected_epoch: u64,
1936) -> bool {
1937 ConnectionMode::from_atomic(connection_mode).is_active()
1938 && connection_epoch.load(Ordering::Acquire) == expected_epoch
1939}
1940
1941async fn subscribe_via_handler(
1942 rate_limiter: &WsRateLimiter,
1943 cmd_tx: &tokio::sync::mpsc::UnboundedSender<HandlerCommand>,
1944 channels: Vec<String>,
1945 timeout: Duration,
1946 connection_epoch: u64,
1947) -> Result<()> {
1948 if channels.is_empty() {
1949 return Ok(());
1950 }
1951
1952 let params = WsSubscribeParams {
1953 channels: channels.into_iter().map(DeriveWsChannel::from).collect(),
1954 };
1955 let result: WsSubscribeResult = send_request_on_connection(
1956 rate_limiter,
1957 cmd_tx,
1958 methods::PUBLIC_SUBSCRIBE,
1959 ¶ms,
1960 timeout,
1961 connection_epoch,
1962 )
1963 .await?;
1964
1965 let (_, failure) = subscription_outcome(
1966 ¶ms.channels,
1967 &result,
1968 SUBSCRIPTION_REPLAY_ACCEPTED_STATUSES,
1969 );
1970 failure.map_or(Ok(()), Err)
1971}
1972
1973fn subscription_outcome(
1974 requested: &[DeriveWsChannel],
1975 result: &WsSubscribeResult,
1976 accepted_statuses: &[&str],
1977) -> (Vec<String>, Option<DeriveWsError>) {
1978 let mut confirmed = Vec::with_capacity(requested.len());
1979 let mut failures = Vec::new();
1980
1981 for channel in requested {
1982 let topic = channel.to_string();
1983 match result.status.get(channel) {
1984 Some(status) if accepted_statuses.contains(&status.as_str()) => confirmed.push(topic),
1985 Some(status) => failures.push(format!("{topic}: {status}")),
1986 None if result.channels.contains(channel) => confirmed.push(topic),
1987 None => failures.push(format!("{topic}: missing channel status")),
1988 }
1989 }
1990
1991 let failure = (!failures.is_empty()).then(|| DeriveWsError::Subscription {
1992 details: failures.join(", "),
1993 });
1994 (confirmed, failure)
1995}
1996
1997#[cfg(test)]
1998mod tests {
1999 use rstest::rstest;
2000
2001 use super::*;
2002 use crate::common::rate_limit::RateBucket;
2003
2004 #[rstest]
2005 fn test_public_client_defaults_to_environment_url() {
2006 let client = DeriveWebSocketClient::new(
2007 None,
2008 DeriveEnvironment::Mainnet,
2009 TransportBackend::default(),
2010 None,
2011 );
2012 assert!(client.url().starts_with("wss://"));
2013 assert!(client.url().contains("api.lyra.finance"));
2014 assert!(!client.is_authenticated());
2015 assert!(!client.is_active());
2016 assert_eq!(client.subscription_count(), 0);
2017 }
2018
2019 #[tokio::test]
2020 async fn test_execution_auth_barrier_waits_for_authentication() {
2021 let client = DeriveWebSocketClient::with_credentials(
2022 None,
2023 DeriveEnvironment::Mainnet,
2024 TransportBackend::default(),
2025 None,
2026 DeriveWsCredentials::new(
2027 "0x000000000000000000000000000000000000aaaa",
2028 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd",
2029 )
2030 .unwrap(),
2031 None,
2032 None,
2033 );
2034 let execution = client.execution_handle();
2035 let auth_tracker = execution.auth_tracker.clone();
2036 let _receiver = auth_tracker.begin();
2037 let tracker_for_task = auth_tracker.clone();
2038
2039 get_runtime().spawn(async move {
2040 tokio::time::sleep(Duration::from_millis(10)).await;
2041 tracker_for_task.succeed();
2042 });
2043
2044 execution
2045 .require_authenticated(methods::PRIVATE_ORDER)
2046 .await
2047 .expect("barrier should wait for successful authentication");
2048 }
2049
2050 #[tokio::test]
2051 async fn test_execution_auth_barrier_fails_on_terminal_auth_failure() {
2052 let client = DeriveWebSocketClient::with_credentials(
2053 None,
2054 DeriveEnvironment::Mainnet,
2055 TransportBackend::default(),
2056 None,
2057 DeriveWsCredentials::new(
2058 "0x000000000000000000000000000000000000aaaa",
2059 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd",
2060 )
2061 .unwrap(),
2062 None,
2063 None,
2064 );
2065 let execution = client.execution_handle();
2066 let _receiver = execution.auth_tracker.begin();
2067 execution.auth_tracker.fail("bad signature");
2068
2069 let error = execution
2070 .require_authenticated(methods::PRIVATE_ORDER)
2071 .await
2072 .expect_err("terminal auth failure must reject private operations");
2073
2074 assert!(matches!(error, DeriveWsError::Authentication { .. }));
2075 }
2076
2077 #[tokio::test]
2078 async fn test_session_recovery_waits_for_reconnecting_transport() {
2079 let connection_mode = Arc::new(AtomicU8::new(ConnectionMode::Reconnect as u8));
2080 let connection_epoch = Arc::new(AtomicU64::new(1));
2081 let mode_for_task = Arc::clone(&connection_mode);
2082 let epoch_for_task = Arc::clone(&connection_epoch);
2083
2084 get_runtime().spawn(async move {
2085 tokio::time::sleep(Duration::from_millis(10)).await;
2086 epoch_for_task.store(2, Ordering::Release);
2087 mode_for_task.store(ConnectionMode::Active as u8, Ordering::Release);
2088 });
2089
2090 let epoch = tokio::time::timeout(
2091 Duration::from_secs(1),
2092 wait_for_session_connection(&connection_mode, &connection_epoch),
2093 )
2094 .await
2095 .expect("session recovery should resume after reconnect")
2096 .expect("active replacement connection should be accepted");
2097
2098 assert_eq!(epoch, 2);
2099 }
2100
2101 #[rstest]
2102 fn test_testnet_client_routes_to_demo_url() {
2103 let client = DeriveWebSocketClient::new(
2104 None,
2105 DeriveEnvironment::Testnet,
2106 TransportBackend::default(),
2107 None,
2108 );
2109 assert!(client.url().contains("demo"));
2110 }
2111
2112 #[rstest]
2113 fn test_credentials_constructor_parses_session_key() {
2114 let creds = DeriveWsCredentials::new(
2115 "0x000000000000000000000000000000000000aaaa",
2116 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd",
2117 )
2118 .unwrap();
2119 assert!(creds.wallet_address.starts_with("0x"));
2120 let client = DeriveWebSocketClient::with_credentials(
2121 None,
2122 DeriveEnvironment::Testnet,
2123 TransportBackend::default(),
2124 None,
2125 creds,
2126 None,
2127 None,
2128 );
2129 assert!(client.url().contains("demo"));
2130 assert!(!client.is_authenticated());
2131 }
2132
2133 #[rstest]
2134 fn test_credentials_debug_redacts_signer() {
2135 let creds = DeriveWsCredentials::new(
2136 "0xWALLET",
2137 "0x2ae8be44db8a590d20bffbe3b6872df9b569147d3bf6801a35a28281a4816bbd",
2138 )
2139 .unwrap();
2140 let debug = format!("{creds:?}");
2141 assert!(debug.contains("redacted"));
2142 assert!(debug.contains("0xWALLET"));
2143 assert!(!debug.contains("2ae8be44"));
2144 }
2145
2146 #[rstest]
2147 fn test_credentials_constructor_rejects_invalid_session_key() {
2148 let err = DeriveWsCredentials::new("0xWALLET", "not-a-hex-key").unwrap_err();
2149 assert!(err.to_string().contains("invalid session key"));
2150 }
2151
2152 #[rstest]
2153 #[tokio::test]
2154 async fn test_send_raw_times_out_when_no_response_arrives() {
2155 let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::unbounded_channel::<HandlerCommand>();
2158 let rate_limiter: WsRateLimiter =
2159 FixedWindowLimiter::new(FixedWindowLimits::websocket(None, None), MonotonicClock {});
2160 let err = send_raw(
2161 &rate_limiter,
2162 &cmd_tx,
2163 methods::PRIVATE_ORDER,
2164 &serde_json::json!({}),
2165 Duration::from_millis(50),
2166 )
2167 .await
2168 .expect_err("must time out");
2169
2170 match err {
2171 DeriveWsError::Timeout { method } => assert_eq!(method, methods::PRIVATE_ORDER),
2172 other => panic!("expected Timeout, was {other:?}"),
2173 }
2174 }
2175
2176 #[rstest]
2177 #[tokio::test]
2178 async fn test_send_request_typed_rejects_null_result() {
2179 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<HandlerCommand>();
2183 tokio::spawn(async move {
2184 if let Some(HandlerCommand::Request { response_tx, .. }) = cmd_rx.recv().await {
2185 let _ = response_tx.send(Ok(Value::Null));
2186 }
2187 });
2188 let rate_limiter: WsRateLimiter =
2189 FixedWindowLimiter::new(FixedWindowLimits::websocket(None, None), MonotonicClock {});
2190 let result: Result<DeriveOrderResult> = send_request_typed(
2191 &rate_limiter,
2192 &cmd_tx,
2193 methods::PRIVATE_ORDER,
2194 &serde_json::json!({}),
2195 Duration::from_secs(1),
2196 )
2197 .await;
2198 assert!(matches!(result, Err(DeriveWsError::Serde(_))));
2199 }
2200
2201 #[rstest]
2202 #[tokio::test]
2203 async fn test_reserved_send_does_not_wait_for_or_consume_second_quota_cell() {
2204 let rate_limiter: WsRateLimiter =
2205 FixedWindowLimiter::new(FixedWindowLimits::websocket(None, None), MonotonicClock {});
2206
2207 for _ in 0..5 {
2208 rate_limiter
2209 .check_bucket(RateBucket::Matching)
2210 .expect("reservation consumes the matching window");
2211 }
2212 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<HandlerCommand>();
2213 tokio::spawn(async move {
2214 if let Some(HandlerCommand::Request { response_tx, .. }) = cmd_rx.recv().await {
2215 let _ = response_tx.send(Ok(serde_json::json!({"accepted": true})));
2216 }
2217 });
2218
2219 let response = tokio::time::timeout(
2220 Duration::from_millis(100),
2221 send_raw_after_rate_limit(
2222 &rate_limiter,
2223 &cmd_tx,
2224 methods::PRIVATE_ORDER,
2225 &serde_json::json!({}),
2226 Duration::from_secs(1),
2227 ),
2228 )
2229 .await
2230 .expect("reserved send must not wait for quota")
2231 .expect("reserved send succeeds");
2232
2233 assert_eq!(response, serde_json::json!({"accepted": true}));
2234 assert!(
2235 rate_limiter.check_bucket(RateBucket::Matching).is_err(),
2236 "reserved send must not consume a second cell",
2237 );
2238 }
2239}