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 = "pu")]
254 pub prev_final_update_id: u64,
255 #[serde(rename = "b")]
256 pub bids: Vec<[String; 2]>,
257 #[serde(rename = "a")]
258 pub asks: Vec<[String; 2]>,
259}
260
261#[derive(Debug)]
278pub enum WebSocketMessage {
279 AggTrade(AggTradeEvent),
281 MarkPrice(MarkPriceEvent),
283 AllMarkPrices(Vec<MarkPriceEvent>),
285 Kline(KlineEvent),
287 MiniTicker(MiniTickerEvent),
289 AllMiniTickers(Vec<MiniTickerEvent>),
291 Ticker(TickerEvent),
293 AllTickers(Vec<TickerEvent>),
295 BookTicker(BookTickerEvent),
297 ForceOrder(ForceOrderEvent),
299 AllForceOrders(Vec<ForceOrderEvent>),
301 PartialDepth(PartialDepthEvent),
303 DiffDepth(DiffDepthEvent),
305 Unknown(serde_json::Value),
307}
308
309impl<'de> serde::Deserialize<'de> for WebSocketMessage {
311 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
312 let v = serde_json::Value::deserialize(deserializer)?;
313
314 if let Some(arr) = v.as_array() {
318 if arr.is_empty() {
319 tracing::warn!("Received empty WebSocket array message");
320 return Ok(WebSocketMessage::Unknown(v));
321 }
322 let first = &arr[0];
323 match first.get("e").and_then(|e| e.as_str()).unwrap_or("") {
324 "markPriceUpdate" => {
325 match serde_json::from_value::<Vec<MarkPriceEvent>>(v.clone()) {
326 Ok(events) => return Ok(WebSocketMessage::AllMarkPrices(events)),
327 Err(err) => {
328 tracing::warn!(error = ?err, "Failed to parse AllMarkPrices array");
329 return Ok(WebSocketMessage::Unknown(v));
330 }
331 }
332 }
333 "24hrMiniTicker" => {
334 match serde_json::from_value::<Vec<MiniTickerEvent>>(v.clone()) {
335 Ok(events) => return Ok(WebSocketMessage::AllMiniTickers(events)),
336 Err(err) => {
337 tracing::warn!(error = ?err, "Failed to parse AllMiniTickers array");
338 return Ok(WebSocketMessage::Unknown(v));
339 }
340 }
341 }
342 "24hrTicker" => {
343 match serde_json::from_value::<Vec<TickerEvent>>(v.clone()) {
344 Ok(events) => return Ok(WebSocketMessage::AllTickers(events)),
345 Err(err) => {
346 tracing::warn!(error = ?err, "Failed to parse AllTickers array");
347 return Ok(WebSocketMessage::Unknown(v));
348 }
349 }
350 }
351 "forceOrder" => {
352 match serde_json::from_value::<Vec<ForceOrderEvent>>(v.clone()) {
353 Ok(events) => return Ok(WebSocketMessage::AllForceOrders(events)),
354 Err(err) => {
355 tracing::warn!(error = ?err, "Failed to parse AllForceOrders array");
356 return Ok(WebSocketMessage::Unknown(v));
357 }
358 }
359 }
360 other => {
361 tracing::warn!(event_type = other, "Unknown WebSocket array event type");
362 return Ok(WebSocketMessage::Unknown(v));
363 }
364 }
365 }
366
367 if v.get("e").is_none() && v.get("lastUpdateId").is_some() {
373 match serde_json::from_value::<PartialDepthEvent>(v.clone()) {
374 Ok(event) => return Ok(WebSocketMessage::PartialDepth(event)),
375 Err(err) => {
376 tracing::warn!(error = ?err, "Failed to parse PartialDepth event");
377 return Ok(WebSocketMessage::Unknown(v));
378 }
379 }
380 }
381
382 if v.get("e").is_none() && v.get("u").is_some() {
384 match serde_json::from_value::<BookTickerEvent>(v.clone()) {
385 Ok(event) => return Ok(WebSocketMessage::BookTicker(event)),
386 Err(err) => {
387 tracing::warn!(error = ?err, "Failed to parse BookTicker event");
388 return Ok(WebSocketMessage::Unknown(v));
389 }
390 }
391 }
392
393 let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
397
398 macro_rules! parse_event {
400 ($variant:ident, $type:ty, $label:literal) => {
401 match serde_json::from_value::<$type>(v.clone()) {
402 Ok(event) => Ok(WebSocketMessage::$variant(event)),
403 Err(err) => {
404 tracing::warn!(
405 error = ?err,
406 event_type = $label,
407 "Failed to parse known WebSocket event; falling back to Unknown"
408 );
409 Ok(WebSocketMessage::Unknown(v))
410 }
411 }
412 };
413 }
414
415 match event_type {
416 "aggTrade" => parse_event!(AggTrade, AggTradeEvent, "aggTrade"),
417 "markPriceUpdate" => parse_event!(MarkPrice, MarkPriceEvent, "markPriceUpdate"),
418 "kline" => parse_event!(Kline, KlineEvent, "kline"),
419 "24hrMiniTicker" => parse_event!(MiniTicker, MiniTickerEvent, "24hrMiniTicker"),
420 "24hrTicker" => parse_event!(Ticker, TickerEvent, "24hrTicker"),
421 "forceOrder" => parse_event!(ForceOrder, ForceOrderEvent, "forceOrder"),
422 "depthUpdate" => parse_event!(DiffDepth, DiffDepthEvent, "depthUpdate"),
423 other => {
424 tracing::warn!(event_type = other, "Unknown WebSocket message event type");
425 Ok(WebSocketMessage::Unknown(v))
426 }
427 }
428 }
429}
430
431#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn agg_trade_deserializes_correctly() {
441 let json = r#"{
442 "e": "aggTrade",
443 "E": 1625184000000,
444 "s": "BTCUSDT",
445 "a": 12345,
446 "p": "30000.00",
447 "q": "0.001",
448 "f": 100,
449 "l": 105,
450 "T": 1625184000000,
451 "m": false
452 }"#;
453 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
454 assert!(matches!(msg, WebSocketMessage::AggTrade(ref e) if e.symbol == "BTCUSDT"));
455 if let WebSocketMessage::AggTrade(event) = msg {
456 assert_eq!(event.price, "30000.00");
457 assert_eq!(event.quantity, "0.001");
458 assert!(!event.is_buyer_maker);
459 }
460 }
461
462 #[test]
463 fn unknown_event_type_maps_to_unknown_variant() {
464 let json = r#"{"e":"futureData","x":"someValue"}"#;
465 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
466 assert!(matches!(msg, WebSocketMessage::Unknown(_)));
467 }
468
469 #[test]
470 fn kline_event_deserializes_correctly() {
471 let json = r#"{
472 "e": "kline",
473 "E": 1625184000000,
474 "s": "BTCUSDT",
475 "k": {
476 "t": 1625184000000, "T": 1625184059999, "s": "BTCUSDT",
477 "i": "1m", "f": 1, "L": 10,
478 "o": "30000", "c": "30100", "h": "30200", "l": "29900",
479 "v": "100", "n": 10, "x": false,
480 "q": "3010000", "V": "50", "Q": "1505000"
481 }
482 }"#;
483 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
484 assert!(matches!(msg, WebSocketMessage::Kline(ref e) if e.kline.interval == "1m"));
485 if let WebSocketMessage::Kline(event) = msg {
486 assert!(!event.kline.is_closed);
487 assert_eq!(event.kline.open, "30000");
488 assert_eq!(event.kline.close, "30100");
489 }
490 }
491
492 #[test]
493 fn partial_depth_deserializes_correctly() {
494 let json = r#"{"lastUpdateId":123456,"bids":[["30000","1.5"],["29999","2.0"]],"asks":[["30001","0.5"]]}"#;
495 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
496 assert!(matches!(msg, WebSocketMessage::PartialDepth(_)));
497 if let WebSocketMessage::PartialDepth(event) = msg {
498 assert_eq!(event.bids[0][0], "30000");
499 assert_eq!(event.bids[0][1], "1.5");
500 }
501 }
502
503 #[test]
504 fn all_mark_prices_array_deserializes_correctly() {
505 let json = r#"[{"e":"markPriceUpdate","E":1625184000000,"s":"BTCUSDT","p":"30000","i":"30001","P":"29999","r":"0.0001","T":1625198400000}]"#;
506 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
507 assert!(matches!(msg, WebSocketMessage::AllMarkPrices(_)));
508 if let WebSocketMessage::AllMarkPrices(events) = msg {
509 assert_eq!(events.len(), 1);
510 assert_eq!(events[0].symbol, "BTCUSDT");
511 }
512 }
513
514 #[test]
515 fn mark_price_single_deserializes_correctly() {
516 let json = r#"{
517 "e": "markPriceUpdate",
518 "E": 1625184000000,
519 "s": "BTCUSDT",
520 "p": "30000.00",
521 "i": "30001.00",
522 "P": "29999.00",
523 "r": "0.0001",
524 "R": "0.000050",
525 "T": 1625198400000
526 }"#;
527 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
528 assert!(matches!(msg, WebSocketMessage::MarkPrice(_)));
529 if let WebSocketMessage::MarkPrice(event) = msg {
530 assert_eq!(event.symbol, "BTCUSDT");
531 assert_eq!(event.mark_price, "30000.00");
532 assert_eq!(event.last_funding_rate, "0.0001");
533 assert_eq!(event.interest_rate, "0.000050");
534 assert_eq!(event.next_funding_time, 1625198400000);
535 }
536 }
537
538 #[test]
539 fn book_ticker_deserializes_correctly() {
540 let json = r#"{"u":12345,"s":"BTCUSDT","b":"30000","B":"1.5","a":"30001","A":"0.5"}"#;
541 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
542 assert!(matches!(msg, WebSocketMessage::BookTicker(ref e) if e.symbol == "BTCUSDT"));
543 if let WebSocketMessage::BookTicker(event) = msg {
544 assert_eq!(event.best_bid_price, "30000");
545 assert_eq!(event.best_ask_price, "30001");
546 }
547 }
548
549 #[test]
550 fn diff_depth_deserializes_correctly() {
551 let json = r#"{
552 "e": "depthUpdate",
553 "E": 1625184000000,
554 "s": "BTCUSDT",
555 "U": 100,
556 "u": 200,
557 "b": [["29999","1.0"],["29998","2.0"]],
558 "a": [["30001","0.5"]]
559 }"#;
560 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
561 assert!(matches!(msg, WebSocketMessage::DiffDepth(ref e) if e.symbol == "BTCUSDT"));
562 if let WebSocketMessage::DiffDepth(event) = msg {
563 assert_eq!(event.first_update_id, 100);
564 assert_eq!(event.final_update_id, 200);
565 assert_eq!(event.bids[0][0], "29999");
566 }
567 }
568
569 #[test]
570 fn mini_ticker_deserializes_correctly() {
571 let json = r#"{
572 "e": "24hrMiniTicker",
573 "E": 1625184000000,
574 "s": "ETHUSDT",
575 "c": "2000.00",
576 "o": "1950.00",
577 "h": "2050.00",
578 "l": "1900.00",
579 "v": "5000",
580 "q": "9750000"
581 }"#;
582 let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
583 assert!(matches!(msg, WebSocketMessage::MiniTicker(ref e) if e.symbol == "ETHUSDT"));
584 if let WebSocketMessage::MiniTicker(event) = msg {
585 assert_eq!(event.close_price, "2000.00");
586 assert_eq!(event.volume, "5000");
587 }
588 }
589}