nautilus_hyperliquid/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use serde::{Deserialize, Serialize};
17
18use crate::common::enums::HyperliquidSide;
19
20/// Represents metadata about available markets from `POST /info`.
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct HyperliquidMeta {
23    #[serde(default)]
24    pub universe: Vec<HyperliquidAssetInfo>,
25}
26
27/// Represents asset information from the meta endpoint.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct HyperliquidAssetInfo {
30    /// Asset name (e.g., "BTC").
31    pub name: String,
32    /// Number of decimal places for size.
33    #[serde(rename = "szDecimals")]
34    pub sz_decimals: u32,
35}
36
37/// Represents an L2 order book snapshot from `POST /info`.
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct HyperliquidL2Book {
40    /// Coin symbol.
41    pub coin: String,
42    /// Order book levels: [bids, asks].
43    pub levels: Vec<Vec<HyperliquidLevel>>,
44    /// Timestamp in milliseconds.
45    pub time: u64,
46}
47
48/// Represents an order book level with price and size.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct HyperliquidLevel {
51    /// Price level.
52    pub px: String,
53    /// Size at this level.
54    pub sz: String,
55}
56
57/// Represents user fills response from `POST /info`.
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct HyperliquidFills {
60    #[serde(default)]
61    pub fills: Vec<HyperliquidFill>,
62}
63
64/// Represents an individual fill from user fills.
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct HyperliquidFill {
67    /// Coin symbol.
68    pub coin: String,
69    /// Fill price.
70    pub px: String,
71    /// Fill size.
72    pub sz: String,
73    /// Order side (buy/sell).
74    pub side: HyperliquidSide,
75    /// Fill timestamp in milliseconds.
76    pub time: u64,
77    /// Position size before this fill.
78    #[serde(rename = "startPosition")]
79    pub start_position: String,
80    /// Directory (order book path).
81    pub dir: String,
82    /// Closed P&L from this fill.
83    #[serde(rename = "closedPnl")]
84    pub closed_pnl: String,
85    /// Hash reference.
86    pub hash: String,
87    /// Order ID that generated this fill.
88    pub oid: u64,
89    /// Crossed status.
90    pub crossed: bool,
91    /// Fee paid for this fill.
92    pub fee: String,
93}
94
95/// Represents order status response from `POST /info`.
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct HyperliquidOrderStatus {
98    #[serde(default)]
99    pub statuses: Vec<HyperliquidOrderStatusEntry>,
100}
101
102/// Represents an individual order status entry.
103#[derive(Debug, Clone, Serialize, Deserialize)]
104pub struct HyperliquidOrderStatusEntry {
105    /// Order information.
106    pub order: HyperliquidOrderInfo,
107    /// Current status string.
108    pub status: String,
109    /// Status timestamp in milliseconds.
110    #[serde(rename = "statusTimestamp")]
111    pub status_timestamp: u64,
112}
113
114/// Represents order information within an order status entry.
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct HyperliquidOrderInfo {
117    /// Coin symbol.
118    pub coin: String,
119    /// Order side (buy/sell).
120    pub side: HyperliquidSide,
121    /// Limit price.
122    #[serde(rename = "limitPx")]
123    pub limit_px: String,
124    /// Order size.
125    pub sz: String,
126    /// Order ID.
127    pub oid: u64,
128    /// Order timestamp in milliseconds.
129    pub timestamp: u64,
130    /// Original order size.
131    #[serde(rename = "origSz")]
132    pub orig_sz: String,
133}
134
135/// Represents an exchange action request wrapper for `POST /exchange`.
136#[derive(Debug, Clone, Serialize)]
137pub struct HyperliquidExchangeRequest<T> {
138    /// The action to perform.
139    pub action: T,
140    /// Request nonce for replay protection.
141    #[serde(rename = "nonce")]
142    pub nonce: u64,
143    /// ECC signature over the action.
144    #[serde(rename = "signature")]
145    pub signature: String,
146    /// Optional vault address for sub-account trading.
147    #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
148    pub vault_address: Option<String>,
149}
150
151impl<T> HyperliquidExchangeRequest<T>
152where
153    T: Serialize,
154{
155    /// Create a new exchange request with the given action.
156    pub fn new(action: T, nonce: u64, signature: String) -> Self {
157        Self {
158            action,
159            nonce,
160            signature,
161            vault_address: None,
162        }
163    }
164
165    /// Create a new exchange request with vault address for sub-account trading.
166    pub fn with_vault(action: T, nonce: u64, signature: String, vault_address: String) -> Self {
167        Self {
168            action,
169            nonce,
170            signature,
171            vault_address: Some(vault_address),
172        }
173    }
174
175    /// Convert to JSON value for signing purposes.
176    pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
177        serde_json::to_value(self)
178    }
179}
180
181/// Represents an exchange response wrapper from `POST /exchange`.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(untagged)]
184pub enum HyperliquidExchangeResponse {
185    /// Successful response with status.
186    Status {
187        /// Status message.
188        status: String,
189        /// Response payload.
190        response: serde_json::Value,
191    },
192    /// Error response.
193    Error {
194        /// Error message.
195        error: String,
196    },
197}
198
199////////////////////////////////////////////////////////////////////////////////
200// Tests
201////////////////////////////////////////////////////////////////////////////////
202
203#[cfg(test)]
204mod tests {
205    use rstest::rstest;
206
207    use super::*;
208
209    #[rstest]
210    fn test_meta_deserialization() {
211        let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
212
213        let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
214
215        assert_eq!(meta.universe.len(), 1);
216        assert_eq!(meta.universe[0].name, "BTC");
217        assert_eq!(meta.universe[0].sz_decimals, 5);
218    }
219
220    #[rstest]
221    fn test_l2_book_deserialization() {
222        let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
223
224        let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
225
226        assert_eq!(book.coin, "BTC");
227        assert_eq!(book.levels.len(), 2);
228        assert_eq!(book.time, 1234567890);
229    }
230
231    #[rstest]
232    fn test_exchange_response_deserialization() {
233        let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
234
235        let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
236
237        match response {
238            HyperliquidExchangeResponse::Status { status, .. } => assert_eq!(status, "ok"),
239            _ => panic!("Expected status response"),
240        }
241    }
242}