1use serde::ser::SerializeTuple;
2use serde::{Deserialize, Serialize};
3
4use crate::{RequestId, WSError};
5
6pub type ClientOrderId = u64;
8
9#[derive(Serialize, Deserialize, Clone, Debug)]
10pub enum MessageType {
11 #[serde(rename = "s")]
12 Snapshot,
13 #[serde(rename = "u")]
14 Update,
15}
16
17#[derive(Serialize, Deserialize, Clone, Debug)]
18#[serde(rename_all = "camelCase")]
19pub struct DataMessage {
20 pub channel: String,
21 pub symbol: String,
22 pub ts: u64,
23 #[serde(rename = "mt")]
24 pub msg_type: MessageType,
25 pub data: serde_json::Value,
26}
27
28#[derive(Serialize, Deserialize, Clone, Debug)]
30#[serde(rename_all = "camelCase")]
31pub struct StatusMessage {
32 #[serde(rename = "E")]
34 pub event_time: u64,
35 pub status: String,
36 pub client_id: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub reason: Option<String>,
39}
40
41#[derive(Serialize, Deserialize, Clone, Debug)]
42#[serde(rename_all = "camelCase")]
43pub struct PongMessage {
44 pub id: Option<RequestId>,
45 #[serde(rename = "E")]
47 pub event_time: u64,
48}
49
50#[derive(Serialize, Deserialize, Clone, Debug)]
52#[serde(rename_all = "camelCase")]
53pub struct ErrorMessage {
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub id: Option<RequestId>,
56 #[serde(rename = "E")]
58 pub event_time: u64,
59 pub error: WSError,
60}
61
62#[derive(Serialize, Deserialize, Clone, Debug)]
66pub struct MethodResult {
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub id: Option<RequestId>,
69 #[serde(rename = "E")]
71 pub event_time: u64,
72 pub result: String,
74}
75
76#[derive(Serialize, Deserialize, Clone, Debug)]
78#[serde(transparent)]
79pub struct SubscribeOk(pub MethodResult);
80
81#[derive(Serialize, Deserialize, Clone, Debug)]
83#[serde(transparent)]
84pub struct UnsubscribeOk(pub MethodResult);
85
86#[derive(Serialize, Deserialize, Clone, Debug)]
90pub struct ListSubscriptionsMessage {
91 #[serde(skip_serializing_if = "Option::is_none")]
92 pub id: Option<RequestId>,
93 #[serde(rename = "E")]
95 pub event_time: u64,
96 pub result: Vec<String>,
97}
98
99#[derive(Clone, Debug)]
101pub struct PriceLevel(pub String, pub String);
102
103impl Serialize for PriceLevel {
104 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
105 let mut tuple = serializer.serialize_tuple(2)?;
106 tuple.serialize_element(&self.0)?;
107 tuple.serialize_element(&self.1)?;
108 tuple.end()
109 }
110}
111
112impl<'de> Deserialize<'de> for PriceLevel {
113 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
114 let (price, qty) = <(String, String)>::deserialize(deserializer)?;
115 Ok(PriceLevel(price, qty))
116 }
117}
118
119#[derive(Serialize, Deserialize, Clone, Debug)]
121pub struct DepthUpdate {
122 #[serde(rename = "e")]
123 pub event_type: String,
124 #[serde(rename = "E")]
125 pub event_time: u64,
126 #[serde(rename = "T")]
127 pub transaction_time: u64,
128 #[serde(rename = "s")]
129 pub symbol: String,
130 #[serde(rename = "U")]
131 pub first_update_id: u64,
132 #[serde(rename = "u")]
133 pub last_update_id: u64,
134 #[serde(rename = "pu")]
135 pub prev_update_id: u64,
136 #[serde(rename = "b")]
137 pub bids: Vec<PriceLevel>,
138 #[serde(rename = "a")]
139 pub asks: Vec<PriceLevel>,
140 #[serde(rename = "mt")]
141 pub msg_type: MessageType,
142}
143
144#[derive(Serialize, Deserialize, Clone, Debug)]
151pub struct AggTradeMessage {
152 #[serde(rename = "e")]
153 pub event_type: String,
154 #[serde(rename = "E")]
155 pub event_time: u64,
156 #[serde(rename = "s")]
157 pub symbol: String,
158 #[serde(rename = "a")]
159 pub agg_trade_id: u64,
160 #[serde(rename = "p")]
161 pub price: String,
162 #[serde(rename = "q")]
163 pub quantity: String,
164 #[serde(rename = "f")]
165 pub first_trade_id: u64,
166 #[serde(rename = "l")]
167 pub last_trade_id: u64,
168 #[serde(rename = "T")]
169 pub trade_time: u64,
170 #[serde(rename = "m")]
171 pub is_buyer_maker: bool,
172 #[serde(rename = "th")]
174 pub tx_hash: String,
175 #[serde(rename = "ua", default, skip_serializing_if = "Option::is_none")]
177 pub user_address: Option<String>,
178 #[serde(rename = "oi", default, skip_serializing_if = "Option::is_none")]
180 pub order_id: Option<u64>,
181 #[serde(rename = "mk", default, skip_serializing_if = "Option::is_none")]
183 pub is_maker: Option<bool>,
184 #[serde(rename = "ff", default, skip_serializing_if = "Option::is_none")]
186 pub is_full_fill: Option<bool>,
187 #[serde(rename = "lq", default, skip_serializing_if = "Option::is_none")]
189 pub is_liquidation: Option<bool>,
190 #[serde(rename = "fe", default, skip_serializing_if = "Option::is_none")]
192 pub fee: Option<String>,
193 #[serde(rename = "nf", default, skip_serializing_if = "Option::is_none")]
195 pub net_fee: Option<String>,
196 #[serde(rename = "fa", default, skip_serializing_if = "Option::is_none")]
198 pub fee_asset: Option<String>,
199 #[serde(rename = "co", default, skip_serializing_if = "Option::is_none")]
201 pub client_order_id: Option<ClientOrderId>,
202 #[serde(rename = "sd", default, skip_serializing_if = "Option::is_none")]
204 pub side: Option<String>,
205 #[serde(rename = "ft", default, skip_serializing_if = "Option::is_none")]
207 pub fill_type: Option<String>,
208 #[serde(rename = "z", default, skip_serializing_if = "Option::is_none")]
210 pub cumulative_filled_size: Option<String>,
211 #[serde(rename = "Z", default, skip_serializing_if = "Option::is_none")]
213 pub cumulative_filled_cot: Option<String>,
214 #[serde(rename = "rs", default, skip_serializing_if = "Option::is_none")]
216 pub remaining_size: Option<String>,
217}
218
219#[derive(Serialize, Deserialize, Clone, Debug)]
221pub struct BookTickerMessage {
222 #[serde(rename = "e")]
223 pub event_type: String,
224 #[serde(rename = "u")]
225 pub update_id: u64,
226 #[serde(rename = "E")]
227 pub event_time: u64,
228 #[serde(rename = "T")]
229 pub transaction_time: u64,
230 #[serde(rename = "s")]
231 pub symbol: String,
232 #[serde(rename = "b")]
233 pub best_bid_price: String,
234 #[serde(rename = "B")]
235 pub best_bid_qty: String,
236 #[serde(rename = "a")]
237 pub best_ask_price: String,
238 #[serde(rename = "A")]
239 pub best_ask_qty: String,
240 #[serde(rename = "mt")]
241 pub msg_type: MessageType,
242}
243
244#[derive(Serialize, Deserialize, Clone, Debug)]
246pub struct ForceOrderMessage {
247 #[serde(rename = "e")]
248 pub event_type: String,
249 #[serde(rename = "E")]
250 pub event_time: u64,
251 #[serde(rename = "o")]
252 pub order: ForceOrderDetails,
253}
254
255#[derive(Serialize, Deserialize, Clone, Debug)]
257pub struct ForceOrderDetails {
258 #[serde(rename = "s")]
259 pub symbol: String,
260 #[serde(rename = "S")]
261 pub side: String,
262 #[serde(rename = "o")]
263 pub order_type: String,
264 #[serde(rename = "f")]
265 pub time_in_force: String,
266 #[serde(rename = "q", skip_serializing_if = "Option::is_none")]
267 pub quantity: Option<String>,
268 #[serde(rename = "z", skip_serializing_if = "Option::is_none")]
269 pub filled_qty: Option<String>,
270 #[serde(rename = "p")]
271 pub price: String,
272 #[serde(rename = "ap")]
273 pub avg_price: String,
274 #[serde(rename = "X")]
275 pub status: String,
276 #[serde(rename = "l")]
277 pub last_filled_qty: String,
278 #[serde(rename = "T")]
279 pub trade_time: u64,
280 #[serde(rename = "th")]
282 pub tx_hash: String,
283 #[serde(rename = "ua")]
284 pub user_address: String,
285 #[serde(rename = "oi")]
286 pub order_id: u64,
287 #[serde(rename = "ti")]
288 pub trade_id: u64,
289}
290
291#[derive(Serialize, Deserialize, Clone, Debug)]
293pub struct MarkPriceMessage {
294 #[serde(rename = "e")]
295 pub event_type: String,
296 #[serde(rename = "E")]
297 pub event_time: u64,
298 #[serde(rename = "s")]
299 pub symbol: String,
300 #[serde(rename = "p")]
301 pub mark_price: String,
302 #[serde(rename = "i")]
303 pub index_price: String,
304 #[serde(rename = "P", skip_serializing_if = "Option::is_none")]
305 pub estimated_settle_price: Option<String>,
306 #[serde(rename = "r")]
307 pub funding_rate: String,
308 #[serde(rename = "T", skip_serializing_if = "Option::is_none")]
309 pub next_funding_time: Option<u64>,
310 #[serde(rename = "th", skip_serializing_if = "Option::is_none")]
311 pub tx_hash: Option<String>,
312}
313
314#[derive(Serialize, Deserialize, Clone, Debug)]
316pub struct OrderUpdateMessage {
317 #[serde(rename = "e")]
318 pub event_type: String,
319 #[serde(rename = "E")]
320 pub event_time: u64,
321 #[serde(rename = "o")]
322 pub order: OrderUpdateData,
323}
324
325#[derive(Serialize, Deserialize, Clone, Debug)]
327pub struct OrderUpdateCommon {
328 #[serde(rename = "s")]
329 pub symbol: String,
330 #[serde(rename = "i")]
331 pub order_id: u64,
332 #[serde(rename = "co", skip_serializing_if = "Option::is_none")]
333 pub client_order_id: Option<ClientOrderId>,
334 #[serde(rename = "X")]
335 pub status: String,
336 #[serde(rename = "x")]
337 pub execution_type: String,
338 #[serde(rename = "T")]
339 pub transaction_time: u64,
340 #[serde(rename = "th")]
341 pub tx_hash: String,
342 #[serde(rename = "ua", default, skip_serializing_if = "Option::is_none")]
346 pub user_address: Option<String>,
347}
348
349#[derive(Serialize, Deserialize, Clone, Debug)]
351pub struct PlaceOrderData {
352 #[serde(flatten)]
353 pub common: OrderUpdateCommon,
354 #[serde(rename = "S")]
355 pub side: String,
356 #[serde(rename = "o")]
357 pub order_type: String,
358 #[serde(rename = "f")]
359 pub time_in_force: String,
360 #[serde(rename = "p")]
361 pub price: String,
362 #[serde(rename = "q")]
363 pub quantity: String,
364}
365
366#[derive(Serialize, Deserialize, Clone, Debug)]
368pub struct CancelOrderData {
369 #[serde(flatten)]
370 pub common: OrderUpdateCommon,
371}
372
373#[derive(Serialize, Deserialize, Clone, Debug)]
380pub struct TradeFillData {
381 #[serde(flatten)]
382 pub common: OrderUpdateCommon,
383 #[serde(rename = "S")]
384 pub side: String,
385 #[serde(rename = "p", default, skip_serializing_if = "Option::is_none")]
386 pub price: Option<String>,
387 #[serde(rename = "ap", default, skip_serializing_if = "Option::is_none")]
389 pub avg_price: Option<String>,
390 #[serde(rename = "q", default, skip_serializing_if = "Option::is_none")]
391 pub quantity: Option<String>,
392 #[serde(rename = "l")]
393 pub last_filled_qty: String,
394 #[serde(rename = "L")]
395 pub last_filled_price: String,
396 #[serde(rename = "n")]
397 pub commission: String,
398 #[serde(rename = "N")]
400 pub commission_asset: String,
401 #[serde(rename = "m")]
403 pub is_maker: bool,
404 #[serde(rename = "t")]
406 pub trade_id: u64,
407 #[serde(rename = "rp")]
409 pub realized_pnl: String,
410 #[serde(rename = "ft", default, skip_serializing_if = "Option::is_none")]
413 pub fill_type: Option<String>,
414 #[serde(rename = "z", default, skip_serializing_if = "Option::is_none")]
417 pub cumulative_filled_size: Option<String>,
418 #[serde(rename = "Z", default, skip_serializing_if = "Option::is_none")]
420 pub cumulative_filled_cot: Option<String>,
421 #[serde(rename = "rs", default, skip_serializing_if = "Option::is_none")]
424 pub remaining_size: Option<String>,
425}
426
427#[derive(Serialize, Deserialize, Clone, Debug)]
429#[serde(untagged)]
430pub enum OrderUpdateData {
431 TradeFill(Box<TradeFillData>),
434 PlaceOrder(PlaceOrderData),
435 Cancel(CancelOrderData),
436}
437
438#[cfg(test)]
439mod wire_format_tests {
440 use super::*;
445
446 #[test]
447 fn subscribe_ok_round_trips_wire_payload() {
448 let wire = r#"{"e":"subscribe","id":1,"E":1779600272876932,"result":"success"}"#;
449 let parsed: MethodResult = serde_json::from_str(wire).expect("MethodResult deserializes");
450 assert_eq!(parsed.id, Some(RequestId::from(1)));
451 assert_eq!(parsed.event_time, 1779600272876932);
452 assert_eq!(parsed.result, "success");
453 }
454
455 #[test]
456 fn list_subscriptions_round_trips_wire_payload() {
457 let wire = r#"{"e":"list_subscriptions","id":2,"E":1779600273218722,"result":["ETH-USD@aggTrade","BTC-USD@bookTicker"]}"#;
458 let parsed: ListSubscriptionsMessage =
459 serde_json::from_str(wire).expect("ListSubscriptionsMessage deserializes");
460 assert_eq!(parsed.id, Some(RequestId::from(2)));
461 assert_eq!(parsed.event_time, 1779600273218722);
462 assert_eq!(
463 parsed.result,
464 vec!["ETH-USD@aggTrade".to_string(), "BTC-USD@bookTicker".to_string()]
465 );
466 }
467
468 #[test]
469 fn trade_fill_round_trips_wire_payload() {
470 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"}"#;
471 let parsed: TradeFillData = serde_json::from_str(wire).expect("TradeFillData deserializes");
472 assert_eq!(parsed.common.symbol, "SOL-USD");
473 assert_eq!(parsed.common.order_id, 183696108);
474 assert_eq!(parsed.common.status, "FILLED");
475 assert_eq!(parsed.common.execution_type, "TRADE");
476 assert_eq!(parsed.common.user_address.as_deref(), Some(""));
477 assert_eq!(parsed.side, "BUY");
478 assert_eq!(parsed.avg_price.as_deref(), Some("85.96"));
479 assert_eq!(parsed.last_filled_qty, "0.1");
480 assert_eq!(parsed.last_filled_price, "85.96");
481 assert_eq!(parsed.commission, "0.00275072");
482 assert_eq!(parsed.commission_asset, "USDC");
483 assert!(!parsed.is_maker);
484 assert_eq!(parsed.trade_id, 13196983);
485 assert_eq!(parsed.realized_pnl, "-0.0156");
486 assert_eq!(parsed.fill_type.as_deref(), Some("o"));
487 assert_eq!(parsed.cumulative_filled_size.as_deref(), Some("0.1"));
488 assert_eq!(parsed.cumulative_filled_cot.as_deref(), Some("8.596"));
489 assert_eq!(parsed.remaining_size.as_deref(), Some("0"));
490 }
491
492 #[test]
493 fn agg_trade_round_trips_wire_payload() {
494 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"}"#;
500 let parsed: AggTradeMessage =
501 serde_json::from_str(wire).expect("AggTradeMessage deserializes");
502 assert_eq!(parsed.symbol, "SOL-USD");
504 assert_eq!(parsed.price, "85.65");
505 assert_eq!(parsed.quantity, "0.01");
506 assert!(parsed.is_buyer_maker);
507 assert_eq!(parsed.tx_hash, "0x94764a");
508 assert_eq!(parsed.user_address.as_deref(), Some(""));
512 assert_eq!(parsed.order_id, Some(0));
513 assert_eq!(parsed.is_maker, Some(false));
514 assert_eq!(parsed.is_full_fill, Some(false));
515 assert_eq!(parsed.fill_type.as_deref(), Some(""));
516 assert_eq!(parsed.cumulative_filled_size.as_deref(), Some("0"));
517 assert_eq!(parsed.remaining_size.as_deref(), Some("0"));
518 }
519
520 #[test]
521 fn trade_fill_in_order_update_data() {
522 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"}"#;
523 let parsed: OrderUpdateData =
524 serde_json::from_str(wire).expect("untagged variant resolves to TradeFill");
525 match parsed {
526 OrderUpdateData::TradeFill(_) => {}
527 other => panic!("expected TradeFill, got {other:?}"),
528 }
529 }
530}