Skip to main content

hl_types/
response.rs

1use std::collections::HashMap;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6use crate::order::OrderStatus;
7
8/// Response returned after placing an order.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11#[non_exhaustive]
12pub struct OrderResponse {
13    /// Exchange-assigned order identifier.
14    pub order_id: String,
15    /// Price at which the order was (partially) filled, if any.
16    pub filled_price: Option<Decimal>,
17    /// Size that was filled.
18    pub filled_size: Decimal,
19    /// Size that was originally requested.
20    pub requested_size: Decimal,
21    /// Order status.
22    pub status: OrderStatus,
23}
24
25impl OrderResponse {
26    /// Creates a new `OrderResponse`.
27    pub fn new(
28        order_id: String,
29        filled_price: Option<Decimal>,
30        filled_size: Decimal,
31        requested_size: Decimal,
32        status: OrderStatus,
33    ) -> Self {
34        Self {
35            order_id,
36            filled_price,
37            filled_size,
38            requested_size,
39            status,
40        }
41    }
42}
43
44/// Generic response from an exchange action (cancel, transfer, etc.).
45///
46/// The Hyperliquid exchange returns `{"status": "ok", "response": {...}}` for
47/// successful actions. This struct captures the top-level status and preserves
48/// the inner response payload and any extra fields.
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51#[non_exhaustive]
52pub struct HlActionResponse {
53    /// Top-level status string (typically `"ok"`).
54    pub status: String,
55    /// Inner response payload, if present.
56    #[serde(default)]
57    pub response: Option<serde_json::Value>,
58    /// Any additional top-level fields returned by the API.
59    #[serde(flatten)]
60    pub extra: HashMap<String, serde_json::Value>,
61}
62
63impl HlActionResponse {
64    /// Returns `true` if the exchange reported status `"ok"`.
65    pub fn is_ok(&self) -> bool {
66        self.status == "ok"
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    use std::str::FromStr;
74
75    #[test]
76    fn order_response_serde_roundtrip() {
77        let resp = OrderResponse {
78            order_id: "abc123".into(),
79            filled_price: Some(Decimal::from_str("50000.0").unwrap()),
80            filled_size: Decimal::from_str("0.1").unwrap(),
81            requested_size: Decimal::from_str("0.1").unwrap(),
82            status: OrderStatus::Filled,
83        };
84        let json = serde_json::to_string(&resp).unwrap();
85        let parsed: OrderResponse = serde_json::from_str(&json).unwrap();
86        assert_eq!(parsed.order_id, "abc123");
87        assert_eq!(
88            parsed.filled_price,
89            Some(Decimal::from_str("50000.0").unwrap())
90        );
91        assert_eq!(parsed.filled_size, Decimal::from_str("0.1").unwrap());
92        assert_eq!(parsed.requested_size, Decimal::from_str("0.1").unwrap());
93        assert_eq!(parsed.status, OrderStatus::Filled);
94    }
95
96    #[test]
97    fn order_response_no_fill_price_roundtrip() {
98        let resp = OrderResponse {
99            order_id: "xyz".into(),
100            filled_price: None,
101            filled_size: Decimal::ZERO,
102            requested_size: Decimal::ONE,
103            status: OrderStatus::Open,
104        };
105        let json = serde_json::to_string(&resp).unwrap();
106        let parsed: OrderResponse = serde_json::from_str(&json).unwrap();
107        assert!(parsed.filled_price.is_none());
108        assert_eq!(parsed.status, OrderStatus::Open);
109    }
110
111    #[test]
112    fn order_response_camel_case_keys() {
113        let resp = OrderResponse {
114            order_id: "x".into(),
115            filled_price: None,
116            filled_size: Decimal::ZERO,
117            requested_size: Decimal::ZERO,
118            status: OrderStatus::Open,
119        };
120        let json = serde_json::to_string(&resp).unwrap();
121        assert!(json.contains("orderId"));
122        assert!(json.contains("filledPrice"));
123        assert!(json.contains("filledSize"));
124        assert!(json.contains("requestedSize"));
125    }
126
127    #[test]
128    fn action_response_ok_roundtrip() {
129        let json = serde_json::json!({
130            "status": "ok",
131            "response": {"type": "cancel", "data": {"statuses": ["success"]}}
132        });
133        let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
134        assert_eq!(parsed.status, "ok");
135        assert!(parsed.is_ok());
136        assert!(parsed.response.is_some());
137    }
138
139    #[test]
140    fn action_response_error() {
141        let json = serde_json::json!({
142            "status": "err",
143            "response": "Order not found"
144        });
145        let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
146        assert_eq!(parsed.status, "err");
147        assert!(!parsed.is_ok());
148    }
149
150    #[test]
151    fn action_response_no_response_field() {
152        let json = serde_json::json!({ "status": "ok" });
153        let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
154        assert!(parsed.is_ok());
155        assert!(parsed.response.is_none());
156    }
157
158    #[test]
159    fn action_response_extra_fields_captured() {
160        let json = serde_json::json!({
161            "status": "ok",
162            "timestamp": 1700000000
163        });
164        let parsed: HlActionResponse = serde_json::from_value(json).unwrap();
165        assert!(parsed.extra.contains_key("timestamp"));
166    }
167
168    #[test]
169    fn action_response_serde_roundtrip() {
170        let resp = HlActionResponse {
171            status: "ok".into(),
172            response: Some(serde_json::json!({"data": "test"})),
173            extra: HashMap::new(),
174        };
175        let json = serde_json::to_string(&resp).unwrap();
176        let parsed: HlActionResponse = serde_json::from_str(&json).unwrap();
177        assert_eq!(parsed.status, "ok");
178        assert!(parsed.response.is_some());
179    }
180}