1use serde::Deserialize;
6
7#[derive(Debug, Deserialize)]
13pub struct AggTradeEvent {
14 #[serde(rename = "e")]
15 pub event_type: String,
16 #[serde(rename = "E")]
17 pub event_time: u64,
18 #[serde(rename = "s")]
19 pub symbol: String,
20 #[serde(rename = "a")]
21 pub agg_trade_id: u64,
22 #[serde(rename = "p")]
23 pub price: String,
24 #[serde(rename = "q")]
25 pub quantity: String,
26 #[serde(rename = "f")]
27 pub first_trade_id: u64,
28 #[serde(rename = "l")]
29 pub last_trade_id: u64,
30 #[serde(rename = "T")]
31 pub trade_time: u64,
32 #[serde(rename = "m")]
33 pub is_buyer_maker: bool,
34}
35
36#[derive(Debug, Deserialize)]
38pub struct MarkPriceEvent {
39 #[serde(rename = "e")]
40 pub event_type: String,
41 #[serde(rename = "E")]
42 pub event_time: u64,
43 #[serde(rename = "s")]
44 pub symbol: String,
45 #[serde(rename = "p")]
46 pub mark_price: String,
47 #[serde(rename = "i")]
48 pub index_price: String,
49 #[serde(rename = "P")]
50 pub estimated_settle_price: String,
51 #[serde(rename = "r")]
52 pub last_funding_rate: String,
53 #[serde(rename = "R", default)]
54 pub interest_rate: String,
55 #[serde(rename = "T")]
56 pub next_funding_time: u64,
57}
58
59#[derive(Debug, Deserialize)]
61pub struct KlineEvent {
62 #[serde(rename = "e")]
63 pub event_type: String,
64 #[serde(rename = "E")]
65 pub event_time: u64,
66 #[serde(rename = "s")]
67 pub symbol: String,
68 #[serde(rename = "k")]
69 pub kline: KlineData,
70}
71
72#[derive(Debug, Deserialize)]
74pub struct KlineData {
75 #[serde(rename = "t")]
76 pub start_time: u64,
77 #[serde(rename = "T")]
78 pub close_time: u64,
79 #[serde(rename = "s")]
80 pub symbol: String,
81 #[serde(rename = "i")]
83 pub interval: String,
84 #[serde(rename = "f")]
85 pub first_trade_id: i64,
86 #[serde(rename = "L")]
87 pub last_trade_id: i64,
88 #[serde(rename = "o")]
89 pub open: String,
90 #[serde(rename = "c")]
91 pub close: String,
92 #[serde(rename = "h")]
93 pub high: String,
94 #[serde(rename = "l")]
95 pub low: String,
96 #[serde(rename = "v")]
97 pub volume: String,
98 #[serde(rename = "n")]
99 pub trade_count: u64,
100 #[serde(rename = "x")]
101 pub is_closed: bool,
102 #[serde(rename = "q")]
103 pub quote_volume: String,
104 #[serde(rename = "V")]
105 pub taker_buy_volume: String,
106 #[serde(rename = "Q")]
107 pub taker_buy_quote_volume: String,
108}
109
110#[derive(Debug, Deserialize)]
112pub struct MiniTickerEvent {
113 #[serde(rename = "e")]
114 pub event_type: String,
115 #[serde(rename = "E")]
116 pub event_time: u64,
117 #[serde(rename = "s")]
118 pub symbol: String,
119 #[serde(rename = "c")]
120 pub close_price: String,
121 #[serde(rename = "o")]
122 pub open_price: String,
123 #[serde(rename = "h")]
124 pub high_price: String,
125 #[serde(rename = "l")]
126 pub low_price: String,
127 #[serde(rename = "v")]
128 pub volume: String,
129 #[serde(rename = "q")]
130 pub quote_volume: String,
131}
132
133#[derive(Debug, Deserialize)]
135pub struct TickerEvent {
136 #[serde(rename = "e")]
137 pub event_type: String,
138 #[serde(rename = "E")]
139 pub event_time: u64,
140 #[serde(rename = "s")]
141 pub symbol: String,
142 #[serde(rename = "p")]
143 pub price_change: String,
144 #[serde(rename = "P")]
145 pub price_change_percent: String,
146 #[serde(rename = "w")]
147 pub weighted_avg_price: String,
148 #[serde(rename = "c")]
149 pub last_price: String,
150 #[serde(rename = "Q")]
151 pub last_qty: String,
152 #[serde(rename = "o")]
153 pub open_price: String,
154 #[serde(rename = "h")]
155 pub high_price: String,
156 #[serde(rename = "l")]
157 pub low_price: String,
158 #[serde(rename = "v")]
159 pub volume: String,
160 #[serde(rename = "q")]
161 pub quote_volume: String,
162 #[serde(rename = "O")]
163 pub open_time: u64,
164 #[serde(rename = "C")]
165 pub close_time: u64,
166 #[serde(rename = "F")]
167 pub first_trade_id: i64,
168 #[serde(rename = "L")]
169 pub last_trade_id: i64,
170 #[serde(rename = "n")]
171 pub trade_count: u64,
172}
173
174#[derive(Debug, Deserialize)]
176pub struct BookTickerEvent {
177 #[serde(rename = "u")]
178 pub update_id: u64,
179 #[serde(rename = "s")]
180 pub symbol: String,
181 #[serde(rename = "b")]
182 pub best_bid_price: String,
183 #[serde(rename = "B")]
184 pub best_bid_qty: String,
185 #[serde(rename = "a")]
186 pub best_ask_price: String,
187 #[serde(rename = "A")]
188 pub best_ask_qty: String,
189}
190
191#[derive(Debug, Deserialize)]
193pub struct ForceOrderEvent {
194 #[serde(rename = "e")]
195 pub event_type: String,
196 #[serde(rename = "E")]
197 pub event_time: u64,
198 #[serde(rename = "o")]
199 pub order: ForceOrderData,
200}
201
202#[derive(Debug, Deserialize)]
204pub struct ForceOrderData {
205 #[serde(rename = "s")]
206 pub symbol: String,
207 #[serde(rename = "S")]
208 pub side: String,
209 #[serde(rename = "o")]
210 pub order_type: String,
211 #[serde(rename = "f")]
212 pub time_in_force: String,
213 #[serde(rename = "q")]
214 pub quantity: String,
215 #[serde(rename = "p")]
216 pub price: String,
217 #[serde(rename = "ap")]
219 pub average_price: String,
220 #[serde(rename = "X")]
221 pub order_status: String,
222 #[serde(rename = "l")]
223 pub last_filled_qty: String,
224 #[serde(rename = "z")]
225 pub accumulated_qty: String,
226 #[serde(rename = "T")]
227 pub trade_time: u64,
228}
229
230#[derive(Debug, Deserialize)]
233pub struct PartialDepthEvent {
234 #[serde(rename = "lastUpdateId")]
235 pub last_update_id: u64,
236 pub bids: Vec<[String; 2]>,
237 pub asks: Vec<[String; 2]>,
238}
239
240#[derive(Debug, Deserialize)]
242pub struct DiffDepthEvent {
243 #[serde(rename = "e")]
244 pub event_type: String,
245 #[serde(rename = "E")]
246 pub event_time: u64,
247 #[serde(rename = "s")]
248 pub symbol: String,
249 #[serde(rename = "U")]
250 pub first_update_id: u64,
251 #[serde(rename = "u")]
252 pub final_update_id: u64,
253 #[serde(rename = "b")]
254 pub bids: Vec<[String; 2]>,
255 #[serde(rename = "a")]
256 pub asks: Vec<[String; 2]>,
257}
258
259#[derive(Debug)]
276pub enum WebSocketMessage {
277 AggTrade(AggTradeEvent),
279 MarkPrice(MarkPriceEvent),
281 AllMarkPrices(Vec<MarkPriceEvent>),
283 Kline(KlineEvent),
285 MiniTicker(MiniTickerEvent),
287 AllMiniTickers(Vec<MiniTickerEvent>),
289 Ticker(TickerEvent),
291 AllTickers(Vec<TickerEvent>),
293 BookTicker(BookTickerEvent),
295 ForceOrder(ForceOrderEvent),
297 AllForceOrders(Vec<ForceOrderEvent>),
299 PartialDepth(PartialDepthEvent),
301 DiffDepth(DiffDepthEvent),
303 Unknown(serde_json::Value),
305}
306
307impl<'de> serde::Deserialize<'de> for WebSocketMessage {
309 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
310 let v = serde_json::Value::deserialize(deserializer)?;
311
312 if let Some(arr) = v.as_array() {
316 if arr.is_empty() {
317 tracing::warn!("Received empty WebSocket array message");
318 return Ok(WebSocketMessage::Unknown(v));
319 }
320 let first = &arr[0];
321 match first.get("e").and_then(|e| e.as_str()).unwrap_or("") {
322 "markPriceUpdate" => {
323 match serde_json::from_value::<Vec<MarkPriceEvent>>(v.clone()) {
324 Ok(events) => return Ok(WebSocketMessage::AllMarkPrices(events)),
325 Err(err) => {
326 tracing::warn!(error = ?err, "Failed to parse AllMarkPrices array");
327 return Ok(WebSocketMessage::Unknown(v));
328 }
329 }
330 }
331 "24hrMiniTicker" => {
332 match serde_json::from_value::<Vec<MiniTickerEvent>>(v.clone()) {
333 Ok(events) => return Ok(WebSocketMessage::AllMiniTickers(events)),
334 Err(err) => {
335 tracing::warn!(error = ?err, "Failed to parse AllMiniTickers array");
336 return Ok(WebSocketMessage::Unknown(v));
337 }
338 }
339 }
340 "24hrTicker" => {
341 match serde_json::from_value::<Vec<TickerEvent>>(v.clone()) {
342 Ok(events) => return Ok(WebSocketMessage::AllTickers(events)),
343 Err(err) => {
344 tracing::warn!(error = ?err, "Failed to parse AllTickers array");
345 return Ok(WebSocketMessage::Unknown(v));
346 }
347 }
348 }
349 "forceOrder" => {
350 match serde_json::from_value::<Vec<ForceOrderEvent>>(v.clone()) {
351 Ok(events) => return Ok(WebSocketMessage::AllForceOrders(events)),
352 Err(err) => {
353 tracing::warn!(error = ?err, "Failed to parse AllForceOrders array");
354 return Ok(WebSocketMessage::Unknown(v));
355 }
356 }
357 }
358 other => {
359 tracing::warn!(event_type = other, "Unknown WebSocket array event type");
360 return Ok(WebSocketMessage::Unknown(v));
361 }
362 }
363 }
364
365 if v.get("e").is_none() && v.get("lastUpdateId").is_some() {
371 match serde_json::from_value::<PartialDepthEvent>(v.clone()) {
372 Ok(event) => return Ok(WebSocketMessage::PartialDepth(event)),
373 Err(err) => {
374 tracing::warn!(error = ?err, "Failed to parse PartialDepth event");
375 return Ok(WebSocketMessage::Unknown(v));
376 }
377 }
378 }
379
380 if v.get("e").is_none() && v.get("u").is_some() {
382 match serde_json::from_value::<BookTickerEvent>(v.clone()) {
383 Ok(event) => return Ok(WebSocketMessage::BookTicker(event)),
384 Err(err) => {
385 tracing::warn!(error = ?err, "Failed to parse BookTicker event");
386 return Ok(WebSocketMessage::Unknown(v));
387 }
388 }
389 }
390
391 let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
395
396 macro_rules! parse_event {
398 ($variant:ident, $type:ty, $label:literal) => {
399 match serde_json::from_value::<$type>(v.clone()) {
400 Ok(event) => Ok(WebSocketMessage::$variant(event)),
401 Err(err) => {
402 tracing::warn!(
403 error = ?err,
404 event_type = $label,
405 "Failed to parse known WebSocket event; falling back to Unknown"
406 );
407 Ok(WebSocketMessage::Unknown(v))
408 }
409 }
410 };
411 }
412
413 match event_type {
414 "aggTrade" => parse_event!(AggTrade, AggTradeEvent, "aggTrade"),
415 "markPriceUpdate" => parse_event!(MarkPrice, MarkPriceEvent, "markPriceUpdate"),
416 "kline" => parse_event!(Kline, KlineEvent, "kline"),
417 "24hrMiniTicker" => parse_event!(MiniTicker, MiniTickerEvent, "24hrMiniTicker"),
418 "24hrTicker" => parse_event!(Ticker, TickerEvent, "24hrTicker"),
419 "forceOrder" => parse_event!(ForceOrder, ForceOrderEvent, "forceOrder"),
420 "depthUpdate" => parse_event!(DiffDepth, DiffDepthEvent, "depthUpdate"),
421 other => {
422 tracing::warn!(event_type = other, "Unknown WebSocket message event type");
423 Ok(WebSocketMessage::Unknown(v))
424 }
425 }
426 }
427}
428
429#[cfg(test)]
434mod tests {
435 use super::*;
436
437 #[test]
438 fn agg_trade_deserializes_correctly() {
439 let json = r#"{
440 "e": "aggTrade",
441 "E": 1625184000000,
442 "s": "BTCUSDT",
443 "a": 12345,
444 "p": "30000.00",
445 "q": "0.001",
446 "f": 100,
447 "l": 105,
448 "T": 1625184000000,
449 "m": false
450 }"#;
451 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
452 assert!(matches!(msg, WebSocketMessage::AggTrade(ref e) if e.symbol == "BTCUSDT"));
453 if let WebSocketMessage::AggTrade(event) = msg {
454 assert_eq!(event.price, "30000.00");
455 assert_eq!(event.quantity, "0.001");
456 assert!(!event.is_buyer_maker);
457 }
458 }
459
460 #[test]
461 fn unknown_event_type_maps_to_unknown_variant() {
462 let json = r#"{"e":"futureData","x":"someValue"}"#;
463 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
464 assert!(matches!(msg, WebSocketMessage::Unknown(_)));
465 }
466
467 #[test]
468 fn kline_event_deserializes_correctly() {
469 let json = r#"{
470 "e": "kline",
471 "E": 1625184000000,
472 "s": "BTCUSDT",
473 "k": {
474 "t": 1625184000000, "T": 1625184059999, "s": "BTCUSDT",
475 "i": "1m", "f": 1, "L": 10,
476 "o": "30000", "c": "30100", "h": "30200", "l": "29900",
477 "v": "100", "n": 10, "x": false,
478 "q": "3010000", "V": "50", "Q": "1505000"
479 }
480 }"#;
481 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
482 assert!(matches!(msg, WebSocketMessage::Kline(ref e) if e.kline.interval == "1m"));
483 if let WebSocketMessage::Kline(event) = msg {
484 assert!(!event.kline.is_closed);
485 assert_eq!(event.kline.open, "30000");
486 assert_eq!(event.kline.close, "30100");
487 }
488 }
489
490 #[test]
491 fn partial_depth_deserializes_correctly() {
492 let json = r#"{"lastUpdateId":123456,"bids":[["30000","1.5"],["29999","2.0"]],"asks":[["30001","0.5"]]}"#;
493 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
494 assert!(matches!(msg, WebSocketMessage::PartialDepth(_)));
495 if let WebSocketMessage::PartialDepth(event) = msg {
496 assert_eq!(event.bids[0][0], "30000");
497 assert_eq!(event.bids[0][1], "1.5");
498 }
499 }
500
501 #[test]
502 fn all_mark_prices_array_deserializes_correctly() {
503 let json = r#"[{"e":"markPriceUpdate","E":1625184000000,"s":"BTCUSDT","p":"30000","i":"30001","P":"29999","r":"0.0001","T":1625198400000}]"#;
504 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
505 assert!(matches!(msg, WebSocketMessage::AllMarkPrices(_)));
506 if let WebSocketMessage::AllMarkPrices(events) = msg {
507 assert_eq!(events.len(), 1);
508 assert_eq!(events[0].symbol, "BTCUSDT");
509 }
510 }
511
512 #[test]
513 fn mark_price_single_deserializes_correctly() {
514 let json = r#"{
515 "e": "markPriceUpdate",
516 "E": 1625184000000,
517 "s": "BTCUSDT",
518 "p": "30000.00",
519 "i": "30001.00",
520 "P": "29999.00",
521 "r": "0.0001",
522 "R": "0.000050",
523 "T": 1625198400000
524 }"#;
525 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
526 assert!(matches!(msg, WebSocketMessage::MarkPrice(_)));
527 if let WebSocketMessage::MarkPrice(event) = msg {
528 assert_eq!(event.symbol, "BTCUSDT");
529 assert_eq!(event.mark_price, "30000.00");
530 assert_eq!(event.last_funding_rate, "0.0001");
531 assert_eq!(event.interest_rate, "0.000050");
532 assert_eq!(event.next_funding_time, 1625198400000);
533 }
534 }
535
536 #[test]
537 fn book_ticker_deserializes_correctly() {
538 let json = r#"{"u":12345,"s":"BTCUSDT","b":"30000","B":"1.5","a":"30001","A":"0.5"}"#;
539 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
540 assert!(matches!(msg, WebSocketMessage::BookTicker(ref e) if e.symbol == "BTCUSDT"));
541 if let WebSocketMessage::BookTicker(event) = msg {
542 assert_eq!(event.best_bid_price, "30000");
543 assert_eq!(event.best_ask_price, "30001");
544 }
545 }
546
547 #[test]
548 fn diff_depth_deserializes_correctly() {
549 let json = r#"{
550 "e": "depthUpdate",
551 "E": 1625184000000,
552 "s": "BTCUSDT",
553 "U": 100,
554 "u": 200,
555 "b": [["29999","1.0"],["29998","2.0"]],
556 "a": [["30001","0.5"]]
557 }"#;
558 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
559 assert!(matches!(msg, WebSocketMessage::DiffDepth(ref e) if e.symbol == "BTCUSDT"));
560 if let WebSocketMessage::DiffDepth(event) = msg {
561 assert_eq!(event.first_update_id, 100);
562 assert_eq!(event.final_update_id, 200);
563 assert_eq!(event.bids[0][0], "29999");
564 }
565 }
566
567 #[test]
568 fn mini_ticker_deserializes_correctly() {
569 let json = r#"{
570 "e": "24hrMiniTicker",
571 "E": 1625184000000,
572 "s": "ETHUSDT",
573 "c": "2000.00",
574 "o": "1950.00",
575 "h": "2050.00",
576 "l": "1900.00",
577 "v": "5000",
578 "q": "9750000"
579 }"#;
580 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
581 assert!(matches!(msg, WebSocketMessage::MiniTicker(ref e) if e.symbol == "ETHUSDT"));
582 if let WebSocketMessage::MiniTicker(event) = msg {
583 assert_eq!(event.close_price, "2000.00");
584 assert_eq!(event.volume, "5000");
585 }
586 }
587}