Skip to main content

crypto_ws_client/clients/mexc/
mexc_swap.rs

1use async_trait::async_trait;
2use std::collections::HashMap;
3use tokio_tungstenite::tungstenite::Message;
4
5use super::EXCHANGE_NAME;
6use crate::{
7    clients::common_traits::{
8        Candlestick, Level3OrderBook, OrderBook, OrderBookTopK, Ticker, Trade, BBO,
9    },
10    common::{
11        command_translator::CommandTranslator,
12        message_handler::{MessageHandler, MiscMessage},
13        ws_client_internal::WSClientInternal,
14    },
15    WSClient,
16};
17
18use log::*;
19use serde_json::Value;
20
21pub(super) const SWAP_WEBSOCKET_URL: &str = "wss://contract.mexc.com/ws";
22
23/// MEXC Swap market.
24///
25///   * WebSocket API doc: <https://mxcdevelop.github.io/APIDoc/contract.api.en.html#websocket-api>
26///   * Trading at: <https://contract.mexc.com/exchange>
27pub struct MexcSwapWSClient {
28    client: WSClientInternal<MexcMessageHandler>,
29    translator: MexcCommandTranslator,
30}
31
32impl_new_constructor!(
33    MexcSwapWSClient,
34    EXCHANGE_NAME,
35    SWAP_WEBSOCKET_URL,
36    MexcMessageHandler {},
37    MexcCommandTranslator {}
38);
39
40#[rustfmt::skip]
41impl_trait!(Trade, MexcSwapWSClient, subscribe_trade, "deal");
42#[rustfmt::skip]
43impl_trait!(Ticker, MexcSwapWSClient, subscribe_ticker, "ticker");
44#[rustfmt::skip]
45impl_trait!(OrderBook, MexcSwapWSClient, subscribe_orderbook, "depth");
46#[rustfmt::skip]
47impl_trait!(OrderBookTopK, MexcSwapWSClient, subscribe_orderbook_topk, "depth.full");
48impl_candlestick!(MexcSwapWSClient);
49
50panic_bbo!(MexcSwapWSClient);
51panic_l3_orderbook!(MexcSwapWSClient);
52
53impl_ws_client_trait!(MexcSwapWSClient);
54
55struct MexcMessageHandler {}
56struct MexcCommandTranslator {}
57
58impl MessageHandler for MexcMessageHandler {
59    fn handle_message(&mut self, msg: &str) -> MiscMessage {
60        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();
61        if obj.contains_key("channel") && obj.contains_key("data") && obj.contains_key("ts") {
62            let channel = obj.get("channel").unwrap().as_str().unwrap();
63            match channel {
64                "pong" => MiscMessage::Pong,
65                "rs.error" => {
66                    error!("Received {} from {}", msg, EXCHANGE_NAME);
67                    panic!("Received {msg} from {EXCHANGE_NAME}");
68                }
69                _ => {
70                    if obj.contains_key("symbol") && channel.starts_with("push.") {
71                        MiscMessage::Normal
72                    } else {
73                        info!("Received {} from {}", msg, EXCHANGE_NAME);
74                        MiscMessage::Other
75                    }
76                }
77            }
78        } else {
79            error!("Received {} from {}", msg, SWAP_WEBSOCKET_URL);
80            MiscMessage::Other
81        }
82    }
83
84    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
85        // more than 60 seconds no response, close the channel
86        Some((Message::Text(r#"{"method":"ping"}"#.to_string()), 60))
87    }
88}
89
90impl MexcCommandTranslator {
91    fn topic_to_command(channel: &str, symbol: &str, subscribe: bool) -> String {
92        format!(
93            r#"{{"method":"{}.{}","param":{{"symbol":"{}"}}}}"#,
94            if subscribe { "sub" } else { "unsub" },
95            channel,
96            symbol
97        )
98    }
99
100    fn interval_to_string(interval: usize) -> String {
101        let tmp = match interval {
102            60 => "Min1",
103            300 => "Min5",
104            900 => "Min15",
105            1800 => "Min30",
106            3600 => "Min60",
107            14400 => "Hour4",
108            28800 => "Hour8",
109            86400 => "Day1",
110            604800 => "Week1",
111            2592000 => "Month1",
112            _ => panic!(
113                "MEXC has intervals Min1,Min5,Min15,Min30,Min60,Hour4,Hour8,Day1,Week1,Month1"
114            ),
115        };
116        tmp.to_string()
117    }
118}
119
120impl CommandTranslator for MexcCommandTranslator {
121    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
122        topics
123            .iter()
124            .map(|(channel, symbol)| {
125                MexcCommandTranslator::topic_to_command(channel, symbol, subscribe)
126            })
127            .collect()
128    }
129
130    fn translate_to_candlestick_commands(
131        &self,
132        subscribe: bool,
133        symbol_interval_list: &[(String, usize)],
134    ) -> Vec<String> {
135        symbol_interval_list
136            .iter()
137            .map(|(symbol, interval)| {
138                format!(
139                    r#"{{"method":"{}.kline","param":{{"symbol":"{}","interval":"{}"}}}}"#,
140                    if subscribe { "sub" } else { "unsub" },
141                    symbol,
142                    Self::interval_to_string(*interval)
143                )
144            })
145            .collect()
146    }
147}
148
149#[cfg(test)]
150mod tests {
151    use crate::common::command_translator::CommandTranslator;
152
153    #[test]
154    fn test_one_topic() {
155        let translator = super::MexcCommandTranslator {};
156        let commands =
157            translator.translate_to_commands(true, &[("deal".to_string(), "BTC_USDT".to_string())]);
158
159        assert_eq!(1, commands.len());
160        assert_eq!(r#"{"method":"sub.deal","param":{"symbol":"BTC_USDT"}}"#, commands[0]);
161    }
162
163    #[test]
164    fn test_two_topic() {
165        let translator = super::MexcCommandTranslator {};
166        let commands = translator.translate_to_commands(
167            true,
168            &[
169                ("deal".to_string(), "BTC_USDT".to_string()),
170                ("depth".to_string(), "ETH_USDT".to_string()),
171            ],
172        );
173
174        assert_eq!(2, commands.len());
175        assert_eq!(r#"{"method":"sub.deal","param":{"symbol":"BTC_USDT"}}"#, commands[0]);
176        assert_eq!(r#"{"method":"sub.depth","param":{"symbol":"ETH_USDT"}}"#, commands[1]);
177    }
178
179    #[test]
180    fn test_candlestick() {
181        let translator = super::MexcCommandTranslator {};
182        let commands =
183            translator.translate_to_candlestick_commands(true, &[("BTC_USDT".to_string(), 60)]);
184
185        assert_eq!(1, commands.len());
186        assert_eq!(
187            r#"{"method":"sub.kline","param":{"symbol":"BTC_USDT","interval":"Min1"}}"#,
188            commands[0]
189        );
190    }
191}