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 spot clearinghouse state request.
129#[derive(Debug, Clone, Serialize)]
130pub struct SpotClearinghouseStateParams {
131    pub user: String,
132}
133
134/// Parameters for candle snapshot request.
135#[derive(Debug, Clone, Serialize)]
136#[serde(rename_all = "camelCase")]
137pub struct CandleSnapshotReq {
138    pub coin: String,
139    pub interval: HyperliquidBarInterval,
140    pub start_time: u64,
141    pub end_time: u64,
142}
143
144/// Wrapper for candle snapshot parameters.
145#[derive(Debug, Clone, Serialize)]
146pub struct CandleSnapshotParams {
147    pub req: CandleSnapshotReq,
148}
149
150/// Parameters for funding history request.
151#[derive(Debug, Clone, Serialize)]
152#[serde(rename_all = "camelCase")]
153pub struct FundingHistoryParams {
154    pub coin: String,
155    pub start_time: u64,
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub end_time: Option<u64>,
158}
159
160/// Info request parameters.
161#[derive(Debug, Clone, Serialize)]
162#[serde(untagged)]
163pub enum InfoRequestParams {
164    L2Book(L2BookParams),
165    UserFills(UserFillsParams),
166    OrderStatus(OrderStatusParams),
167    OpenOrders(OpenOrdersParams),
168    ClearinghouseState(ClearinghouseStateParams),
169    SpotClearinghouseState(SpotClearinghouseStateParams),
170    CandleSnapshot(CandleSnapshotParams),
171    FundingHistory(FundingHistoryParams),
172    None,
173}
174
175/// Represents an info request wrapper for `POST /info`.
176#[derive(Debug, Clone, Serialize)]
177pub struct InfoRequest {
178    #[serde(rename = "type")]
179    pub request_type: HyperliquidInfoRequestType,
180    #[serde(flatten)]
181    pub params: InfoRequestParams,
182}
183
184impl InfoRequest {
185    /// Creates a request to get metadata about available markets.
186    pub fn meta() -> Self {
187        Self {
188            request_type: HyperliquidInfoRequestType::Meta,
189            params: InfoRequestParams::None,
190        }
191    }
192
193    /// Creates a request to get metadata for all perp dexes (standard + HIP-3).
194    pub fn all_perp_metas() -> Self {
195        Self {
196            request_type: HyperliquidInfoRequestType::AllPerpMetas,
197            params: InfoRequestParams::None,
198        }
199    }
200
201    /// Creates a request to get the list of perp dexes.
202    pub fn perp_dexs() -> Self {
203        Self {
204            request_type: HyperliquidInfoRequestType::PerpDexs,
205            params: InfoRequestParams::None,
206        }
207    }
208
209    /// Creates a request to get spot metadata (tokens and pairs).
210    pub fn spot_meta() -> Self {
211        Self {
212            request_type: HyperliquidInfoRequestType::SpotMeta,
213            params: InfoRequestParams::None,
214        }
215    }
216
217    /// Creates a request to get metadata with asset contexts (for price precision).
218    pub fn meta_and_asset_ctxs() -> Self {
219        Self {
220            request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
221            params: InfoRequestParams::None,
222        }
223    }
224
225    /// Creates a request to get spot metadata with asset contexts.
226    pub fn spot_meta_and_asset_ctxs() -> Self {
227        Self {
228            request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
229            params: InfoRequestParams::None,
230        }
231    }
232
233    /// Creates a request to get outcome metadata.
234    pub fn outcome_meta() -> Self {
235        Self {
236            request_type: HyperliquidInfoRequestType::OutcomeMeta,
237            params: InfoRequestParams::None,
238        }
239    }
240
241    /// Creates a request to get L2 order book for a coin.
242    pub fn l2_book(coin: &str) -> Self {
243        Self {
244            request_type: HyperliquidInfoRequestType::L2Book,
245            params: InfoRequestParams::L2Book(L2BookParams {
246                coin: coin.to_string(),
247            }),
248        }
249    }
250
251    /// Creates a request to get user fills.
252    pub fn user_fills(user: &str) -> Self {
253        Self {
254            request_type: HyperliquidInfoRequestType::UserFills,
255            params: InfoRequestParams::UserFills(UserFillsParams {
256                user: user.to_string(),
257            }),
258        }
259    }
260
261    /// Creates a request to get order status for a user.
262    pub fn order_status(user: &str, oid: u64) -> Self {
263        Self {
264            request_type: HyperliquidInfoRequestType::OrderStatus,
265            params: InfoRequestParams::OrderStatus(OrderStatusParams {
266                user: user.to_string(),
267                oid,
268            }),
269        }
270    }
271
272    /// Creates a request to get all open orders for a user.
273    pub fn open_orders(user: &str) -> Self {
274        Self {
275            request_type: HyperliquidInfoRequestType::OpenOrders,
276            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
277                user: user.to_string(),
278            }),
279        }
280    }
281
282    /// Creates a request to get frontend open orders (includes more detail).
283    pub fn frontend_open_orders(user: &str) -> Self {
284        Self {
285            request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
286            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
287                user: user.to_string(),
288            }),
289        }
290    }
291
292    /// Creates a request to get user state (balances, positions, margin).
293    pub fn clearinghouse_state(user: &str) -> Self {
294        Self {
295            request_type: HyperliquidInfoRequestType::ClearinghouseState,
296            params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
297                user: user.to_string(),
298            }),
299        }
300    }
301
302    /// Creates a request to get spot clearinghouse state (per-token spot balances).
303    pub fn spot_clearinghouse_state(user: &str) -> Self {
304        Self {
305            request_type: HyperliquidInfoRequestType::SpotClearinghouseState,
306            params: InfoRequestParams::SpotClearinghouseState(SpotClearinghouseStateParams {
307                user: user.to_string(),
308            }),
309        }
310    }
311
312    /// Creates a request to get user fee schedule and effective rates.
313    pub fn user_fees(user: &str) -> Self {
314        Self {
315            request_type: HyperliquidInfoRequestType::UserFees,
316            params: InfoRequestParams::OpenOrders(OpenOrdersParams {
317                user: user.to_string(),
318            }),
319        }
320    }
321
322    /// Creates a request to get candle/bar data.
323    pub fn candle_snapshot(
324        coin: &str,
325        interval: HyperliquidBarInterval,
326        start_time: u64,
327        end_time: u64,
328    ) -> Self {
329        Self {
330            request_type: HyperliquidInfoRequestType::CandleSnapshot,
331            params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
332                req: CandleSnapshotReq {
333                    coin: coin.to_string(),
334                    interval,
335                    start_time,
336                    end_time,
337                },
338            }),
339        }
340    }
341
342    /// Creates a request to get funding rate history for a coin.
343    pub fn funding_history(coin: &str, start_time: u64, end_time: Option<u64>) -> Self {
344        Self {
345            request_type: HyperliquidInfoRequestType::FundingHistory,
346            params: InfoRequestParams::FundingHistory(FundingHistoryParams {
347                coin: coin.to_string(),
348                start_time,
349                end_time,
350            }),
351        }
352    }
353}
354
355/// Exchange action parameters.
356#[derive(Debug, Clone, Serialize)]
357#[serde(untagged)]
358pub enum ExchangeActionParams {
359    Order(OrderParams),
360    Cancel(CancelParams),
361    Modify(ModifyParams),
362    UpdateLeverage(UpdateLeverageParams),
363    UpdateIsolatedMargin(UpdateIsolatedMarginParams),
364}
365
366/// Represents an exchange action wrapper for `POST /exchange`.
367#[derive(Debug, Clone, Serialize)]
368pub struct ExchangeAction {
369    #[serde(rename = "type", serialize_with = "serialize_action_type")]
370    pub action_type: ExchangeActionType,
371    #[serde(flatten)]
372    pub params: ExchangeActionParams,
373}
374
375fn serialize_action_type<S>(
376    action_type: &ExchangeActionType,
377    serializer: S,
378) -> Result<S::Ok, S::Error>
379where
380    S: serde::Serializer,
381{
382    serializer.serialize_str(action_type.as_ref())
383}
384
385impl ExchangeAction {
386    /// Creates an action to place orders with builder attribution.
387    pub fn order(
388        orders: Vec<HyperliquidExecPlaceOrderRequest>,
389        builder: Option<HyperliquidExecBuilderFee>,
390    ) -> Self {
391        Self {
392            action_type: ExchangeActionType::Order,
393            params: ExchangeActionParams::Order(OrderParams {
394                orders,
395                grouping: HyperliquidExecGrouping::Na,
396                builder,
397            }),
398        }
399    }
400
401    /// Creates an action to cancel orders.
402    pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
403        Self {
404            action_type: ExchangeActionType::Cancel,
405            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
406        }
407    }
408
409    /// Creates an action to cancel orders by client order ID.
410    pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
411        Self {
412            action_type: ExchangeActionType::CancelByCloid,
413            params: ExchangeActionParams::Cancel(CancelParams { cancels }),
414        }
415    }
416
417    /// Creates an action to modify an order.
418    pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
419        Self {
420            action_type: ExchangeActionType::Modify,
421            params: ExchangeActionParams::Modify(ModifyParams { request }),
422        }
423    }
424
425    /// Creates an action to update leverage for an asset.
426    pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
427        Self {
428            action_type: ExchangeActionType::UpdateLeverage,
429            params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
430                asset,
431                is_cross,
432                leverage,
433            }),
434        }
435    }
436
437    /// Creates an action to update isolated margin for an asset.
438    pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
439        Self {
440            action_type: ExchangeActionType::UpdateIsolatedMargin,
441            params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
442                asset,
443                is_buy,
444                ntli,
445            }),
446        }
447    }
448}
449
450#[cfg(test)]
451mod tests {
452    use rstest::rstest;
453    use rust_decimal::Decimal;
454
455    use super::*;
456    use crate::http::models::{
457        Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
458        HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
459        HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
460    };
461
462    #[rstest]
463    fn test_info_request_meta() {
464        let req = InfoRequest::meta();
465
466        assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
467        assert!(matches!(req.params, InfoRequestParams::None));
468    }
469
470    #[rstest]
471    fn test_info_request_all_perp_metas() {
472        let req = InfoRequest::all_perp_metas();
473
474        assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
475        let json = serde_json::to_string(&req).unwrap();
476        assert!(json.contains(r#""type":"allPerpMetas""#));
477    }
478
479    #[rstest]
480    fn test_info_request_outcome_meta() {
481        let req = InfoRequest::outcome_meta();
482
483        assert_eq!(req.request_type, HyperliquidInfoRequestType::OutcomeMeta);
484        assert!(matches!(req.params, InfoRequestParams::None));
485        let json = serde_json::to_string(&req).unwrap();
486        assert_eq!(json, r#"{"type":"outcomeMeta"}"#);
487    }
488
489    #[rstest]
490    fn test_info_request_l2_book() {
491        let req = InfoRequest::l2_book("BTC");
492
493        assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
494        let json = serde_json::to_string(&req).unwrap();
495        assert!(json.contains("\"coin\":\"BTC\""));
496    }
497
498    #[rstest]
499    fn test_info_request_spot_clearinghouse_state() {
500        let req = InfoRequest::spot_clearinghouse_state("0xabc");
501
502        assert_eq!(
503            req.request_type,
504            HyperliquidInfoRequestType::SpotClearinghouseState
505        );
506        let json = serde_json::to_string(&req).unwrap();
507        assert!(json.contains(r#""type":"spotClearinghouseState""#));
508        assert!(json.contains(r#""user":"0xabc""#));
509    }
510
511    #[rstest]
512    fn test_info_request_funding_history_with_end_time() {
513        let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, Some(1_700_003_600_000));
514
515        assert_eq!(req.request_type, HyperliquidInfoRequestType::FundingHistory);
516        let json = serde_json::to_string(&req).unwrap();
517        assert!(json.contains(r#""type":"fundingHistory""#));
518        assert!(json.contains(r#""coin":"BTC""#));
519        assert!(json.contains(r#""startTime":1700000000000"#));
520        assert!(json.contains(r#""endTime":1700003600000"#));
521    }
522
523    #[rstest]
524    fn test_info_request_funding_history_omits_end_time_when_none() {
525        // Hyperliquid defaults `endTime` to current time when absent; the
526        // serializer must omit the field rather than emit `null`.
527        let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, None);
528        let json = serde_json::to_string(&req).unwrap();
529        assert!(json.contains(r#""startTime":1700000000000"#));
530        assert!(
531            !json.contains("endTime"),
532            "endTime must be omitted when None; json={json}",
533        );
534    }
535
536    #[rstest]
537    fn test_exchange_action_order() {
538        let order = HyperliquidExecPlaceOrderRequest {
539            asset: 0,
540            is_buy: true,
541            price: Decimal::new(50000, 0),
542            size: Decimal::new(1, 0),
543            reduce_only: false,
544            kind: HyperliquidExecOrderKind::Limit {
545                limit: HyperliquidExecLimitParams {
546                    tif: HyperliquidExecTif::Gtc,
547                },
548            },
549            cloid: None,
550        };
551
552        let action = ExchangeAction::order(vec![order], None);
553
554        assert_eq!(action.action_type, ExchangeActionType::Order);
555        let json = serde_json::to_string(&action).unwrap();
556        assert!(json.contains("\"orders\""));
557    }
558
559    #[rstest]
560    fn test_exchange_action_cancel() {
561        let cancel = HyperliquidExecCancelByCloidRequest {
562            asset: 0,
563            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
564        };
565
566        let action = ExchangeAction::cancel(vec![cancel]);
567
568        assert_eq!(action.action_type, ExchangeActionType::Cancel);
569    }
570
571    #[rstest]
572    fn test_exchange_action_serialization() {
573        let order = HyperliquidExecPlaceOrderRequest {
574            asset: 0,
575            is_buy: true,
576            price: Decimal::new(50000, 0),
577            size: Decimal::new(1, 0),
578            reduce_only: false,
579            kind: HyperliquidExecOrderKind::Limit {
580                limit: HyperliquidExecLimitParams {
581                    tif: HyperliquidExecTif::Gtc,
582                },
583            },
584            cloid: None,
585        };
586
587        let action = ExchangeAction::order(vec![order], None);
588
589        let json = serde_json::to_string(&action).unwrap();
590        // Verify that action_type is serialized as "type" with the correct string value
591        assert!(json.contains(r#""type":"order""#));
592        assert!(json.contains(r#""orders""#));
593        assert!(json.contains(r#""grouping":"na""#));
594    }
595
596    #[rstest]
597    fn test_exchange_action_type_as_ref() {
598        assert_eq!(ExchangeActionType::Order.as_ref(), "order");
599        assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
600        assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
601        assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
602        assert_eq!(
603            ExchangeActionType::UpdateLeverage.as_ref(),
604            "updateLeverage"
605        );
606        assert_eq!(
607            ExchangeActionType::UpdateIsolatedMargin.as_ref(),
608            "updateIsolatedMargin"
609        );
610    }
611
612    #[rstest]
613    fn test_update_leverage_serialization() {
614        let action = ExchangeAction::update_leverage(1, true, 10);
615        let json = serde_json::to_string(&action).unwrap();
616
617        assert!(json.contains(r#""type":"updateLeverage""#));
618        assert!(json.contains(r#""asset":1"#));
619        assert!(json.contains(r#""isCross":true"#));
620        assert!(json.contains(r#""leverage":10"#));
621    }
622
623    #[rstest]
624    fn test_update_isolated_margin_serialization() {
625        let action = ExchangeAction::update_isolated_margin(2, false, 1000);
626        let json = serde_json::to_string(&action).unwrap();
627
628        assert!(json.contains(r#""type":"updateIsolatedMargin""#));
629        assert!(json.contains(r#""asset":2"#));
630        assert!(json.contains(r#""isBuy":false"#));
631        assert!(json.contains(r#""ntli":1000"#));
632    }
633
634    #[rstest]
635    fn test_cancel_by_cloid_serialization() {
636        let cancel_request = HyperliquidExecCancelByCloidRequest {
637            asset: 0,
638            cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
639        };
640        let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
641        let json = serde_json::to_string(&action).unwrap();
642
643        assert!(json.contains(r#""type":"cancelByCloid""#));
644        assert!(json.contains(r#""cancels""#));
645    }
646
647    #[rstest]
648    fn test_modify_serialization() {
649        let modify_request = HyperliquidExecModifyOrderRequest {
650            oid: 12345,
651            order: HyperliquidExecPlaceOrderRequest {
652                asset: 0,
653                is_buy: true,
654                price: Decimal::new(51000, 0),
655                size: Decimal::new(2, 0),
656                reduce_only: false,
657                kind: HyperliquidExecOrderKind::Limit {
658                    limit: HyperliquidExecLimitParams {
659                        tif: HyperliquidExecTif::Gtc,
660                    },
661                },
662                cloid: None,
663            },
664        };
665        let action = ExchangeAction::modify(modify_request);
666        let json = serde_json::to_string(&action).unwrap();
667
668        assert!(json.contains(r#""type":"modify""#));
669        assert!(json.contains(r#""oid":12345"#));
670        assert!(json.contains(r#""order""#));
671    }
672}