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 = "T")]
54 pub next_funding_time: u64,
55}
56
57#[derive(Debug, Deserialize)]
59pub struct KlineEvent {
60 #[serde(rename = "e")]
61 pub event_type: String,
62 #[serde(rename = "E")]
63 pub event_time: u64,
64 #[serde(rename = "s")]
65 pub symbol: String,
66 #[serde(rename = "k")]
67 pub kline: KlineData,
68}
69
70#[derive(Debug, Deserialize)]
72pub struct KlineData {
73 #[serde(rename = "t")]
74 pub start_time: u64,
75 #[serde(rename = "T")]
76 pub close_time: u64,
77 #[serde(rename = "s")]
78 pub symbol: String,
79 #[serde(rename = "i")]
81 pub interval: String,
82 #[serde(rename = "f")]
83 pub first_trade_id: i64,
84 #[serde(rename = "L")]
85 pub last_trade_id: i64,
86 #[serde(rename = "o")]
87 pub open: String,
88 #[serde(rename = "c")]
89 pub close: String,
90 #[serde(rename = "h")]
91 pub high: String,
92 #[serde(rename = "l")]
93 pub low: String,
94 #[serde(rename = "v")]
95 pub volume: String,
96 #[serde(rename = "n")]
97 pub trade_count: u64,
98 #[serde(rename = "x")]
99 pub is_closed: bool,
100 #[serde(rename = "q")]
101 pub quote_volume: String,
102 #[serde(rename = "V")]
103 pub taker_buy_volume: String,
104 #[serde(rename = "Q")]
105 pub taker_buy_quote_volume: String,
106 #[serde(rename = "B", default)]
109 pub ignore: String,
110}
111
112#[derive(Debug, Deserialize)]
114pub struct MiniTickerEvent {
115 #[serde(rename = "e")]
116 pub event_type: String,
117 #[serde(rename = "E")]
118 pub event_time: u64,
119 #[serde(rename = "s")]
120 pub symbol: String,
121 #[serde(rename = "c")]
122 pub close_price: String,
123 #[serde(rename = "o")]
124 pub open_price: String,
125 #[serde(rename = "h")]
126 pub high_price: String,
127 #[serde(rename = "l")]
128 pub low_price: String,
129 #[serde(rename = "v")]
130 pub volume: String,
131 #[serde(rename = "q")]
132 pub quote_volume: String,
133}
134
135#[derive(Debug, Deserialize)]
137pub struct TickerEvent {
138 #[serde(rename = "e")]
139 pub event_type: String,
140 #[serde(rename = "E")]
141 pub event_time: u64,
142 #[serde(rename = "s")]
143 pub symbol: String,
144 #[serde(rename = "p")]
145 pub price_change: String,
146 #[serde(rename = "P")]
147 pub price_change_percent: String,
148 #[serde(rename = "w")]
149 pub weighted_avg_price: String,
150 #[serde(rename = "c")]
151 pub last_price: String,
152 #[serde(rename = "Q")]
153 pub last_qty: String,
154 #[serde(rename = "o")]
155 pub open_price: String,
156 #[serde(rename = "h")]
157 pub high_price: String,
158 #[serde(rename = "l")]
159 pub low_price: String,
160 #[serde(rename = "v")]
161 pub volume: String,
162 #[serde(rename = "q")]
163 pub quote_volume: String,
164 #[serde(rename = "O")]
165 pub open_time: u64,
166 #[serde(rename = "C")]
167 pub close_time: u64,
168 #[serde(rename = "F")]
169 pub first_trade_id: i64,
170 #[serde(rename = "L")]
171 pub last_trade_id: i64,
172 #[serde(rename = "n")]
173 pub trade_count: u64,
174}
175
176#[derive(Debug, Deserialize)]
183pub struct BookTickerEvent {
184 #[serde(rename = "e", default)]
186 pub event_type: String,
187 #[serde(rename = "u")]
188 pub update_id: u64,
189 #[serde(rename = "E", default)]
191 pub event_time: u64,
192 #[serde(rename = "T", default)]
194 pub transaction_time: u64,
195 #[serde(rename = "s")]
196 pub symbol: String,
197 #[serde(rename = "b")]
198 pub best_bid_price: String,
199 #[serde(rename = "B")]
200 pub best_bid_qty: String,
201 #[serde(rename = "a")]
202 pub best_ask_price: String,
203 #[serde(rename = "A")]
204 pub best_ask_qty: String,
205}
206
207#[derive(Debug, Deserialize)]
209pub struct ForceOrderEvent {
210 #[serde(rename = "e")]
211 pub event_type: String,
212 #[serde(rename = "E")]
213 pub event_time: u64,
214 #[serde(rename = "o")]
215 pub order: ForceOrderData,
216}
217
218#[derive(Debug, Deserialize)]
220pub struct ForceOrderData {
221 #[serde(rename = "s")]
222 pub symbol: String,
223 #[serde(rename = "S")]
224 pub side: String,
225 #[serde(rename = "o")]
226 pub order_type: String,
227 #[serde(rename = "f")]
228 pub time_in_force: String,
229 #[serde(rename = "q")]
230 pub quantity: String,
231 #[serde(rename = "p")]
232 pub price: String,
233 #[serde(rename = "ap")]
235 pub average_price: String,
236 #[serde(rename = "X")]
237 pub order_status: String,
238 #[serde(rename = "l")]
239 pub last_filled_qty: String,
240 #[serde(rename = "z")]
241 pub accumulated_qty: String,
242 #[serde(rename = "T")]
243 pub trade_time: u64,
244}
245
246#[derive(Debug, Deserialize)]
253pub struct PartialDepthEvent {
254 #[serde(rename = "lastUpdateId")]
255 pub last_update_id: u64,
256 pub bids: Vec<[String; 2]>,
257 pub asks: Vec<[String; 2]>,
258}
259
260#[derive(Debug, Deserialize)]
265pub struct DiffDepthEvent {
266 #[serde(rename = "e")]
267 pub event_type: String,
268 #[serde(rename = "E")]
269 pub event_time: u64,
270 #[serde(rename = "T", default)]
272 pub transaction_time: u64,
273 #[serde(rename = "s")]
274 pub symbol: String,
275 #[serde(rename = "U")]
276 pub first_update_id: u64,
277 #[serde(rename = "u")]
278 pub final_update_id: u64,
279 #[serde(rename = "pu", default)]
280 pub prev_final_update_id: u64,
281 #[serde(rename = "b")]
282 pub bids: Vec<[String; 2]>,
283 #[serde(rename = "a")]
284 pub asks: Vec<[String; 2]>,
285}
286
287#[derive(Debug)]
305pub enum WebSocketMessage {
306 AggTrade(AggTradeEvent),
308 MarkPrice(MarkPriceEvent),
310 AllMarkPrices(Vec<MarkPriceEvent>),
312 Kline(KlineEvent),
314 MiniTicker(MiniTickerEvent),
316 AllMiniTickers(Vec<MiniTickerEvent>),
318 Ticker(TickerEvent),
320 AllTickers(Vec<TickerEvent>),
322 BookTicker(BookTickerEvent),
324 ForceOrder(ForceOrderEvent),
326 AllForceOrders(Vec<ForceOrderEvent>),
328 PartialDepth(PartialDepthEvent),
330 DiffDepth(DiffDepthEvent),
332 Unknown(serde_json::Value),
334}
335
336impl<'de> serde::Deserialize<'de> for WebSocketMessage {
338 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
339 let v = serde_json::Value::deserialize(deserializer)?;
340
341 if let Some(arr) = v.as_array() {
345 if arr.is_empty() {
346 tracing::warn!("Received empty WebSocket array message");
347 return Ok(WebSocketMessage::Unknown(v));
348 }
349 let first = &arr[0];
350 match first.get("e").and_then(|e| e.as_str()).unwrap_or("") {
351 "markPriceUpdate" => {
352 match serde_json::from_value::<Vec<MarkPriceEvent>>(v.clone()) {
353 Ok(events) => return Ok(WebSocketMessage::AllMarkPrices(events)),
354 Err(err) => {
355 tracing::warn!(error = ?err, "Failed to parse AllMarkPrices array");
356 return Ok(WebSocketMessage::Unknown(v));
357 }
358 }
359 }
360 "24hrMiniTicker" => {
361 match serde_json::from_value::<Vec<MiniTickerEvent>>(v.clone()) {
362 Ok(events) => return Ok(WebSocketMessage::AllMiniTickers(events)),
363 Err(err) => {
364 tracing::warn!(error = ?err, "Failed to parse AllMiniTickers array");
365 return Ok(WebSocketMessage::Unknown(v));
366 }
367 }
368 }
369 "24hrTicker" => {
370 match serde_json::from_value::<Vec<TickerEvent>>(v.clone()) {
371 Ok(events) => return Ok(WebSocketMessage::AllTickers(events)),
372 Err(err) => {
373 tracing::warn!(error = ?err, "Failed to parse AllTickers array");
374 return Ok(WebSocketMessage::Unknown(v));
375 }
376 }
377 }
378 "forceOrder" => {
379 match serde_json::from_value::<Vec<ForceOrderEvent>>(v.clone()) {
380 Ok(events) => return Ok(WebSocketMessage::AllForceOrders(events)),
381 Err(err) => {
382 tracing::warn!(error = ?err, "Failed to parse AllForceOrders array");
383 return Ok(WebSocketMessage::Unknown(v));
384 }
385 }
386 }
387 other => {
388 tracing::warn!(event_type = other, "Unknown WebSocket array event type");
389 return Ok(WebSocketMessage::Unknown(v));
390 }
391 }
392 }
393
394 if v.get("e").is_none() && v.get("lastUpdateId").is_some() {
400 match serde_json::from_value::<PartialDepthEvent>(v.clone()) {
401 Ok(event) => return Ok(WebSocketMessage::PartialDepth(event)),
402 Err(err) => {
403 tracing::warn!(error = ?err, "Failed to parse PartialDepth event");
404 return Ok(WebSocketMessage::Unknown(v));
405 }
406 }
407 }
408
409 if v.get("e").is_none() && v.get("u").is_some() {
411 match serde_json::from_value::<BookTickerEvent>(v.clone()) {
412 Ok(event) => return Ok(WebSocketMessage::BookTicker(event)),
413 Err(err) => {
414 tracing::warn!(error = ?err, "Failed to parse BookTicker event");
415 return Ok(WebSocketMessage::Unknown(v));
416 }
417 }
418 }
419
420 let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
424
425 macro_rules! parse_event {
427 ($variant:ident, $type:ty, $label:literal) => {
428 match serde_json::from_value::<$type>(v.clone()) {
429 Ok(event) => Ok(WebSocketMessage::$variant(event)),
430 Err(err) => {
431 tracing::warn!(
432 error = ?err,
433 event_type = $label,
434 "Failed to parse known WebSocket event; falling back to Unknown"
435 );
436 Ok(WebSocketMessage::Unknown(v))
437 }
438 }
439 };
440 }
441
442 match event_type {
443 "aggTrade" => parse_event!(AggTrade, AggTradeEvent, "aggTrade"),
444 "markPriceUpdate" => parse_event!(MarkPrice, MarkPriceEvent, "markPriceUpdate"),
445 "kline" => parse_event!(Kline, KlineEvent, "kline"),
446 "24hrMiniTicker" => parse_event!(MiniTicker, MiniTickerEvent, "24hrMiniTicker"),
447 "24hrTicker" => parse_event!(Ticker, TickerEvent, "24hrTicker"),
448 "bookTicker" => parse_event!(BookTicker, BookTickerEvent, "bookTicker"),
449 "forceOrder" => parse_event!(ForceOrder, ForceOrderEvent, "forceOrder"),
450 "depthUpdate" => parse_event!(DiffDepth, DiffDepthEvent, "depthUpdate"),
451 other => {
452 tracing::warn!(event_type = other, "Unknown WebSocket message event type");
453 Ok(WebSocketMessage::Unknown(v))
454 }
455 }
456 }
457}
458
459#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn agg_trade_deserializes_correctly() {
469 let json = r#"{
470 "e": "aggTrade",
471 "E": 1625184000000,
472 "s": "BTCUSDT",
473 "a": 12345,
474 "p": "30000.00",
475 "q": "0.001",
476 "f": 100,
477 "l": 105,
478 "T": 1625184000000,
479 "m": false
480 }"#;
481 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
482 assert!(matches!(msg, WebSocketMessage::AggTrade(ref e) if e.symbol == "BTCUSDT"));
483 if let WebSocketMessage::AggTrade(event) = msg {
484 assert_eq!(event.price, "30000.00");
485 assert_eq!(event.quantity, "0.001");
486 assert!(!event.is_buyer_maker);
487 }
488 }
489
490 #[test]
491 fn unknown_event_type_maps_to_unknown_variant() {
492 let json = r#"{"e":"futureData","x":"someValue"}"#;
493 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
494 assert!(matches!(msg, WebSocketMessage::Unknown(_)));
495 }
496
497 #[test]
498 fn kline_event_deserializes_correctly() {
499 let json = r#"{
500 "e": "kline",
501 "E": 1625184000000,
502 "s": "BTCUSDT",
503 "k": {
504 "t": 1625184000000, "T": 1625184059999, "s": "BTCUSDT",
505 "i": "1m", "f": 1, "L": 10,
506 "o": "30000", "c": "30100", "h": "30200", "l": "29900",
507 "v": "100", "n": 10, "x": false,
508 "q": "3010000", "V": "50", "Q": "1505000"
509 }
510 }"#;
511 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
512 assert!(matches!(msg, WebSocketMessage::Kline(ref e) if e.kline.interval == "1m"));
513 if let WebSocketMessage::Kline(event) = msg {
514 assert!(!event.kline.is_closed);
515 assert_eq!(event.kline.open, "30000");
516 assert_eq!(event.kline.close, "30100");
517 }
518 }
519
520 #[test]
521 fn partial_depth_deserializes_correctly() {
522 let json = r#"{"lastUpdateId":123456,"bids":[["30000","1.5"],["29999","2.0"]],"asks":[["30001","0.5"]]}"#;
523 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
524 assert!(matches!(msg, WebSocketMessage::PartialDepth(_)));
525 if let WebSocketMessage::PartialDepth(event) = msg {
526 assert_eq!(event.bids[0][0], "30000");
527 assert_eq!(event.bids[0][1], "1.5");
528 }
529 }
530
531 #[test]
532 fn all_mark_prices_array_deserializes_correctly() {
533 let json = r#"[{"e":"markPriceUpdate","E":1625184000000,"s":"BTCUSDT","p":"30000","i":"30001","P":"29999","r":"0.0001","T":1625198400000}]"#;
534 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
535 assert!(matches!(msg, WebSocketMessage::AllMarkPrices(_)));
536 if let WebSocketMessage::AllMarkPrices(events) = msg {
537 assert_eq!(events.len(), 1);
538 assert_eq!(events[0].symbol, "BTCUSDT");
539 }
540 }
541
542 #[test]
543 fn mark_price_single_deserializes_correctly() {
544 let json = r#"{
545 "e": "markPriceUpdate",
546 "E": 1625184000000,
547 "s": "BTCUSDT",
548 "p": "30000.00",
549 "i": "30001.00",
550 "P": "29999.00",
551 "r": "0.0001",
552 "T": 1625198400000
553 }"#;
554 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
555 assert!(matches!(msg, WebSocketMessage::MarkPrice(_)));
556 if let WebSocketMessage::MarkPrice(event) = msg {
557 assert_eq!(event.symbol, "BTCUSDT");
558 assert_eq!(event.mark_price, "30000.00");
559 assert_eq!(event.last_funding_rate, "0.0001");
560 assert_eq!(event.next_funding_time, 1625198400000);
561 }
562 }
563
564 #[test]
565 fn book_ticker_deserializes_correctly() {
566 let json = r#"{"u":12345,"s":"BTCUSDT","b":"30000","B":"1.5","a":"30001","A":"0.5"}"#;
567 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
568 assert!(matches!(msg, WebSocketMessage::BookTicker(ref e) if e.symbol == "BTCUSDT"));
569 if let WebSocketMessage::BookTicker(event) = msg {
570 assert_eq!(event.best_bid_price, "30000");
571 assert_eq!(event.best_ask_price, "30001");
572 }
573 }
574
575 #[test]
576 fn diff_depth_deserializes_correctly() {
577 let json = r#"{
578 "e": "depthUpdate",
579 "E": 1625184000000,
580 "T": 1625183999000,
581 "s": "BTCUSDT",
582 "U": 100,
583 "u": 200,
584 "pu": 99,
585 "b": [["29999","1.0"],["29998","2.0"]],
586 "a": [["30001","0.5"]]
587 }"#;
588 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
589 assert!(matches!(msg, WebSocketMessage::DiffDepth(ref e) if e.symbol == "BTCUSDT"));
590 if let WebSocketMessage::DiffDepth(event) = msg {
591 assert_eq!(event.first_update_id, 100);
592 assert_eq!(event.final_update_id, 200);
593 assert_eq!(event.prev_final_update_id, 99);
594 assert_eq!(event.transaction_time, 1625183999000);
595 assert_eq!(event.bids[0][0], "29999");
596 }
597 }
598
599 #[test]
600 fn diff_depth_without_transaction_time_uses_default() {
601 let json = r#"{
603 "e": "depthUpdate",
604 "E": 1625184000000,
605 "s": "BTCUSDT",
606 "U": 100,
607 "u": 200,
608 "b": [["29999","1.0"]],
609 "a": [["30001","0.5"]]
610 }"#;
611 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
612 if let WebSocketMessage::DiffDepth(event) = msg {
613 assert_eq!(event.transaction_time, 0);
614 } else {
615 panic!("expected DiffDepth");
616 }
617 }
618
619 #[test]
620 fn book_ticker_v3_with_event_type_deserializes() {
621 let json = r#"{
623 "e": "bookTicker",
624 "u": 400900217,
625 "E": 1568014460893,
626 "T": 1568014460891,
627 "s": "BNBUSDT",
628 "b": "25.35190000",
629 "B": "31.21000000",
630 "a": "25.36520000",
631 "A": "40.66000000"
632 }"#;
633 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
634 assert!(matches!(msg, WebSocketMessage::BookTicker(ref e) if e.symbol == "BNBUSDT"));
635 if let WebSocketMessage::BookTicker(event) = msg {
636 assert_eq!(event.event_type, "bookTicker");
637 assert_eq!(event.event_time, 1568014460893);
638 assert_eq!(event.transaction_time, 1568014460891);
639 assert_eq!(event.update_id, 400900217);
640 }
641 }
642
643 #[test]
644 fn mini_ticker_deserializes_correctly() {
645 let json = r#"{
646 "e": "24hrMiniTicker",
647 "E": 1625184000000,
648 "s": "ETHUSDT",
649 "c": "2000.00",
650 "o": "1950.00",
651 "h": "2050.00",
652 "l": "1900.00",
653 "v": "5000",
654 "q": "9750000"
655 }"#;
656 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
657 assert!(matches!(msg, WebSocketMessage::MiniTicker(ref e) if e.symbol == "ETHUSDT"));
658 if let WebSocketMessage::MiniTicker(event) = msg {
659 assert_eq!(event.close_price, "2000.00");
660 assert_eq!(event.volume, "5000");
661 }
662 }
663}