use serde::ser::SerializeTuple;
use serde::{Deserialize, Serialize};
use crate::{RequestId, WSError};
pub type ClientOrderId = u64;
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum MessageType {
#[serde(rename = "s")]
Snapshot,
#[serde(rename = "u")]
Update,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct DataMessage {
pub channel: String,
pub symbol: String,
pub ts: u64,
#[serde(rename = "mt")]
pub msg_type: MessageType,
pub data: serde_json::Value,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct StatusMessage {
#[serde(rename = "E")]
pub event_time: u64,
pub status: String,
pub client_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct PongMessage {
pub id: Option<RequestId>,
#[serde(rename = "E")]
pub event_time: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")]
pub struct ErrorMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
#[serde(rename = "E")]
pub event_time: u64,
pub error: WSError,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MethodResult {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
#[serde(rename = "E")]
pub event_time: u64,
pub result: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(transparent)]
pub struct SubscribeOk(pub MethodResult);
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(transparent)]
pub struct UnsubscribeOk(pub MethodResult);
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ListSubscriptionsMessage {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<RequestId>,
#[serde(rename = "E")]
pub event_time: u64,
pub result: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct PriceLevel(pub String, pub String);
impl Serialize for PriceLevel {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut tuple = serializer.serialize_tuple(2)?;
tuple.serialize_element(&self.0)?;
tuple.serialize_element(&self.1)?;
tuple.end()
}
}
impl<'de> Deserialize<'de> for PriceLevel {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let (price, qty) = <(String, String)>::deserialize(deserializer)?;
Ok(PriceLevel(price, qty))
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct DepthUpdate {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "T")]
pub transaction_time: u64,
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "U")]
pub first_update_id: u64,
#[serde(rename = "u")]
pub last_update_id: u64,
#[serde(rename = "pu")]
pub prev_update_id: u64,
#[serde(rename = "b")]
pub bids: Vec<PriceLevel>,
#[serde(rename = "a")]
pub asks: Vec<PriceLevel>,
#[serde(rename = "mt")]
pub msg_type: MessageType,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct AggTradeMessage {
#[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,
#[serde(rename = "th")]
pub tx_hash: String,
#[serde(rename = "ua", default, skip_serializing_if = "Option::is_none")]
pub user_address: Option<String>,
#[serde(rename = "oi", default, skip_serializing_if = "Option::is_none")]
pub order_id: Option<u64>,
#[serde(rename = "mk", default, skip_serializing_if = "Option::is_none")]
pub is_maker: Option<bool>,
#[serde(rename = "ff", default, skip_serializing_if = "Option::is_none")]
pub is_full_fill: Option<bool>,
#[serde(rename = "lq", default, skip_serializing_if = "Option::is_none")]
pub is_liquidation: Option<bool>,
#[serde(rename = "fe", default, skip_serializing_if = "Option::is_none")]
pub fee: Option<String>,
#[serde(rename = "nf", default, skip_serializing_if = "Option::is_none")]
pub net_fee: Option<String>,
#[serde(rename = "fa", default, skip_serializing_if = "Option::is_none")]
pub fee_asset: Option<String>,
#[serde(rename = "co", default, skip_serializing_if = "Option::is_none")]
pub client_order_id: Option<ClientOrderId>,
#[serde(rename = "sd", default, skip_serializing_if = "Option::is_none")]
pub side: Option<String>,
#[serde(rename = "ft", default, skip_serializing_if = "Option::is_none")]
pub fill_type: Option<String>,
#[serde(rename = "z", default, skip_serializing_if = "Option::is_none")]
pub cumulative_filled_size: Option<String>,
#[serde(rename = "Z", default, skip_serializing_if = "Option::is_none")]
pub cumulative_filled_cot: Option<String>,
#[serde(rename = "rs", default, skip_serializing_if = "Option::is_none")]
pub remaining_size: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct BookTickerMessage {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "u")]
pub update_id: u64,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "T")]
pub transaction_time: 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,
#[serde(rename = "mt")]
pub msg_type: MessageType,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ForceOrderMessage {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "o")]
pub order: ForceOrderDetails,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ForceOrderDetails {
#[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", skip_serializing_if = "Option::is_none")]
pub quantity: Option<String>,
#[serde(rename = "z", skip_serializing_if = "Option::is_none")]
pub filled_qty: Option<String>,
#[serde(rename = "p")]
pub price: String,
#[serde(rename = "ap")]
pub avg_price: String,
#[serde(rename = "X")]
pub status: String,
#[serde(rename = "l")]
pub last_filled_qty: String,
#[serde(rename = "T")]
pub trade_time: u64,
#[serde(rename = "th")]
pub tx_hash: String,
#[serde(rename = "ua")]
pub user_address: String,
#[serde(rename = "oi")]
pub order_id: u64,
#[serde(rename = "ti")]
pub trade_id: u64,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct MarkPriceMessage {
#[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", skip_serializing_if = "Option::is_none")]
pub estimated_settle_price: Option<String>,
#[serde(rename = "r")]
pub funding_rate: String,
#[serde(rename = "T", skip_serializing_if = "Option::is_none")]
pub next_funding_time: Option<u64>,
#[serde(rename = "th", skip_serializing_if = "Option::is_none")]
pub tx_hash: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct OrderUpdateMessage {
#[serde(rename = "e")]
pub event_type: String,
#[serde(rename = "E")]
pub event_time: u64,
#[serde(rename = "o")]
pub order: OrderUpdateData,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct OrderUpdateCommon {
#[serde(rename = "s")]
pub symbol: String,
#[serde(rename = "i")]
pub order_id: u64,
#[serde(rename = "co", skip_serializing_if = "Option::is_none")]
pub client_order_id: Option<ClientOrderId>,
#[serde(rename = "X")]
pub status: String,
#[serde(rename = "x")]
pub execution_type: String,
#[serde(rename = "T")]
pub transaction_time: u64,
#[serde(rename = "th")]
pub tx_hash: String,
#[serde(rename = "ua", default, skip_serializing_if = "Option::is_none")]
pub user_address: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct PlaceOrderData {
#[serde(flatten)]
pub common: OrderUpdateCommon,
#[serde(rename = "S")]
pub side: String,
#[serde(rename = "o")]
pub order_type: String,
#[serde(rename = "f")]
pub time_in_force: String,
#[serde(rename = "p")]
pub price: String,
#[serde(rename = "q")]
pub quantity: String,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct CancelOrderData {
#[serde(flatten)]
pub common: OrderUpdateCommon,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TradeFillData {
#[serde(flatten)]
pub common: OrderUpdateCommon,
#[serde(rename = "S")]
pub side: String,
#[serde(rename = "p", default, skip_serializing_if = "Option::is_none")]
pub price: Option<String>,
#[serde(rename = "ap", default, skip_serializing_if = "Option::is_none")]
pub avg_price: Option<String>,
#[serde(rename = "q", default, skip_serializing_if = "Option::is_none")]
pub quantity: Option<String>,
#[serde(rename = "l")]
pub last_filled_qty: String,
#[serde(rename = "L")]
pub last_filled_price: String,
#[serde(rename = "n")]
pub commission: String,
#[serde(rename = "N")]
pub commission_asset: String,
#[serde(rename = "m")]
pub is_maker: bool,
#[serde(rename = "t")]
pub trade_id: u64,
#[serde(rename = "rp")]
pub realized_pnl: String,
#[serde(rename = "ft", default, skip_serializing_if = "Option::is_none")]
pub fill_type: Option<String>,
#[serde(rename = "z", default, skip_serializing_if = "Option::is_none")]
pub cumulative_filled_size: Option<String>,
#[serde(rename = "Z", default, skip_serializing_if = "Option::is_none")]
pub cumulative_filled_cot: Option<String>,
#[serde(rename = "rs", default, skip_serializing_if = "Option::is_none")]
pub remaining_size: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(untagged)]
pub enum OrderUpdateData {
TradeFill(Box<TradeFillData>),
PlaceOrder(PlaceOrderData),
Cancel(CancelOrderData),
}
#[cfg(test)]
mod wire_format_tests {
use super::*;
#[test]
fn subscribe_ok_round_trips_wire_payload() {
let wire = r#"{"e":"subscribe","id":1,"E":1779600272876932,"result":"success"}"#;
let parsed: MethodResult = serde_json::from_str(wire).expect("MethodResult deserializes");
assert_eq!(parsed.id, Some(RequestId::from(1)));
assert_eq!(parsed.event_time, 1779600272876932);
assert_eq!(parsed.result, "success");
}
#[test]
fn list_subscriptions_round_trips_wire_payload() {
let wire = r#"{"e":"list_subscriptions","id":2,"E":1779600273218722,"result":["ETH-USD@aggTrade","BTC-USD@bookTicker"]}"#;
let parsed: ListSubscriptionsMessage =
serde_json::from_str(wire).expect("ListSubscriptionsMessage deserializes");
assert_eq!(parsed.id, Some(RequestId::from(2)));
assert_eq!(parsed.event_time, 1779600273218722);
assert_eq!(
parsed.result,
vec!["ETH-USD@aggTrade".to_string(), "BTC-USD@bookTicker".to_string()]
);
}
#[test]
fn trade_fill_round_trips_wire_payload() {
let wire = r#"{"s":"SOL-USD","i":183696108,"X":"FILLED","x":"TRADE","T":1779598581565646,"th":"0x44dd","ua":"","S":"BUY","ap":"85.96","l":"0.1","L":"85.96","n":"0.00275072","N":"USDC","m":false,"t":13196983,"rp":"-0.0156","ft":"o","z":"0.1","Z":"8.596","rs":"0"}"#;
let parsed: TradeFillData = serde_json::from_str(wire).expect("TradeFillData deserializes");
assert_eq!(parsed.common.symbol, "SOL-USD");
assert_eq!(parsed.common.order_id, 183696108);
assert_eq!(parsed.common.status, "FILLED");
assert_eq!(parsed.common.execution_type, "TRADE");
assert_eq!(parsed.common.user_address.as_deref(), Some(""));
assert_eq!(parsed.side, "BUY");
assert_eq!(parsed.avg_price.as_deref(), Some("85.96"));
assert_eq!(parsed.last_filled_qty, "0.1");
assert_eq!(parsed.last_filled_price, "85.96");
assert_eq!(parsed.commission, "0.00275072");
assert_eq!(parsed.commission_asset, "USDC");
assert!(!parsed.is_maker);
assert_eq!(parsed.trade_id, 13196983);
assert_eq!(parsed.realized_pnl, "-0.0156");
assert_eq!(parsed.fill_type.as_deref(), Some("o"));
assert_eq!(parsed.cumulative_filled_size.as_deref(), Some("0.1"));
assert_eq!(parsed.cumulative_filled_cot.as_deref(), Some("8.596"));
assert_eq!(parsed.remaining_size.as_deref(), Some("0"));
}
#[test]
fn agg_trade_round_trips_wire_payload() {
let wire = r#"{"e":"aggTrade","E":1779600413218736,"s":"SOL-USD","a":13205413,"p":"85.65","q":"0.01","f":13205413,"l":13205413,"T":1779600413211110,"m":true,"th":"0x94764a","ua":"","oi":0,"mk":false,"ff":false,"lq":false,"fe":"0","nf":"0","fa":"","co":0,"sd":"","ft":"","z":"0","Z":"0","rs":"0"}"#;
let parsed: AggTradeMessage =
serde_json::from_str(wire).expect("AggTradeMessage deserializes");
assert_eq!(parsed.symbol, "SOL-USD");
assert_eq!(parsed.price, "85.65");
assert_eq!(parsed.quantity, "0.01");
assert!(parsed.is_buyer_maker);
assert_eq!(parsed.tx_hash, "0x94764a");
assert_eq!(parsed.user_address.as_deref(), Some(""));
assert_eq!(parsed.order_id, Some(0));
assert_eq!(parsed.is_maker, Some(false));
assert_eq!(parsed.is_full_fill, Some(false));
assert_eq!(parsed.fill_type.as_deref(), Some(""));
assert_eq!(parsed.cumulative_filled_size.as_deref(), Some("0"));
assert_eq!(parsed.remaining_size.as_deref(), Some("0"));
}
#[test]
fn trade_fill_in_order_update_data() {
let wire = r#"{"s":"SOL-USD","i":1,"X":"FILLED","x":"TRADE","T":1,"th":"0x","S":"BUY","l":"0.1","L":"85","n":"0","N":"USDC","m":false,"t":1,"rp":"0"}"#;
let parsed: OrderUpdateData =
serde_json::from_str(wire).expect("untagged variant resolves to TradeFill");
match parsed {
OrderUpdateData::TradeFill(_) => {}
other => panic!("expected TradeFill, got {other:?}"),
}
}
}