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