1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
29#[serde(rename_all = "camelCase")]
30pub enum ExchangeActionType {
31 Order,
33 Cancel,
35 CancelByCloid,
37 Modify,
39 UpdateLeverage,
41 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#[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#[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#[derive(Debug, Clone, Serialize)]
77pub struct ModifyParams {
78 #[serde(flatten)]
79 pub request: HyperliquidExchangeModifyOrderRequest,
80}
81
82#[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#[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#[derive(Debug, Clone, Serialize)]
102pub struct L2BookParams {
103 pub coin: String,
104}
105
106#[derive(Debug, Clone, Serialize)]
108pub struct RecentTradesParams {
109 pub coin: String,
110}
111
112#[derive(Debug, Clone, Serialize)]
114pub struct UserFillsParams {
115 pub user: String,
116}
117
118#[derive(Debug, Clone, Serialize)]
120pub struct OrderStatusParams {
121 pub user: String,
122 pub oid: u64,
123}
124
125#[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#[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#[derive(Debug, Clone, Serialize)]
143pub struct SpotClearinghouseStateParams {
144 pub user: String,
145}
146
147#[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#[derive(Debug, Clone, Serialize)]
159pub struct CandleSnapshotParams {
160 pub req: CandleSnapshotReq,
161}
162
163#[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#[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#[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 pub fn meta() -> Self {
201 Self {
202 request_type: HyperliquidInfoRequestType::Meta,
203 params: InfoRequestParams::None,
204 }
205 }
206
207 pub fn all_perp_metas() -> Self {
209 Self {
210 request_type: HyperliquidInfoRequestType::AllPerpMetas,
211 params: InfoRequestParams::None,
212 }
213 }
214
215 pub fn perp_dexs() -> Self {
217 Self {
218 request_type: HyperliquidInfoRequestType::PerpDexs,
219 params: InfoRequestParams::None,
220 }
221 }
222
223 pub fn spot_meta() -> Self {
225 Self {
226 request_type: HyperliquidInfoRequestType::SpotMeta,
227 params: InfoRequestParams::None,
228 }
229 }
230
231 pub fn meta_and_asset_ctxs() -> Self {
233 Self {
234 request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
235 params: InfoRequestParams::None,
236 }
237 }
238
239 pub fn spot_meta_and_asset_ctxs() -> Self {
241 Self {
242 request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
243 params: InfoRequestParams::None,
244 }
245 }
246
247 pub fn outcome_meta() -> Self {
249 Self {
250 request_type: HyperliquidInfoRequestType::OutcomeMeta,
251 params: InfoRequestParams::None,
252 }
253 }
254
255 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 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 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 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 pub fn open_orders(user: &str) -> Self {
298 Self::open_orders_for_dex(user, None)
299 }
300
301 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 pub fn frontend_open_orders(user: &str) -> Self {
314 Self::frontend_open_orders_for_dex(user, None)
315 }
316
317 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 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 pub fn clearinghouse_state(user: &str) -> Self {
341 Self::clearinghouse_state_for_dex(user, None)
342 }
343
344 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 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 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 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 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#[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#[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 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 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 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 pub fn modify(request: HyperliquidExchangeModifyOrderRequest) -> Self {
479 Self {
480 action_type: ExchangeActionType::Modify,
481 params: ExchangeActionParams::Modify(ModifyParams { request }),
482 }
483 }
484
485 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 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 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 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}