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