Skip to main content

bullet_rust_sdk/ws/
models.rs

1//! WebSocket message models for the Trading SDK.
2//!
3//! Re-exports server-message types from `bullet-ws-interface`, and stitches
4//! them into the `TaggedMessage` / `ServerMessage` enums used by the SDK's
5//! WS client. As of `bullet-ws-interface = "0.2"`, response envelope types
6//! (`MethodResult`, `ListSubscriptionsMessage`, `OrderResultMessage`) all
7//! derive both `Serialize` and `Deserialize` upstream, so the local
8//! deserialization-only duplicates of the past are no longer needed.
9//!
10//! IMPORTANT: When new message types are added to the server, they must be
11//! manually added to the `ServerMessage` enum below.
12
13pub 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/// Tagged messages from the server (have an "e" event type field)
24#[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    // Order RPC acks. The four `e` tags are dotted, not snake_case, so we
35    // override the default rename. Aliases match the WS spec's
36    // case-insensitive `ORDER.PLACE` form just like client.rs does.
37    #[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/// All possible server messages.
53///
54/// Uses untagged deserialization - serde tries each variant in order until one matches.
55/// The `Unknown` variant captures any message that doesn't match known types.
56#[derive(Serialize, Deserialize, Clone, Debug, strum::AsRefStr)]
57#[strum(serialize_all = "camelCase")]
58#[serde(untagged)]
59pub enum ServerMessage {
60    // Tagged messages with "e" field - try these first
61    Tagged(TaggedMessage),
62
63    // Binance-style messages (identified by "e" event type field)
64    DepthUpdate(DepthUpdate),
65    AggTrade(AggTradeMessage),
66    BookTicker(BookTickerMessage),
67    MarkPrice(MarkPriceMessage),
68    ForceOrder(ForceOrderMessage),
69    OrderUpdate(OrderUpdateMessage),
70
71    // Untagged error response (e.g., order errors without "e" field)
72    Error(ErrorMessage),
73
74    /// Failed to parse message - contains (error message, raw text)
75    #[serde(skip)]
76    Unknown(String, String),
77}
78
79impl ServerMessage {
80    /// Returns true if this is an error message
81    pub fn is_error(&self) -> bool {
82        matches!(self, ServerMessage::Tagged(TaggedMessage::Error(_)) | ServerMessage::Error(_))
83    }
84
85    /// Returns the request ID if present
86    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        // Order errors come without the "e" tag
328        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        // The spec accepts `order.amend`, `order.modify`, and `ORDER.AMEND`.
421        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            // client_order_ids is optional — absent in payload means empty Vec.
453            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        // Each SCREAMING alias must map to the specific expected variant, not just any order
478        // variant.
479        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        // Server may omit `id` on unsolicited pushes; request_id() must return None.
509        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        // Status messages must NOT be misclassified as an order result.
528        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}