use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AggTradeEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "a")]
pub agg_trade_id: u64,
#[serde(rename = "p")]
pub price: String,
#[serde(rename = "q")]
pub quantity: String,
#[serde(rename = "f")]
pub first_trade_id: u64,
#[serde(rename = "l")]
pub last_trade_id: u64,
#[serde(rename = "T")]
pub trade_time: u64,
#[serde(rename = "m")]
pub is_buyer_maker: bool,
}
#[derive(Debug, Deserialize)]
pub struct MarkPriceEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "p")]
pub mark_price: String,
#[serde(rename = "i")]
pub index_price: String,
#[serde(rename = "P")]
pub estimated_settle_price: String,
#[serde(rename = "r")]
pub last_funding_rate: String,
#[serde(rename = "R", default)]
pub interest_rate: String,
#[serde(rename = "T")]
pub next_funding_time: u64,
}
#[derive(Debug, Deserialize)]
pub struct KlineEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "k")]
pub kline: KlineData,
}
#[derive(Debug, Deserialize)]
pub struct KlineData {
#[serde(rename = "t")]
pub start_time: u64,
#[serde(rename = "T")]
pub close_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "i")]
pub interval: String,
#[serde(rename = "f")]
pub first_trade_id: i64,
#[serde(rename = "L")]
pub last_trade_id: i64,
#[serde(rename = "o")]
pub open: String,
#[serde(rename = "c")]
pub close: String,
#[serde(rename = "h")]
pub high: String,
#[serde(rename = "l")]
pub low: String,
#[serde(rename = "v")]
pub volume: String,
#[serde(rename = "n")]
pub trade_count: u64,
#[serde(rename = "x")]
pub is_closed: bool,
#[serde(rename = "q")]
pub quote_volume: String,
#[serde(rename = "V")]
pub taker_buy_volume: String,
#[serde(rename = "Q")]
pub taker_buy_quote_volume: String,
}
#[derive(Debug, Deserialize)]
pub struct MiniTickerEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "c")]
pub close_price: String,
#[serde(rename = "o")]
pub open_price: String,
#[serde(rename = "h")]
pub high_price: String,
#[serde(rename = "l")]
pub low_price: String,
#[serde(rename = "v")]
pub volume: String,
#[serde(rename = "q")]
pub quote_volume: String,
}
#[derive(Debug, Deserialize)]
pub struct TickerEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "p")]
pub price_change: String,
#[serde(rename = "P")]
pub price_change_percent: String,
#[serde(rename = "w")]
pub weighted_avg_price: String,
#[serde(rename = "c")]
pub last_price: String,
#[serde(rename = "Q")]
pub last_qty: String,
#[serde(rename = "o")]
pub open_price: String,
#[serde(rename = "h")]
pub high_price: String,
#[serde(rename = "l")]
pub low_price: String,
#[serde(rename = "v")]
pub volume: String,
#[serde(rename = "q")]
pub quote_volume: String,
#[serde(rename = "O")]
pub open_time: u64,
#[serde(rename = "C")]
pub close_time: u64,
#[serde(rename = "F")]
pub first_trade_id: i64,
#[serde(rename = "L")]
pub last_trade_id: i64,
#[serde(rename = "n")]
pub trade_count: u64,
}
#[derive(Debug, Deserialize)]
pub struct BookTickerEvent {
#[serde(rename = "u")]
pub update_id: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "b")]
pub best_bid_price: String,
#[serde(rename = "B")]
pub best_bid_qty: String,
#[serde(rename = "a")]
pub best_ask_price: String,
#[serde(rename = "A")]
pub best_ask_qty: String,
}
#[derive(Debug, Deserialize)]
pub struct ForceOrderEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "o")]
pub order: ForceOrderData,
}
#[derive(Debug, Deserialize)]
pub struct ForceOrderData {
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "S")]
pub side: String,
#[serde(rename = "o")]
pub order_type: String,
#[serde(rename = "f")]
pub time_in_force: String,
#[serde(rename = "q")]
pub quantity: String,
#[serde(rename = "p")]
pub price: String,
#[serde(rename = "ap")]
pub average_price: String,
#[serde(rename = "X")]
pub order_status: String,
#[serde(rename = "l")]
pub last_filled_qty: String,
#[serde(rename = "z")]
pub accumulated_qty: String,
#[serde(rename = "T")]
pub trade_time: u64,
}
#[derive(Debug, Deserialize)]
pub struct PartialDepthEvent {
#[serde(rename = "lastUpdateId")]
pub last_update_id: u64,
pub bids: Vec<[String; 2]>,
pub asks: Vec<[String; 2]>,
}
#[derive(Debug, Deserialize)]
pub struct DiffDepthEvent {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "U")]
pub first_update_id: u64,
#[serde(rename = "u")]
pub final_update_id: u64,
#[serde(rename = "pu")]
pub prev_final_update_id: u64,
#[serde(rename = "b")]
pub bids: Vec<[String; 2]>,
#[serde(rename = "a")]
pub asks: Vec<[String; 2]>,
}
#[derive(Debug)]
pub enum WebSocketMessage {
AggTrade(AggTradeEvent),
MarkPrice(MarkPriceEvent),
AllMarkPrices(Vec<MarkPriceEvent>),
Kline(KlineEvent),
MiniTicker(MiniTickerEvent),
AllMiniTickers(Vec<MiniTickerEvent>),
Ticker(TickerEvent),
AllTickers(Vec<TickerEvent>),
BookTicker(BookTickerEvent),
ForceOrder(ForceOrderEvent),
AllForceOrders(Vec<ForceOrderEvent>),
PartialDepth(PartialDepthEvent),
DiffDepth(DiffDepthEvent),
Unknown(serde_json::Value),
}
impl<'de> serde::Deserialize<'de> for WebSocketMessage {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let v = serde_json::Value::deserialize(deserializer)?;
if let Some(arr) = v.as_array() {
if arr.is_empty() {
tracing::warn!("Received empty WebSocket array message");
return Ok(WebSocketMessage::Unknown(v));
}
let first = &arr[0];
match first.get("e").and_then(|e| e.as_str()).unwrap_or("") {
"markPriceUpdate" => {
match serde_json::from_value::<Vec<MarkPriceEvent>>(v.clone()) {
Ok(events) => return Ok(WebSocketMessage::AllMarkPrices(events)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse AllMarkPrices array");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
"24hrMiniTicker" => {
match serde_json::from_value::<Vec<MiniTickerEvent>>(v.clone()) {
Ok(events) => return Ok(WebSocketMessage::AllMiniTickers(events)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse AllMiniTickers array");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
"24hrTicker" => {
match serde_json::from_value::<Vec<TickerEvent>>(v.clone()) {
Ok(events) => return Ok(WebSocketMessage::AllTickers(events)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse AllTickers array");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
"forceOrder" => {
match serde_json::from_value::<Vec<ForceOrderEvent>>(v.clone()) {
Ok(events) => return Ok(WebSocketMessage::AllForceOrders(events)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse AllForceOrders array");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
other => {
tracing::warn!(event_type = other, "Unknown WebSocket array event type");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
if v.get("e").is_none() && v.get("lastUpdateId").is_some() {
match serde_json::from_value::<PartialDepthEvent>(v.clone()) {
Ok(event) => return Ok(WebSocketMessage::PartialDepth(event)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse PartialDepth event");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
if v.get("e").is_none() && v.get("u").is_some() {
match serde_json::from_value::<BookTickerEvent>(v.clone()) {
Ok(event) => return Ok(WebSocketMessage::BookTicker(event)),
Err(err) => {
tracing::warn!(error = ?err, "Failed to parse BookTicker event");
return Ok(WebSocketMessage::Unknown(v));
}
}
}
let event_type = v.get("e").and_then(|e| e.as_str()).unwrap_or("");
macro_rules! parse_event {
($variant:ident, $type:ty, $label:literal) => {
match serde_json::from_value::<$type>(v.clone()) {
Ok(event) => Ok(WebSocketMessage::$variant(event)),
Err(err) => {
tracing::warn!(
error = ?err,
event_type = $label,
"Failed to parse known WebSocket event; falling back to Unknown"
);
Ok(WebSocketMessage::Unknown(v))
}
}
};
}
match event_type {
"aggTrade" => parse_event!(AggTrade, AggTradeEvent, "aggTrade"),
"markPriceUpdate" => parse_event!(MarkPrice, MarkPriceEvent, "markPriceUpdate"),
"kline" => parse_event!(Kline, KlineEvent, "kline"),
"24hrMiniTicker" => parse_event!(MiniTicker, MiniTickerEvent, "24hrMiniTicker"),
"24hrTicker" => parse_event!(Ticker, TickerEvent, "24hrTicker"),
"forceOrder" => parse_event!(ForceOrder, ForceOrderEvent, "forceOrder"),
"depthUpdate" => parse_event!(DiffDepth, DiffDepthEvent, "depthUpdate"),
other => {
tracing::warn!(event_type = other, "Unknown WebSocket message event type");
Ok(WebSocketMessage::Unknown(v))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agg_trade_deserializes_correctly() {
let json = r#"{
"e": "aggTrade",
"E": 1625184000000,
"s": "BTCUSDT",
"a": 12345,
"p": "30000.00",
"q": "0.001",
"f": 100,
"l": 105,
"T": 1625184000000,
"m": false
}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::AggTrade(ref e) if e.symbol == "BTCUSDT"));
if let WebSocketMessage::AggTrade(event) = msg {
assert_eq!(event.price, "30000.00");
assert_eq!(event.quantity, "0.001");
assert!(!event.is_buyer_maker);
}
}
#[test]
fn unknown_event_type_maps_to_unknown_variant() {
let json = r#"{"e":"futureData","x":"someValue"}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::Unknown(_)));
}
#[test]
fn kline_event_deserializes_correctly() {
let json = r#"{
"e": "kline",
"E": 1625184000000,
"s": "BTCUSDT",
"k": {
"t": 1625184000000, "T": 1625184059999, "s": "BTCUSDT",
"i": "1m", "f": 1, "L": 10,
"o": "30000", "c": "30100", "h": "30200", "l": "29900",
"v": "100", "n": 10, "x": false,
"q": "3010000", "V": "50", "Q": "1505000"
}
}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::Kline(ref e) if e.kline.interval == "1m"));
if let WebSocketMessage::Kline(event) = msg {
assert!(!event.kline.is_closed);
assert_eq!(event.kline.open, "30000");
assert_eq!(event.kline.close, "30100");
}
}
#[test]
fn partial_depth_deserializes_correctly() {
let json = r#"{"lastUpdateId":123456,"bids":[["30000","1.5"],["29999","2.0"]],"asks":[["30001","0.5"]]}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::PartialDepth(_)));
if let WebSocketMessage::PartialDepth(event) = msg {
assert_eq!(event.bids[0][0], "30000");
assert_eq!(event.bids[0][1], "1.5");
}
}
#[test]
fn all_mark_prices_array_deserializes_correctly() {
let json = r#"[{"e":"markPriceUpdate","E":1625184000000,"s":"BTCUSDT","p":"30000","i":"30001","P":"29999","r":"0.0001","T":1625198400000}]"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::AllMarkPrices(_)));
if let WebSocketMessage::AllMarkPrices(events) = msg {
assert_eq!(events.len(), 1);
assert_eq!(events[0].symbol, "BTCUSDT");
}
}
#[test]
fn mark_price_single_deserializes_correctly() {
let json = r#"{
"e": "markPriceUpdate",
"E": 1625184000000,
"s": "BTCUSDT",
"p": "30000.00",
"i": "30001.00",
"P": "29999.00",
"r": "0.0001",
"R": "0.000050",
"T": 1625198400000
}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::MarkPrice(_)));
if let WebSocketMessage::MarkPrice(event) = msg {
assert_eq!(event.symbol, "BTCUSDT");
assert_eq!(event.mark_price, "30000.00");
assert_eq!(event.last_funding_rate, "0.0001");
assert_eq!(event.interest_rate, "0.000050");
assert_eq!(event.next_funding_time, 1625198400000);
}
}
#[test]
fn book_ticker_deserializes_correctly() {
let json = r#"{"u":12345,"s":"BTCUSDT","b":"30000","B":"1.5","a":"30001","A":"0.5"}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::BookTicker(ref e) if e.symbol == "BTCUSDT"));
if let WebSocketMessage::BookTicker(event) = msg {
assert_eq!(event.best_bid_price, "30000");
assert_eq!(event.best_ask_price, "30001");
}
}
#[test]
fn diff_depth_deserializes_correctly() {
let json = r#"{
"e": "depthUpdate",
"E": 1625184000000,
"s": "BTCUSDT",
"U": 100,
"u": 200,
"b": [["29999","1.0"],["29998","2.0"]],
"a": [["30001","0.5"]]
}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::DiffDepth(ref e) if e.symbol == "BTCUSDT"));
if let WebSocketMessage::DiffDepth(event) = msg {
assert_eq!(event.first_update_id, 100);
assert_eq!(event.final_update_id, 200);
assert_eq!(event.bids[0][0], "29999");
}
}
#[test]
fn mini_ticker_deserializes_correctly() {
let json = r#"{
"e": "24hrMiniTicker",
"E": 1625184000000,
"s": "ETHUSDT",
"c": "2000.00",
"o": "1950.00",
"h": "2050.00",
"l": "1900.00",
"v": "5000",
"q": "9750000"
}"#;
let msg: WebSocketMessage = serde_json::from_str(json).unwrap();
assert!(matches!(msg, WebSocketMessage::MiniTicker(ref e) if e.symbol == "ETHUSDT"));
if let WebSocketMessage::MiniTicker(event) = msg {
assert_eq!(event.close_price, "2000.00");
assert_eq!(event.volume, "5000");
}
}
}