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