1use std::{
19 collections::HashMap,
20 sync::{
21 Arc,
22 atomic::{AtomicBool, AtomicU8, Ordering},
23 },
24};
25
26use arc_swap::ArcSwap;
27#[cfg(test)]
28use nautilus_core::string::secret::REDACTED;
29use nautilus_core::{
30 AtomicMap,
31 string::secret::{SecretString, zeroize_json_value},
32};
33use nautilus_live::{
34 SocketControl,
35 task::{TaskGroup, TaskShutdownError},
36};
37use nautilus_model::{
38 identifiers::{
39 AccountId, ClientOrderId, InstrumentId, StrategyId, Symbol, TraderId, VenueOrderId,
40 },
41 instruments::{Instrument, InstrumentAny},
42};
43use nautilus_network::{
44 ConnectionUrl,
45 http::create_standard_nautilus_headers,
46 mode::ConnectionMode,
47 websocket::{
48 AUTHENTICATION_TIMEOUT_SECS, AuthTracker, SubscriptionState, TransportBackend,
49 WebSocketClient, WebSocketConfig, channel_message_handler,
50 },
51};
52use parking_lot::RwLock;
53use tokio_util::sync::CancellationToken;
54use zeroize::Zeroizing;
55
56use super::{
57 handler::{FuturesFeedHandler, FuturesHandlerCommand},
58 messages::{
59 KrakenFuturesChallengeRequest, KrakenFuturesEvent, KrakenFuturesFeed,
60 KrakenFuturesPrivateSubscribeRequest, KrakenFuturesRequest, KrakenFuturesWsMessage,
61 },
62};
63use crate::{
64 common::{
65 consts::{KRAKEN_FUTURES_WS_SUBSCRIPTION_QUOTA, KRAKEN_RATE_LIMIT_KEY_SUBSCRIPTION},
66 credential::KrakenCredential,
67 parse::truncate_cl_ord_id,
68 },
69 websocket::error::KrakenWsError,
70};
71
72pub const KRAKEN_FUTURES_WS_TOPIC_DELIMITER: char = ':';
76
77#[derive(Debug)]
79pub struct KrakenFuturesWebSocketClient {
80 url: ConnectionUrl,
81 heartbeat_secs: u64,
82 auth_timeout_secs: u64,
83 signal: Arc<AtomicBool>,
84 connection_mode: Arc<ArcSwap<AtomicU8>>,
85 cmd_tx: Arc<tokio::sync::RwLock<tokio::sync::mpsc::UnboundedSender<FuturesHandlerCommand>>>,
86 out_rx: Option<Arc<tokio::sync::mpsc::UnboundedReceiver<KrakenFuturesWsMessage>>>,
87 handler_tasks: Arc<TaskGroup>,
88 connect_lock: Arc<tokio::sync::Mutex<()>>,
89 subscriptions: SubscriptionState,
90 subscription_payloads: Arc<tokio::sync::RwLock<HashMap<String, SecretString>>>,
91 auth_tracker: AuthTracker,
92 cancellation_token: CancellationToken,
93 credential: Option<KrakenCredential>,
94 original_challenge: Arc<tokio::sync::RwLock<Option<SecretString>>>,
95 signed_challenge: Arc<tokio::sync::RwLock<Option<SecretString>>>,
96 account_id: Arc<RwLock<Option<AccountId>>>,
97 truncated_id_map: Arc<AtomicMap<String, ClientOrderId>>,
98 order_instrument_map: Arc<AtomicMap<String, InstrumentId>>,
99 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
100 transport_backend: TransportBackend,
101 proxy_url: Option<SecretString>,
102 socket_control: Option<SocketControl>,
103}
104
105impl Clone for KrakenFuturesWebSocketClient {
106 fn clone(&self) -> Self {
107 Self {
108 url: self.url.clone(),
109 heartbeat_secs: self.heartbeat_secs,
110 auth_timeout_secs: self.auth_timeout_secs,
111 signal: Arc::clone(&self.signal),
112 connection_mode: Arc::clone(&self.connection_mode),
113 cmd_tx: Arc::clone(&self.cmd_tx),
114 out_rx: self.out_rx.clone(),
115 handler_tasks: Arc::clone(&self.handler_tasks),
116 connect_lock: Arc::clone(&self.connect_lock),
117 subscriptions: self.subscriptions.clone(),
118 subscription_payloads: Arc::clone(&self.subscription_payloads),
119 auth_tracker: self.auth_tracker.clone(),
120 cancellation_token: self.cancellation_token.clone(),
121 credential: self.credential.clone(),
122 original_challenge: Arc::clone(&self.original_challenge),
123 signed_challenge: Arc::clone(&self.signed_challenge),
124 account_id: Arc::clone(&self.account_id),
125 truncated_id_map: Arc::clone(&self.truncated_id_map),
126 order_instrument_map: Arc::clone(&self.order_instrument_map),
127 instruments: Arc::clone(&self.instruments),
128 transport_backend: self.transport_backend,
129 proxy_url: self.proxy_url.clone(),
130 socket_control: self.socket_control.clone(),
131 }
132 }
133}
134
135impl KrakenFuturesWebSocketClient {
136 #[must_use]
138 pub fn new(url: String, heartbeat_secs: u64, proxy_url: Option<String>) -> Self {
139 Self::with_credentials(
140 url,
141 heartbeat_secs,
142 None,
143 None,
144 TransportBackend::default(),
145 proxy_url,
146 )
147 }
148
149 #[must_use]
151 pub fn with_credentials(
152 url: String,
153 heartbeat_secs: u64,
154 credential: Option<KrakenCredential>,
155 auth_timeout_secs: Option<u64>,
156 transport_backend: TransportBackend,
157 proxy_url: Option<String>,
158 ) -> Self {
159 let (cmd_tx, _cmd_rx) = tokio::sync::mpsc::unbounded_channel::<FuturesHandlerCommand>();
160 let initial_mode = AtomicU8::new(ConnectionMode::Closed.as_u8());
161 let connection_mode = Arc::new(ArcSwap::from_pointee(initial_mode));
162
163 Self {
164 url: url.into(),
165 heartbeat_secs,
166 auth_timeout_secs: auth_timeout_secs.unwrap_or(AUTHENTICATION_TIMEOUT_SECS),
167 signal: Arc::new(AtomicBool::new(false)),
168 connection_mode,
169 cmd_tx: Arc::new(tokio::sync::RwLock::new(cmd_tx)),
170 out_rx: None,
171 handler_tasks: Arc::new(TaskGroup::new()),
172 connect_lock: Arc::new(tokio::sync::Mutex::new(())),
173 subscriptions: SubscriptionState::new(KRAKEN_FUTURES_WS_TOPIC_DELIMITER),
174 subscription_payloads: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
175 auth_tracker: AuthTracker::new(),
176 cancellation_token: CancellationToken::new(),
177 credential,
178 original_challenge: Arc::new(tokio::sync::RwLock::new(None)),
179 signed_challenge: Arc::new(tokio::sync::RwLock::new(None)),
180 account_id: Arc::new(RwLock::new(None)),
181 truncated_id_map: Arc::new(AtomicMap::new()),
182 order_instrument_map: Arc::new(AtomicMap::new()),
183 instruments: Arc::new(AtomicMap::new()),
184 transport_backend,
185 proxy_url: proxy_url.map(SecretString::from),
186 socket_control: None,
187 }
188 }
189
190 pub(crate) fn begin_shutdown(&self) {
191 self.handler_tasks.begin_shutdown();
192 self.cancellation_token.cancel();
193 self.signal.store(true, Ordering::Relaxed);
194 }
195
196 #[must_use]
198 pub fn with_socket_control(mut self, control: SocketControl) -> Self {
199 self.socket_control = Some(control);
200 self
201 }
202
203 #[must_use]
205 pub fn has_credentials(&self) -> bool {
206 self.credential.is_some()
207 }
208
209 #[must_use]
211 pub fn url(&self) -> &str {
212 self.url.expose()
213 }
214
215 #[must_use]
217 pub fn is_closed(&self) -> bool {
218 ConnectionMode::from_u8(self.connection_mode.load().load(Ordering::Relaxed))
219 == ConnectionMode::Closed
220 }
221
222 #[must_use]
224 pub fn is_active(&self) -> bool {
225 ConnectionMode::from_u8(self.connection_mode.load().load(Ordering::Relaxed))
226 == ConnectionMode::Active
227 }
228
229 pub async fn wait_until_active(&self, timeout_secs: f64) -> Result<(), KrakenWsError> {
231 let timeout = tokio::time::Duration::from_secs_f64(timeout_secs);
232
233 tokio::time::timeout(timeout, async {
234 while !self.is_active() {
235 tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
236 }
237 })
238 .await
239 .map_err(|_| {
240 KrakenWsError::ConnectionError(format!(
241 "WebSocket connection timeout after {timeout_secs} seconds"
242 ))
243 })?;
244
245 Ok(())
246 }
247
248 #[must_use]
250 pub fn is_authenticated(&self) -> bool {
251 self.auth_tracker.is_authenticated()
252 }
253
254 pub async fn wait_until_authenticated(&self, timeout_secs: f64) -> Result<(), KrakenWsError> {
258 let timeout = tokio::time::Duration::from_secs_f64(timeout_secs);
259 if self.auth_tracker.wait_for_authenticated(timeout).await {
260 Ok(())
261 } else {
262 Err(KrakenWsError::AuthenticationError(format!(
263 "Authentication not completed within {timeout_secs} seconds"
264 )))
265 }
266 }
267
268 pub async fn authenticate(&self) -> Result<(), KrakenWsError> {
274 let credential = self.credential.as_ref().ok_or_else(|| {
275 KrakenWsError::AuthenticationError("API credentials required".to_string())
276 })?;
277
278 let payload = build_challenge_payload(credential)
279 .map_err(|e| KrakenWsError::JsonError(e.to_string()))?;
280
281 let receiver = self.auth_tracker.begin();
282
283 self.cmd_tx
284 .read()
285 .await
286 .send(FuturesHandlerCommand::RequestChallenge { payload })
287 .map_err(|e| KrakenWsError::ChannelError(e.to_string()))?;
288
289 self.auth_tracker
290 .wait_for_result::<KrakenWsError>(
291 tokio::time::Duration::from_secs(self.auth_timeout_secs),
292 receiver,
293 )
294 .await?;
295
296 log::debug!("Futures WebSocket authentication successful");
297 Ok(())
298 }
299
300 pub async fn connect(&mut self) -> Result<(), KrakenWsError> {
302 let connect_lock = Arc::clone(&self.connect_lock);
303 let _connect_guard = connect_lock.lock().await;
304
305 log::debug!("Connecting to Futures WebSocket: {}", self.url.origin());
306
307 if !self.handler_tasks.is_open() || !self.handler_tasks.is_empty() {
308 self.disconnect_locked().await?;
309 self.handler_tasks.start_generation().map_err(|e| {
310 KrakenWsError::ConnectionError(format!(
311 "Failed to start WebSocket handler task generation: {e}"
312 ))
313 })?;
314 }
315 let handler_spawner = self.handler_tasks.spawner().map_err(|e| {
316 KrakenWsError::ConnectionError(format!(
317 "Failed to acquire WebSocket handler task spawner: {e}"
318 ))
319 })?;
320
321 if self.cancellation_token.is_cancelled() {
322 self.cancellation_token = CancellationToken::new();
323 }
324
325 self.signal.store(false, Ordering::Relaxed);
326
327 let (raw_handler, raw_rx) = channel_message_handler();
328 let headers = create_standard_nautilus_headers();
329
330 let ws_config = WebSocketConfig {
331 url: self.url.expose().to_owned(),
332 headers,
333 heartbeat_interval_secs: Some(self.heartbeat_secs),
334 heartbeat_payload: None, connect_timeout_ms: Some(5_000),
336 reconnect_delay_initial_ms: Some(500),
337 reconnect_delay_max_ms: Some(5_000),
338 reconnect_backoff_factor: Some(1.5),
339 reconnect_jitter_ms: Some(250),
340 reconnect_max_attempts: None,
341 heartbeat_timeout_secs: None,
342 idle_timeout_ms: None,
343 writer_capacity: None,
344 backend: self.transport_backend,
345 proxy_url: self
346 .proxy_url
347 .as_ref()
348 .map(|value| value.expose_secret().to_owned()),
349 max_message_size_bytes: None,
350 max_frame_size_bytes: None,
351 };
352
353 let keyed_quotas = vec![(
354 KRAKEN_RATE_LIMIT_KEY_SUBSCRIPTION[0].to_string(),
355 *KRAKEN_FUTURES_WS_SUBSCRIPTION_QUOTA,
356 )];
357
358 let ws_client = WebSocketClient::builder()
359 .config(ws_config)
360 .message_handler(raw_handler)
361 .keyed_quotas(keyed_quotas)
362 .maybe_state_sink(self.socket_control.as_ref().map(SocketControl::sink))
363 .connect()
364 .await
365 .map_err(|e| KrakenWsError::ConnectionError(e.to_string()))?;
366
367 self.connection_mode
368 .store(ws_client.connection_mode_atomic());
369 let reconnect_handle = ws_client.reconnect_handle();
370
371 let (out_tx, out_rx) = tokio::sync::mpsc::unbounded_channel::<KrakenFuturesWsMessage>();
372 self.out_rx = Some(Arc::new(out_rx));
373
374 let (cmd_tx, cmd_rx) = tokio::sync::mpsc::unbounded_channel::<FuturesHandlerCommand>();
375 *self.cmd_tx.write().await = cmd_tx.clone();
376
377 if let Err(e) = cmd_tx.send(FuturesHandlerCommand::SetClient(ws_client)) {
378 return Err(KrakenWsError::ConnectionError(format!(
379 "Failed to send WebSocketClient to handler: {e}"
380 )));
381 }
382
383 if let Some(control) = &self.socket_control {
384 control.register(move || reconnect_handle.request_reconnect());
385 }
386
387 let signal = self.signal.clone();
388 let subscriptions = self.subscriptions.clone();
389 let subscription_payloads = self.subscription_payloads.clone();
390 let cmd_tx_for_reconnect = cmd_tx.clone();
391 let credential_for_reconnect = self.credential.clone();
392 let original_challenge_for_reconnect = self.original_challenge.clone();
393 let signed_challenge_for_reconnect = self.signed_challenge.clone();
394 let auth_tracker_for_reconnect = self.auth_tracker.clone();
395
396 let handler_task = async move {
397 let mut handler =
398 FuturesFeedHandler::new(signal.clone(), cmd_rx, raw_rx, subscriptions.clone());
399 let mut pending_resubscribe = false;
400
401 loop {
402 match handler.next().await {
403 Some(KrakenFuturesWsMessage::Reconnected) => {
404 if signal.load(Ordering::Relaxed) {
405 continue;
406 }
407 log::info!("WebSocket reconnected");
408
409 subscriptions.reset_after_reconnect();
410
411 auth_tracker_for_reconnect.invalidate();
412 *original_challenge_for_reconnect.write().await = None;
413 *signed_challenge_for_reconnect.write().await = None;
414
415 let payloads = subscription_payloads.read().await.clone();
416
417 resubscribe_public(&cmd_tx_for_reconnect, &subscriptions, &payloads);
420
421 let has_private =
422 payloads.keys().any(|k| k == "open_orders" || k == "fills");
423
424 pending_resubscribe = false;
425
426 if has_private {
427 if let Some(ref cred) = credential_for_reconnect {
428 match build_challenge_payload(cred) {
429 Ok(payload) => {
430 let _rx = auth_tracker_for_reconnect.begin();
431
432 if let Err(e) = cmd_tx_for_reconnect.send(
433 FuturesHandlerCommand::RequestChallenge { payload },
434 ) {
435 log::error!("Failed to queue reconnect challenge: {e}");
436 } else {
437 pending_resubscribe = true;
438 }
439 }
440 Err(e) => {
441 log::error!("Failed to serialize reconnect challenge: {e}");
442 }
443 }
444 } else {
445 log::warn!(
446 "Private subscriptions exist but no credentials available"
447 );
448 }
449 }
450
451 if let Err(e) = out_tx.send(KrakenFuturesWsMessage::Reconnected) {
452 log::debug!("Output channel closed: {e}");
453 break;
454 }
455 }
456 Some(KrakenFuturesWsMessage::Challenge(challenge)) => {
457 let Some(ref cred) = credential_for_reconnect else {
458 log::warn!("Challenge received but no credentials configured");
459 auth_tracker_for_reconnect.fail("no credentials");
460 continue;
461 };
462
463 let challenge = SecretString::from(challenge);
464 match cred
465 .sign_ws_challenge(challenge.expose_secret())
466 .map(SecretString::from)
467 {
468 Ok(signed) => {
469 *original_challenge_for_reconnect.write().await =
470 Some(challenge.clone());
471 *signed_challenge_for_reconnect.write().await =
472 Some(signed.clone());
473 auth_tracker_for_reconnect.succeed();
474 log::debug!("Signed WebSocket challenge");
475
476 if pending_resubscribe {
477 let payloads = subscription_payloads.read().await;
478 resubscribe_private(
479 &cmd_tx_for_reconnect,
480 &subscriptions,
481 &payloads,
482 cred,
483 challenge.expose_secret(),
484 signed.expose_secret(),
485 );
486 pending_resubscribe = false;
487 }
488 }
489 Err(e) => {
490 log::error!("Failed to sign challenge: {e}");
491 auth_tracker_for_reconnect.fail(e.to_string());
492 pending_resubscribe = false;
493 }
494 }
495 }
496 Some(msg) => {
497 if let Err(e) = out_tx.send(msg) {
498 log::debug!("Output channel closed: {e}");
499 break;
500 }
501 }
502 None => {
503 log::debug!("Handler stream ended");
504 break;
505 }
506 }
507 }
508
509 log::debug!("Futures handler task exiting");
510 };
511
512 if let Err(e) = handler_spawner.spawn(handler_task) {
513 if let Some(control) = &self.socket_control {
514 control.deregister();
515 }
516 self.out_rx = None;
517 return Err(KrakenWsError::ConnectionError(format!(
518 "Failed to register WebSocket handler task: {e}"
519 )));
520 }
521
522 log::debug!("Futures WebSocket connected successfully");
523 Ok(())
524 }
525
526 pub async fn disconnect(&mut self) -> Result<(), KrakenWsError> {
528 let connect_lock = Arc::clone(&self.connect_lock);
529 let _connect_guard = connect_lock.lock().await;
530 self.disconnect_locked().await
531 }
532
533 async fn disconnect_locked(&self) -> Result<(), KrakenWsError> {
534 log::debug!("Disconnecting Futures WebSocket");
535
536 self.handler_tasks.begin_shutdown();
537 self.signal.store(true, Ordering::Relaxed);
538
539 if let Err(e) = self
540 .cmd_tx
541 .read()
542 .await
543 .send(FuturesHandlerCommand::Disconnect)
544 {
545 log::debug!(
546 "Failed to send disconnect command (handler may already be shut down): {e}"
547 );
548 }
549
550 let task_result = self
551 .handler_tasks
552 .finish_shutdown(
553 tokio::time::Duration::from_secs(2),
554 tokio::time::Duration::from_secs(2),
555 )
556 .await;
557
558 self.subscriptions.clear();
559 self.subscription_payloads.write().await.clear();
560 self.auth_tracker.fail("Disconnected");
561
562 if let Some(control) = &self.socket_control {
563 control.deregister();
564 }
565
566 match task_result {
567 Ok(()) => Ok(()),
568 Err(error @ TaskShutdownError::Timeout { .. }) => Err(KrakenWsError::Timeout(format!(
569 "Futures WebSocket handler shutdown timed out: {error}"
570 ))),
571 Err(e) => Err(KrakenWsError::Disconnected(format!(
572 "Futures WebSocket handler shutdown failed: {e}"
573 ))),
574 }
575 }
576
577 pub async fn close(&mut self) -> Result<(), KrakenWsError> {
579 self.disconnect().await
580 }
581
582 pub async fn subscribe_mark_price(
584 &self,
585 instrument_id: InstrumentId,
586 ) -> Result<(), KrakenWsError> {
587 let symbol = instrument_id.symbol;
588 let key = format!("mark:{symbol}");
589
590 if !self.subscriptions.add_reference(&key) {
591 return Ok(());
592 }
593
594 self.subscriptions.mark_subscribe(&key);
595 self.subscriptions.confirm_subscribe(&key);
596 self.ensure_ticker_subscribed(symbol).await
597 }
598
599 pub async fn unsubscribe_mark_price(
601 &self,
602 instrument_id: InstrumentId,
603 ) -> Result<(), KrakenWsError> {
604 let symbol = instrument_id.symbol;
605 let key = format!("mark:{symbol}");
606
607 if !self.subscriptions.remove_reference(&key) {
608 return Ok(());
609 }
610
611 self.subscriptions.mark_unsubscribe(&key);
612 self.subscriptions.confirm_unsubscribe(&key);
613 self.maybe_unsubscribe_ticker(symbol).await
614 }
615
616 pub async fn subscribe_index_price(
618 &self,
619 instrument_id: InstrumentId,
620 ) -> Result<(), KrakenWsError> {
621 let symbol = instrument_id.symbol;
622 let key = format!("index:{symbol}");
623
624 if !self.subscriptions.add_reference(&key) {
625 return Ok(());
626 }
627
628 self.subscriptions.mark_subscribe(&key);
629 self.subscriptions.confirm_subscribe(&key);
630 self.ensure_ticker_subscribed(symbol).await
631 }
632
633 pub async fn unsubscribe_index_price(
635 &self,
636 instrument_id: InstrumentId,
637 ) -> Result<(), KrakenWsError> {
638 let symbol = instrument_id.symbol;
639 let key = format!("index:{symbol}");
640
641 if !self.subscriptions.remove_reference(&key) {
642 return Ok(());
643 }
644
645 self.subscriptions.mark_unsubscribe(&key);
646 self.subscriptions.confirm_unsubscribe(&key);
647 self.maybe_unsubscribe_ticker(symbol).await
648 }
649
650 pub async fn subscribe_funding_rate(
652 &self,
653 instrument_id: InstrumentId,
654 ) -> Result<(), KrakenWsError> {
655 let symbol = instrument_id.symbol;
656 let key = format!("funding:{symbol}");
657
658 if !self.subscriptions.add_reference(&key) {
659 return Ok(());
660 }
661
662 self.subscriptions.mark_subscribe(&key);
663 self.subscriptions.confirm_subscribe(&key);
664 self.ensure_ticker_subscribed(symbol).await
665 }
666
667 pub async fn unsubscribe_funding_rate(
669 &self,
670 instrument_id: InstrumentId,
671 ) -> Result<(), KrakenWsError> {
672 let symbol = instrument_id.symbol;
673 let key = format!("funding:{symbol}");
674
675 if !self.subscriptions.remove_reference(&key) {
676 return Ok(());
677 }
678
679 self.subscriptions.mark_unsubscribe(&key);
680 self.subscriptions.confirm_unsubscribe(&key);
681 self.maybe_unsubscribe_ticker(symbol).await
682 }
683
684 pub async fn subscribe_quotes(&self, instrument_id: InstrumentId) -> Result<(), KrakenWsError> {
688 let symbol = instrument_id.symbol;
689 let key = format!("quotes:{symbol}");
690
691 if !self.subscriptions.add_reference(&key) {
692 return Ok(());
693 }
694
695 self.subscriptions.mark_subscribe(&key);
696 self.subscriptions.confirm_subscribe(&key);
697
698 self.ensure_book_subscribed(symbol).await
700 }
701
702 pub async fn unsubscribe_quotes(
704 &self,
705 instrument_id: InstrumentId,
706 ) -> Result<(), KrakenWsError> {
707 let symbol = instrument_id.symbol;
708 let key = format!("quotes:{symbol}");
709
710 if !self.subscriptions.remove_reference(&key) {
711 return Ok(());
712 }
713
714 self.subscriptions.mark_unsubscribe(&key);
715 self.subscriptions.confirm_unsubscribe(&key);
716 self.maybe_unsubscribe_book(symbol).await
717 }
718
719 pub async fn subscribe_trades(&self, instrument_id: InstrumentId) -> Result<(), KrakenWsError> {
721 let symbol = instrument_id.symbol;
722 let key = format!("trades:{symbol}");
723
724 if !self.subscriptions.add_reference(&key) {
725 return Ok(());
726 }
727
728 self.subscriptions.mark_subscribe(&key);
729 let payload = self
730 .send_subscribe_feed(KrakenFuturesFeed::Trade, vec![symbol.to_string()])
731 .await?;
732 self.subscriptions.confirm_subscribe(&key);
733 self.subscription_payloads
734 .write()
735 .await
736 .insert(key, payload);
737 Ok(())
738 }
739
740 pub async fn unsubscribe_trades(
742 &self,
743 instrument_id: InstrumentId,
744 ) -> Result<(), KrakenWsError> {
745 let symbol = instrument_id.symbol;
746 let key = format!("trades:{symbol}");
747
748 if !self.subscriptions.remove_reference(&key) {
749 return Ok(());
750 }
751
752 self.subscriptions.mark_unsubscribe(&key);
753 self.send_unsubscribe_feed(KrakenFuturesFeed::Trade, vec![symbol.to_string()])
754 .await?;
755 self.subscriptions.confirm_unsubscribe(&key);
756 self.subscription_payloads.write().await.remove(&key);
757 Ok(())
758 }
759
760 pub async fn subscribe_book(
765 &self,
766 instrument_id: InstrumentId,
767 _depth: Option<u32>,
768 ) -> Result<(), KrakenWsError> {
769 let symbol = instrument_id.symbol;
770
771 let deltas_key = format!("deltas:{symbol}");
772 self.subscriptions.add_reference(&deltas_key);
773 self.subscriptions.mark_subscribe(&deltas_key);
774 self.subscriptions.confirm_subscribe(&deltas_key);
775
776 self.ensure_book_subscribed(symbol).await
777 }
778
779 pub async fn unsubscribe_book(&self, instrument_id: InstrumentId) -> Result<(), KrakenWsError> {
781 let symbol = instrument_id.symbol;
782
783 let deltas_key = format!("deltas:{symbol}");
784 self.subscriptions.remove_reference(&deltas_key);
785 self.subscriptions.mark_unsubscribe(&deltas_key);
786 self.subscriptions.confirm_unsubscribe(&deltas_key);
787
788 self.maybe_unsubscribe_book(symbol).await
789 }
790
791 async fn ensure_ticker_subscribed(&self, symbol: Symbol) -> Result<(), KrakenWsError> {
792 let ticker_key = format!("ticker:{symbol}");
793
794 if !self.subscriptions.add_reference(&ticker_key) {
795 return Ok(());
796 }
797
798 self.subscriptions.mark_subscribe(&ticker_key);
799 let payload = self
800 .send_subscribe_feed(KrakenFuturesFeed::Ticker, vec![symbol.to_string()])
801 .await?;
802 self.subscriptions.confirm_subscribe(&ticker_key);
803 self.subscription_payloads
804 .write()
805 .await
806 .insert(ticker_key, payload);
807 Ok(())
808 }
809
810 async fn maybe_unsubscribe_ticker(&self, symbol: Symbol) -> Result<(), KrakenWsError> {
811 let ticker_key = format!("ticker:{symbol}");
812
813 if !self.subscriptions.remove_reference(&ticker_key) {
814 return Ok(());
815 }
816
817 self.subscriptions.mark_unsubscribe(&ticker_key);
818 self.send_unsubscribe_feed(KrakenFuturesFeed::Ticker, vec![symbol.to_string()])
819 .await?;
820 self.subscriptions.confirm_unsubscribe(&ticker_key);
821 self.subscription_payloads.write().await.remove(&ticker_key);
822 Ok(())
823 }
824
825 async fn ensure_book_subscribed(&self, symbol: Symbol) -> Result<(), KrakenWsError> {
826 let book_key = format!("book:{symbol}");
827
828 if !self.subscriptions.add_reference(&book_key) {
829 return Ok(());
830 }
831
832 self.subscriptions.mark_subscribe(&book_key);
833 let payload = self
834 .send_subscribe_feed(KrakenFuturesFeed::Book, vec![symbol.to_string()])
835 .await?;
836 self.subscriptions.confirm_subscribe(&book_key);
837 self.subscription_payloads
838 .write()
839 .await
840 .insert(book_key, payload);
841 Ok(())
842 }
843
844 async fn maybe_unsubscribe_book(&self, symbol: Symbol) -> Result<(), KrakenWsError> {
845 let book_key = format!("book:{symbol}");
846
847 if !self.subscriptions.remove_reference(&book_key) {
848 return Ok(());
849 }
850
851 self.subscriptions.mark_unsubscribe(&book_key);
852 self.send_unsubscribe_feed(KrakenFuturesFeed::Book, vec![symbol.to_string()])
853 .await?;
854 self.subscriptions.confirm_unsubscribe(&book_key);
855 self.subscription_payloads.write().await.remove(&book_key);
856 Ok(())
857 }
858
859 pub fn take_output_rx(
861 &mut self,
862 ) -> Option<tokio::sync::mpsc::UnboundedReceiver<KrakenFuturesWsMessage>> {
863 self.out_rx.take().and_then(|arc| Arc::try_unwrap(arc).ok())
864 }
865
866 pub async fn set_auth_credentials(
868 &self,
869 original_challenge: String,
870 signed_challenge: String,
871 ) -> Result<(), KrakenWsError> {
872 let _credential = self.credential.as_ref().ok_or_else(|| {
873 KrakenWsError::AuthenticationError("API credentials required".to_string())
874 })?;
875
876 let original_challenge = SecretString::from(original_challenge);
877 let signed_challenge = SecretString::from(signed_challenge);
878 *self.original_challenge.write().await = Some(original_challenge);
879 *self.signed_challenge.write().await = Some(signed_challenge);
880 self.auth_tracker.succeed();
881
882 Ok(())
883 }
884
885 pub fn sign_challenge(&self, challenge: &str) -> Result<String, KrakenWsError> {
889 let credential = self.credential.as_ref().ok_or_else(|| {
890 KrakenWsError::AuthenticationError("API credentials required".to_string())
891 })?;
892
893 credential.sign_ws_challenge(challenge).map_err(|e| {
894 KrakenWsError::AuthenticationError(format!("Failed to sign challenge: {e}"))
895 })
896 }
897
898 pub async fn authenticate_with_challenge(&self, challenge: &str) -> Result<(), KrakenWsError> {
900 let credential = self.credential.as_ref().ok_or_else(|| {
901 KrakenWsError::AuthenticationError("API credentials required".to_string())
902 })?;
903
904 let signed_challenge = credential.sign_ws_challenge(challenge).map_err(|e| {
905 KrakenWsError::AuthenticationError(format!("Failed to sign challenge: {e}"))
906 })?;
907
908 self.set_auth_credentials(challenge.to_string(), signed_challenge)
909 .await
910 }
911
912 pub fn set_account_id(&self, account_id: AccountId) {
914 *self.account_id.write() = Some(account_id);
915 }
916
917 #[must_use]
919 pub fn account_id(&self) -> Option<AccountId> {
920 *self.account_id.read()
921 }
922
923 #[must_use]
925 pub fn account_id_shared(&self) -> &Arc<RwLock<Option<AccountId>>> {
926 &self.account_id
927 }
928
929 #[must_use]
931 pub fn truncated_id_map(&self) -> &Arc<AtomicMap<String, ClientOrderId>> {
932 &self.truncated_id_map
933 }
934
935 #[must_use]
937 pub fn order_instrument_map(&self) -> &Arc<AtomicMap<String, InstrumentId>> {
938 &self.order_instrument_map
939 }
940
941 #[must_use]
943 pub fn instruments_shared(&self) -> &Arc<AtomicMap<InstrumentId, InstrumentAny>> {
944 &self.instruments
945 }
946
947 #[must_use]
949 pub fn subscriptions(&self) -> &SubscriptionState {
950 &self.subscriptions
951 }
952
953 pub fn cache_instrument(&self, instrument: InstrumentAny) {
955 self.instruments.insert(instrument.id(), instrument);
956 }
957
958 pub fn cache_instruments(&self, instruments: &[InstrumentAny]) {
960 self.instruments.rcu(|m| {
961 for instrument in instruments {
962 m.insert(instrument.id(), instrument.clone());
963 }
964 });
965 }
966
967 pub fn cache_client_order(
973 &self,
974 client_order_id: ClientOrderId,
975 venue_order_id: Option<VenueOrderId>,
976 instrument_id: InstrumentId,
977 _trader_id: TraderId,
978 _strategy_id: StrategyId,
979 ) {
980 let truncated = truncate_cl_ord_id(&client_order_id);
981
982 if truncated != client_order_id.as_str() {
983 self.truncated_id_map.insert(truncated, client_order_id);
984 }
985
986 if let Some(venue_id) = venue_order_id {
987 self.order_instrument_map
988 .insert(venue_id.to_string(), instrument_id);
989 }
990 }
991
992 pub async fn subscribe_open_orders(&self) -> Result<(), KrakenWsError> {
994 let key = "open_orders";
995 if !self.subscriptions.add_reference(key) {
996 return Ok(());
997 }
998
999 self.subscriptions.mark_subscribe(key);
1000 let payload = self
1001 .send_private_subscribe_feed(KrakenFuturesFeed::OpenOrders)
1002 .await?;
1003 self.subscriptions.confirm_subscribe(key);
1004 self.subscription_payloads
1005 .write()
1006 .await
1007 .insert(key.to_string(), payload);
1008 Ok(())
1009 }
1010
1011 pub async fn subscribe_fills(&self) -> Result<(), KrakenWsError> {
1013 let key = "fills";
1014 if !self.subscriptions.add_reference(key) {
1015 return Ok(());
1016 }
1017
1018 self.subscriptions.mark_subscribe(key);
1019 let payload = self
1020 .send_private_subscribe_feed(KrakenFuturesFeed::Fills)
1021 .await?;
1022 self.subscriptions.confirm_subscribe(key);
1023 self.subscription_payloads
1024 .write()
1025 .await
1026 .insert(key.to_string(), payload);
1027 Ok(())
1028 }
1029
1030 pub async fn subscribe_executions(&self) -> Result<(), KrakenWsError> {
1032 self.subscribe_open_orders().await?;
1033 self.subscribe_fills().await?;
1034 Ok(())
1035 }
1036
1037 async fn send_subscribe_feed(
1038 &self,
1039 feed: KrakenFuturesFeed,
1040 product_ids: Vec<String>,
1041 ) -> Result<SecretString, KrakenWsError> {
1042 let request = KrakenFuturesRequest {
1043 event: KrakenFuturesEvent::Subscribe,
1044 feed,
1045 product_ids,
1046 };
1047 let payload = SecretString::from(
1048 serde_json::to_string(&request).map_err(|e| KrakenWsError::JsonError(e.to_string()))?,
1049 );
1050 self.cmd_tx
1051 .read()
1052 .await
1053 .send(FuturesHandlerCommand::Subscribe {
1054 payload: payload.clone(),
1055 })
1056 .map_err(|e| KrakenWsError::ChannelError(e.to_string()))?;
1057 Ok(payload)
1058 }
1059
1060 async fn send_unsubscribe_feed(
1061 &self,
1062 feed: KrakenFuturesFeed,
1063 product_ids: Vec<String>,
1064 ) -> Result<(), KrakenWsError> {
1065 let request = KrakenFuturesRequest {
1066 event: KrakenFuturesEvent::Unsubscribe,
1067 feed,
1068 product_ids,
1069 };
1070 let payload = SecretString::from(
1071 serde_json::to_string(&request).map_err(|e| KrakenWsError::JsonError(e.to_string()))?,
1072 );
1073 self.cmd_tx
1074 .read()
1075 .await
1076 .send(FuturesHandlerCommand::Unsubscribe { payload })
1077 .map_err(|e| KrakenWsError::ChannelError(e.to_string()))?;
1078 Ok(())
1079 }
1080
1081 async fn send_private_subscribe_feed(
1082 &self,
1083 feed: KrakenFuturesFeed,
1084 ) -> Result<SecretString, KrakenWsError> {
1085 let credential = self.credential.as_ref().ok_or_else(|| {
1086 KrakenWsError::AuthenticationError("API credentials required".to_string())
1087 })?;
1088 let original_challenge = self
1089 .original_challenge
1090 .read()
1091 .await
1092 .clone()
1093 .ok_or_else(|| {
1094 KrakenWsError::AuthenticationError(
1095 "Must authenticate before subscribing to private feeds".to_string(),
1096 )
1097 })?;
1098 let signed_challenge = self.signed_challenge.read().await.clone().ok_or_else(|| {
1099 KrakenWsError::AuthenticationError(
1100 "Must authenticate before subscribing to private feeds".to_string(),
1101 )
1102 })?;
1103
1104 let request = Zeroizing::new(KrakenFuturesPrivateSubscribeRequest {
1105 event: KrakenFuturesEvent::Subscribe,
1106 feed,
1107 api_key: credential.api_key().into(),
1108 original_challenge,
1109 signed_challenge,
1110 });
1111 let payload = SecretString::from(
1112 serde_json::to_string(&*request)
1113 .map_err(|e| KrakenWsError::JsonError(e.to_string()))?,
1114 );
1115 drop(request);
1116 self.cmd_tx
1117 .read()
1118 .await
1119 .send(FuturesHandlerCommand::Subscribe {
1120 payload: payload.clone(),
1121 })
1122 .map_err(|e| KrakenWsError::ChannelError(e.to_string()))?;
1123 Ok(payload)
1124 }
1125}
1126
1127fn update_private_payload_credentials(
1128 payload: &str,
1129 api_key: &str,
1130 original_challenge: &str,
1131 signed_challenge: &str,
1132) -> Option<SecretString> {
1133 let mut value: serde_json::Value = serde_json::from_str(payload).ok()?;
1134 let obj = value.as_object_mut()?;
1135 obj.insert(
1136 "api_key".to_string(),
1137 serde_json::Value::String(api_key.to_string()),
1138 );
1139 obj.insert(
1140 "original_challenge".to_string(),
1141 serde_json::Value::String(original_challenge.to_string()),
1142 );
1143 obj.insert(
1144 "signed_challenge".to_string(),
1145 serde_json::Value::String(signed_challenge.to_string()),
1146 );
1147 let payload = serde_json::to_string(&value).ok().map(SecretString::from);
1148 zeroize_json_value(&mut value);
1149 payload
1150}
1151
1152fn build_challenge_payload(credential: &KrakenCredential) -> serde_json::Result<SecretString> {
1153 let request = Zeroizing::new(KrakenFuturesChallengeRequest {
1154 event: KrakenFuturesEvent::Challenge,
1155 api_key: credential.api_key().into(),
1156 });
1157 serde_json::to_string(&*request).map(SecretString::from)
1158}
1159
1160fn is_private_feed_key(key: &str) -> bool {
1161 key == "open_orders" || key == "fills"
1162}
1163
1164fn resubscribe_public(
1165 cmd_tx: &tokio::sync::mpsc::UnboundedSender<FuturesHandlerCommand>,
1166 subscriptions: &SubscriptionState,
1167 payloads: &HashMap<String, SecretString>,
1168) {
1169 for (key, payload) in payloads {
1170 if is_private_feed_key(key) {
1171 continue;
1172 }
1173
1174 if let Err(e) = cmd_tx.send(FuturesHandlerCommand::Subscribe {
1175 payload: payload.clone(),
1176 }) {
1177 log::error!("Failed to send resubscribe: error={e}, topic={key}");
1178 continue;
1179 }
1180
1181 subscriptions.mark_subscribe(key);
1182 }
1183}
1184
1185fn resubscribe_private(
1186 cmd_tx: &tokio::sync::mpsc::UnboundedSender<FuturesHandlerCommand>,
1187 subscriptions: &SubscriptionState,
1188 payloads: &HashMap<String, SecretString>,
1189 credential: &KrakenCredential,
1190 original_challenge: &str,
1191 signed_challenge: &str,
1192) {
1193 for (key, payload) in payloads {
1194 if !is_private_feed_key(key) {
1195 continue;
1196 }
1197
1198 let Some(updated) = update_private_payload_credentials(
1199 payload.expose_secret(),
1200 credential.api_key(),
1201 original_challenge,
1202 signed_challenge,
1203 ) else {
1204 log::error!("Failed to update private payload for {key}");
1205 continue;
1206 };
1207
1208 if let Err(e) = cmd_tx.send(FuturesHandlerCommand::Subscribe { payload: updated }) {
1209 log::error!("Failed to send resubscribe: error={e}, topic={key}");
1210 continue;
1211 }
1212
1213 subscriptions.mark_subscribe(key);
1214 }
1215}
1216
1217#[cfg(test)]
1218mod tests {
1219 use base64::{Engine, engine::general_purpose::STANDARD};
1220 use nautilus_network::websocket::AuthTracker;
1221 use rstest::rstest;
1222
1223 use super::*;
1224
1225 struct DropSignal(Arc<AtomicBool>);
1226
1227 impl Drop for DropSignal {
1228 fn drop(&mut self) {
1229 self.0.store(true, Ordering::Release);
1230 }
1231 }
1232
1233 fn test_credential() -> KrakenCredential {
1234 let secret = STANDARD.encode(b"test_secret_key_24bytes!");
1235 KrakenCredential::new("test_key", secret)
1236 }
1237
1238 #[rstest]
1239 #[case("wss://stream.example.com/feed", "wss://stream.example.com")]
1240 #[case(
1241 "wss://SYN-user:SYN-password@stream.example.com:9443/SYN-path?token=SYN-query#SYN-fragment",
1242 "wss://stream.example.com:9443"
1243 )]
1244 fn test_debug_preserves_url_origin(#[case] url: &str, #[case] origin: &str) {
1245 let client = KrakenFuturesWebSocketClient::new(url.to_string(), 30, None);
1246 let debug = format!("{client:?}");
1247 assert!(debug.contains(&format!("url: {origin:?}")));
1248 assert!(!debug.contains("SYN-"));
1249 assert_eq!(client.url.expose(), url);
1250 }
1251
1252 #[rstest]
1253 #[tokio::test]
1254 async fn test_debug_redacts_auth_state_and_proxy_url() {
1255 let client = KrakenFuturesWebSocketClient::with_credentials(
1256 "wss://test".to_string(),
1257 30,
1258 Some(test_credential()),
1259 None,
1260 TransportBackend::default(),
1261 Some("http://user:proxy-secret@localhost".to_string()),
1262 );
1263 client
1264 .set_auth_credentials(
1265 "original-challenge".to_string(),
1266 "signed-challenge".to_string(),
1267 )
1268 .await
1269 .unwrap();
1270
1271 let debug = format!("{client:?}");
1272
1273 assert!(debug.contains(REDACTED));
1274 assert!(!debug.contains("proxy-secret"));
1275 assert!(!debug.contains("original-challenge"));
1276 assert!(!debug.contains("signed-challenge"));
1277 }
1278
1279 #[tokio::test]
1280 async fn test_last_client_owner_drop_aborts_handler_task() {
1281 let client = KrakenFuturesWebSocketClient::new("wss://test".to_string(), 30, None);
1282 let (started_tx, started_rx) = tokio::sync::oneshot::channel();
1283 let dropped = Arc::new(AtomicBool::new(false));
1284 let drop_signal = DropSignal(Arc::clone(&dropped));
1285 client
1286 .handler_tasks
1287 .spawn(async move {
1288 let _drop_signal = drop_signal;
1289 started_tx.send(()).expect("started receiver");
1290 std::future::pending::<()>().await;
1291 })
1292 .expect("handler task should register");
1293 started_rx.await.expect("handler task started");
1294 let clone = client.clone();
1295
1296 drop(client);
1297 assert!(!dropped.load(Ordering::Acquire));
1298 drop(clone);
1299
1300 tokio::time::timeout(tokio::time::Duration::from_secs(1), async {
1301 while !dropped.load(Ordering::Acquire) {
1302 tokio::task::yield_now().await;
1303 }
1304 })
1305 .await
1306 .expect("handler task aborted");
1307 }
1308
1309 #[rstest]
1310 fn test_build_challenge_payload_emits_expected_event() {
1311 let credential = test_credential();
1312 let payload = build_challenge_payload(&credential).expect("serializes");
1313 assert!(payload.expose_secret().contains(r#""event":"challenge""#));
1314 assert!(payload.expose_secret().contains(r#""api_key":"test_key""#));
1315 }
1316
1317 #[rstest]
1318 #[tokio::test]
1319 async fn test_resubscribe_public_skips_private_feeds() {
1320 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<FuturesHandlerCommand>();
1321 let subscriptions = SubscriptionState::new(KRAKEN_FUTURES_WS_TOPIC_DELIMITER);
1322
1323 let mut payloads = HashMap::new();
1324 payloads.insert(
1325 "trades:PI_XBTUSD".to_string(),
1326 SecretString::from(
1327 r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD"]}"#.to_string(),
1328 ),
1329 );
1330 payloads.insert(
1331 "open_orders".to_string(),
1332 SecretString::from(r#"{"event":"subscribe","feed":"open_orders"}"#.to_string()),
1333 );
1334
1335 resubscribe_public(&cmd_tx, &subscriptions, &payloads);
1336
1337 let mut subscribed = Vec::new();
1338 while let Ok(FuturesHandlerCommand::Subscribe { payload }) = cmd_rx.try_recv() {
1339 subscribed.push(payload);
1340 }
1341
1342 assert_eq!(
1343 subscribed.len(),
1344 1,
1345 "only the public feed should resubscribe"
1346 );
1347 assert!(subscribed[0].expose_secret().contains("PI_XBTUSD"));
1348 }
1349
1350 #[rstest]
1351 #[tokio::test]
1352 async fn test_resubscribe_public_restores_publics_even_with_credentialed_client() {
1353 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<FuturesHandlerCommand>();
1356 let subscriptions = SubscriptionState::new(KRAKEN_FUTURES_WS_TOPIC_DELIMITER);
1357
1358 let mut payloads = HashMap::new();
1359 payloads.insert(
1360 "trades:PI_XBTUSD".to_string(),
1361 SecretString::from(
1362 r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD"]}"#.to_string(),
1363 ),
1364 );
1365
1366 resubscribe_public(&cmd_tx, &subscriptions, &payloads);
1367
1368 match cmd_rx.try_recv().expect("public subscribe expected") {
1369 FuturesHandlerCommand::Subscribe { payload } => {
1370 assert!(payload.expose_secret().contains("PI_XBTUSD"));
1371 }
1372 other => panic!("expected Subscribe, was {other:?}"),
1373 }
1374 }
1375
1376 #[rstest]
1377 #[tokio::test]
1378 async fn test_resubscribe_private_patches_credentials() {
1379 let (cmd_tx, mut cmd_rx) = tokio::sync::mpsc::unbounded_channel::<FuturesHandlerCommand>();
1380 let subscriptions = SubscriptionState::new(KRAKEN_FUTURES_WS_TOPIC_DELIMITER);
1381 let credential = test_credential();
1382
1383 let mut payloads = HashMap::new();
1384 payloads.insert(
1385 "open_orders".to_string(),
1386 SecretString::from(
1387 r#"{"event":"subscribe","feed":"open_orders","api_key":"","original_challenge":"","signed_challenge":""}"#
1388 .to_string(),
1389 ),
1390 );
1391 payloads.insert(
1392 "trades:PI_XBTUSD".to_string(),
1393 SecretString::from(
1394 r#"{"event":"subscribe","feed":"trade","product_ids":["PI_XBTUSD"]}"#.to_string(),
1395 ),
1396 );
1397
1398 resubscribe_private(
1399 &cmd_tx,
1400 &subscriptions,
1401 &payloads,
1402 &credential,
1403 "server-challenge",
1404 "signed-value",
1405 );
1406
1407 let mut subscribed = Vec::new();
1408 while let Ok(FuturesHandlerCommand::Subscribe { payload }) = cmd_rx.try_recv() {
1409 subscribed.push(payload);
1410 }
1411
1412 assert_eq!(
1413 subscribed.len(),
1414 1,
1415 "only the private feed should resubscribe"
1416 );
1417 let value: serde_json::Value =
1418 serde_json::from_str(subscribed[0].expose_secret()).expect("payload is valid JSON");
1419 assert_eq!(value["event"], "subscribe");
1420 assert_eq!(value["feed"], "open_orders");
1421 assert_eq!(value["api_key"], "test_key");
1422 assert_eq!(value["original_challenge"], "server-challenge");
1423 assert_eq!(value["signed_challenge"], "signed-value");
1424 }
1425
1426 #[rstest]
1427 #[tokio::test]
1428 async fn test_auth_tracker_succeed_completes_wait_for_result() {
1429 let tracker = AuthTracker::new();
1430 let receiver = tracker.begin();
1431
1432 let tracker_for_responder = tracker.clone();
1433
1434 tokio::spawn(async move {
1435 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
1436 tracker_for_responder.succeed();
1437 });
1438
1439 tracker
1440 .wait_for_result::<KrakenWsError>(tokio::time::Duration::from_secs(1), receiver)
1441 .await
1442 .expect("auth should succeed");
1443
1444 assert!(tracker.is_authenticated());
1445 }
1446
1447 #[rstest]
1448 #[tokio::test]
1449 async fn test_auth_tracker_wait_for_result_times_out() {
1450 let tracker = AuthTracker::new();
1451 let receiver = tracker.begin();
1452
1453 let err = tracker
1454 .wait_for_result::<KrakenWsError>(tokio::time::Duration::from_millis(20), receiver)
1455 .await
1456 .expect_err("should time out");
1457
1458 assert!(matches!(err, KrakenWsError::AuthenticationError(_)));
1459 assert!(!tracker.is_authenticated());
1460 }
1461
1462 #[rstest]
1463 #[tokio::test]
1464 async fn test_authenticate_without_credentials_errors() {
1465 let client = KrakenFuturesWebSocketClient::new(
1466 "wss://futures.kraken.com/ws/v1".to_string(),
1467 60,
1468 None,
1469 );
1470
1471 let err = client.authenticate().await.expect_err("should fail");
1472 assert!(
1473 matches!(err, KrakenWsError::AuthenticationError(ref msg) if msg.contains("API credentials required")),
1474 "unexpected error: {err:?}"
1475 );
1476 }
1477
1478 #[rstest]
1479 #[tokio::test]
1480 async fn test_set_auth_credentials_marks_tracker_authenticated() {
1481 let client = KrakenFuturesWebSocketClient::with_credentials(
1482 "wss://futures.kraken.com/ws/v1".to_string(),
1483 60,
1484 Some(test_credential()),
1485 None,
1486 TransportBackend::default(),
1487 None,
1488 );
1489
1490 assert!(!client.is_authenticated());
1491
1492 client
1493 .set_auth_credentials("orig-challenge".to_string(), "signed-challenge".to_string())
1494 .await
1495 .expect("should succeed");
1496
1497 assert!(client.is_authenticated());
1498 client
1499 .wait_until_authenticated(0.05)
1500 .await
1501 .expect("should return immediately");
1502 }
1503
1504 #[rstest]
1505 #[tokio::test]
1506 async fn test_set_auth_credentials_without_credentials_errors() {
1507 let client = KrakenFuturesWebSocketClient::new(
1508 "wss://futures.kraken.com/ws/v1".to_string(),
1509 60,
1510 None,
1511 );
1512
1513 let err = client
1514 .set_auth_credentials("orig".to_string(), "signed".to_string())
1515 .await
1516 .expect_err("should fail");
1517 assert!(matches!(err, KrakenWsError::AuthenticationError(_)));
1518 assert!(!client.is_authenticated());
1519 }
1520
1521 #[rstest]
1522 #[tokio::test]
1523 async fn test_authenticate_with_challenge_updates_state() {
1524 let client = KrakenFuturesWebSocketClient::with_credentials(
1525 "wss://futures.kraken.com/ws/v1".to_string(),
1526 60,
1527 Some(test_credential()),
1528 None,
1529 TransportBackend::default(),
1530 None,
1531 );
1532
1533 client
1534 .authenticate_with_challenge("server-challenge")
1535 .await
1536 .expect("should succeed");
1537
1538 assert!(client.is_authenticated());
1539 }
1540
1541 #[rstest]
1542 #[tokio::test]
1543 async fn test_wait_until_authenticated_resolves_after_success() {
1544 let client = KrakenFuturesWebSocketClient::with_credentials(
1545 "wss://futures.kraken.com/ws/v1".to_string(),
1546 60,
1547 Some(test_credential()),
1548 None,
1549 TransportBackend::default(),
1550 None,
1551 );
1552
1553 let client_for_responder = client.clone();
1554
1555 tokio::spawn(async move {
1556 tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
1557 client_for_responder
1558 .set_auth_credentials("orig".to_string(), "signed".to_string())
1559 .await
1560 .expect("succeeds");
1561 });
1562
1563 client
1564 .wait_until_authenticated(1.0)
1565 .await
1566 .expect("should resolve once credentials are set");
1567 }
1568
1569 #[rstest]
1570 #[tokio::test]
1571 async fn test_wait_until_authenticated_times_out() {
1572 let client = KrakenFuturesWebSocketClient::with_credentials(
1573 "wss://futures.kraken.com/ws/v1".to_string(),
1574 60,
1575 Some(test_credential()),
1576 None,
1577 TransportBackend::default(),
1578 None,
1579 );
1580
1581 let err = client
1582 .wait_until_authenticated(0.05)
1583 .await
1584 .expect_err("should time out");
1585 assert!(matches!(err, KrakenWsError::AuthenticationError(_)));
1586 }
1587}