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}
129
130#[derive(Debug, Clone, Serialize)]
132pub struct ClearinghouseStateParams {
133 pub user: String,
134}
135
136#[derive(Debug, Clone, Serialize)]
138pub struct SpotClearinghouseStateParams {
139 pub user: String,
140}
141
142#[derive(Debug, Clone, Serialize)]
144#[serde(rename_all = "camelCase")]
145pub struct CandleSnapshotReq {
146 pub coin: String,
147 pub interval: HyperliquidBarInterval,
148 pub start_time: u64,
149 pub end_time: u64,
150}
151
152#[derive(Debug, Clone, Serialize)]
154pub struct CandleSnapshotParams {
155 pub req: CandleSnapshotReq,
156}
157
158#[derive(Debug, Clone, Serialize)]
160#[serde(rename_all = "camelCase")]
161pub struct FundingHistoryParams {
162 pub coin: String,
163 pub start_time: u64,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub end_time: Option<u64>,
166}
167
168#[derive(Debug, Clone, Serialize)]
170#[serde(untagged)]
171pub enum InfoRequestParams {
172 L2Book(L2BookParams),
173 RecentTrades(RecentTradesParams),
174 UserFills(UserFillsParams),
175 OrderStatus(OrderStatusParams),
176 OpenOrders(OpenOrdersParams),
177 ClearinghouseState(ClearinghouseStateParams),
178 SpotClearinghouseState(SpotClearinghouseStateParams),
179 CandleSnapshot(CandleSnapshotParams),
180 FundingHistory(FundingHistoryParams),
181 None,
182}
183
184#[derive(Debug, Clone, Serialize)]
186pub struct InfoRequest {
187 #[serde(rename = "type")]
188 pub request_type: HyperliquidInfoRequestType,
189 #[serde(flatten)]
190 pub params: InfoRequestParams,
191}
192
193impl InfoRequest {
194 pub fn meta() -> Self {
196 Self {
197 request_type: HyperliquidInfoRequestType::Meta,
198 params: InfoRequestParams::None,
199 }
200 }
201
202 pub fn all_perp_metas() -> Self {
204 Self {
205 request_type: HyperliquidInfoRequestType::AllPerpMetas,
206 params: InfoRequestParams::None,
207 }
208 }
209
210 pub fn perp_dexs() -> Self {
212 Self {
213 request_type: HyperliquidInfoRequestType::PerpDexs,
214 params: InfoRequestParams::None,
215 }
216 }
217
218 pub fn spot_meta() -> Self {
220 Self {
221 request_type: HyperliquidInfoRequestType::SpotMeta,
222 params: InfoRequestParams::None,
223 }
224 }
225
226 pub fn meta_and_asset_ctxs() -> Self {
228 Self {
229 request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
230 params: InfoRequestParams::None,
231 }
232 }
233
234 pub fn spot_meta_and_asset_ctxs() -> Self {
236 Self {
237 request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
238 params: InfoRequestParams::None,
239 }
240 }
241
242 pub fn outcome_meta() -> Self {
244 Self {
245 request_type: HyperliquidInfoRequestType::OutcomeMeta,
246 params: InfoRequestParams::None,
247 }
248 }
249
250 pub fn l2_book(coin: &str) -> Self {
252 Self {
253 request_type: HyperliquidInfoRequestType::L2Book,
254 params: InfoRequestParams::L2Book(L2BookParams {
255 coin: coin.to_string(),
256 }),
257 }
258 }
259
260 pub fn recent_trades(coin: &str) -> Self {
262 Self {
263 request_type: HyperliquidInfoRequestType::RecentTrades,
264 params: InfoRequestParams::RecentTrades(RecentTradesParams {
265 coin: coin.to_string(),
266 }),
267 }
268 }
269
270 pub fn user_fills(user: &str) -> Self {
272 Self {
273 request_type: HyperliquidInfoRequestType::UserFills,
274 params: InfoRequestParams::UserFills(UserFillsParams {
275 user: user.to_string(),
276 }),
277 }
278 }
279
280 pub fn order_status(user: &str, oid: u64) -> Self {
282 Self {
283 request_type: HyperliquidInfoRequestType::OrderStatus,
284 params: InfoRequestParams::OrderStatus(OrderStatusParams {
285 user: user.to_string(),
286 oid,
287 }),
288 }
289 }
290
291 pub fn open_orders(user: &str) -> Self {
293 Self {
294 request_type: HyperliquidInfoRequestType::OpenOrders,
295 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
296 user: user.to_string(),
297 }),
298 }
299 }
300
301 pub fn frontend_open_orders(user: &str) -> Self {
303 Self {
304 request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
305 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
306 user: user.to_string(),
307 }),
308 }
309 }
310
311 pub fn historical_orders(user: &str) -> Self {
313 Self {
314 request_type: HyperliquidInfoRequestType::HistoricalOrders,
315 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
316 user: user.to_string(),
317 }),
318 }
319 }
320
321 pub fn clearinghouse_state(user: &str) -> Self {
323 Self {
324 request_type: HyperliquidInfoRequestType::ClearinghouseState,
325 params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
326 user: user.to_string(),
327 }),
328 }
329 }
330
331 pub fn spot_clearinghouse_state(user: &str) -> Self {
333 Self {
334 request_type: HyperliquidInfoRequestType::SpotClearinghouseState,
335 params: InfoRequestParams::SpotClearinghouseState(SpotClearinghouseStateParams {
336 user: user.to_string(),
337 }),
338 }
339 }
340
341 pub fn user_fees(user: &str) -> Self {
343 Self {
344 request_type: HyperliquidInfoRequestType::UserFees,
345 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
346 user: user.to_string(),
347 }),
348 }
349 }
350
351 pub fn candle_snapshot(
353 coin: &str,
354 interval: HyperliquidBarInterval,
355 start_time: u64,
356 end_time: u64,
357 ) -> Self {
358 Self {
359 request_type: HyperliquidInfoRequestType::CandleSnapshot,
360 params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
361 req: CandleSnapshotReq {
362 coin: coin.to_string(),
363 interval,
364 start_time,
365 end_time,
366 },
367 }),
368 }
369 }
370
371 pub fn funding_history(coin: &str, start_time: u64, end_time: Option<u64>) -> Self {
373 Self {
374 request_type: HyperliquidInfoRequestType::FundingHistory,
375 params: InfoRequestParams::FundingHistory(FundingHistoryParams {
376 coin: coin.to_string(),
377 start_time,
378 end_time,
379 }),
380 }
381 }
382}
383
384#[derive(Debug, Clone, Serialize)]
386#[serde(untagged)]
387pub enum ExchangeActionParams {
388 Order(OrderParams),
389 Cancel(CancelParams),
390 Modify(ModifyParams),
391 UpdateLeverage(UpdateLeverageParams),
392 UpdateIsolatedMargin(UpdateIsolatedMarginParams),
393}
394
395#[derive(Debug, Clone, Serialize)]
397pub struct ExchangeAction {
398 #[serde(rename = "type", serialize_with = "serialize_action_type")]
399 pub action_type: ExchangeActionType,
400 #[serde(flatten)]
401 pub params: ExchangeActionParams,
402}
403
404fn serialize_action_type<S>(
405 action_type: &ExchangeActionType,
406 serializer: S,
407) -> Result<S::Ok, S::Error>
408where
409 S: serde::Serializer,
410{
411 serializer.serialize_str(action_type.as_ref())
412}
413
414impl ExchangeAction {
415 pub fn order(
417 orders: Vec<HyperliquidExecPlaceOrderRequest>,
418 builder: Option<HyperliquidExecBuilderFee>,
419 ) -> Self {
420 Self {
421 action_type: ExchangeActionType::Order,
422 params: ExchangeActionParams::Order(OrderParams {
423 orders,
424 grouping: HyperliquidExecGrouping::Na,
425 builder,
426 }),
427 }
428 }
429
430 pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
432 Self {
433 action_type: ExchangeActionType::Cancel,
434 params: ExchangeActionParams::Cancel(CancelParams {
435 cancels,
436 fast: None,
437 }),
438 }
439 }
440
441 pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
443 Self {
444 action_type: ExchangeActionType::CancelByCloid,
445 params: ExchangeActionParams::Cancel(CancelParams {
446 cancels,
447 fast: None,
448 }),
449 }
450 }
451
452 pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
454 Self {
455 action_type: ExchangeActionType::Modify,
456 params: ExchangeActionParams::Modify(ModifyParams { request }),
457 }
458 }
459
460 pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
462 Self {
463 action_type: ExchangeActionType::UpdateLeverage,
464 params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
465 asset,
466 is_cross,
467 leverage,
468 }),
469 }
470 }
471
472 pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
474 Self {
475 action_type: ExchangeActionType::UpdateIsolatedMargin,
476 params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
477 asset,
478 is_buy,
479 ntli,
480 }),
481 }
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use rstest::rstest;
488 use rust_decimal::Decimal;
489
490 use super::*;
491 use crate::http::models::{
492 Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
493 HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
494 HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
495 };
496
497 #[rstest]
498 fn test_info_request_meta() {
499 let req = InfoRequest::meta();
500
501 assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
502 assert!(matches!(req.params, InfoRequestParams::None));
503 }
504
505 #[rstest]
506 fn test_info_request_all_perp_metas() {
507 let req = InfoRequest::all_perp_metas();
508
509 assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
510 let json = serde_json::to_string(&req).unwrap();
511 assert!(json.contains(r#""type":"allPerpMetas""#));
512 }
513
514 #[rstest]
515 fn test_info_request_outcome_meta() {
516 let req = InfoRequest::outcome_meta();
517
518 assert_eq!(req.request_type, HyperliquidInfoRequestType::OutcomeMeta);
519 assert!(matches!(req.params, InfoRequestParams::None));
520 let json = serde_json::to_string(&req).unwrap();
521 assert_eq!(json, r#"{"type":"outcomeMeta"}"#);
522 }
523
524 #[rstest]
525 fn test_info_request_l2_book() {
526 let req = InfoRequest::l2_book("BTC");
527
528 assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
529 let json = serde_json::to_string(&req).unwrap();
530 assert!(json.contains("\"coin\":\"BTC\""));
531 }
532
533 #[rstest]
534 fn test_info_request_recent_trades() {
535 let req = InfoRequest::recent_trades("BTC");
536
537 assert_eq!(req.request_type, HyperliquidInfoRequestType::RecentTrades);
538 let json = serde_json::to_string(&req).unwrap();
539 assert_eq!(json, r#"{"type":"recentTrades","coin":"BTC"}"#);
540 }
541
542 #[rstest]
543 fn test_info_request_spot_clearinghouse_state() {
544 let req = InfoRequest::spot_clearinghouse_state("0xabc");
545
546 assert_eq!(
547 req.request_type,
548 HyperliquidInfoRequestType::SpotClearinghouseState
549 );
550 let json = serde_json::to_string(&req).unwrap();
551 assert!(json.contains(r#""type":"spotClearinghouseState""#));
552 assert!(json.contains(r#""user":"0xabc""#));
553 }
554
555 #[rstest]
556 fn test_info_request_funding_history_with_end_time() {
557 let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, Some(1_700_003_600_000));
558
559 assert_eq!(req.request_type, HyperliquidInfoRequestType::FundingHistory);
560 let json = serde_json::to_string(&req).unwrap();
561 assert!(json.contains(r#""type":"fundingHistory""#));
562 assert!(json.contains(r#""coin":"BTC""#));
563 assert!(json.contains(r#""startTime":1700000000000"#));
564 assert!(json.contains(r#""endTime":1700003600000"#));
565 }
566
567 #[rstest]
568 fn test_info_request_funding_history_omits_end_time_when_none() {
569 let req = InfoRequest::funding_history("BTC", 1_700_000_000_000, None);
572 let json = serde_json::to_string(&req).unwrap();
573 assert!(json.contains(r#""startTime":1700000000000"#));
574 assert!(
575 !json.contains("endTime"),
576 "endTime must be omitted when None; json={json}",
577 );
578 }
579
580 #[rstest]
581 fn test_exchange_action_order() {
582 let order = HyperliquidExecPlaceOrderRequest {
583 asset: 0,
584 is_buy: true,
585 price: Decimal::new(50000, 0),
586 size: Decimal::new(1, 0),
587 reduce_only: false,
588 kind: HyperliquidExecOrderKind::Limit {
589 limit: HyperliquidExecLimitParams {
590 tif: HyperliquidExecTif::Gtc,
591 },
592 },
593 cloid: None,
594 };
595
596 let action = ExchangeAction::order(vec![order], None);
597
598 assert_eq!(action.action_type, ExchangeActionType::Order);
599 let json = serde_json::to_string(&action).unwrap();
600 assert!(json.contains("\"orders\""));
601 }
602
603 #[rstest]
604 fn test_exchange_action_cancel() {
605 let cancel = HyperliquidExecCancelByCloidRequest {
606 asset: 0,
607 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
608 };
609
610 let action = ExchangeAction::cancel(vec![cancel]);
611
612 assert_eq!(action.action_type, ExchangeActionType::Cancel);
613 }
614
615 #[rstest]
616 fn test_exchange_action_serialization() {
617 let order = HyperliquidExecPlaceOrderRequest {
618 asset: 0,
619 is_buy: true,
620 price: Decimal::new(50000, 0),
621 size: Decimal::new(1, 0),
622 reduce_only: false,
623 kind: HyperliquidExecOrderKind::Limit {
624 limit: HyperliquidExecLimitParams {
625 tif: HyperliquidExecTif::Gtc,
626 },
627 },
628 cloid: None,
629 };
630
631 let action = ExchangeAction::order(vec![order], None);
632
633 let json = serde_json::to_string(&action).unwrap();
634 assert!(json.contains(r#""type":"order""#));
636 assert!(json.contains(r#""orders""#));
637 assert!(json.contains(r#""grouping":"na""#));
638 }
639
640 #[rstest]
641 fn test_exchange_action_type_as_ref() {
642 assert_eq!(ExchangeActionType::Order.as_ref(), "order");
643 assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
644 assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
645 assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
646 assert_eq!(
647 ExchangeActionType::UpdateLeverage.as_ref(),
648 "updateLeverage"
649 );
650 assert_eq!(
651 ExchangeActionType::UpdateIsolatedMargin.as_ref(),
652 "updateIsolatedMargin"
653 );
654 }
655
656 #[rstest]
657 fn test_update_leverage_serialization() {
658 let action = ExchangeAction::update_leverage(1, true, 10);
659 let json = serde_json::to_string(&action).unwrap();
660
661 assert!(json.contains(r#""type":"updateLeverage""#));
662 assert!(json.contains(r#""asset":1"#));
663 assert!(json.contains(r#""isCross":true"#));
664 assert!(json.contains(r#""leverage":10"#));
665 }
666
667 #[rstest]
668 fn test_update_isolated_margin_serialization() {
669 let action = ExchangeAction::update_isolated_margin(2, false, 1000);
670 let json = serde_json::to_string(&action).unwrap();
671
672 assert!(json.contains(r#""type":"updateIsolatedMargin""#));
673 assert!(json.contains(r#""asset":2"#));
674 assert!(json.contains(r#""isBuy":false"#));
675 assert!(json.contains(r#""ntli":1000"#));
676 }
677
678 #[rstest]
679 fn test_cancel_by_cloid_serialization() {
680 let cancel_request = HyperliquidExecCancelByCloidRequest {
681 asset: 0,
682 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
683 };
684 let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
685 let json = serde_json::to_string(&action).unwrap();
686
687 assert!(json.contains(r#""type":"cancelByCloid""#));
688 assert!(json.contains(r#""cancels""#));
689 }
690
691 #[rstest]
692 fn test_modify_serialization() {
693 let modify_request = HyperliquidExecModifyOrderRequest {
694 oid: 12345.into(),
695 order: HyperliquidExecPlaceOrderRequest {
696 asset: 0,
697 is_buy: true,
698 price: Decimal::new(51000, 0),
699 size: Decimal::new(2, 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::modify(modify_request);
710 let json = serde_json::to_string(&action).unwrap();
711
712 assert!(json.contains(r#""type":"modify""#));
713 assert!(json.contains(r#""oid":12345"#));
714 assert!(json.contains(r#""order""#));
715 }
716}