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