deribit_websocket/model/
ws_types.rs

1//! WebSocket-specific types and models for Deribit API
2//!
3//! This module contains data structures specific to WebSocket communication,
4//! including JSON-RPC message types, connection states, and WebSocket-specific
5//! request/response structures.
6
7use deribit_base::{impl_json_debug_pretty, impl_json_display};
8use serde::{Deserialize, Serialize};
9
10/// WebSocket message types for JSON-RPC communication
11#[derive(Clone, Serialize, Deserialize, PartialEq)]
12pub enum WebSocketMessage {
13    /// JSON-RPC request message
14    Request(JsonRpcRequest),
15    /// JSON-RPC response message
16    Response(JsonRpcResponse),
17    /// JSON-RPC notification message (no response expected)
18    Notification(JsonRpcNotification),
19}
20
21/// JSON-RPC 2.0 request structure
22#[derive(Clone, Serialize, Deserialize, PartialEq)]
23pub struct JsonRpcRequest {
24    /// JSON-RPC version (always "2.0")
25    pub jsonrpc: String,
26    /// Request identifier for correlation with response
27    pub id: serde_json::Value,
28    /// Method name to call
29    pub method: String,
30    /// Optional parameters for the method
31    pub params: Option<serde_json::Value>,
32}
33
34/// JSON-RPC 2.0 response structure
35#[derive(Clone, Serialize, Deserialize, PartialEq)]
36pub struct JsonRpcResponse {
37    /// JSON-RPC version (always "2.0")
38    pub jsonrpc: String,
39    /// Request identifier for correlation
40    pub id: serde_json::Value,
41    /// Result or error information
42    #[serde(flatten)]
43    pub result: JsonRpcResult,
44}
45
46/// JSON-RPC 2.0 result or error union
47#[derive(Clone, Serialize, Deserialize, PartialEq)]
48#[serde(untagged)]
49pub enum JsonRpcResult {
50    /// Successful result
51    Success {
52        /// Result data
53        result: serde_json::Value,
54    },
55    /// Error result
56    Error {
57        /// Error information
58        error: JsonRpcError,
59    },
60}
61
62/// JSON-RPC 2.0 error structure
63#[derive(Clone, Serialize, Deserialize, PartialEq)]
64pub struct JsonRpcError {
65    /// Error code
66    pub code: i32,
67    /// Error message
68    pub message: String,
69    /// Optional additional error data
70    pub data: Option<serde_json::Value>,
71}
72
73/// JSON-RPC 2.0 notification structure (no response expected)
74#[derive(Clone, Serialize, Deserialize, PartialEq)]
75pub struct JsonRpcNotification {
76    /// JSON-RPC version (always "2.0")
77    pub jsonrpc: String,
78    /// Method name
79    pub method: String,
80    /// Optional parameters
81    pub params: Option<serde_json::Value>,
82}
83
84/// WebSocket connection state
85#[derive(Debug, Clone, PartialEq, Eq, Hash)]
86pub enum ConnectionState {
87    /// Not connected
88    Disconnected,
89    /// Attempting to connect
90    Connecting,
91    /// Connected but not authenticated
92    Connected,
93    /// Connected and authenticated
94    Authenticated,
95    /// Attempting to reconnect
96    Reconnecting,
97    /// Connection failed
98    Failed,
99}
100
101/// Heartbeat monitoring status
102#[derive(Debug, Clone)]
103pub struct HeartbeatStatus {
104    /// Last ping sent timestamp
105    pub last_ping: Option<std::time::Instant>,
106    /// Last pong received timestamp
107    pub last_pong: Option<std::time::Instant>,
108    /// Number of consecutive missed pongs
109    pub missed_pongs: u32,
110}
111
112/// WebSocket subscription channel types
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
114pub enum SubscriptionChannel {
115    /// Ticker data for a specific instrument
116    Ticker(String),
117    /// Order book data for a specific instrument
118    OrderBook(String),
119    /// Trade data for a specific instrument
120    Trades(String),
121    /// Chart trade data for a specific instrument with resolution
122    ChartTrades {
123        /// The trading instrument (e.g., "BTC-PERPETUAL")
124        instrument: String,
125        /// Chart resolution (e.g., "1", "5", "15", "60" for minutes)
126        resolution: String,
127    },
128    /// User's order updates
129    UserOrders,
130    /// User's trade updates
131    UserTrades,
132    /// User's portfolio updates
133    UserPortfolio,
134    /// User's position changes for a specific instrument with interval
135    UserChanges {
136        /// The trading instrument (e.g., "BTC-PERPETUAL")
137        instrument: String,
138        /// Update interval (e.g., "raw", "100ms")
139        interval: String,
140    },
141    /// Price index updates
142    PriceIndex(String),
143    /// Estimated delivery price
144    EstimatedExpirationPrice(String),
145    /// Mark price updates
146    MarkPrice(String),
147    /// Funding rate updates
148    Funding(String),
149    /// Perpetual updates
150    Perpetual(String),
151    /// Quote updates
152    Quote(String),
153}
154
155/// WebSocket request structure for Deribit API
156#[derive(Clone, Serialize, Deserialize, PartialEq)]
157pub struct WsRequest {
158    /// JSON-RPC version
159    pub jsonrpc: String,
160    /// Request ID for correlation with responses
161    pub id: serde_json::Value,
162    /// API method name to call
163    pub method: String,
164    /// Parameters for the API method
165    pub params: Option<serde_json::Value>,
166}
167
168/// WebSocket response structure for Deribit API
169#[derive(Clone, Serialize, Deserialize, PartialEq)]
170pub struct WsResponse {
171    /// JSON-RPC version
172    pub jsonrpc: String,
173    /// Request ID for correlation (None for notifications)
174    pub id: Option<serde_json::Value>,
175    /// Result data if the request was successful
176    pub result: Option<serde_json::Value>,
177    /// Error information if the request failed
178    pub error: Option<JsonRpcError>,
179}
180
181impl JsonRpcRequest {
182    /// Create a new JSON-RPC request
183    pub fn new<T: Serialize>(id: serde_json::Value, method: &str, params: Option<T>) -> Self {
184        Self {
185            jsonrpc: "2.0".to_string(),
186            id,
187            method: method.to_string(),
188            params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
189        }
190    }
191}
192
193impl JsonRpcResponse {
194    /// Create a new successful JSON-RPC response
195    pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
196        Self {
197            jsonrpc: "2.0".to_string(),
198            id,
199            result: JsonRpcResult::Success { result },
200        }
201    }
202
203    /// Create a new error JSON-RPC response
204    pub fn error(id: serde_json::Value, error: JsonRpcError) -> Self {
205        Self {
206            jsonrpc: "2.0".to_string(),
207            id,
208            result: JsonRpcResult::Error { error },
209        }
210    }
211}
212
213impl JsonRpcNotification {
214    /// Create a new JSON-RPC notification
215    pub fn new<T: Serialize>(method: &str, params: Option<T>) -> Self {
216        Self {
217            jsonrpc: "2.0".to_string(),
218            method: method.to_string(),
219            params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
220        }
221    }
222}
223
224impl SubscriptionChannel {
225    /// Convert subscription channel to channel name
226    pub fn channel_name(&self) -> String {
227        match self {
228            SubscriptionChannel::Ticker(instrument) => format!("ticker.{}", instrument),
229            SubscriptionChannel::OrderBook(instrument) => format!("book.{}.raw", instrument),
230            SubscriptionChannel::Trades(instrument) => format!("trades.{}.raw", instrument),
231            SubscriptionChannel::ChartTrades {
232                instrument,
233                resolution,
234            } => {
235                format!("chart.trades.{}.{}", instrument, resolution)
236            }
237            SubscriptionChannel::UserOrders => "user.orders.any.any.raw".to_string(),
238            SubscriptionChannel::UserTrades => "user.trades.any.any.raw".to_string(),
239            SubscriptionChannel::UserPortfolio => "user.portfolio.any".to_string(),
240            SubscriptionChannel::UserChanges {
241                instrument,
242                interval,
243            } => {
244                format!("user.changes.{}.{}", instrument, interval)
245            }
246            SubscriptionChannel::PriceIndex(currency) => {
247                format!("deribit_price_index.{}_usd", currency.to_lowercase())
248            }
249            SubscriptionChannel::EstimatedExpirationPrice(instrument) => {
250                format!("estimated_expiration_price.{}", instrument)
251            }
252            SubscriptionChannel::MarkPrice(instrument) => {
253                format!("markprice.options.{}", instrument)
254            }
255            SubscriptionChannel::Funding(instrument) => format!("perpetual.{}.raw", instrument),
256            SubscriptionChannel::Perpetual(instrument) => format!("perpetual.{}.raw", instrument),
257            SubscriptionChannel::Quote(instrument) => format!("quote.{}", instrument),
258        }
259    }
260
261    /// Parse subscription channel from string
262    pub fn from_string(s: &str) -> Option<Self> {
263        let parts: Vec<&str> = s.split('.').collect();
264        match parts.as_slice() {
265            ["ticker", instrument] => Some(SubscriptionChannel::Ticker(instrument.to_string())),
266            ["book", instrument, "raw"] => {
267                Some(SubscriptionChannel::OrderBook(instrument.to_string()))
268            }
269            ["trades", instrument, "raw"] => {
270                Some(SubscriptionChannel::Trades(instrument.to_string()))
271            }
272            ["chart", "trades", instrument, resolution] => Some(SubscriptionChannel::ChartTrades {
273                instrument: instrument.to_string(),
274                resolution: resolution.to_string(),
275            }),
276            ["user", "orders", "any", "any", "raw"] => Some(SubscriptionChannel::UserOrders),
277            ["user", "trades", "any", "any", "raw"] => Some(SubscriptionChannel::UserTrades),
278            ["user", "portfolio", "any"] => Some(SubscriptionChannel::UserPortfolio),
279            ["user", "changes", instrument, interval] => Some(SubscriptionChannel::UserChanges {
280                instrument: instrument.to_string(),
281                interval: interval.to_string(),
282            }),
283            ["deribit_price_index", currency_pair] => currency_pair
284                .strip_suffix("_usd")
285                .map(|currency| SubscriptionChannel::PriceIndex(currency.to_uppercase())),
286            ["estimated_expiration_price", instrument] => Some(
287                SubscriptionChannel::EstimatedExpirationPrice(instrument.to_string()),
288            ),
289            ["markprice", "options", instrument] => {
290                Some(SubscriptionChannel::MarkPrice(instrument.to_string()))
291            }
292            ["perpetual", instrument, "raw"] => {
293                Some(SubscriptionChannel::Perpetual(instrument.to_string()))
294            }
295            ["quote", instrument] => Some(SubscriptionChannel::Quote(instrument.to_string())),
296            _ => None,
297        }
298    }
299}
300
301impl std::fmt::Display for SubscriptionChannel {
302    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
303        write!(f, "{}", self.channel_name())
304    }
305}
306
307impl ConnectionState {
308    /// Check if the connection is in a connected state
309    pub fn is_connected(&self) -> bool {
310        matches!(
311            self,
312            ConnectionState::Connected | ConnectionState::Authenticated
313        )
314    }
315
316    /// Check if the connection is authenticated
317    pub fn is_authenticated(&self) -> bool {
318        matches!(self, ConnectionState::Authenticated)
319    }
320
321    /// Check if the connection is in a transitional state
322    pub fn is_transitional(&self) -> bool {
323        matches!(
324            self,
325            ConnectionState::Connecting | ConnectionState::Reconnecting
326        )
327    }
328}
329
330impl HeartbeatStatus {
331    /// Create a new heartbeat status
332    pub fn new() -> Self {
333        Self {
334            last_ping: None,
335            last_pong: None,
336            missed_pongs: 0,
337        }
338    }
339
340    /// Record a ping sent
341    pub fn ping_sent(&mut self) {
342        self.last_ping = Some(std::time::Instant::now());
343    }
344
345    /// Record a pong received
346    pub fn pong_received(&mut self) {
347        self.last_pong = Some(std::time::Instant::now());
348        self.missed_pongs = 0;
349    }
350
351    /// Record a missed pong
352    pub fn missed_pong(&mut self) {
353        self.missed_pongs += 1;
354    }
355
356    /// Check if connection is considered stale
357    pub fn is_stale(&self, max_missed_pongs: u32) -> bool {
358        self.missed_pongs >= max_missed_pongs
359    }
360}
361
362impl Default for HeartbeatStatus {
363    fn default() -> Self {
364        Self::new()
365    }
366}
367
368// JSON formatting implementations
369impl_json_display!(WebSocketMessage);
370impl_json_debug_pretty!(WebSocketMessage);
371
372impl_json_display!(JsonRpcRequest);
373impl_json_debug_pretty!(JsonRpcRequest);
374
375impl_json_display!(JsonRpcResponse);
376impl_json_debug_pretty!(JsonRpcResponse);
377
378impl_json_display!(JsonRpcResult);
379impl_json_debug_pretty!(JsonRpcResult);
380
381impl_json_display!(JsonRpcError);
382impl_json_debug_pretty!(JsonRpcError);
383
384impl_json_display!(JsonRpcNotification);
385impl_json_debug_pretty!(JsonRpcNotification);
386
387impl_json_display!(WsRequest);
388impl_json_debug_pretty!(WsRequest);
389
390impl_json_display!(WsResponse);
391impl_json_debug_pretty!(WsResponse);