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