Skip to main content

bullet_ws_interface/
response.rs

1//! Server response types for order RPC acks (`order.place`, `order.cancel`,
2//! `order.amend`, `order.cancelAll`).
3
4use serde::{Deserialize, Serialize};
5
6use crate::RequestId;
7
8/// Transaction status returned by the sequencer in an order RPC ack.
9/// See <https://tradingapi.bullet.xyz/docs/ws/index.html#request-response>.
10#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum TxStatus {
13    /// Executed by the sequencer — `order_ids` is populated.
14    Processed,
15    /// Accepted, will be processed in a subsequent block.
16    Published,
17    /// Received by the sequencer but not yet published.
18    Submitted,
19    /// Finalized on-chain.
20    Finalized,
21    /// Dropped (expired uniqueness, duplicate generation value).
22    Dropped,
23    /// Status could not be determined, or an unrecognized value was received.
24    #[serde(other)]
25    Unknown,
26}
27
28impl TxStatus {
29    /// Returns `true` for statuses that mean the order is (or will be) live on
30    /// the book. `Dropped` and `Unknown` are non-success — the caller should
31    /// treat the operation as failed and reconcile.
32    #[must_use]
33    pub fn is_success(self) -> bool {
34        matches!(self, Self::Processed | Self::Published | Self::Submitted | Self::Finalized)
35    }
36}
37
38/// Inner `results` payload for a successful order RPC ack.
39#[derive(Serialize, Deserialize, Clone, Debug)]
40pub struct OrderResultPayload {
41    /// Transaction hash.
42    pub tx_id: String,
43    /// Sequencer status — see [`TxStatus`].
44    pub status: TxStatus,
45    /// Order IDs affected by this transaction. Populated when status is
46    /// `processed`; may be empty otherwise.
47    #[serde(default)]
48    pub order_ids: Vec<u64>,
49    /// Client order IDs corresponding to `order_ids`, in matching positions.
50    #[serde(default)]
51    pub client_order_ids: Vec<u64>,
52}
53
54/// Result message for `order.place` / `order.cancel` / `order.amend` /
55/// `order.cancelAll` RPCs. Correlate to the originating request via [`id`].
56#[derive(Serialize, Deserialize, Clone, Debug)]
57pub struct OrderResultMessage {
58    pub id: Option<RequestId>,
59    /// Event time (µs).
60    #[serde(rename = "E")]
61    pub event_time: u64,
62    pub results: OrderResultPayload,
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn round_trip_full_payload() {
71        let json = r#"{
72            "id": 42,
73            "E": 1706745600000000,
74            "results": {
75                "tx_id": "0xabc123",
76                "status": "processed",
77                "order_ids": [1, 2],
78                "client_order_ids": [100, 101]
79            }
80        }"#;
81        let msg: OrderResultMessage = serde_json::from_str(json).unwrap();
82        assert_eq!(msg.id, Some(RequestId::from(42u64)));
83        assert_eq!(msg.event_time, 1_706_745_600_000_000);
84        assert_eq!(msg.results.tx_id, "0xabc123");
85        assert_eq!(msg.results.status, TxStatus::Processed);
86        assert_eq!(msg.results.order_ids, vec![1, 2]);
87        assert_eq!(msg.results.client_order_ids, vec![100, 101]);
88    }
89
90    #[test]
91    fn id_absent_defaults_to_none() {
92        // Server may omit `id` for unsolicited pushes.
93        let json = r#"{"E":1706745600000000,"results":{"tx_id":"0x1","status":"submitted"}}"#;
94        let msg: OrderResultMessage = serde_json::from_str(json).unwrap();
95        assert_eq!(msg.id, None);
96    }
97
98    #[test]
99    fn optional_vec_fields_absent() {
100        // cancelAll acks commonly omit client_order_ids.
101        let json = r#"{"id":1,"E":1706745600000000,"results":{"tx_id":"0x2","status":"processed","order_ids":[5]}}"#;
102        let msg: OrderResultMessage = serde_json::from_str(json).unwrap();
103        assert!(msg.results.client_order_ids.is_empty());
104    }
105
106    #[test]
107    fn unknown_status_is_catch_all() {
108        // Any unrecognized status string should deserialize to Unknown rather
109        // than failing — forward-compat for new sequencer statuses.
110        let json = r#"{"id":1,"E":0,"results":{"tx_id":"0x1","status":"pending"}}"#;
111        let msg: OrderResultMessage = serde_json::from_str(json).unwrap();
112        assert_eq!(msg.results.status, TxStatus::Unknown);
113        assert!(!msg.results.status.is_success());
114    }
115
116    #[test]
117    fn tx_status_is_success() {
118        assert!(TxStatus::Processed.is_success());
119        assert!(TxStatus::Published.is_success());
120        assert!(TxStatus::Submitted.is_success());
121        assert!(TxStatus::Finalized.is_success());
122        assert!(!TxStatus::Dropped.is_success());
123        assert!(!TxStatus::Unknown.is_success());
124    }
125}