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    #[serde(flatten)]
76    pub request: 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 metadata for all perp dexes (standard + HIP-3).
176    pub fn all_perp_metas() -> Self {
177        Self {
178            request_type: HyperliquidInfoRequestType::AllPerpMetas,
179            params: InfoRequestParams::None,
180        }
181    }
182
183    /// Creates a request to get spot metadata (tokens and pairs).
184    pub fn spot_meta() -> Self {
185        Self {
186            request_type: HyperliquidInfoRequestType::SpotMeta,
187            params: InfoRequestParams::None,
188        }
189    }
190
191    /// Creates a request to get metadata with asset contexts (for price precision).
192    pub fn meta_and_asset_ctxs() -> Self {
193        Self {
194            request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
195            params: InfoRequestParams::None,
196        }
197    }
198
199    /// Creates a request to get spot metadata with asset contexts.
200    pub fn spot_meta_and_asset_ctxs() -> Self {
201        Self {
202            request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
203            params: InfoRequestParams::None,
204        }
205    }
206
207    /// Creates a request to get L2 order book for a coin.
208    pub fn l2_book(coin: &str) -> Self {
209        Self {
210            request_type: HyperliquidInfoRequestType::L2Book,
211            params: InfoRequestParams::L2Book(L2BookParams {
212                coin: coin.to_string(),
213            }),
214        }
215    }
216
217    /// Creates a request to get user fills.
218    pub fn user_fills(user: &str) -> Self {
219        Self {
220            request_type: HyperliquidInfoRequestType::UserFills,
221            params: InfoRequestParams::UserFills(UserFillsParams {
222                user: user.to_string(),
223            }),
224        }
225    }
226
227    /// Creates a request to get order status for a user.
228    pub fn order_status(user: &str, oid: u64) -> Self {
229        Self {
230            request_type: HyperliquidInfoRequestType::OrderStatus,
231            params: InfoRequestParams::OrderStatus(OrderStatusParams {
232                user: user.to_string(),
233                oid,
234            }),
235        }
236    }
237
238    /// Creates a request to get all open orders for a user.
239    pub fn open_orders(user: &str) -> Self {
240        Self {
241            request_type: HyperliquidInfoRequestType::OpenOrders,
242            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
243                user: user.to_string(),
244            }),
245        }
246    }
247
248    /// Creates a request to get frontend open orders (includes more detail).
249    pub fn frontend_open_orders(user: &str) -> Self {
250        Self {
251            request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
252            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
253                user: user.to_string(),
254            }),
255        }
256    }
257
258    /// Creates a request to get user state (balances, positions, margin).
259    pub fn clearinghouse_state(user: &str) -> Self {
260        Self {
261            request_type: HyperliquidInfoRequestType::ClearinghouseState,
262            params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
263                user: user.to_string(),
264            }),
265        }
266    }
267
268    /// Creates a request to get user fee schedule and effective rates.
269    pub fn user_fees(user: &str) -> Self {
270        Self {
271            request_type: HyperliquidInfoRequestType::UserFees,
272            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
273                user: user.to_string(),
274            }),
275        }
276    }
277
278    /// Creates a request to get candle/bar data.
279    pub fn candle_snapshot(
280        coin: &str,
281        interval: HyperliquidBarInterval,
282        start_time: u64,
283        end_time: u64,
284    ) -> Self {
285        Self {
286            request_type: HyperliquidInfoRequestType::CandleSnapshot,
287            params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
288                req: CandleSnapshotReq {
289                    coin: coin.to_string(),
290                    interval,
291                    start_time,
292                    end_time,
293                },
294            }),
295        }
296    }
297}
298
299/// Exchange action parameters.
300#[derive(Debug, Clone, Serialize)]
301#[serde(untagged)]
302pub enum ExchangeActionParams {
303    Order(OrderParams),
304    Cancel(CancelParams),
305    Modify(ModifyParams),
306    UpdateLeverage(UpdateLeverageParams),
307    UpdateIsolatedMargin(UpdateIsolatedMarginParams),
308}
309
310/// Represents an exchange action wrapper for `POST /exchange`.
311#[derive(Debug, Clone, Serialize)]
312pub struct ExchangeAction {
313    #[serde(rename = "type", serialize_with = "serialize_action_type")]
314    pub action_type: ExchangeActionType,
315    #[serde(flatten)]
316    pub params: ExchangeActionParams,
317}
318
319fn serialize_action_type<S>(
320    action_type: &ExchangeActionType,
321    serializer: S,
322) -> Result<S::Ok, S::Error>
323where
324    S: serde::Serializer,
325{
326    serializer.serialize_str(action_type.as_ref())
327}
328
329impl ExchangeAction {
330    /// Creates an action to place orders with builder attribution.
331    pub fn order(
332        orders: Vec<HyperliquidExecPlaceOrderRequest>,
333        builder: Option<HyperliquidExecBuilderFee>,
334    ) -> Self {
335        Self {
336            action_type: ExchangeActionType::Order,
337            params: ExchangeActionParams::Order(OrderParams {
338                orders,
339                grouping: HyperliquidExecGrouping::Na,
340                builder,
341            }),
342        }
343    }
344
345    /// Creates an action to cancel orders.
346    pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
347        Self {
348            action_type: ExchangeActionType::Cancel,
349            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
350        }
351    }
352
353    /// Creates an action to cancel orders by client order ID.
354    pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
355        Self {
356            action_type: ExchangeActionType::CancelByCloid,
357            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
358        }
359    }
360
361    /// Creates an action to modify an order.
362    pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
363        Self {
364            action_type: ExchangeActionType::Modify,
365            params: ExchangeActionParams::Modify(ModifyParams { request }),
366        }
367    }
368
369    /// Creates an action to update leverage for an asset.
370    pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
371        Self {
372            action_type: ExchangeActionType::UpdateLeverage,
373            params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
374                asset,
375                is_cross,
376                leverage,
377            }),
378        }
379    }
380
381    /// Creates an action to update isolated margin for an asset.
382    pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
383        Self {
384            action_type: ExchangeActionType::UpdateIsolatedMargin,
385            params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
386                asset,
387                is_buy,
388                ntli,
389            }),
390        }
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use rstest::rstest;
397    use rust_decimal::Decimal;
398
399    use super::*;
400    use crate::http::models::{
401        Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
402        HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
403        HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
404    };
405
406    #[rstest]
407    fn test_info_request_meta() {
408        let req = InfoRequest::meta();
409
410        assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
411        assert!(matches!(req.params, InfoRequestParams::None));
412    }
413
414    #[rstest]
415    fn test_info_request_all_perp_metas() {
416        let req = InfoRequest::all_perp_metas();
417
418        assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
419        let json = serde_json::to_string(&req).unwrap();
420        assert!(json.contains(r#""type":"allPerpMetas""#));
421    }
422
423    #[rstest]
424    fn test_info_request_l2_book() {
425        let req = InfoRequest::l2_book("BTC");
426
427        assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
428        let json = serde_json::to_string(&req).unwrap();
429        assert!(json.contains("\"coin\":\"BTC\""));
430    }
431
432    #[rstest]
433    fn test_exchange_action_order() {
434        let order = HyperliquidExecPlaceOrderRequest {
435            asset: 0,
436            is_buy: true,
437            price: Decimal::new(50000, 0),
438            size: Decimal::new(1, 0),
439            reduce_only: false,
440            kind: HyperliquidExecOrderKind::Limit {
441                limit: HyperliquidExecLimitParams {
442                    tif: HyperliquidExecTif::Gtc,
443                },
444            },
445            cloid: None,
446        };
447
448        let action = ExchangeAction::order(vec![order], None);
449
450        assert_eq!(action.action_type, ExchangeActionType::Order);
451        let json = serde_json::to_string(&action).unwrap();
452        assert!(json.contains("\"orders\""));
453    }
454
455    #[rstest]
456    fn test_exchange_action_cancel() {
457        let cancel = HyperliquidExecCancelByCloidRequest {
458            asset: 0,
459            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
460        };
461
462        let action = ExchangeAction::cancel(vec![cancel]);
463
464        assert_eq!(action.action_type, ExchangeActionType::Cancel);
465    }
466
467    #[rstest]
468    fn test_exchange_action_serialization() {
469        let order = HyperliquidExecPlaceOrderRequest {
470            asset: 0,
471            is_buy: true,
472            price: Decimal::new(50000, 0),
473            size: Decimal::new(1, 0),
474            reduce_only: false,
475            kind: HyperliquidExecOrderKind::Limit {
476                limit: HyperliquidExecLimitParams {
477                    tif: HyperliquidExecTif::Gtc,
478                },
479            },
480            cloid: None,
481        };
482
483        let action = ExchangeAction::order(vec![order], None);
484
485        let json = serde_json::to_string(&action).unwrap();
486        // Verify that action_type is serialized as "type" with the correct string value
487        assert!(json.contains(r#""type":"order""#));
488        assert!(json.contains(r#""orders""#));
489        assert!(json.contains(r#""grouping":"na""#));
490    }
491
492    #[rstest]
493    fn test_exchange_action_type_as_ref() {
494        assert_eq!(ExchangeActionType::Order.as_ref(), "order");
495        assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
496        assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
497        assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
498        assert_eq!(
499            ExchangeActionType::UpdateLeverage.as_ref(),
500            "updateLeverage"
501        );
502        assert_eq!(
503            ExchangeActionType::UpdateIsolatedMargin.as_ref(),
504            "updateIsolatedMargin"
505        );
506    }
507
508    #[rstest]
509    fn test_update_leverage_serialization() {
510        let action = ExchangeAction::update_leverage(1, true, 10);
511        let json = serde_json::to_string(&action).unwrap();
512
513        assert!(json.contains(r#""type":"updateLeverage""#));
514        assert!(json.contains(r#""asset":1"#));
515        assert!(json.contains(r#""isCross":true"#));
516        assert!(json.contains(r#""leverage":10"#));
517    }
518
519    #[rstest]
520    fn test_update_isolated_margin_serialization() {
521        let action = ExchangeAction::update_isolated_margin(2, false, 1000);
522        let json = serde_json::to_string(&action).unwrap();
523
524        assert!(json.contains(r#""type":"updateIsolatedMargin""#));
525        assert!(json.contains(r#""asset":2"#));
526        assert!(json.contains(r#""isBuy":false"#));
527        assert!(json.contains(r#""ntli":1000"#));
528    }
529
530    #[rstest]
531    fn test_cancel_by_cloid_serialization() {
532        let cancel_request = HyperliquidExecCancelByCloidRequest {
533            asset: 0,
534            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
535        };
536        let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
537        let json = serde_json::to_string(&action).unwrap();
538
539        assert!(json.contains(r#""type":"cancelByCloid""#));
540        assert!(json.contains(r#""cancels""#));
541    }
542
543    #[rstest]
544    fn test_modify_serialization() {
545        let modify_request = HyperliquidExecModifyOrderRequest {
546            oid: 12345,
547            order: HyperliquidExecPlaceOrderRequest {
548                asset: 0,
549                is_buy: true,
550                price: Decimal::new(51000, 0),
551                size: Decimal::new(2, 0),
552                reduce_only: false,
553                kind: HyperliquidExecOrderKind::Limit {
554                    limit: HyperliquidExecLimitParams {
555                        tif: HyperliquidExecTif::Gtc,
556                    },
557                },
558                cloid: None,
559            },
560        };
561        let action = ExchangeAction::modify(modify_request);
562        let json = serde_json::to_string(&action).unwrap();
563
564        assert!(json.contains(r#""type":"modify""#));
565        assert!(json.contains(r#""oid":12345"#));
566        assert!(json.contains(r#""order""#));
567    }
568}