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