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