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
14pub(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 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 let code = err.get("code").unwrap().as_i64().unwrap();
40 match code {
41 1 | 2 => panic!("Received {msg} from {EXCHANGE_NAME}"), _ => error!("Received {} from {}", msg, EXCHANGE_NAME), }
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 Some((Message::Text(r#"{"channel":"spot.ping"}"#.to_string()), 60))
67 } else {
68 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}