1use serde::{Serialize, ser::SerializeStruct};
17
18use crate::{
19 common::{
20 enums::{HyperliquidBarInterval, HyperliquidInfoRequestType},
21 parse::is_false_or_none,
22 },
23 http::models::{
24 HyperliquidExchangeBuilderFee, HyperliquidExchangeCancelByCloidRequest,
25 HyperliquidExchangeGrouping, HyperliquidExchangeModifyOrderRequest,
26 HyperliquidExchangePlaceOrderRequest,
27 },
28};
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
32#[serde(rename_all = "camelCase")]
33pub enum ExchangeActionType {
34 Order,
36 Cancel,
38 CancelByCloid,
40 Modify,
42 UpdateLeverage,
44 UpdateIsolatedMargin,
46}
47
48impl AsRef<str> for ExchangeActionType {
49 fn as_ref(&self) -> &str {
50 match self {
51 Self::Order => "order",
52 Self::Cancel => "cancel",
53 Self::CancelByCloid => "cancelByCloid",
54 Self::Modify => "modify",
55 Self::UpdateLeverage => "updateLeverage",
56 Self::UpdateIsolatedMargin => "updateIsolatedMargin",
57 }
58 }
59}
60
61#[derive(Debug, Clone, Serialize)]
63pub struct OrderParams {
64 pub orders: Vec<HyperliquidExchangePlaceOrderRequest>,
65 pub grouping: HyperliquidExchangeGrouping,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 pub builder: Option<HyperliquidExchangeBuilderFee>,
68}
69
70#[derive(Debug, Clone, Serialize)]
74pub struct CancelParams {
75 pub cancels: Vec<HyperliquidExchangeCancelByCloidRequest>,
76 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
77 pub fast: Option<bool>,
78}
79
80#[derive(Debug, Clone, Serialize)]
82pub struct ModifyParams {
83 #[serde(flatten)]
84 pub request: HyperliquidExchangeModifyOrderRequest,
85}
86
87#[derive(Debug, Clone, Serialize)]
89#[serde(rename_all = "camelCase")]
90pub struct UpdateLeverageParams {
91 pub asset: u32,
92 pub is_cross: bool,
93 pub leverage: u32,
94}
95
96#[derive(Debug, Clone, Serialize)]
98#[serde(rename_all = "camelCase")]
99pub struct UpdateIsolatedMarginParams {
100 pub asset: u32,
101 pub is_buy: bool,
102 pub ntli: i64,
103}
104
105#[derive(Debug, Clone, Serialize)]
107pub struct L2BookParams {
108 pub coin: String,
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct RecentTradesParams {
114 pub coin: String,
115}
116
117#[derive(Debug, Clone)]
119pub struct UserFillsParams {
120 pub user: String,
121}
122
123impl Serialize for UserFillsParams {
124 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
125 where
126 S: serde::Serializer,
127 {
128 let mut state = serializer.serialize_struct("UserFillsParams", 2)?;
129 state.serialize_field("user", &self.user)?;
130 state.serialize_field("aggregateByTime", &false)?;
131 state.end()
132 }
133}
134
135#[derive(Debug, Clone, Serialize)]
137pub struct OrderStatusParams {
138 pub user: String,
139 pub oid: u64,
140}
141
142#[derive(Debug, Clone, Serialize)]
144pub struct OpenOrdersParams {
145 pub user: String,
146 #[serde(skip_serializing_if = "Option::is_none")]
147 pub dex: Option<String>,
148}
149
150#[derive(Debug, Clone, Serialize)]
152pub struct ClearinghouseStateParams {
153 pub user: String,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub dex: Option<String>,
156}
157
158#[derive(Debug, Clone, Serialize)]
160pub struct SpotClearinghouseStateParams {
161 pub user: String,
162}
163
164#[derive(Debug, Clone, Serialize)]
166#[serde(rename_all = "camelCase")]
167pub struct CandleSnapshotReq {
168 pub coin: String,
169 pub interval: HyperliquidBarInterval,
170 pub start_time: u64,
171 pub end_time: u64,
172}
173
174#[derive(Debug, Clone, Serialize)]
176pub struct CandleSnapshotParams {
177 pub req: CandleSnapshotReq,
178}
179
180#[derive(Debug, Clone, Serialize)]
182#[serde(rename_all = "camelCase")]
183pub struct FundingHistoryParams {
184 pub coin: String,
185 pub start_time: u64,
186 #[serde(skip_serializing_if = "Option::is_none")]
187 pub end_time: Option<u64>,
188}
189
190#[derive(Debug, Clone, Serialize)]
192#[serde(untagged)]
193pub enum InfoRequestParams {
194 L2Book(L2BookParams),
195 RecentTrades(RecentTradesParams),
196 UserFills(UserFillsParams),
197 OrderStatus(OrderStatusParams),
198 OpenOrders(OpenOrdersParams),
199 ClearinghouseState(ClearinghouseStateParams),
200 SpotClearinghouseState(SpotClearinghouseStateParams),
201 CandleSnapshot(CandleSnapshotParams),
202 FundingHistory(FundingHistoryParams),
203 None,
204}
205
206#[derive(Debug, Clone, Serialize)]
208pub struct InfoRequest {
209 #[serde(rename = "type")]
210 pub request_type: HyperliquidInfoRequestType,
211 #[serde(flatten)]
212 pub params: InfoRequestParams,
213}
214
215impl InfoRequest {
216 pub fn meta() -> Self {
218 Self {
219 request_type: HyperliquidInfoRequestType::Meta,
220 params: InfoRequestParams::None,
221 }
222 }
223
224 pub fn all_perp_metas() -> Self {
226 Self {
227 request_type: HyperliquidInfoRequestType::AllPerpMetas,
228 params: InfoRequestParams::None,
229 }
230 }
231
232 pub fn perp_dexs() -> Self {
234 Self {
235 request_type: HyperliquidInfoRequestType::PerpDexs,
236 params: InfoRequestParams::None,
237 }
238 }
239
240 pub fn spot_meta() -> Self {
242 Self {
243 request_type: HyperliquidInfoRequestType::SpotMeta,
244 params: InfoRequestParams::None,
245 }
246 }
247
248 pub fn meta_and_asset_ctxs() -> Self {
250 Self {
251 request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
252 params: InfoRequestParams::None,
253 }
254 }
255
256 pub fn spot_meta_and_asset_ctxs() -> Self {
258 Self {
259 request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
260 params: InfoRequestParams::None,
261 }
262 }
263
264 pub fn outcome_meta() -> Self {
266 Self {
267 request_type: HyperliquidInfoRequestType::OutcomeMeta,
268 params: InfoRequestParams::None,
269 }
270 }
271
272 pub fn l2_book(coin: &str) -> Self {
274 Self {
275 request_type: HyperliquidInfoRequestType::L2Book,
276 params: InfoRequestParams::L2Book(L2BookParams {
277 coin: coin.to_string(),
278 }),
279 }
280 }
281
282 pub fn recent_trades(coin: &str) -> Self {
284 Self {
285 request_type: HyperliquidInfoRequestType::RecentTrades,
286 params: InfoRequestParams::RecentTrades(RecentTradesParams {
287 coin: coin.to_string(),
288 }),
289 }
290 }
291
292 pub fn user_fills(user: &str) -> Self {
294 Self {
295 request_type: HyperliquidInfoRequestType::UserFills,
296 params: InfoRequestParams::UserFills(UserFillsParams {
297 user: user.to_string(),
298 }),
299 }
300 }
301
302 pub fn order_status(user: &str, oid: u64) -> Self {
304 Self {
305 request_type: HyperliquidInfoRequestType::OrderStatus,
306 params: InfoRequestParams::OrderStatus(OrderStatusParams {
307 user: user.to_string(),
308 oid,
309 }),
310 }
311 }
312
313 pub fn open_orders(user: &str) -> Self {
315 Self::open_orders_for_dex(user, None)
316 }
317
318 pub(crate) fn open_orders_for_dex(user: &str, dex: Option<&str>) -> Self {
320 Self {
321 request_type: HyperliquidInfoRequestType::OpenOrders,
322 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
323 user: user.to_string(),
324 dex: dex.map(str::to_string),
325 }),
326 }
327 }
328
329 pub fn frontend_open_orders(user: &str) -> Self {
331 Self::frontend_open_orders_for_dex(user, None)
332 }
333
334 pub(crate) fn frontend_open_orders_for_dex(user: &str, dex: Option<&str>) -> Self {
336 Self {
337 request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
338 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
339 user: user.to_string(),
340 dex: dex.map(str::to_string),
341 }),
342 }
343 }
344
345 pub fn historical_orders(user: &str) -> Self {
347 Self {
348 request_type: HyperliquidInfoRequestType::HistoricalOrders,
349 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
350 user: user.to_string(),
351 dex: None,
352 }),
353 }
354 }
355
356 pub fn clearinghouse_state(user: &str) -> Self {
358 Self::clearinghouse_state_for_dex(user, None)
359 }
360
361 pub(crate) fn clearinghouse_state_for_dex(user: &str, dex: Option<&str>) -> Self {
363 Self {
364 request_type: HyperliquidInfoRequestType::ClearinghouseState,
365 params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
366 user: user.to_string(),
367 dex: dex.map(str::to_string),
368 }),
369 }
370 }
371
372 pub fn spot_clearinghouse_state(user: &str) -> Self {
374 Self {
375 request_type: HyperliquidInfoRequestType::SpotClearinghouseState,
376 params: InfoRequestParams::SpotClearinghouseState(SpotClearinghouseStateParams {
377 user: user.to_string(),
378 }),
379 }
380 }
381
382 pub fn user_abstraction(user: &str) -> Self {
384 Self {
385 request_type: HyperliquidInfoRequestType::UserAbstraction,
386 params: InfoRequestParams::SpotClearinghouseState(SpotClearinghouseStateParams {
387 user: user.to_string(),
388 }),
389 }
390 }
391
392 pub fn user_fees(user: &str) -> Self {
394 Self {
395 request_type: HyperliquidInfoRequestType::UserFees,
396 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
397 user: user.to_string(),
398 dex: None,
399 }),
400 }
401 }
402
403 pub fn candle_snapshot(
405 coin: &str,
406 interval: HyperliquidBarInterval,
407 start_time: u64,
408 end_time: u64,
409 ) -> Self {
410 Self {
411 request_type: HyperliquidInfoRequestType::CandleSnapshot,
412 params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
413 req: CandleSnapshotReq {
414 coin: coin.to_string(),
415 interval,
416 start_time,
417 end_time,
418 },
419 }),
420 }
421 }
422
423 pub fn funding_history(coin: &str, start_time: u64, end_time: Option<u64>) -> Self {
425 Self {
426 request_type: HyperliquidInfoRequestType::FundingHistory,
427 params: InfoRequestParams::FundingHistory(FundingHistoryParams {
428 coin: coin.to_string(),
429 start_time,
430 end_time,
431 }),
432 }
433 }
434}
435
436#[derive(Debug, Clone, Serialize)]
438#[serde(untagged)]
439pub enum ExchangeActionParams {
440 Order(OrderParams),
441 Cancel(CancelParams),
442 Modify(ModifyParams),
443 UpdateLeverage(UpdateLeverageParams),
444 UpdateIsolatedMargin(UpdateIsolatedMarginParams),
445}
446
447#[derive(Debug, Clone, Serialize)]
449pub struct ExchangeAction {
450 #[serde(rename = "type", serialize_with = "serialize_action_type")]
451 pub action_type: ExchangeActionType,
452 #[serde(flatten)]
453 pub params: ExchangeActionParams,
454}
455
456fn serialize_action_type<S>(
457 action_type: &ExchangeActionType,
458 serializer: S,
459) -> Result<S::Ok, S::Error>
460where
461 S: serde::Serializer,
462{
463 serializer.serialize_str(action_type.as_ref())
464}
465
466impl ExchangeAction {
467 pub fn order(
469 orders: Vec<HyperliquidExchangePlaceOrderRequest>,
470 builder: Option<HyperliquidExchangeBuilderFee>,
471 ) -> Self {
472 Self {
473 action_type: ExchangeActionType::Order,
474 params: ExchangeActionParams::Order(OrderParams {
475 orders,
476 grouping: HyperliquidExchangeGrouping::Na,
477 builder,
478 }),
479 }
480 }
481
482 pub fn cancel(cancels: Vec<HyperliquidExchangeCancelByCloidRequest>) -> Self {
484 Self {
485 action_type: ExchangeActionType::Cancel,
486 params: ExchangeActionParams::Cancel(CancelParams {
487 cancels,
488 fast: None,
489 }),
490 }
491 }
492
493 pub fn cancel_by_cloid(cancels: Vec<HyperliquidExchangeCancelByCloidRequest>) -> Self {
495 Self {
496 action_type: ExchangeActionType::CancelByCloid,
497 params: ExchangeActionParams::Cancel(CancelParams {
498 cancels,
499 fast: None,
500 }),
501 }
502 }
503
504 pub fn modify(request: HyperliquidExchangeModifyOrderRequest) -> Self {
506 Self {
507 action_type: ExchangeActionType::Modify,
508 params: ExchangeActionParams::Modify(ModifyParams { request }),
509 }
510 }
511
512 pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
514 Self {
515 action_type: ExchangeActionType::UpdateLeverage,
516 params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
517 asset,
518 is_cross,
519 leverage,
520 }),
521 }
522 }
523
524 pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
526 Self {
527 action_type: ExchangeActionType::UpdateIsolatedMargin,
528 params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
529 asset,
530 is_buy,
531 ntli,
532 }),
533 }
534 }
535}
536
537#[cfg(test)]
538mod tests {
539 use rstest::rstest;
540 use rust_decimal::Decimal;
541
542 use super::*;
543 use crate::http::models::{
544 Cloid, HyperliquidExchangeCancelByCloidRequest, HyperliquidExchangeLimitParams,
545 HyperliquidExchangeModifyOrderRequest, HyperliquidExchangeOrderKind,
546 HyperliquidExchangePlaceOrderRequest, HyperliquidExchangeTif,
547 };
548
549 #[rstest]
550 fn test_cancel_action_serialization(
551 #[values(None, Some(false), Some(true))] fast: Option<bool>,
552 #[values(false, true)] by_cloid: bool,
553 ) {
554 let cancels = vec![HyperliquidExchangeCancelByCloidRequest {
555 asset: 7,
556 cloid: Cloid::from_hex("0x0123456789abcdef0123456789abcdef").unwrap(),
557 }];
558
559 let (mut action, name) = if by_cloid {
560 (ExchangeAction::cancel_by_cloid(cancels), "cancel_by_cloid")
561 } else {
562 (ExchangeAction::cancel(cancels), "query_cancel")
563 };
564
565 let ExchangeActionParams::Cancel(params) = &mut action.params else {
566 panic!("Expected cancel parameters");
567 };
568
569 params.fast = fast;
570 let fixtures: serde_json::Value =
571 serde_json::from_str(include_str!("../../test_data/cancel_actions.json")).unwrap();
572 let expected = &fixtures[name][if fast == Some(true) { "fast" } else { "normal" }];
573 let json = serde_json::to_value(&action).unwrap();
574 let msgpack = rmp_serde::to_vec_named(&action).unwrap();
575
576 assert_eq!(json, expected["json"]);
577 assert_eq!(
578 nautilus_core::hex::encode(msgpack),
579 expected["msgpack"].as_str().unwrap()
580 );
581 }
582
583 #[rstest]
584 fn test_info_request_meta() {
585 let req = InfoRequest::meta();
586
587 assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
588 assert!(matches!(req.params, InfoRequestParams::None));
589 }
590
591 #[rstest]
592 fn test_info_request_all_perp_metas() {
593 let req = InfoRequest::all_perp_metas();
594
595 assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
596 let json = serde_json::to_string(&req).unwrap();
597 assert!(json.contains(r#""type":"allPerpMetas""#));
598 }
599
600 #[rstest]
601 fn test_info_request_outcome_meta() {
602 let req = InfoRequest::outcome_meta();
603
604 assert_eq!(req.request_type, HyperliquidInfoRequestType::OutcomeMeta);
605 assert!(matches!(req.params, InfoRequestParams::None));
606 let json = serde_json::to_string(&req).unwrap();
607 assert_eq!(json, r#"{"type":"outcomeMeta"}"#);
608 }
609
610 #[rstest]
611 fn test_info_request_l2_book() {
612 let req = InfoRequest::l2_book("BTC");
613
614 assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
615 let json = serde_json::to_string(&req).unwrap();
616 assert!(json.contains("\"coin\":\"BTC\""));
617 }
618
619 #[rstest]
620 fn test_info_request_recent_trades() {
621 let req = InfoRequest::recent_trades("BTC");
622
623 assert_eq!(req.request_type, HyperliquidInfoRequestType::RecentTrades);
624 let json = serde_json::to_string(&req).unwrap();
625 assert_eq!(json, r#"{"type":"recentTrades","coin":"BTC"}"#);
626 }
627
628 #[rstest]
629 fn test_info_request_open_orders_dex_serialization() {
630 let default = serde_json::to_value(InfoRequest::open_orders("0xabc")).unwrap();
631 let xyz =
632 serde_json::to_value(InfoRequest::open_orders_for_dex("0xabc", Some("xyz"))).unwrap();
633
634 assert_eq!(
635 default,
636 serde_json::json!({"type": "openOrders", "user": "0xabc"})
637 );
638 assert_eq!(
639 xyz,
640 serde_json::json!({"type": "openOrders", "user": "0xabc", "dex": "xyz"})
641 );
642 }
643
644 #[rstest]
645 fn test_info_request_frontend_open_orders_dex_serialization() {
646 let default = serde_json::to_value(InfoRequest::frontend_open_orders("0xabc")).unwrap();
647 let xyz = serde_json::to_value(InfoRequest::frontend_open_orders_for_dex(
648 "0xabc",
649 Some("xyz"),
650 ))
651 .unwrap();
652
653 assert_eq!(
654 default,
655 serde_json::json!({"type": "frontendOpenOrders", "user": "0xabc"})
656 );
657 assert_eq!(
658 xyz,
659 serde_json::json!({"type": "frontendOpenOrders", "user": "0xabc", "dex": "xyz"})
660 );
661 }
662
663 #[rstest]
664 fn test_info_request_clearinghouse_state_dex_serialization() {
665 let default = serde_json::to_value(InfoRequest::clearinghouse_state("0xabc")).unwrap();
666 let xyz = serde_json::to_value(InfoRequest::clearinghouse_state_for_dex(
667 "0xabc",
668 Some("xyz"),
669 ))
670 .unwrap();
671
672 assert_eq!(
673 default,
674 serde_json::json!({"type": "clearinghouseState", "user": "0xabc"})
675 );
676 assert_eq!(
677 xyz,
678 serde_json::json!({"type": "clearinghouseState", "user": "0xabc", "dex": "xyz"})
679 );
680 }
681
682 #[rstest]
683 fn test_info_request_spot_clearinghouse_state() {
684 let req = InfoRequest::spot_clearinghouse_state("0xabc");
685
686 assert_eq!(
687 req.request_type,
688 HyperliquidInfoRequestType::SpotClearinghouseState
689 );
690 let json = serde_json::to_string(&req).unwrap();
691 assert!(json.contains(r#""type":"spotClearinghouseState""#));
692 assert!(json.contains(r#""user":"0xabc""#));
693 }
694
695 #[rstest]
696 fn test_info_request_user_abstraction() {
697 let req = InfoRequest::user_abstraction("0xabc");
698
699 assert_eq!(
700 serde_json::to_value(&req).unwrap(),
701 serde_json::json!({"type": "userAbstraction", "user": "0xabc"})
702 );
703 }
704
705 #[rstest]
706 fn test_info_request_funding_history_with_end_time() {
707 let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, Some(1_700_003_600_000));
708
709 assert_eq!(req.request_type, HyperliquidInfoRequestType::FundingHistory);
710 let json = serde_json::to_string(&req).unwrap();
711 assert!(json.contains(r#""type":"fundingHistory""#));
712 assert!(json.contains(r#""coin":"BTC""#));
713 assert!(json.contains(r#""startTime":1700000000000"#));
714 assert!(json.contains(r#""endTime":1700003600000"#));
715 }
716
717 #[rstest]
718 fn test_info_request_funding_history_omits_end_time_when_none() {
719 let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, None);
722 let json = serde_json::to_string(&req).unwrap();
723 assert!(json.contains(r#""startTime":1700000000000"#));
724 assert!(
725 !json.contains("endTime"),
726 "endTime must be omitted when None; json={json}",
727 );
728 }
729
730 #[rstest]
731 fn test_exchange_action_order() {
732 let order = HyperliquidExchangePlaceOrderRequest {
733 asset: 0,
734 is_buy: true,
735 price: Decimal::new(50000, 0),
736 size: Decimal::new(1, 0),
737 reduce_only: false,
738 kind: HyperliquidExchangeOrderKind::Limit {
739 limit: HyperliquidExchangeLimitParams {
740 tif: HyperliquidExchangeTif::Gtc,
741 },
742 },
743 cloid: None,
744 };
745
746 let action = ExchangeAction::order(vec![order], None);
747
748 assert_eq!(action.action_type, ExchangeActionType::Order);
749 let json = serde_json::to_string(&action).unwrap();
750 assert!(json.contains("\"orders\""));
751 }
752
753 #[rstest]
754 fn test_exchange_action_cancel() {
755 let cancel = HyperliquidExchangeCancelByCloidRequest {
756 asset: 0,
757 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
758 };
759
760 let action = ExchangeAction::cancel(vec![cancel]);
761
762 assert_eq!(action.action_type, ExchangeActionType::Cancel);
763 }
764
765 #[rstest]
766 fn test_exchange_action_serialization() {
767 let order = HyperliquidExchangePlaceOrderRequest {
768 asset: 0,
769 is_buy: true,
770 price: Decimal::new(50000, 0),
771 size: Decimal::new(1, 0),
772 reduce_only: false,
773 kind: HyperliquidExchangeOrderKind::Limit {
774 limit: HyperliquidExchangeLimitParams {
775 tif: HyperliquidExchangeTif::Gtc,
776 },
777 },
778 cloid: None,
779 };
780
781 let action = ExchangeAction::order(vec![order], None);
782
783 let json = serde_json::to_string(&action).unwrap();
784 assert!(json.contains(r#""type":"order""#));
786 assert!(json.contains(r#""orders""#));
787 assert!(json.contains(r#""grouping":"na""#));
788 }
789
790 #[rstest]
791 fn test_exchange_action_type_as_ref() {
792 assert_eq!(ExchangeActionType::Order.as_ref(), "order");
793 assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
794 assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
795 assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
796 assert_eq!(
797 ExchangeActionType::UpdateLeverage.as_ref(),
798 "updateLeverage"
799 );
800 assert_eq!(
801 ExchangeActionType::UpdateIsolatedMargin.as_ref(),
802 "updateIsolatedMargin"
803 );
804 }
805
806 #[rstest]
807 fn test_update_leverage_serialization() {
808 let action = ExchangeAction::update_leverage(1, true, 10);
809 let json = serde_json::to_string(&action).unwrap();
810
811 assert!(json.contains(r#""type":"updateLeverage""#));
812 assert!(json.contains(r#""asset":1"#));
813 assert!(json.contains(r#""isCross":true"#));
814 assert!(json.contains(r#""leverage":10"#));
815 }
816
817 #[rstest]
818 fn test_update_isolated_margin_serialization() {
819 let action = ExchangeAction::update_isolated_margin(2, false, 1000);
820 let json = serde_json::to_string(&action).unwrap();
821
822 assert!(json.contains(r#""type":"updateIsolatedMargin""#));
823 assert!(json.contains(r#""asset":2"#));
824 assert!(json.contains(r#""isBuy":false"#));
825 assert!(json.contains(r#""ntli":1000"#));
826 }
827
828 #[rstest]
829 fn test_cancel_by_cloid_serialization() {
830 let cancel_request = HyperliquidExchangeCancelByCloidRequest {
831 asset: 0,
832 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
833 };
834 let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
835 let json = serde_json::to_string(&action).unwrap();
836
837 assert!(json.contains(r#""type":"cancelByCloid""#));
838 assert!(json.contains(r#""cancels""#));
839 }
840
841 #[rstest]
842 fn test_modify_serialization() {
843 let modify_request = HyperliquidExchangeModifyOrderRequest {
844 oid: 12345.into(),
845 order: HyperliquidExchangePlaceOrderRequest {
846 asset: 0,
847 is_buy: true,
848 price: Decimal::new(51000, 0),
849 size: Decimal::new(2, 0),
850 reduce_only: false,
851 kind: HyperliquidExchangeOrderKind::Limit {
852 limit: HyperliquidExchangeLimitParams {
853 tif: HyperliquidExchangeTif::Gtc,
854 },
855 },
856 cloid: None,
857 },
858 };
859 let action = ExchangeAction::modify(modify_request);
860 let json = serde_json::to_string(&action).unwrap();
861
862 assert!(json.contains(r#""type":"modify""#));
863 assert!(json.contains(r#""oid":12345"#));
864 assert!(json.contains(r#""order""#));
865 }
866}