Skip to main content

crypto_ws_client/clients/dydx/
dydx_swap.rs

1use async_trait::async_trait;
2use std::collections::HashMap;
3use tokio_tungstenite::tungstenite::Message;
4
5use crate::{
6    clients::common_traits::{
7        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,
8    },
9    common::{
10        command_translator::CommandTranslator,
11        message_handler::{MessageHandler, MiscMessage},
12        ws_client_internal::WSClientInternal,
13    },
14    WSClient,
15};
16
17use super::EXCHANGE_NAME;
18use log::*;
19use serde_json::Value;
20
21const WEBSOCKET_URL: &str = "wss://api.dydx.exchange/v3/ws";
22
23/// The WebSocket client for dYdX perpetual markets.
24///
25/// * WebSocket API doc: <https://docs.dydx.exchange/#v3-websocket-api>
26/// * Trading at: <https://trade.dydx.exchange/trade>
27pub struct DydxSwapWSClient {
28    client: WSClientInternal<DydxMessageHandler>,
29    translator: DydxCommandTranslator,
30}
31
32impl_new_constructor!(
33    DydxSwapWSClient,
34    EXCHANGE_NAME,
35    WEBSOCKET_URL,
36    DydxMessageHandler {},
37    DydxCommandTranslator {}
38);
39
40impl_trait!(Trade, DydxSwapWSClient, subscribe_trade, "v3_trades");
41#[rustfmt::skip]
42impl_trait!(OrderBook, DydxSwapWSClient, subscribe_orderbook, "v3_orderbook");
43
44panic_ticker!(DydxSwapWSClient);
45panic_bbo!(DydxSwapWSClient);
46panic_l2_topk!(DydxSwapWSClient);
47panic_l3_orderbook!(DydxSwapWSClient);
48panic_candlestick!(DydxSwapWSClient);
49
50impl_ws_client_trait!(DydxSwapWSClient);
51
52struct DydxMessageHandler {}
53struct DydxCommandTranslator {}
54
55impl MessageHandler for DydxMessageHandler {
56    fn handle_message(&mut self, msg: &str) -> MiscMessage {
57        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();
58
59        match obj.get("type").unwrap().as_str().unwrap() {
60            "error" => {
61                error!("Received {} from {}", msg, EXCHANGE_NAME);
62                if obj.contains_key("message")
63                    && obj
64                        .get("message")
65                        .unwrap()
66                        .as_str()
67                        .unwrap()
68                        .starts_with("Invalid subscription id for channel")
69                {
70                    panic!("Received {msg} from {EXCHANGE_NAME}");
71                } else {
72                    MiscMessage::Other
73                }
74            }
75            "connected" | "pong" => {
76                debug!("Received {} from {}", msg, EXCHANGE_NAME);
77                MiscMessage::Other
78            }
79            "channel_data" | "subscribed" => MiscMessage::Normal,
80            _ => {
81                warn!("Received {} from {}", msg, EXCHANGE_NAME);
82                MiscMessage::Other
83            }
84        }
85    }
86
87    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
88        // https://docs.dydx.exchange/#v3-websocket-api
89        // The server will send pings every 30s and expects a pong within 10s.
90        // The server does not expect pings, but will respond with a pong if sent one.
91        Some((Message::Text(r#"{"type":"ping"}"#.to_string()), 30))
92    }
93}
94
95impl DydxCommandTranslator {
96    fn topic_to_command(topic: &(String, String), subscribe: bool) -> String {
97        format!(
98            r#"{{"type": "{}", "channel": "{}", "id": "{}"}}"#,
99            if subscribe { "subscribe" } else { "unsubscribe" },
100            topic.0,
101            topic.1,
102        )
103    }
104}
105
106impl CommandTranslator for DydxCommandTranslator {
107    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
108        topics.iter().map(|t| Self::topic_to_command(t, subscribe)).collect()
109    }
110
111    fn translate_to_candlestick_commands(
112        &self,
113        _subscribe: bool,
114        _symbol_interval_list: &[(String, usize)],
115    ) -> Vec<String> {
116        panic!("dYdX does NOT have candlestick channel");
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use crate::common::command_translator::CommandTranslator;
123
124    #[test]
125    fn test_one_topic() {
126        let translator = super::DydxCommandTranslator {};
127        let commands = translator
128            .translate_to_commands(true, &[("v3_trades".to_string(), "BTC-USD".to_string())]);
129
130        assert_eq!(1, commands.len());
131        assert_eq!(
132            r#"{"type": "subscribe", "channel": "v3_trades", "id": "BTC-USD"}"#,
133            commands[0]
134        );
135    }
136
137    #[test]
138    fn test_two_topic() {
139        let translator = super::DydxCommandTranslator {};
140        let commands = translator.translate_to_commands(
141            true,
142            &[
143                ("v3_trades".to_string(), "BTC-USD".to_string()),
144                ("v3_orderbook".to_string(), "BTC-USD".to_string()),
145            ],
146        );
147
148        assert_eq!(2, commands.len());
149        assert_eq!(
150            r#"{"type": "subscribe", "channel": "v3_trades", "id": "BTC-USD"}"#,
151            commands[0]
152        );
153        assert_eq!(
154            r#"{"type": "subscribe", "channel": "v3_orderbook", "id": "BTC-USD"}"#,
155            commands[1]
156        );
157    }
158}