1use std::{fmt::Debug, str::FromStr};
19
20use nautilus_core::{
21 serialization::{deserialize_decimal, deserialize_optional_decimal},
22 string::secret::{REDACTED, SecretString},
23};
24use nautilus_model::{
25 data::{
26 Data, FundingRateUpdate, InstrumentStatus, OrderBookDeltas, greeks::OptionGreekValues,
27 option_chain::OptionGreeks,
28 },
29 events::{
30 AccountState, OrderAccepted, OrderCancelRejected, OrderCanceled, OrderExpired, OrderFilled,
31 OrderModifyRejected, OrderRejected, OrderUpdated,
32 },
33 instruments::InstrumentAny,
34 reports::{FillReport, OrderStatusReport},
35};
36use rust_decimal::{Decimal, prelude::ToPrimitive};
37use serde::{Deserialize, Deserializer, Serialize, de};
38use ustr::Ustr;
39use zeroize::{Zeroize, ZeroizeOnDrop};
40
41use super::enums::{DeribitBookAction, DeribitBookMsgType, DeribitHeartbeatType};
42pub use crate::common::{
43 enums::DeribitInstrumentState,
44 rpc::{DeribitJsonRpcError, DeribitJsonRpcRequest, DeribitJsonRpcResponse},
45};
46use crate::{common::models::DeribitTradeLeg, websocket::error::DeribitWsError};
47
48#[derive(Debug, Clone, Deserialize)]
50pub struct DeribitSubscriptionNotification<T> {
51 pub jsonrpc: String,
53 pub method: String,
55 pub params: DeribitSubscriptionParams<T>,
57}
58
59#[derive(Debug, Clone, Deserialize)]
61pub struct DeribitSubscriptionParams<T> {
62 pub channel: String,
64 pub data: T,
66}
67
68#[derive(Debug, Clone, Serialize, Zeroize)]
70pub struct DeribitAuthParams {
71 pub grant_type: String,
73 pub client_id: SecretString,
75 pub timestamp: u64,
77 pub signature: SecretString,
79 pub nonce: String,
81 pub data: SecretString,
83 #[serde(skip_serializing_if = "Option::is_none")]
87 pub scope: Option<String>,
88}
89
90#[derive(Debug, Clone, Serialize, Zeroize)]
92pub struct DeribitRefreshTokenParams {
93 pub grant_type: String,
95 pub refresh_token: SecretString,
97}
98
99#[derive(Debug, Clone, Deserialize, Zeroize, ZeroizeOnDrop)]
101pub struct DeribitAuthResult {
102 pub access_token: SecretString,
104 pub expires_in: u64,
106 pub refresh_token: SecretString,
108 pub scope: String,
110 pub token_type: String,
112 #[serde(default)]
114 pub enabled_features: Vec<String>,
115}
116
117#[derive(Debug, Clone, Serialize)]
119pub struct DeribitSubscribeParams {
120 pub channels: Vec<String>,
122}
123
124#[derive(Debug, Clone, Deserialize)]
126pub struct DeribitSubscribeResult(pub Vec<String>);
127
128#[derive(Debug, Clone, Serialize)]
130pub struct DeribitHeartbeatParams {
131 pub interval: u64,
133}
134
135#[derive(Debug, Clone, Deserialize)]
137pub struct DeribitHeartbeatData {
138 #[serde(rename = "type")]
140 pub heartbeat_type: DeribitHeartbeatType,
141}
142
143#[derive(Debug, Clone, Deserialize)]
145pub struct DeribitTradeMsg {
146 pub trade_id: String,
148 pub instrument_name: Ustr,
150 #[serde(deserialize_with = "deserialize_decimal")]
152 pub price: Decimal,
153 #[serde(deserialize_with = "deserialize_decimal")]
155 pub amount: Decimal,
156 pub direction: String,
158 pub timestamp: u64,
160 pub trade_seq: u64,
162 pub tick_direction: i8,
164 #[serde(deserialize_with = "deserialize_decimal")]
166 pub index_price: Decimal,
167 #[serde(deserialize_with = "deserialize_decimal")]
169 pub mark_price: Decimal,
170 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
172 pub iv: Option<Decimal>,
173 pub liquidation: Option<String>,
175 pub combo_trade_id: Option<String>,
177 pub block_trade_id: Option<String>,
179 #[serde(default)]
181 pub block_rfq_id: Option<i64>,
182 pub combo_id: Option<String>,
184 #[serde(default)]
186 pub legs: Option<Vec<DeribitTradeLeg>>,
187}
188
189#[derive(Debug, Clone, Deserialize)]
194pub struct DeribitBookMsg {
195 #[serde(rename = "type", default = "default_book_msg_type")]
197 pub msg_type: DeribitBookMsgType,
198 pub instrument_name: Ustr,
200 pub timestamp: u64,
202 pub change_id: u64,
204 pub prev_change_id: Option<u64>,
206 pub bids: Vec<Vec<serde_json::Value>>,
208 pub asks: Vec<Vec<serde_json::Value>>,
210}
211
212fn default_book_msg_type() -> DeribitBookMsgType {
214 DeribitBookMsgType::Snapshot
215}
216
217#[derive(Debug, Clone)]
219pub struct DeribitBookLevel {
220 pub price: Decimal,
222 pub amount: Decimal,
224 pub action: Option<DeribitBookAction>,
226}
227
228#[derive(Debug, Clone, Deserialize)]
230pub struct DeribitTickerMsg {
231 pub instrument_name: Ustr,
233 pub timestamp: u64,
235 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
237 pub best_bid_price: Option<Decimal>,
238 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
240 pub best_bid_amount: Option<Decimal>,
241 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
243 pub best_ask_price: Option<Decimal>,
244 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
246 pub best_ask_amount: Option<Decimal>,
247 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
249 pub last_price: Option<Decimal>,
250 #[serde(deserialize_with = "deserialize_decimal")]
252 pub mark_price: Decimal,
253 #[serde(deserialize_with = "deserialize_decimal")]
255 pub index_price: Decimal,
256 #[serde(deserialize_with = "deserialize_decimal")]
258 pub open_interest: Decimal,
259 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
261 pub current_funding: Option<Decimal>,
262 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
264 pub funding_8h: Option<Decimal>,
265 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
267 pub settlement_price: Option<Decimal>,
268 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
270 pub volume: Option<Decimal>,
271 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
273 pub volume_usd: Option<Decimal>,
274 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
276 pub high: Option<Decimal>,
277 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
279 pub low: Option<Decimal>,
280 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
282 pub price_change: Option<Decimal>,
283 pub state: String,
285 pub greeks: Option<DeribitGreeks>,
288 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
290 pub mark_iv: Option<Decimal>,
291 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
293 pub bid_iv: Option<Decimal>,
294 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
296 pub ask_iv: Option<Decimal>,
297 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
299 pub underlying_price: Option<Decimal>,
300 pub underlying_index: Option<String>,
302}
303
304#[derive(Debug, Clone, Deserialize)]
306pub struct DeribitGreeks {
307 #[serde(deserialize_with = "deserialize_decimal")]
308 pub delta: Decimal,
309 #[serde(deserialize_with = "deserialize_decimal")]
310 pub gamma: Decimal,
311 #[serde(deserialize_with = "deserialize_decimal")]
312 pub vega: Decimal,
313 #[serde(deserialize_with = "deserialize_decimal")]
314 pub theta: Decimal,
315 #[serde(deserialize_with = "deserialize_decimal")]
316 pub rho: Decimal,
317}
318
319impl DeribitGreeks {
320 pub fn to_greek_values(&self) -> OptionGreekValues {
322 OptionGreekValues {
323 delta: self.delta.to_f64().unwrap_or(0.0),
324 gamma: self.gamma.to_f64().unwrap_or(0.0),
325 vega: self.vega.to_f64().unwrap_or(0.0),
326 theta: self.theta.to_f64().unwrap_or(0.0),
327 rho: self.rho.to_f64().unwrap_or(0.0),
328 }
329 }
330}
331
332#[derive(Debug, Clone, Deserialize)]
334pub struct DeribitQuoteMsg {
335 pub instrument_name: Ustr,
337 pub timestamp: u64,
339 #[serde(deserialize_with = "deserialize_decimal")]
341 pub best_bid_price: Decimal,
342 #[serde(deserialize_with = "deserialize_decimal")]
344 pub best_bid_amount: Decimal,
345 #[serde(deserialize_with = "deserialize_decimal")]
347 pub best_ask_price: Decimal,
348 #[serde(deserialize_with = "deserialize_decimal")]
350 pub best_ask_amount: Decimal,
351}
352
353#[derive(Debug, Clone, Deserialize)]
358pub struct DeribitInstrumentStateMsg {
359 pub instrument_name: Ustr,
361 pub state: DeribitInstrumentState,
363 pub timestamp: u64,
365}
366
367#[derive(Debug, Clone, Deserialize)]
373pub struct DeribitPerpetualMsg {
374 #[serde(deserialize_with = "deserialize_decimal")]
376 pub index_price: Decimal,
377 #[serde(deserialize_with = "deserialize_decimal")]
379 pub interest: Decimal,
380 pub timestamp: u64,
382}
383
384#[derive(Debug, Clone, Deserialize)]
386pub struct DeribitVolatilityIndexMsg {
387 pub timestamp: u64,
389 pub volatility: f64,
391 pub index_name: String,
393}
394
395#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize)]
400#[serde(rename_all = "lowercase")]
401pub enum DeribitChartStatus {
402 #[default]
404 Ok,
405 Imputed,
407}
408
409#[derive(Debug, Clone, Deserialize)]
411pub struct DeribitChartMsg {
412 pub tick: u64,
414 pub open: f64,
416 pub high: f64,
418 pub low: f64,
420 pub close: f64,
422 pub volume: f64,
424 pub cost: f64,
426 #[serde(default)]
428 pub status: DeribitChartStatus,
429}
430
431#[derive(Debug, Clone, Serialize)]
436pub struct DeribitOrderParams {
437 pub instrument_name: String,
439 #[serde(with = "rust_decimal::serde::float")]
441 pub amount: Decimal,
442 #[serde(rename = "type")]
444 pub order_type: String,
445 #[serde(skip_serializing_if = "Option::is_none")]
447 pub label: Option<String>,
448 #[serde(
450 skip_serializing_if = "Option::is_none",
451 with = "rust_decimal::serde::float_option"
452 )]
453 pub price: Option<Decimal>,
454 #[serde(skip_serializing_if = "Option::is_none")]
456 pub time_in_force: Option<String>,
457 #[serde(skip_serializing_if = "Option::is_none")]
460 pub post_only: Option<bool>,
461 #[serde(skip_serializing_if = "Option::is_none")]
464 pub reject_post_only: Option<bool>,
465 #[serde(skip_serializing_if = "Option::is_none")]
467 pub reduce_only: Option<bool>,
468 #[serde(
470 skip_serializing_if = "Option::is_none",
471 with = "rust_decimal::serde::float_option"
472 )]
473 pub trigger_price: Option<Decimal>,
474 #[serde(skip_serializing_if = "Option::is_none")]
476 pub trigger: Option<String>,
477 #[serde(
479 skip_serializing_if = "Option::is_none",
480 with = "rust_decimal::serde::float_option"
481 )]
482 pub max_show: Option<Decimal>,
483 #[serde(skip_serializing_if = "Option::is_none")]
485 pub valid_until: Option<u64>,
486}
487
488#[derive(Debug, Clone, Serialize)]
490pub struct DeribitCancelParams {
491 pub order_id: String,
493}
494
495#[derive(Debug, Clone, Serialize)]
497pub struct DeribitCancelAllByInstrumentParams {
498 pub instrument_name: String,
500 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
502 pub order_type: Option<String>,
503}
504
505#[derive(Debug, Clone, Serialize)]
510pub struct DeribitEditParams {
511 pub order_id: String,
513 #[serde(with = "rust_decimal::serde::float")]
515 pub amount: Decimal,
516 #[serde(
518 skip_serializing_if = "Option::is_none",
519 with = "rust_decimal::serde::float_option"
520 )]
521 pub price: Option<Decimal>,
522 #[serde(
524 skip_serializing_if = "Option::is_none",
525 with = "rust_decimal::serde::float_option"
526 )]
527 pub trigger_price: Option<Decimal>,
528 #[serde(skip_serializing_if = "Option::is_none")]
531 pub post_only: Option<bool>,
532 #[serde(skip_serializing_if = "Option::is_none")]
535 pub reject_post_only: Option<bool>,
536 #[serde(skip_serializing_if = "Option::is_none")]
538 pub reduce_only: Option<bool>,
539}
540
541#[derive(Debug, Clone, Serialize)]
543pub struct DeribitGetOrderStateParams {
544 pub order_id: String,
546}
547
548fn deserialize_optional_decimal_or_market<'de, D>(
553 deserializer: D,
554) -> Result<Option<Decimal>, D::Error>
555where
556 D: Deserializer<'de>,
557{
558 struct Visitor;
559
560 impl<'de> de::Visitor<'de> for Visitor {
561 type Value = Option<Decimal>;
562
563 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
564 formatter.write_str(
565 "null, a decimal as string/integer/float, or the literal \"market_price\"",
566 )
567 }
568
569 fn visit_str<E: de::Error>(self, v: &str) -> Result<Self::Value, E> {
570 if v.is_empty() || v == "market_price" {
571 return Ok(None);
572 }
573
574 if v.contains('e') || v.contains('E') {
575 Decimal::from_scientific(v).map(Some).map_err(E::custom)
576 } else {
577 Decimal::from_str(v).map(Some).map_err(E::custom)
578 }
579 }
580
581 fn visit_string<E: de::Error>(self, v: String) -> Result<Self::Value, E> {
582 self.visit_str(&v)
583 }
584
585 fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
586 Ok(Some(Decimal::from(v)))
587 }
588
589 fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
590 Ok(Some(Decimal::from(v)))
591 }
592
593 fn visit_i128<E: de::Error>(self, v: i128) -> Result<Self::Value, E> {
594 Ok(Some(Decimal::from(v)))
595 }
596
597 fn visit_u128<E: de::Error>(self, v: u128) -> Result<Self::Value, E> {
598 Ok(Some(Decimal::from(v)))
599 }
600
601 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
602 if v.is_nan() || v.is_infinite() {
603 return Err(E::invalid_value(de::Unexpected::Float(v), &self));
604 }
605 Decimal::try_from(v).map(Some).map_err(E::custom)
606 }
607
608 fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
609 Ok(None)
610 }
611
612 fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
613 Ok(None)
614 }
615 }
616
617 deserializer.deserialize_any(Visitor)
618}
619
620#[derive(Debug, Clone, Deserialize)]
624pub struct DeribitOrderResponse {
625 pub order: DeribitOrderMsg,
627 #[serde(default)]
629 pub trades: Vec<DeribitUserTradeMsg>,
630}
631
632#[derive(Debug, Clone, Deserialize)]
636pub struct DeribitOrderMsg {
637 pub order_id: String,
639 pub label: Option<String>,
641 pub instrument_name: Ustr,
643 pub direction: String,
645 pub order_type: String,
647 pub order_state: String,
649 #[serde(default)]
651 pub replaced: bool,
652 #[serde(default, deserialize_with = "deserialize_optional_decimal_or_market")]
655 pub price: Option<Decimal>,
656 #[serde(deserialize_with = "nautilus_core::serialization::deserialize_decimal")]
658 pub amount: Decimal,
659 #[serde(default, deserialize_with = "deserialize_decimal")]
662 pub filled_amount: Decimal,
663 #[serde(
665 default,
666 deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
667 )]
668 pub average_price: Option<Decimal>,
669 pub creation_timestamp: u64,
671 pub last_update_timestamp: u64,
673 pub time_in_force: String,
675 #[serde(
677 default,
678 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
679 )]
680 pub commission: Decimal,
681 #[serde(default)]
683 pub post_only: bool,
684 #[serde(default)]
686 pub reduce_only: bool,
687 #[serde(
689 default,
690 deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
691 )]
692 pub trigger_price: Option<Decimal>,
693 pub trigger: Option<String>,
695 #[serde(
697 default,
698 deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
699 )]
700 pub max_show: Option<Decimal>,
701 #[serde(default)]
703 pub api: bool,
704 pub reject_reason: Option<String>,
706 pub cancel_reason: Option<String>,
708}
709
710#[derive(Debug, Clone, Serialize, Deserialize)]
714pub struct DeribitUserTradeMsg {
715 pub trade_id: String,
717 pub order_id: String,
719 pub instrument_name: Ustr,
721 pub direction: String,
723 #[serde(
725 serialize_with = "nautilus_core::serialization::serialize_decimal",
726 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
727 )]
728 pub price: Decimal,
729 #[serde(
731 serialize_with = "nautilus_core::serialization::serialize_decimal",
732 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
733 )]
734 pub amount: Decimal,
735 #[serde(
737 serialize_with = "nautilus_core::serialization::serialize_decimal",
738 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
739 )]
740 pub fee: Decimal,
741 pub fee_currency: String,
743 pub timestamp: u64,
745 pub trade_seq: u64,
747 pub liquidity: String,
749 pub order_type: String,
751 #[serde(
753 serialize_with = "nautilus_core::serialization::serialize_decimal",
754 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
755 )]
756 pub index_price: Decimal,
757 #[serde(
759 serialize_with = "nautilus_core::serialization::serialize_decimal",
760 deserialize_with = "nautilus_core::serialization::deserialize_decimal"
761 )]
762 pub mark_price: Decimal,
763 pub tick_direction: i8,
765 pub state: String,
767 pub label: Option<String>,
769 #[serde(default)]
771 pub reduce_only: bool,
772 #[serde(default)]
774 pub post_only: bool,
775 #[serde(default)]
777 pub liquidation: Option<String>,
778 #[serde(
780 default,
781 serialize_with = "nautilus_core::serialization::serialize_optional_decimal",
782 deserialize_with = "nautilus_core::serialization::deserialize_optional_decimal"
783 )]
784 pub profit_loss: Option<Decimal>,
785}
786
787#[derive(Debug, Clone, Deserialize)]
789pub struct DeribitPortfolioMsg {
790 pub currency: String,
792 #[serde(with = "rust_decimal::serde::float")]
794 pub equity: Decimal,
795 #[serde(with = "rust_decimal::serde::float")]
797 pub balance: Decimal,
798 #[serde(with = "rust_decimal::serde::float")]
800 pub available_funds: Decimal,
801 #[serde(with = "rust_decimal::serde::float")]
803 pub margin_balance: Decimal,
804 #[serde(with = "rust_decimal::serde::float")]
806 pub initial_margin: Decimal,
807 #[serde(with = "rust_decimal::serde::float")]
809 pub maintenance_margin: Decimal,
810 #[serde(default)]
812 pub margin_model: Option<String>,
813 #[serde(default)]
815 pub cross_collateral_enabled: Option<bool>,
816 #[serde(default, deserialize_with = "deserialize_optional_decimal")]
818 pub available_withdrawal_funds: Option<Decimal>,
819}
820
821#[derive(Clone)]
823pub enum DeribitWsMessage {
824 Response(DeribitJsonRpcResponse<serde_json::Value>),
826 Notification(DeribitSubscriptionNotification<serde_json::Value>),
828 Heartbeat(DeribitHeartbeatData),
830 Error(DeribitJsonRpcError),
832 Reconnected,
834}
835
836impl Debug for DeribitWsMessage {
837 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
838 match self {
839 Self::Response(response)
840 if response.result.as_ref().is_some_and(|result| {
841 result.get("access_token").is_some() || result.get("refresh_token").is_some()
842 }) =>
843 {
844 f.debug_tuple("Response").field(&REDACTED).finish()
845 }
846 Self::Response(response) => f.debug_tuple("Response").field(response).finish(),
847 Self::Notification(notification) => {
848 f.debug_tuple("Notification").field(notification).finish()
849 }
850 Self::Heartbeat(heartbeat) => f.debug_tuple("Heartbeat").field(heartbeat).finish(),
851 Self::Error(error) => f.debug_tuple("Error").field(error).finish(),
852 Self::Reconnected => f.write_str("Reconnected"),
853 }
854 }
855}
856
857#[derive(Debug, Clone, Serialize, Deserialize)]
859pub struct DeribitWebSocketError {
860 pub code: i64,
862 pub message: String,
864 pub timestamp: u64,
866}
867
868impl From<DeribitJsonRpcError> for DeribitWebSocketError {
869 fn from(err: DeribitJsonRpcError) -> Self {
870 Self {
871 code: err.code,
872 message: err.message,
873 timestamp: 0,
874 }
875 }
876}
877
878#[derive(Debug, Clone)]
880pub enum NautilusWsMessage {
881 Data(Vec<Data>),
883 Deltas(OrderBookDeltas),
885 Instrument(Box<InstrumentAny>),
887 FundingRates(Vec<FundingRateUpdate>),
889 OptionGreeks(OptionGreeks),
891 OrderStatusReports(Vec<OrderStatusReport>),
893 FillReports(Vec<FillReport>),
895 OrderFilled(OrderFilled),
897 OrderAccepted(OrderAccepted),
899 OrderCanceled(OrderCanceled),
901 OrderExpired(OrderExpired),
903 OrderRejected(OrderRejected),
905 OrderCancelRejected(OrderCancelRejected),
907 OrderModifyRejected(OrderModifyRejected),
909 OrderUpdated(OrderUpdated),
911 AccountState(AccountState),
913 InstrumentStatus(InstrumentStatus),
915 Error(DeribitWsError),
917 Raw(serde_json::Value),
919 Reconnected,
921 Authenticated(Box<DeribitAuthResult>),
923 AuthenticationFailed(String),
925}
926
927pub fn parse_raw_message(text: &str) -> Result<DeribitWsMessage, DeribitWsError> {
933 let value: serde_json::Value =
934 serde_json::from_str(text).map_err(|e| DeribitWsError::Json(e.to_string()))?;
935
936 if let Some(method) = value.get("method").and_then(|m| m.as_str()) {
938 if method == "subscription" {
939 let notification: DeribitSubscriptionNotification<serde_json::Value> =
940 serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
941 return Ok(DeribitWsMessage::Notification(notification));
942 }
943 if method == "heartbeat"
945 && let Some(params) = value.get("params")
946 {
947 let heartbeat: DeribitHeartbeatData = serde_json::from_value(params.clone())
948 .map_err(|e| DeribitWsError::Json(e.to_string()))?;
949 return Ok(DeribitWsMessage::Heartbeat(heartbeat));
950 }
951 }
952
953 if value.get("id").is_some() {
958 let response: DeribitJsonRpcResponse<serde_json::Value> =
959 serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
960 return Ok(DeribitWsMessage::Response(response));
961 }
962
963 let response: DeribitJsonRpcResponse<serde_json::Value> =
965 serde_json::from_value(value).map_err(|e| DeribitWsError::Json(e.to_string()))?;
966 Ok(DeribitWsMessage::Response(response))
967}
968
969pub fn extract_instrument_from_channel(channel: &str) -> Option<&str> {
973 let parts: Vec<&str> = channel.split('.').collect();
974 if parts.len() >= 2 {
975 Some(parts[1])
976 } else {
977 None
978 }
979}
980
981#[cfg(test)]
982mod tests {
983 use rstest::rstest;
984
985 use super::*;
986
987 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
988
989 #[rstest]
990 fn auth_messages_preserve_wire_values_and_redact_debug() {
991 let params = DeribitAuthParams {
992 grant_type: "client_signature".to_string(),
993 client_id: SecretString::from("client-id-value"),
994 timestamp: 1_700_000_000_000,
995 signature: SecretString::from("signature-value"),
996 nonce: "nonce-value".to_string(),
997 data: SecretString::from("data-value"),
998 scope: Some("session:test".to_string()),
999 };
1000 let refresh = DeribitRefreshTokenParams {
1001 grant_type: "refresh_token".to_string(),
1002 refresh_token: SecretString::from("refresh-token-value"),
1003 };
1004
1005 let params_json = serde_json::to_value(¶ms).unwrap();
1006 let refresh_json = serde_json::to_value(&refresh).unwrap();
1007 let formatted = format!("{params:?} {refresh:?}");
1008
1009 assert_eq!(params_json["client_id"], "client-id-value");
1010 assert_eq!(params_json["signature"], "signature-value");
1011 assert_eq!(params_json["data"], "data-value");
1012 assert_eq!(refresh_json["refresh_token"], "refresh-token-value");
1013 assert!(!formatted.contains("client-id-value"));
1014 assert!(!formatted.contains("signature-value"));
1015 assert!(!formatted.contains("data-value"));
1016 assert!(!formatted.contains("refresh-token-value"));
1017
1018 let DeribitAuthParams {
1019 client_id,
1020 signature,
1021 data,
1022 ..
1023 } = params;
1024 let DeribitRefreshTokenParams { refresh_token, .. } = refresh;
1025 assert_eq!(client_id.expose_secret(), "client-id-value");
1026 assert_eq!(signature.expose_secret(), "signature-value");
1027 assert_eq!(data.expose_secret(), "data-value");
1028 assert_eq!(refresh_token.expose_secret(), "refresh-token-value");
1029 }
1030
1031 #[rstest]
1032 fn auth_result_zeroizes_on_drop() {
1033 assert_zeroize_on_drop::<DeribitAuthResult>();
1034
1035 let result = DeribitAuthResult {
1036 access_token: SecretString::from("access-token-value"),
1037 expires_in: 900,
1038 refresh_token: SecretString::from("refresh-token-value"),
1039 scope: "session:test".to_string(),
1040 token_type: "bearer".to_string(),
1041 enabled_features: vec!["feature".to_string()],
1042 };
1043 let formatted = format!("{result:?}");
1044
1045 assert_eq!(formatted.matches(REDACTED).count(), 2);
1046 assert!(!formatted.contains(result.access_token.expose_secret()));
1047 assert!(!formatted.contains(result.refresh_token.expose_secret()));
1048 }
1049
1050 #[rstest]
1051 fn test_parse_subscription_notification() {
1052 let json = r#"{
1053 "jsonrpc": "2.0",
1054 "method": "subscription",
1055 "params": {
1056 "channel": "trades.BTC-PERPETUAL.raw",
1057 "data": [{"trade_id": "123", "price": 50000.0}]
1058 }
1059 }"#;
1060
1061 let msg = parse_raw_message(json).unwrap();
1062 assert!(matches!(msg, DeribitWsMessage::Notification(_)));
1063 }
1064
1065 #[rstest]
1066 fn test_parse_response() {
1067 let json = r#"{
1068 "jsonrpc": "2.0",
1069 "id": 1,
1070 "result": ["trades.BTC-PERPETUAL.raw"],
1071 "testnet": true,
1072 "usIn": 1234567890,
1073 "usOut": 1234567891,
1074 "usDiff": 1
1075 }"#;
1076
1077 let msg = parse_raw_message(json).unwrap();
1078 assert!(matches!(msg, DeribitWsMessage::Response(_)));
1079 }
1080
1081 #[rstest]
1082 fn test_auth_response_debug_redacts_tokens() {
1083 let access_token = "access-token-value";
1084 let refresh_token = "refresh-token-value";
1085 let json = format!(
1086 r#"{{
1087 "jsonrpc": "2.0",
1088 "id": 1,
1089 "result": {{
1090 "access_token": "{access_token}",
1091 "refresh_token": "{refresh_token}"
1092 }}
1093 }}"#,
1094 );
1095
1096 let msg = parse_raw_message(&json).unwrap();
1097 let debug = format!("{msg:?}");
1098
1099 assert!(debug.contains(REDACTED));
1100 assert!(!debug.contains(access_token));
1101 assert!(!debug.contains(refresh_token));
1102 }
1103
1104 #[rstest]
1105 fn test_parse_error_response() {
1106 let json = r#"{
1109 "jsonrpc": "2.0",
1110 "id": 1,
1111 "error": {
1112 "code": 10028,
1113 "message": "too_many_requests"
1114 }
1115 }"#;
1116
1117 let msg = parse_raw_message(json).unwrap();
1118 match msg {
1119 DeribitWsMessage::Response(resp) => {
1120 assert!(resp.error.is_some());
1121 let error = resp.error.unwrap();
1122 assert_eq!(error.code, 10028);
1123 assert_eq!(error.message, "too_many_requests");
1124 }
1125 _ => panic!("Expected Response with error, was {msg:?}"),
1126 }
1127 }
1128
1129 #[rstest]
1130 fn test_extract_instrument_from_channel() {
1131 assert_eq!(
1132 extract_instrument_from_channel("trades.BTC-PERPETUAL.raw"),
1133 Some("BTC-PERPETUAL")
1134 );
1135 assert_eq!(
1136 extract_instrument_from_channel("book.ETH-25DEC25.raw"),
1137 Some("ETH-25DEC25")
1138 );
1139 assert_eq!(extract_instrument_from_channel("platform_state"), None);
1140 }
1141
1142 #[rstest]
1143 fn test_parse_volatility_index_payload() {
1144 let value = serde_json::json!({
1145 "timestamp": 1619777946007_u64,
1146 "volatility": 129.36_f64,
1147 "index_name": "btc_usd",
1148 });
1149
1150 let payload: DeribitVolatilityIndexMsg = serde_json::from_value(value).unwrap();
1151 assert_eq!(payload.index_name, "btc_usd");
1152 assert_eq!(payload.volatility, 129.36);
1153 assert_eq!(payload.timestamp, 1619777946007_u64);
1154 }
1155}