1use serde::Serialize;
17
18use crate::{
19 common::enums::{HyperliquidBarInterval, HyperliquidInfoRequestType},
20 http::models::{
21 HyperliquidExecBuilderFee, HyperliquidExecCancelByCloidRequest, HyperliquidExecGrouping,
22 HyperliquidExecModifyOrderRequest, HyperliquidExecPlaceOrderRequest,
23 },
24};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
28#[serde(rename_all = "camelCase")]
29pub enum ExchangeActionType {
30 Order,
32 Cancel,
34 CancelByCloid,
36 Modify,
38 UpdateLeverage,
40 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#[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#[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#[derive(Debug, Clone, Serialize)]
76pub struct ModifyParams {
77 #[serde(flatten)]
78 pub request: HyperliquidExecModifyOrderRequest,
79}
80
81#[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#[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#[derive(Debug, Clone, Serialize)]
101pub struct L2BookParams {
102 pub coin: String,
103}
104
105#[derive(Debug, Clone, Serialize)]
107pub struct RecentTradesParams {
108 pub coin: String,
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct UserFillsParams {
114 pub user: String,
115}
116
117#[derive(Debug, Clone, Serialize)]
119pub struct OrderStatusParams {
120 pub user: String,
121 pub oid: u64,
122}
123
124#[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#[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#[derive(Debug, Clone, Serialize)]
142pub struct SpotClearinghouseStateParams {
143 pub user: String,
144}
145
146#[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#[derive(Debug, Clone, Serialize)]
158pub struct CandleSnapshotParams {
159 pub req: CandleSnapshotReq,
160}
161
162#[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#[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#[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 pub fn meta() -> Self {
200 Self {
201 request_type: HyperliquidInfoRequestType::Meta,
202 params: InfoRequestParams::None,
203 }
204 }
205
206 pub fn all_perp_metas() -> Self {
208 Self {
209 request_type: HyperliquidInfoRequestType::AllPerpMetas,
210 params: InfoRequestParams::None,
211 }
212 }
213
214 pub fn perp_dexs() -> Self {
216 Self {
217 request_type: HyperliquidInfoRequestType::PerpDexs,
218 params: InfoRequestParams::None,
219 }
220 }
221
222 pub fn spot_meta() -> Self {
224 Self {
225 request_type: HyperliquidInfoRequestType::SpotMeta,
226 params: InfoRequestParams::None,
227 }
228 }
229
230 pub fn meta_and_asset_ctxs() -> Self {
232 Self {
233 request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
234 params: InfoRequestParams::None,
235 }
236 }
237
238 pub fn spot_meta_and_asset_ctxs() -> Self {
240 Self {
241 request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
242 params: InfoRequestParams::None,
243 }
244 }
245
246 pub fn outcome_meta() -> Self {
248 Self {
249 request_type: HyperliquidInfoRequestType::OutcomeMeta,
250 params: InfoRequestParams::None,
251 }
252 }
253
254 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 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 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 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 pub fn open_orders(user: &str) -> Self {
297 Self::open_orders_for_dex(user, None)
298 }
299
300 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 pub fn frontend_open_orders(user: &str) -> Self {
313 Self::frontend_open_orders_for_dex(user, None)
314 }
315
316 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 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 pub fn clearinghouse_state(user: &str) -> Self {
340 Self::clearinghouse_state_for_dex(user, None)
341 }
342
343 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 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 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 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 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#[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#[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 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 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 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 pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
478 Self {
479 action_type: ExchangeActionType::Modify,
480 params: ExchangeActionParams::Modify(ModifyParams { request }),
481 }
482 }
483
484 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 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 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 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}