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