1pub use bullet_ws_interface::{
14 ListSubscriptionsMessage, MethodResult, OrderResultMessage, OrderResultPayload, TxStatus,
15};
16use serde::{Deserialize, Serialize};
17
18use crate::types::{
19 AggTradeMessage, BookTickerMessage, DepthUpdate, ErrorMessage, ForceOrderMessage,
20 MarkPriceMessage, OrderUpdateMessage, PongMessage, RequestId, StatusMessage,
21};
22
23#[derive(Serialize, Deserialize, Clone, Debug, strum::AsRefStr)]
25#[strum(serialize_all = "camelCase")]
26#[serde(tag = "e", rename_all = "snake_case")]
27pub enum TaggedMessage {
28 Status(StatusMessage),
29 Pong(PongMessage),
30 Error(ErrorMessage),
31 Subscribe(MethodResult),
32 Unsubscribe(MethodResult),
33 ListSubscriptions(ListSubscriptionsMessage),
34 #[serde(rename = "order.place", alias = "ORDER.PLACE")]
38 OrderPlace(OrderResultMessage),
39 #[serde(rename = "order.cancel", alias = "ORDER.CANCEL")]
40 OrderCancel(OrderResultMessage),
41 #[serde(
42 rename = "order.amend",
43 alias = "ORDER.AMEND",
44 alias = "order.modify",
45 alias = "ORDER.MODIFY"
46 )]
47 OrderAmend(OrderResultMessage),
48 #[serde(rename = "order.cancelAll", alias = "ORDER.CANCEL_ALL")]
49 OrderCancelAll(OrderResultMessage),
50}
51
52#[derive(Serialize, Deserialize, Clone, Debug, strum::AsRefStr)]
57#[strum(serialize_all = "camelCase")]
58#[serde(untagged)]
59pub enum ServerMessage {
60 Tagged(TaggedMessage),
62
63 DepthUpdate(DepthUpdate),
65 AggTrade(AggTradeMessage),
66 BookTicker(BookTickerMessage),
67 MarkPrice(MarkPriceMessage),
68 ForceOrder(ForceOrderMessage),
69 OrderUpdate(OrderUpdateMessage),
70
71 Error(ErrorMessage),
73
74 #[serde(skip)]
76 Unknown(String, String),
77}
78
79impl ServerMessage {
80 pub fn is_error(&self) -> bool {
82 matches!(self, ServerMessage::Tagged(TaggedMessage::Error(_)) | ServerMessage::Error(_))
83 }
84
85 pub fn request_id(&self) -> Option<RequestId> {
87 match self {
88 ServerMessage::Tagged(msg) => match msg {
89 TaggedMessage::Pong(m) => m.id,
90 TaggedMessage::Error(m) => m.id,
91 TaggedMessage::Subscribe(m) => m.id,
92 TaggedMessage::Unsubscribe(m) => m.id,
93 TaggedMessage::ListSubscriptions(m) => m.id,
94 TaggedMessage::OrderPlace(m)
95 | TaggedMessage::OrderCancel(m)
96 | TaggedMessage::OrderAmend(m)
97 | TaggedMessage::OrderCancelAll(m) => m.id,
98 _ => None,
99 },
100 ServerMessage::Error(m) => m.id,
101 _ => None,
102 }
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn test_depth_update() {
112 let json = r#"{
113 "e": "depthUpdate",
114 "E": 1234567890,
115 "T": 1234567890,
116 "s": "BTCUSDT",
117 "U": 100,
118 "u": 200,
119 "pu": 99,
120 "b": [["50000.00", "1.5"]],
121 "a": [["50001.00", "2.0"]],
122 "mt": "s"
123 }"#;
124
125 let msg: ServerMessage = serde_json::from_str(json).unwrap();
126 assert!(matches!(msg, ServerMessage::DepthUpdate(_)));
127
128 if let ServerMessage::DepthUpdate(d) = msg {
129 assert_eq!(d.symbol, "BTCUSDT");
130 assert_eq!(d.bids.len(), 1);
131 assert_eq!(d.asks.len(), 1);
132 }
133 }
134
135 #[test]
136 fn test_agg_trade() {
137 let json = r#"{
138 "e": "aggTrade",
139 "E": 1234567890,
140 "s": "BTCUSDT",
141 "a": 12345,
142 "p": "50000.00",
143 "q": "1.5",
144 "f": 100,
145 "l": 105,
146 "T": 1234567890,
147 "m": true,
148 "th": "0xabc123",
149 "ua": "0xdef456",
150 "oi": 999,
151 "mk": true,
152 "ff": false,
153 "lq": false,
154 "fe": "0.001",
155 "nf": "0.001",
156 "fa": "USDT",
157 "sd": "BUY"
158 }"#;
159
160 let msg: ServerMessage = serde_json::from_str(json).unwrap();
161 assert!(matches!(msg, ServerMessage::AggTrade(_)));
162
163 if let ServerMessage::AggTrade(t) = msg {
164 assert_eq!(t.symbol, "BTCUSDT");
165 assert_eq!(t.price, "50000.00");
166 assert!(t.is_buyer_maker);
167 }
168 }
169
170 #[test]
171 fn test_book_ticker() {
172 let json = r#"{
173 "e": "bookTicker",
174 "u": 12345,
175 "E": 1234567890,
176 "T": 1234567890,
177 "s": "ETHUSDT",
178 "b": "3000.00",
179 "B": "10.5",
180 "a": "3001.00",
181 "A": "8.2",
182 "mt": "u"
183 }"#;
184
185 let msg: ServerMessage = serde_json::from_str(json).unwrap();
186 assert!(matches!(msg, ServerMessage::BookTicker(_)));
187
188 if let ServerMessage::BookTicker(b) = msg {
189 assert_eq!(b.symbol, "ETHUSDT");
190 assert_eq!(b.best_bid_price, "3000.00");
191 assert_eq!(b.best_ask_price, "3001.00");
192 }
193 }
194
195 #[test]
196 fn test_mark_price() {
197 let json = r#"{
198 "e": "markPriceUpdate",
199 "E": 1234567890,
200 "s": "BTCUSDT",
201 "p": "50000.00",
202 "i": "49999.00",
203 "r": "0.0001"
204 }"#;
205
206 let msg: ServerMessage = serde_json::from_str(json).unwrap();
207 assert!(matches!(msg, ServerMessage::MarkPrice(_)));
208
209 if let ServerMessage::MarkPrice(m) = msg {
210 assert_eq!(m.symbol, "BTCUSDT");
211 assert_eq!(m.mark_price, "50000.00");
212 assert_eq!(m.funding_rate, "0.0001");
213 }
214 }
215
216 #[test]
217 fn test_force_order() {
218 let json = r#"{
219 "e": "liquidation",
220 "E": 1234567890,
221 "o": {
222 "s": "BTCUSDT",
223 "S": "SELL",
224 "o": "LIMIT",
225 "f": "IOC",
226 "p": "49000.00",
227 "ap": "49000.00",
228 "X": "FILLED",
229 "l": "1.0",
230 "T": 1234567890,
231 "th": "0xabc",
232 "ua": "0xdef",
233 "oi": 123,
234 "ti": 456
235 }
236 }"#;
237
238 let msg: ServerMessage = serde_json::from_str(json).unwrap();
239 assert!(matches!(msg, ServerMessage::ForceOrder(_)));
240
241 if let ServerMessage::ForceOrder(f) = msg {
242 assert_eq!(f.order.symbol, "BTCUSDT");
243 assert_eq!(f.order.side, "SELL");
244 }
245 }
246
247 #[test]
248 fn test_order_update() {
249 let json = r#"{
250 "e": "orderTradeUpdate",
251 "E": 1234567890,
252 "o": {
253 "s": "BTCUSDT",
254 "i": 12345,
255 "X": "NEW",
256 "x": "NEW",
257 "T": 1234567890,
258 "th": "0xabc",
259 "ua": "0xdef",
260 "S": "BUY",
261 "o": "LIMIT",
262 "f": "GTC",
263 "p": "50000.00",
264 "q": "1.0"
265 }
266 }"#;
267
268 let msg: ServerMessage = serde_json::from_str(json).unwrap();
269 assert!(matches!(msg, ServerMessage::OrderUpdate(_)));
270
271 if let ServerMessage::OrderUpdate(o) = msg {
272 assert_eq!(o.event_time, 1234567890);
273 }
274 }
275
276 #[test]
277 fn test_status_message() {
278 let json = r#"{
279 "e": "status",
280 "E": 1234567890,
281 "status": "connected",
282 "clientId": "client-123"
283 }"#;
284
285 let msg: ServerMessage = serde_json::from_str(json).unwrap();
286 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::Status(_))));
287
288 if let ServerMessage::Tagged(TaggedMessage::Status(s)) = msg {
289 assert_eq!(s.status, "connected");
290 assert_eq!(s.client_id, "client-123");
291 assert_eq!(s.event_time, 1234567890);
292 }
293 }
294
295 #[test]
296 fn test_pong_message() {
297 let json = r#"{
298 "e": "pong",
299 "id": 42,
300 "E": 1234567890
301 }"#;
302
303 let msg: ServerMessage = serde_json::from_str(json).unwrap();
304 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::Pong(_))));
305 assert_eq!(msg.request_id(), Some(RequestId::from(42)));
306 }
307
308 #[test]
309 fn test_error_message() {
310 let json = r#"{
311 "e": "error",
312 "id": 1,
313 "E": 1234567890,
314 "error": {
315 "code": -1004,
316 "msg": "Invalid subscription format"
317 }
318 }"#;
319
320 let msg: ServerMessage = serde_json::from_str(json).unwrap();
321 assert!(msg.is_error(), "Expected error message, got: {msg:?}");
322 assert_eq!(msg.request_id(), Some(RequestId::from(1)));
323 }
324
325 #[test]
326 fn test_order_error() {
327 let json = r#"{
329 "id": 2,
330 "E": 1234567890,
331 "error": {
332 "code": -2010,
333 "msg": "Transaction execution unsuccessful"
334 }
335 }"#;
336
337 let msg: ServerMessage = serde_json::from_str(json).unwrap();
338 assert!(msg.is_error(), "Expected error message, got: {msg:?}");
339 assert_eq!(msg.request_id(), Some(RequestId::from(2)));
340 assert!(matches!(msg, ServerMessage::Error(_)));
341 }
342
343 #[test]
344 fn test_subscribe_success() {
345 let json = r#"{
346 "e": "subscribe",
347 "id": 5,
348 "E": 1234567890,
349 "result": "success"
350 }"#;
351
352 let msg: ServerMessage = serde_json::from_str(json).unwrap();
353 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::Subscribe(_))));
354 assert_eq!(msg.request_id(), Some(RequestId::from(5)));
355
356 if let ServerMessage::Tagged(TaggedMessage::Subscribe(s)) = msg {
357 assert_eq!(s.result, "success");
358 }
359 }
360
361 #[test]
362 fn test_unsubscribe_success() {
363 let json = r#"{
364 "e": "unsubscribe",
365 "id": 6,
366 "E": 1234567890,
367 "result": "success"
368 }"#;
369
370 let msg: ServerMessage = serde_json::from_str(json).unwrap();
371 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::Unsubscribe(_))));
372 assert_eq!(msg.request_id(), Some(RequestId::from(6)));
373 }
374
375 #[test]
376 fn test_list_subscriptions() {
377 let json = r#"{
378 "e": "list_subscriptions",
379 "id": 7,
380 "E": 1234567890,
381 "result": ["btcusdt@depth10", "ethusdt@aggTrade"]
382 }"#;
383
384 let msg: ServerMessage = serde_json::from_str(json).unwrap();
385 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::ListSubscriptions(_))));
386
387 if let ServerMessage::Tagged(TaggedMessage::ListSubscriptions(l)) = msg {
388 assert_eq!(l.result.len(), 2);
389 assert_eq!(l.result[0], "btcusdt@depth10");
390 }
391 }
392
393 #[test]
394 fn test_order_place_result() {
395 let json = r#"{
396 "e": "order.place",
397 "id": 10,
398 "E": 1706745600000000,
399 "results": {
400 "tx_id": "0xabc",
401 "status": "processed",
402 "order_ids": [1, 2],
403 "client_order_ids": [100, 101]
404 }
405 }"#;
406
407 let msg: ServerMessage = serde_json::from_str(json).unwrap();
408 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::OrderPlace(_))));
409 assert_eq!(msg.request_id(), Some(RequestId::from(10)));
410 if let ServerMessage::Tagged(TaggedMessage::OrderPlace(r)) = msg {
411 assert_eq!(r.results.tx_id, "0xabc");
412 assert_eq!(r.results.status, TxStatus::Processed);
413 assert_eq!(r.results.order_ids, vec![1, 2]);
414 assert_eq!(r.results.client_order_ids, vec![100, 101]);
415 }
416 }
417
418 #[test]
419 fn test_order_amend_with_alias() {
420 for tag in ["order.amend", "order.modify"] {
422 let json = format!(
423 r#"{{"e":"{tag}","id":11,"E":1706745600000000,
424 "results":{{"tx_id":"0xdef","status":"published"}}}}"#
425 );
426 let msg: ServerMessage = serde_json::from_str(&json).unwrap();
427 assert!(
428 matches!(msg, ServerMessage::Tagged(TaggedMessage::OrderAmend(_))),
429 "tag={tag}"
430 );
431 }
432 }
433
434 #[test]
435 fn test_order_cancel_all_result() {
436 let json = r#"{
437 "e": "order.cancelAll",
438 "id": 12,
439 "E": 1706745600000000,
440 "results": {
441 "tx_id": "0xghi",
442 "status": "processed",
443 "order_ids": [1, 2, 3]
444 }
445 }"#;
446
447 let msg: ServerMessage = serde_json::from_str(json).unwrap();
448 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::OrderCancelAll(_))));
449 assert_eq!(msg.request_id(), Some(RequestId::from(12)));
450 if let ServerMessage::Tagged(TaggedMessage::OrderCancelAll(r)) = msg {
451 assert_eq!(r.results.order_ids.len(), 3);
452 assert!(r.results.client_order_ids.is_empty());
454 }
455 }
456
457 #[test]
458 fn test_order_cancel_result() {
459 let json = r#"{
460 "e": "order.cancel",
461 "id": 20,
462 "E": 1706745600000000,
463 "results": {
464 "tx_id": "0xcancel",
465 "status": "processed",
466 "order_ids": [5],
467 "client_order_ids": [50]
468 }
469 }"#;
470 let msg: ServerMessage = serde_json::from_str(json).unwrap();
471 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::OrderCancel(_))));
472 assert_eq!(msg.request_id(), Some(RequestId::from(20)));
473 }
474
475 #[test]
476 fn test_screaming_snake_aliases() {
477 let cases: &[(&str, &str)] = &[
480 ("ORDER.PLACE", "OrderPlace"),
481 ("ORDER.CANCEL", "OrderCancel"),
482 ("ORDER.AMEND", "OrderAmend"),
483 ("ORDER.MODIFY", "OrderAmend"),
484 ("ORDER.CANCEL_ALL", "OrderCancelAll"),
485 ];
486 for (tag, expected) in cases {
487 let json = format!(
488 r#"{{"e":"{tag}","id":1,"E":1706745600000000,"results":{{"tx_id":"0x1","status":"processed"}}}}"#
489 );
490 let msg: ServerMessage = serde_json::from_str(&json)
491 .unwrap_or_else(|e| panic!("failed to parse '{tag}': {e}"));
492 let ServerMessage::Tagged(ref inner) = msg else {
493 panic!("'{tag}' did not parse as Tagged");
494 };
495 let actual = match inner {
496 TaggedMessage::OrderPlace(_) => "OrderPlace",
497 TaggedMessage::OrderCancel(_) => "OrderCancel",
498 TaggedMessage::OrderAmend(_) => "OrderAmend",
499 TaggedMessage::OrderCancelAll(_) => "OrderCancelAll",
500 other => panic!("'{tag}' mapped to unexpected variant: {}", other.as_ref()),
501 };
502 assert_eq!(actual, *expected, "'{tag}' mapped to wrong variant");
503 }
504 }
505
506 #[test]
507 fn test_order_result_id_absent() {
508 let json = r#"{"e":"order.place","E":1706745600000000,"results":{"tx_id":"0x1","status":"processed"}}"#;
510 let msg: ServerMessage = serde_json::from_str(json).unwrap();
511 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::OrderPlace(_))));
512 assert_eq!(msg.request_id(), None);
513 }
514
515 #[test]
516 fn test_tx_status_is_success() {
517 assert!(TxStatus::Processed.is_success());
518 assert!(TxStatus::Published.is_success());
519 assert!(TxStatus::Submitted.is_success());
520 assert!(TxStatus::Finalized.is_success());
521 assert!(!TxStatus::Dropped.is_success());
522 assert!(!TxStatus::Unknown.is_success());
523 }
524
525 #[test]
526 fn test_order_result_does_not_steal_other_tagged() {
527 let json = r#"{"e":"status","E":1234567890,"status":"connected","clientId":"x"}"#;
529 let msg: ServerMessage = serde_json::from_str(json).unwrap();
530 assert!(matches!(msg, ServerMessage::Tagged(TaggedMessage::Status(_))));
531 }
532}