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