Skip to main content

nautilus_hyperliquid/http/
query.rs

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