crypto_ws_client/clients/gate/
utils.rs

1use std::collections::HashMap;
2
3use log::*;
4use serde_json::Value;
5use tokio_tungstenite::tungstenite::Message;
6
7use crate::common::{
8    command_translator::CommandTranslator,
9    message_handler::{MessageHandler, MiscMessage},
10};
11
12pub(super) const EXCHANGE_NAME: &str = "gate";
13
14// MARKET_TYPE: 'S' for spot, 'F' for futures
15pub(super) struct GateMessageHandler<const MARKET_TYPE: char> {}
16pub(super) struct GateCommandTranslator<const MARKET_TYPE: char> {}
17
18impl<const MARKET_TYPE: char> MessageHandler for GateMessageHandler<MARKET_TYPE> {
19    fn handle_message(&mut self, msg: &str) -> MiscMessage {
20        let obj = serde_json::from_str::<HashMap<String, Value>>(msg).unwrap();
21
22        // https://www.gate.io/docs/apiv4/ws/en/#server-response
23        // Null if the server accepts the client request; otherwise, the detailed reason
24        // why request is rejected.
25        let error = match obj.get("error") {
26            None => serde_json::Value::Null,
27            Some(err) => {
28                if err.is_null() {
29                    serde_json::Value::Null
30                } else {
31                    err.clone()
32                }
33            }
34        };
35        if !error.is_null() {
36            let err = error.as_object().unwrap();
37            // https://www.gate.io/docs/apiv4/ws/en/#schema_error
38            // https://www.gate.io/docs/futures/ws/en/#error
39            let code = err.get("code").unwrap().as_i64().unwrap();
40            match code {
41                1 | 2 => panic!("Received {msg} from {EXCHANGE_NAME}"), // client side errors
42                _ => error!("Received {} from {}", msg, EXCHANGE_NAME), // server side errors
43            }
44            return MiscMessage::Other;
45        }
46
47        let channel = obj.get("channel").unwrap().as_str().unwrap();
48        let event = obj.get("event").unwrap().as_str().unwrap();
49
50        if channel == "spot.pong" || channel == "futures.pong" {
51            MiscMessage::Pong
52        } else if event == "update" || event == "all" {
53            MiscMessage::Normal
54        } else if event == "subscribe" || event == "unsubscribe" {
55            debug!("Received {} from {}", msg, EXCHANGE_NAME);
56            MiscMessage::Other
57        } else {
58            warn!("Received {} from {}", msg, EXCHANGE_NAME);
59            MiscMessage::Other
60        }
61    }
62
63    fn get_ping_msg_and_interval(&self) -> Option<(Message, u64)> {
64        if MARKET_TYPE == 'S' {
65            // https://www.gate.io/docs/apiv4/ws/en/#application-ping-pong
66            Some((Message::Text(r#"{"channel":"spot.ping"}"#.to_string()), 60))
67        } else {
68            // https://www.gate.io/docs/futures/ws/en/#ping-and-pong
69            // https://www.gate.io/docs/delivery/ws/en/#ping-and-pong
70            Some((Message::Text(r#"{"channel":"futures.ping"}"#.to_string()), 60))
71        }
72    }
73}
74
75impl<const MARKET_TYPE: char> GateCommandTranslator<MARKET_TYPE> {
76    fn channel_symbols_to_command(
77        channel: &str,
78        symbols: &[String],
79        subscribe: bool,
80    ) -> Vec<String> {
81        let channel = if MARKET_TYPE == 'S' {
82            format!("spot.{channel}")
83        } else if MARKET_TYPE == 'F' {
84            format!("futures.{channel}")
85        } else {
86            panic!("unexpected market type: {MARKET_TYPE}")
87        };
88        if channel.contains(".order_book") {
89            symbols
90                .iter()
91                .map(|symbol| {
92                    format!(
93                        r#"{{"channel":"{}", "event":"{}", "payload":{}}}"#,
94                        channel,
95                        if subscribe { "subscribe" } else { "unsubscribe" },
96                        if channel.ends_with(".order_book") {
97                            if MARKET_TYPE == 'S' {
98                                serde_json::to_string(&[symbol, "20", "1000ms"]).unwrap()
99                            } else if MARKET_TYPE == 'F' {
100                                serde_json::to_string(&[symbol, "20", "0"]).unwrap()
101                            } else {
102                                panic!("unexpected market type: {MARKET_TYPE}")
103                            }
104                        } else if channel.ends_with(".order_book_update") {
105                            if MARKET_TYPE == 'S' {
106                                serde_json::to_string(&[symbol, "100ms"]).unwrap()
107                            } else if MARKET_TYPE == 'F' {
108                                serde_json::to_string(&[symbol, "100ms", "20"]).unwrap()
109                            } else {
110                                panic!("unexpected market type: {MARKET_TYPE}")
111                            }
112                        } else {
113                            panic!("unexpected channel: {channel}")
114                        },
115                    )
116                })
117                .collect()
118        } else {
119            vec![format!(
120                r#"{{"channel":"{}", "event":"{}", "payload":{}}}"#,
121                channel,
122                if subscribe { "subscribe" } else { "unsubscribe" },
123                serde_json::to_string(&symbols).unwrap(),
124            )]
125        }
126    }
127
128    fn to_candlestick_command(symbol: &str, interval: usize, subscribe: bool) -> String {
129        let interval_str = match interval {
130            10 => "10s",
131            60 => "1m",
132            300 => "5m",
133            900 => "15m",
134            1800 => "30m",
135            3600 => "1h",
136            14400 => "4h",
137            28800 => "8h",
138            86400 => "1d",
139            604800 => "7d",
140            _ => panic!("Gate available intervals 10s,1m,5m,15m,30m,1h,4h,8h,1d,7d"),
141        };
142        format!(
143            r#"{{"channel": "{}.candlesticks", "event": "{}", "payload" : ["{}", "{}"]}}"#,
144            if MARKET_TYPE == 'S' { "spot" } else { "futures" },
145            if subscribe { "subscribe" } else { "unsubscribe" },
146            interval_str,
147            symbol
148        )
149    }
150}
151
152impl<const MARKET_TYPE: char> CommandTranslator for GateCommandTranslator<MARKET_TYPE> {
153    fn translate_to_commands(&self, subscribe: bool, topics: &[(String, String)]) -> Vec<String> {
154        let mut commands: Vec<String> = Vec::new();
155
156        let mut channel_symbols = HashMap::<String, Vec<String>>::new();
157        for (channel, symbol) in topics {
158            match channel_symbols.get_mut(channel) {
159                Some(symbols) => symbols.push(symbol.to_string()),
160                None => {
161                    channel_symbols.insert(channel.to_string(), vec![symbol.to_string()]);
162                }
163            }
164        }
165
166        for (channel, symbols) in channel_symbols.iter() {
167            commands.extend(Self::channel_symbols_to_command(channel, symbols, subscribe));
168        }
169
170        commands
171    }
172
173    fn translate_to_candlestick_commands(
174        &self,
175        subscribe: bool,
176        symbol_interval_list: &[(String, usize)],
177    ) -> Vec<String> {
178        symbol_interval_list
179            .iter()
180            .map(|(symbol, interval)| Self::to_candlestick_command(symbol, *interval, subscribe))
181            .collect::<Vec<String>>()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use crate::common::command_translator::CommandTranslator;
188
189    #[test]
190    fn test_spot() {
191        let translator = super::GateCommandTranslator::<'S'> {};
192
193        assert_eq!(
194            r#"{"channel":"spot.trades", "event":"subscribe", "payload":["BTC_USDT","ETH_USDT"]}"#,
195            translator.translate_to_commands(
196                true,
197                &[
198                    ("trades".to_string(), "BTC_USDT".to_string()),
199                    ("trades".to_string(), "ETH_USDT".to_string())
200                ]
201            )[0]
202        );
203
204        let commands = translator.translate_to_commands(
205            true,
206            &[
207                ("order_book".to_string(), "BTC_USDT".to_string()),
208                ("order_book".to_string(), "ETH_USDT".to_string()),
209            ],
210        );
211        assert_eq!(2, commands.len());
212        assert_eq!(
213            r#"{"channel":"spot.order_book", "event":"subscribe", "payload":["BTC_USDT","20","1000ms"]}"#,
214            commands[0]
215        );
216        assert_eq!(
217            r#"{"channel":"spot.order_book", "event":"subscribe", "payload":["ETH_USDT","20","1000ms"]}"#,
218            commands[1]
219        );
220
221        let commands = translator.translate_to_commands(
222            true,
223            &[
224                ("order_book_update".to_string(), "BTC_USDT".to_string()),
225                ("order_book_update".to_string(), "ETH_USDT".to_string()),
226            ],
227        );
228        assert_eq!(2, commands.len());
229        assert_eq!(
230            r#"{"channel":"spot.order_book_update", "event":"subscribe", "payload":["BTC_USDT","100ms"]}"#,
231            commands[0]
232        );
233        assert_eq!(
234            r#"{"channel":"spot.order_book_update", "event":"subscribe", "payload":["ETH_USDT","100ms"]}"#,
235            commands[1]
236        );
237    }
238
239    #[test]
240    fn test_futures() {
241        let translator = super::GateCommandTranslator::<'F'> {};
242
243        assert_eq!(
244            r#"{"channel":"futures.trades", "event":"subscribe", "payload":["BTC_USD","ETH_USD"]}"#,
245            translator.translate_to_commands(
246                true,
247                &[
248                    ("trades".to_string(), "BTC_USD".to_string()),
249                    ("trades".to_string(), "ETH_USD".to_string())
250                ]
251            )[0]
252        );
253
254        let commands = translator.translate_to_commands(
255            true,
256            &[
257                ("order_book".to_string(), "BTC_USD".to_string()),
258                ("order_book".to_string(), "ETH_USD".to_string()),
259            ],
260        );
261        assert_eq!(2, commands.len());
262        assert_eq!(
263            r#"{"channel":"futures.order_book", "event":"subscribe", "payload":["BTC_USD","20","0"]}"#,
264            commands[0]
265        );
266        assert_eq!(
267            r#"{"channel":"futures.order_book", "event":"subscribe", "payload":["ETH_USD","20","0"]}"#,
268            commands[1]
269        );
270
271        let commands = translator.translate_to_commands(
272            true,
273            &[
274                ("order_book_update".to_string(), "BTC_USD".to_string()),
275                ("order_book_update".to_string(), "ETH_USD".to_string()),
276            ],
277        );
278        assert_eq!(2, commands.len());
279        assert_eq!(
280            r#"{"channel":"futures.order_book_update", "event":"subscribe", "payload":["BTC_USD","100ms","20"]}"#,
281            commands[0]
282        );
283        assert_eq!(
284            r#"{"channel":"futures.order_book_update", "event":"subscribe", "payload":["ETH_USD","100ms","20"]}"#,
285            commands[1]
286        );
287    }
288}