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