Skip to main content

bullet_ws_interface/
response.rs

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