1use pretty_simple_display::{DebugPretty, DisplaySimple};
8use serde::{Deserialize, Serialize};
9
10#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
12pub enum WebSocketMessage {
13 Request(JsonRpcRequest),
15 Response(JsonRpcResponse),
17 Notification(JsonRpcNotification),
19}
20
21#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
23pub struct JsonRpcRequest {
24 pub jsonrpc: String,
26 pub id: serde_json::Value,
28 pub method: String,
30 pub params: Option<serde_json::Value>,
32}
33
34#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
36pub struct JsonRpcResponse {
37 pub jsonrpc: String,
39 pub id: serde_json::Value,
41 #[serde(flatten)]
43 pub result: JsonRpcResult,
44}
45
46#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
48#[serde(untagged)]
49pub enum JsonRpcResult {
50 Success {
52 result: serde_json::Value,
54 },
55 Error {
57 error: JsonRpcError,
59 },
60}
61
62#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
64pub struct JsonRpcError {
65 pub code: i32,
67 pub message: String,
69 pub data: Option<serde_json::Value>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75pub struct AuthResponse {
76 pub access_token: String,
78 pub token_type: String,
80 pub expires_in: i64,
82 pub refresh_token: String,
84 pub scope: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90pub struct HelloResponse {
91 pub version: String,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
97pub struct TestResponse {
98 pub version: String,
100}
101
102#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
104pub struct JsonRpcNotification {
105 pub jsonrpc: String,
107 pub method: String,
109 pub params: Option<serde_json::Value>,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Hash)]
115pub enum ConnectionState {
116 Disconnected,
118 Connecting,
120 Connected,
122 Authenticated,
124 Reconnecting,
126 Failed,
128}
129
130#[derive(Debug, Clone)]
132pub struct HeartbeatStatus {
133 pub last_ping: Option<std::time::Instant>,
135 pub last_pong: Option<std::time::Instant>,
137 pub missed_pongs: u32,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
143pub enum SubscriptionChannel {
144 Ticker(String),
146 OrderBook(String),
148 Trades(String),
150 ChartTrades {
152 instrument: String,
154 resolution: String,
156 },
157 UserOrders,
159 UserTrades,
161 UserPortfolio,
163 UserChanges {
165 instrument: String,
167 interval: String,
169 },
170 PriceIndex(String),
172 EstimatedExpirationPrice(String),
174 MarkPrice(String),
176 Funding(String),
178 Perpetual {
180 instrument: String,
182 interval: String,
184 },
185 Quote(String),
187 PlatformState,
189 PlatformStatePublicMethods,
191 InstrumentState {
193 kind: String,
195 currency: String,
197 },
198 GroupedOrderBook {
200 instrument: String,
202 group: String,
204 depth: String,
206 interval: String,
208 },
209 IncrementalTicker(String),
211 TradesByKind {
213 kind: String,
215 currency: String,
217 interval: String,
219 },
220 PriceRanking(String),
222 PriceStatistics(String),
224 VolatilityIndex(String),
226 BlockRfqTrades(String),
228 BlockTradeConfirmations,
230 BlockTradeConfirmationsByCurrency(String),
232 UserMmpTrigger(String),
234 UserAccessLog,
236 UserLock,
238 Unknown(String),
240}
241
242#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
244pub struct WsRequest {
245 pub jsonrpc: String,
247 pub id: serde_json::Value,
249 pub method: String,
251 pub params: Option<serde_json::Value>,
253}
254
255#[derive(Clone, Serialize, Deserialize, PartialEq, DebugPretty, DisplaySimple)]
257pub struct WsResponse {
258 pub jsonrpc: String,
260 pub id: Option<serde_json::Value>,
262 pub result: Option<serde_json::Value>,
264 pub error: Option<JsonRpcError>,
266}
267
268impl JsonRpcRequest {
269 pub fn new<T: Serialize>(id: serde_json::Value, method: &str, params: Option<T>) -> Self {
271 Self {
272 jsonrpc: "2.0".to_string(),
273 id,
274 method: method.to_string(),
275 params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
276 }
277 }
278}
279
280impl JsonRpcResponse {
281 pub fn success(id: serde_json::Value, result: serde_json::Value) -> Self {
283 Self {
284 jsonrpc: "2.0".to_string(),
285 id,
286 result: JsonRpcResult::Success { result },
287 }
288 }
289
290 pub fn error(id: serde_json::Value, error: JsonRpcError) -> Self {
292 Self {
293 jsonrpc: "2.0".to_string(),
294 id,
295 result: JsonRpcResult::Error { error },
296 }
297 }
298}
299
300impl JsonRpcNotification {
301 pub fn new<T: Serialize>(method: &str, params: Option<T>) -> Self {
303 Self {
304 jsonrpc: "2.0".to_string(),
305 method: method.to_string(),
306 params: params.map(|p| serde_json::to_value(p).unwrap_or(serde_json::Value::Null)),
307 }
308 }
309}
310
311impl SubscriptionChannel {
312 pub fn channel_name(&self) -> String {
314 match self {
315 SubscriptionChannel::Ticker(instrument) => format!("ticker.{}", instrument),
316 SubscriptionChannel::OrderBook(instrument) => format!("book.{}.raw", instrument),
317 SubscriptionChannel::Trades(instrument) => format!("trades.{}.raw", instrument),
318 SubscriptionChannel::ChartTrades {
319 instrument,
320 resolution,
321 } => {
322 format!("chart.trades.{}.{}", instrument, resolution)
323 }
324 SubscriptionChannel::UserOrders => "user.orders.any.any.raw".to_string(),
325 SubscriptionChannel::UserTrades => "user.trades.any.any.raw".to_string(),
326 SubscriptionChannel::UserPortfolio => "user.portfolio.any".to_string(),
327 SubscriptionChannel::UserChanges {
328 instrument,
329 interval,
330 } => {
331 format!("user.changes.{}.{}", instrument, interval)
332 }
333 SubscriptionChannel::PriceIndex(currency) => {
334 format!("deribit_price_index.{}_usd", currency.to_lowercase())
335 }
336 SubscriptionChannel::EstimatedExpirationPrice(instrument) => {
337 format!("estimated_expiration_price.{}", instrument)
338 }
339 SubscriptionChannel::MarkPrice(instrument) => {
340 format!("markprice.options.{}", instrument)
341 }
342 SubscriptionChannel::Funding(instrument) => format!("perpetual.{}.raw", instrument),
343 SubscriptionChannel::Perpetual {
344 instrument,
345 interval,
346 } => {
347 format!("perpetual.{}.{}", instrument, interval)
348 }
349 SubscriptionChannel::Quote(instrument) => format!("quote.{}", instrument),
350 SubscriptionChannel::PlatformState => "platform_state".to_string(),
351 SubscriptionChannel::PlatformStatePublicMethods => {
352 "platform_state.public_methods_state".to_string()
353 }
354 SubscriptionChannel::InstrumentState { kind, currency } => {
355 format!("instrument.state.{}.{}", kind, currency)
356 }
357 SubscriptionChannel::GroupedOrderBook {
358 instrument,
359 group,
360 depth,
361 interval,
362 } => {
363 format!("book.{}.{}.{}.{}", instrument, group, depth, interval)
364 }
365 SubscriptionChannel::IncrementalTicker(instrument) => {
366 format!("incremental_ticker.{}", instrument)
367 }
368 SubscriptionChannel::TradesByKind {
369 kind,
370 currency,
371 interval,
372 } => {
373 format!("trades.{}.{}.{}", kind, currency, interval)
374 }
375 SubscriptionChannel::PriceRanking(index_name) => {
376 format!("deribit_price_ranking.{}", index_name)
377 }
378 SubscriptionChannel::PriceStatistics(index_name) => {
379 format!("deribit_price_statistics.{}", index_name)
380 }
381 SubscriptionChannel::VolatilityIndex(index_name) => {
382 format!("deribit_volatility_index.{}", index_name)
383 }
384 SubscriptionChannel::BlockRfqTrades(currency) => {
385 format!("block_rfq.trades.{}", currency)
386 }
387 SubscriptionChannel::BlockTradeConfirmations => "block_trade_confirmations".to_string(),
388 SubscriptionChannel::BlockTradeConfirmationsByCurrency(currency) => {
389 format!("block_trade_confirmations.{}", currency)
390 }
391 SubscriptionChannel::UserMmpTrigger(index_name) => {
392 format!("user.mmp_trigger.{}", index_name)
393 }
394 SubscriptionChannel::UserAccessLog => "user.access_log".to_string(),
395 SubscriptionChannel::UserLock => "user.lock".to_string(),
396 SubscriptionChannel::Unknown(channel) => channel.clone(),
397 }
398 }
399
400 #[must_use]
405 pub fn from_string(s: &str) -> Self {
406 let parts: Vec<&str> = s.split('.').collect();
407 match parts.as_slice() {
408 ["ticker", instrument] => SubscriptionChannel::Ticker(instrument.to_string()),
409 ["ticker", instrument, _interval] => {
410 SubscriptionChannel::Ticker(instrument.to_string())
411 }
412 ["book", instrument, "raw"] => SubscriptionChannel::OrderBook(instrument.to_string()),
413 ["book", instrument, group, depth, interval] => SubscriptionChannel::GroupedOrderBook {
414 instrument: instrument.to_string(),
415 group: group.to_string(),
416 depth: depth.to_string(),
417 interval: interval.to_string(),
418 },
419 ["book", instrument, _depth, _interval] => {
420 SubscriptionChannel::OrderBook(instrument.to_string())
421 }
422 ["incremental_ticker", instrument] => {
423 SubscriptionChannel::IncrementalTicker(instrument.to_string())
424 }
425 ["trades", instrument, "raw"] => SubscriptionChannel::Trades(instrument.to_string()),
426 ["trades", kind, currency, interval] if !Self::looks_like_instrument(kind) => {
427 SubscriptionChannel::TradesByKind {
428 kind: kind.to_string(),
429 currency: currency.to_string(),
430 interval: interval.to_string(),
431 }
432 }
433 ["trades", instrument, _interval] => {
434 SubscriptionChannel::Trades(instrument.to_string())
435 }
436 ["chart", "trades", instrument, resolution] => SubscriptionChannel::ChartTrades {
437 instrument: instrument.to_string(),
438 resolution: resolution.to_string(),
439 },
440 ["user", "orders", ..] => SubscriptionChannel::UserOrders,
441 ["user", "trades", ..] => SubscriptionChannel::UserTrades,
442 ["user", "portfolio", ..] => SubscriptionChannel::UserPortfolio,
443 ["user", "changes", instrument, interval] => SubscriptionChannel::UserChanges {
444 instrument: instrument.to_string(),
445 interval: interval.to_string(),
446 },
447 ["deribit_price_index", currency_pair] => {
448 let currency = currency_pair
449 .strip_suffix("_usd")
450 .map(|c| c.to_uppercase())
451 .unwrap_or_else(|| currency_pair.to_uppercase());
452 SubscriptionChannel::PriceIndex(currency)
453 }
454 ["estimated_expiration_price", instrument] => {
455 SubscriptionChannel::EstimatedExpirationPrice(instrument.to_string())
456 }
457 ["markprice", "options", instrument] => {
458 SubscriptionChannel::MarkPrice(instrument.to_string())
459 }
460 ["perpetual", instrument, interval] => SubscriptionChannel::Perpetual {
461 instrument: instrument.to_string(),
462 interval: interval.to_string(),
463 },
464 ["quote", instrument] => SubscriptionChannel::Quote(instrument.to_string()),
465 ["platform_state"] => SubscriptionChannel::PlatformState,
466 ["platform_state", "public_methods_state"] => {
467 SubscriptionChannel::PlatformStatePublicMethods
468 }
469 ["instrument", "state", kind, currency] => SubscriptionChannel::InstrumentState {
470 kind: kind.to_string(),
471 currency: currency.to_string(),
472 },
473 ["deribit_price_ranking", index_name] => {
474 SubscriptionChannel::PriceRanking(index_name.to_string())
475 }
476 ["deribit_price_statistics", index_name] => {
477 SubscriptionChannel::PriceStatistics(index_name.to_string())
478 }
479 ["deribit_volatility_index", index_name] => {
480 SubscriptionChannel::VolatilityIndex(index_name.to_string())
481 }
482 ["block_rfq", "trades", currency] => {
483 SubscriptionChannel::BlockRfqTrades(currency.to_string())
484 }
485 ["block_trade_confirmations"] => SubscriptionChannel::BlockTradeConfirmations,
486 ["block_trade_confirmations", currency] => {
487 SubscriptionChannel::BlockTradeConfirmationsByCurrency(currency.to_string())
488 }
489 ["user", "mmp_trigger", index_name] => {
490 SubscriptionChannel::UserMmpTrigger(index_name.to_string())
491 }
492 ["user", "access_log"] => SubscriptionChannel::UserAccessLog,
493 ["user", "lock"] => SubscriptionChannel::UserLock,
494 _ => SubscriptionChannel::Unknown(s.to_string()),
495 }
496 }
497
498 #[must_use]
500 pub fn is_unknown(&self) -> bool {
501 matches!(self, SubscriptionChannel::Unknown(_))
502 }
503
504 #[must_use]
509 fn looks_like_instrument(s: &str) -> bool {
510 s.contains('-')
511 }
512}
513
514impl std::fmt::Display for SubscriptionChannel {
515 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
516 write!(f, "{}", self.channel_name())
517 }
518}
519
520impl ConnectionState {
521 pub fn is_connected(&self) -> bool {
523 matches!(
524 self,
525 ConnectionState::Connected | ConnectionState::Authenticated
526 )
527 }
528
529 pub fn is_authenticated(&self) -> bool {
531 matches!(self, ConnectionState::Authenticated)
532 }
533
534 pub fn is_transitional(&self) -> bool {
536 matches!(
537 self,
538 ConnectionState::Connecting | ConnectionState::Reconnecting
539 )
540 }
541}
542
543impl HeartbeatStatus {
544 pub fn new() -> Self {
546 Self {
547 last_ping: None,
548 last_pong: None,
549 missed_pongs: 0,
550 }
551 }
552
553 pub fn ping_sent(&mut self) {
555 self.last_ping = Some(std::time::Instant::now());
556 }
557
558 pub fn pong_received(&mut self) {
560 self.last_pong = Some(std::time::Instant::now());
561 self.missed_pongs = 0;
562 }
563
564 pub fn missed_pong(&mut self) {
566 self.missed_pongs += 1;
567 }
568
569 pub fn is_stale(&self, max_missed_pongs: u32) -> bool {
571 self.missed_pongs >= max_missed_pongs
572 }
573}
574
575impl Default for HeartbeatStatus {
576 fn default() -> Self {
577 Self::new()
578 }
579}