nautilus_hyperliquid/http/
query.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::Serialize;
17
18use crate::http::models::{
19    HyperliquidExecCancelByCloidRequest, HyperliquidExecModifyOrderRequest,
20    HyperliquidExecPlaceOrderRequest,
21};
22
23/// Exchange action types for Hyperliquid.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub enum ExchangeActionType {
27    /// Place orders
28    Order,
29    /// Cancel orders by order ID
30    Cancel,
31    /// Cancel orders by client order ID
32    CancelByCloid,
33    /// Modify an existing order
34    Modify,
35    /// Update leverage for an asset
36    UpdateLeverage,
37    /// Update isolated margin for an asset
38    UpdateIsolatedMargin,
39}
40
41impl AsRef<str> for ExchangeActionType {
42    fn as_ref(&self) -> &str {
43        match self {
44            Self::Order => "order",
45            Self::Cancel => "cancel",
46            Self::CancelByCloid => "cancelByCloid",
47            Self::Modify => "modify",
48            Self::UpdateLeverage => "updateLeverage",
49            Self::UpdateIsolatedMargin => "updateIsolatedMargin",
50        }
51    }
52}
53
54/// Parameters for placing orders.
55#[derive(Debug, Clone, Serialize)]
56pub struct OrderParams {
57    pub orders: Vec<HyperliquidExecPlaceOrderRequest>,
58    pub grouping: String,
59}
60
61/// Parameters for canceling orders.
62#[derive(Debug, Clone, Serialize)]
63pub struct CancelParams {
64    pub cancels: Vec<HyperliquidExecCancelByCloidRequest>,
65}
66
67/// Parameters for modifying an order.
68#[derive(Debug, Clone, Serialize)]
69pub struct ModifyParams {
70    pub oid: u64,
71    pub order: HyperliquidExecModifyOrderRequest,
72}
73
74/// Parameters for updating leverage.
75#[derive(Debug, Clone, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct UpdateLeverageParams {
78    pub asset: u32,
79    pub is_cross: bool,
80    pub leverage: u32,
81}
82
83/// Parameters for updating isolated margin.
84#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct UpdateIsolatedMarginParams {
87    pub asset: u32,
88    pub is_buy: bool,
89    pub ntli: i64,
90}
91
92/// Parameters for L2 book request.
93#[derive(Debug, Clone, Serialize)]
94pub struct L2BookParams {
95    pub coin: String,
96}
97
98/// Parameters for user fills request.
99#[derive(Debug, Clone, Serialize)]
100pub struct UserFillsParams {
101    pub user: String,
102}
103
104/// Parameters for order status request.
105#[derive(Debug, Clone, Serialize)]
106pub struct OrderStatusParams {
107    pub user: String,
108    pub oid: u64,
109}
110
111/// Parameters for open orders request.
112#[derive(Debug, Clone, Serialize)]
113pub struct OpenOrdersParams {
114    pub user: String,
115}
116
117/// Parameters for clearinghouse state request.
118#[derive(Debug, Clone, Serialize)]
119pub struct ClearinghouseStateParams {
120    pub user: String,
121}
122
123/// Parameters for candle snapshot request.
124#[derive(Debug, Clone, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub struct CandleSnapshotReq {
127    pub coin: String,
128    pub interval: String,
129    pub start_time: u64,
130    pub end_time: u64,
131}
132
133/// Wrapper for candle snapshot parameters.
134#[derive(Debug, Clone, Serialize)]
135pub struct CandleSnapshotParams {
136    pub req: CandleSnapshotReq,
137}
138
139/// Info request parameters.
140#[derive(Debug, Clone, Serialize)]
141#[serde(untagged)]
142pub enum InfoRequestParams {
143    L2Book(L2BookParams),
144    UserFills(UserFillsParams),
145    OrderStatus(OrderStatusParams),
146    OpenOrders(OpenOrdersParams),
147    ClearinghouseState(ClearinghouseStateParams),
148    CandleSnapshot(CandleSnapshotParams),
149    None,
150}
151
152/// Represents an info request wrapper for `POST /info`.
153#[derive(Debug, Clone, Serialize)]
154pub struct InfoRequest {
155    #[serde(rename = "type")]
156    pub request_type: String,
157    #[serde(flatten)]
158    pub params: InfoRequestParams,
159}
160
161impl InfoRequest {
162    /// Creates a request to get metadata about available markets.
163    pub fn meta() -> Self {
164        Self {
165            request_type: "meta".to_string(),
166            params: InfoRequestParams::None,
167        }
168    }
169
170    /// Creates a request to get spot metadata (tokens and pairs).
171    pub fn spot_meta() -> Self {
172        Self {
173            request_type: "spotMeta".to_string(),
174            params: InfoRequestParams::None,
175        }
176    }
177
178    /// Creates a request to get metadata with asset contexts (for price precision).
179    pub fn meta_and_asset_ctxs() -> Self {
180        Self {
181            request_type: "metaAndAssetCtxs".to_string(),
182            params: InfoRequestParams::None,
183        }
184    }
185
186    /// Creates a request to get spot metadata with asset contexts.
187    pub fn spot_meta_and_asset_ctxs() -> Self {
188        Self {
189            request_type: "spotMetaAndAssetCtxs".to_string(),
190            params: InfoRequestParams::None,
191        }
192    }
193
194    /// Creates a request to get L2 order book for a coin.
195    pub fn l2_book(coin: &str) -> Self {
196        Self {
197            request_type: "l2Book".to_string(),
198            params: InfoRequestParams::L2Book(L2BookParams {
199                coin: coin.to_string(),
200            }),
201        }
202    }
203
204    /// Creates a request to get user fills.
205    pub fn user_fills(user: &str) -> Self {
206        Self {
207            request_type: "userFills".to_string(),
208            params: InfoRequestParams::UserFills(UserFillsParams {
209                user: user.to_string(),
210            }),
211        }
212    }
213
214    /// Creates a request to get order status for a user.
215    pub fn order_status(user: &str, oid: u64) -> Self {
216        Self {
217            request_type: "orderStatus".to_string(),
218            params: InfoRequestParams::OrderStatus(OrderStatusParams {
219                user: user.to_string(),
220                oid,
221            }),
222        }
223    }
224
225    /// Creates a request to get all open orders for a user.
226    pub fn open_orders(user: &str) -> Self {
227        Self {
228            request_type: "openOrders".to_string(),
229            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
230                user: user.to_string(),
231            }),
232        }
233    }
234
235    /// Creates a request to get frontend open orders (includes more detail).
236    pub fn frontend_open_orders(user: &str) -> Self {
237        Self {
238            request_type: "frontendOpenOrders".to_string(),
239            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
240                user: user.to_string(),
241            }),
242        }
243    }
244
245    /// Creates a request to get user state (balances, positions, margin).
246    pub fn clearinghouse_state(user: &str) -> Self {
247        Self {
248            request_type: "clearinghouseState".to_string(),
249            params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
250                user: user.to_string(),
251            }),
252        }
253    }
254
255    /// Creates a request to get candle/bar data.
256    ///
257    /// # Arguments
258    /// * `coin` - The coin symbol (e.g., "BTC")
259    /// * `interval` - The timeframe (e.g., "1m", "5m", "15m", "1h", "4h", "1d")
260    /// * `start_time` - Start timestamp in milliseconds
261    /// * `end_time` - End timestamp in milliseconds
262    pub fn candle_snapshot(coin: &str, interval: &str, start_time: u64, end_time: u64) -> Self {
263        Self {
264            request_type: "candleSnapshot".to_string(),
265            params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
266                req: CandleSnapshotReq {
267                    coin: coin.to_string(),
268                    interval: interval.to_string(),
269                    start_time,
270                    end_time,
271                },
272            }),
273        }
274    }
275}
276
277/// Exchange action parameters.
278#[derive(Debug, Clone, Serialize)]
279#[serde(untagged)]
280pub enum ExchangeActionParams {
281    Order(OrderParams),
282    Cancel(CancelParams),
283    Modify(ModifyParams),
284    UpdateLeverage(UpdateLeverageParams),
285    UpdateIsolatedMargin(UpdateIsolatedMarginParams),
286}
287
288/// Represents an exchange action wrapper for `POST /exchange`.
289#[derive(Debug, Clone, Serialize)]
290pub struct ExchangeAction {
291    #[serde(rename = "type", serialize_with = "serialize_action_type")]
292    pub action_type: ExchangeActionType,
293    #[serde(flatten)]
294    pub params: ExchangeActionParams,
295}
296
297fn serialize_action_type<S>(
298    action_type: &ExchangeActionType,
299    serializer: S,
300) -> Result<S::Ok, S::Error>
301where
302    S: serde::Serializer,
303{
304    serializer.serialize_str(action_type.as_ref())
305}
306
307impl ExchangeAction {
308    /// Creates an action to place orders.
309    pub fn order(orders: Vec<HyperliquidExecPlaceOrderRequest>) -> Self {
310        Self {
311            action_type: ExchangeActionType::Order,
312            params: ExchangeActionParams::Order(OrderParams {
313                orders,
314                grouping: "na".to_string(),
315            }),
316        }
317    }
318
319    /// Creates an action to cancel orders.
320    pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
321        Self {
322            action_type: ExchangeActionType::Cancel,
323            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
324        }
325    }
326
327    /// Creates an action to cancel orders by client order ID.
328    pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
329        Self {
330            action_type: ExchangeActionType::CancelByCloid,
331            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
332        }
333    }
334
335    /// Creates an action to modify an order.
336    pub fn modify(oid: u64, order: HyperliquidExecModifyOrderRequest) -> Self {
337        Self {
338            action_type: ExchangeActionType::Modify,
339            params: ExchangeActionParams::Modify(ModifyParams { oid, order }),
340        }
341    }
342
343    /// Creates an action to update leverage for an asset.
344    pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
345        Self {
346            action_type: ExchangeActionType::UpdateLeverage,
347            params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
348                asset,
349                is_cross,
350                leverage,
351            }),
352        }
353    }
354
355    /// Creates an action to update isolated margin for an asset.
356    pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
357        Self {
358            action_type: ExchangeActionType::UpdateIsolatedMargin,
359            params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
360                asset,
361                is_buy,
362                ntli,
363            }),
364        }
365    }
366}
367
368////////////////////////////////////////////////////////////////////////////////
369// Tests
370////////////////////////////////////////////////////////////////////////////////
371
372#[cfg(test)]
373mod tests {
374    use rstest::rstest;
375
376    use super::*;
377
378    #[rstest]
379    fn test_info_request_meta() {
380        let req = InfoRequest::meta();
381
382        assert_eq!(req.request_type, "meta");
383        assert!(matches!(req.params, InfoRequestParams::None));
384    }
385
386    #[rstest]
387    fn test_info_request_l2_book() {
388        let req = InfoRequest::l2_book("BTC");
389
390        assert_eq!(req.request_type, "l2Book");
391        let json = serde_json::to_string(&req).unwrap();
392        assert!(json.contains("\"coin\":\"BTC\""));
393    }
394
395    #[rstest]
396    fn test_exchange_action_order() {
397        use rust_decimal::Decimal;
398
399        use crate::http::models::{
400            HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
401            HyperliquidExecTif,
402        };
403
404        let order = HyperliquidExecPlaceOrderRequest {
405            asset: 0,
406            is_buy: true,
407            price: Decimal::new(50000, 0),
408            size: Decimal::new(1, 0),
409            reduce_only: false,
410            kind: HyperliquidExecOrderKind::Limit {
411                limit: HyperliquidExecLimitParams {
412                    tif: HyperliquidExecTif::Gtc,
413                },
414            },
415            cloid: None,
416        };
417
418        let action = ExchangeAction::order(vec![order]);
419
420        assert_eq!(action.action_type, ExchangeActionType::Order);
421        let json = serde_json::to_string(&action).unwrap();
422        assert!(json.contains("\"orders\""));
423    }
424
425    #[rstest]
426    fn test_exchange_action_cancel() {
427        use crate::http::models::HyperliquidExecCancelByCloidRequest;
428
429        let cancel = HyperliquidExecCancelByCloidRequest {
430            asset: 0,
431            cloid: crate::http::models::Cloid::from_hex("0x00000000000000000000000000000000")
432                .unwrap(),
433        };
434
435        let action = ExchangeAction::cancel(vec![cancel]);
436
437        assert_eq!(action.action_type, ExchangeActionType::Cancel);
438    }
439
440    #[rstest]
441    fn test_exchange_action_serialization() {
442        use rust_decimal::Decimal;
443
444        use crate::http::models::{
445            HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
446            HyperliquidExecTif,
447        };
448
449        let order = HyperliquidExecPlaceOrderRequest {
450            asset: 0,
451            is_buy: true,
452            price: Decimal::new(50000, 0),
453            size: Decimal::new(1, 0),
454            reduce_only: false,
455            kind: HyperliquidExecOrderKind::Limit {
456                limit: HyperliquidExecLimitParams {
457                    tif: HyperliquidExecTif::Gtc,
458                },
459            },
460            cloid: None,
461        };
462
463        let action = ExchangeAction::order(vec![order]);
464
465        let json = serde_json::to_string(&action).unwrap();
466        // Verify that action_type is serialized as "type" with the correct string value
467        assert!(json.contains(r#""type":"order""#));
468        assert!(json.contains(r#""orders""#));
469        assert!(json.contains(r#""grouping":"na""#));
470    }
471
472    #[rstest]
473    fn test_exchange_action_type_as_ref() {
474        assert_eq!(ExchangeActionType::Order.as_ref(), "order");
475        assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
476        assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
477        assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
478        assert_eq!(
479            ExchangeActionType::UpdateLeverage.as_ref(),
480            "updateLeverage"
481        );
482        assert_eq!(
483            ExchangeActionType::UpdateIsolatedMargin.as_ref(),
484            "updateIsolatedMargin"
485        );
486    }
487
488    #[rstest]
489    fn test_update_leverage_serialization() {
490        let action = ExchangeAction::update_leverage(1, true, 10);
491        let json = serde_json::to_string(&action).unwrap();
492
493        assert!(json.contains(r#""type":"updateLeverage""#));
494        assert!(json.contains(r#""asset":1"#));
495        assert!(json.contains(r#""isCross":true"#));
496        assert!(json.contains(r#""leverage":10"#));
497    }
498
499    #[rstest]
500    fn test_update_isolated_margin_serialization() {
501        let action = ExchangeAction::update_isolated_margin(2, false, 1000);
502        let json = serde_json::to_string(&action).unwrap();
503
504        assert!(json.contains(r#""type":"updateIsolatedMargin""#));
505        assert!(json.contains(r#""asset":2"#));
506        assert!(json.contains(r#""isBuy":false"#));
507        assert!(json.contains(r#""ntli":1000"#));
508    }
509
510    #[rstest]
511    fn test_cancel_by_cloid_serialization() {
512        use crate::http::models::{Cloid, HyperliquidExecCancelByCloidRequest};
513
514        let cancel_request = HyperliquidExecCancelByCloidRequest {
515            asset: 0,
516            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
517        };
518        let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
519        let json = serde_json::to_string(&action).unwrap();
520
521        assert!(json.contains(r#""type":"cancelByCloid""#));
522        assert!(json.contains(r#""cancels""#));
523    }
524
525    #[rstest]
526    fn test_modify_serialization() {
527        use rust_decimal::Decimal;
528
529        use crate::http::models::HyperliquidExecModifyOrderRequest;
530
531        let modify_request = HyperliquidExecModifyOrderRequest {
532            asset: 0,
533            oid: 12345,
534            price: Some(Decimal::new(51000, 0)),
535            size: Some(Decimal::new(2, 0)),
536            reduce_only: None,
537            kind: None,
538        };
539        let action = ExchangeAction::modify(12345, modify_request);
540        let json = serde_json::to_string(&action).unwrap();
541
542        assert!(json.contains(r#""type":"modify""#));
543        assert!(json.contains(r#""oid":12345"#));
544    }
545}