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)]
130#[serde(rename_all = "camelCase")]
131pub struct CandleSnapshotReq {
132 pub coin: String,
133 pub interval: HyperliquidBarInterval,
134 pub start_time: u64,
135 pub end_time: u64,
136}
137
138#[derive(Debug, Clone, Serialize)]
140pub struct CandleSnapshotParams {
141 pub req: CandleSnapshotReq,
142}
143
144#[derive(Debug, Clone, Serialize)]
146#[serde(untagged)]
147pub enum InfoRequestParams {
148 L2Book(L2BookParams),
149 UserFills(UserFillsParams),
150 OrderStatus(OrderStatusParams),
151 OpenOrders(OpenOrdersParams),
152 ClearinghouseState(ClearinghouseStateParams),
153 CandleSnapshot(CandleSnapshotParams),
154 None,
155}
156
157#[derive(Debug, Clone, Serialize)]
159pub struct InfoRequest {
160 #[serde(rename = "type")]
161 pub request_type: HyperliquidInfoRequestType,
162 #[serde(flatten)]
163 pub params: InfoRequestParams,
164}
165
166impl InfoRequest {
167 pub fn meta() -> Self {
169 Self {
170 request_type: HyperliquidInfoRequestType::Meta,
171 params: InfoRequestParams::None,
172 }
173 }
174
175 pub fn all_perp_metas() -> Self {
177 Self {
178 request_type: HyperliquidInfoRequestType::AllPerpMetas,
179 params: InfoRequestParams::None,
180 }
181 }
182
183 pub fn spot_meta() -> Self {
185 Self {
186 request_type: HyperliquidInfoRequestType::SpotMeta,
187 params: InfoRequestParams::None,
188 }
189 }
190
191 pub fn meta_and_asset_ctxs() -> Self {
193 Self {
194 request_type: HyperliquidInfoRequestType::MetaAndAssetCtxs,
195 params: InfoRequestParams::None,
196 }
197 }
198
199 pub fn spot_meta_and_asset_ctxs() -> Self {
201 Self {
202 request_type: HyperliquidInfoRequestType::SpotMetaAndAssetCtxs,
203 params: InfoRequestParams::None,
204 }
205 }
206
207 pub fn l2_book(coin: &str) -> Self {
209 Self {
210 request_type: HyperliquidInfoRequestType::L2Book,
211 params: InfoRequestParams::L2Book(L2BookParams {
212 coin: coin.to_string(),
213 }),
214 }
215 }
216
217 pub fn user_fills(user: &str) -> Self {
219 Self {
220 request_type: HyperliquidInfoRequestType::UserFills,
221 params: InfoRequestParams::UserFills(UserFillsParams {
222 user: user.to_string(),
223 }),
224 }
225 }
226
227 pub fn order_status(user: &str, oid: u64) -> Self {
229 Self {
230 request_type: HyperliquidInfoRequestType::OrderStatus,
231 params: InfoRequestParams::OrderStatus(OrderStatusParams {
232 user: user.to_string(),
233 oid,
234 }),
235 }
236 }
237
238 pub fn open_orders(user: &str) -> Self {
240 Self {
241 request_type: HyperliquidInfoRequestType::OpenOrders,
242 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
243 user: user.to_string(),
244 }),
245 }
246 }
247
248 pub fn frontend_open_orders(user: &str) -> Self {
250 Self {
251 request_type: HyperliquidInfoRequestType::FrontendOpenOrders,
252 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
253 user: user.to_string(),
254 }),
255 }
256 }
257
258 pub fn clearinghouse_state(user: &str) -> Self {
260 Self {
261 request_type: HyperliquidInfoRequestType::ClearinghouseState,
262 params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
263 user: user.to_string(),
264 }),
265 }
266 }
267
268 pub fn user_fees(user: &str) -> Self {
270 Self {
271 request_type: HyperliquidInfoRequestType::UserFees,
272 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
273 user: user.to_string(),
274 }),
275 }
276 }
277
278 pub fn candle_snapshot(
280 coin: &str,
281 interval: HyperliquidBarInterval,
282 start_time: u64,
283 end_time: u64,
284 ) -> Self {
285 Self {
286 request_type: HyperliquidInfoRequestType::CandleSnapshot,
287 params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
288 req: CandleSnapshotReq {
289 coin: coin.to_string(),
290 interval,
291 start_time,
292 end_time,
293 },
294 }),
295 }
296 }
297}
298
299#[derive(Debug, Clone, Serialize)]
301#[serde(untagged)]
302pub enum ExchangeActionParams {
303 Order(OrderParams),
304 Cancel(CancelParams),
305 Modify(ModifyParams),
306 UpdateLeverage(UpdateLeverageParams),
307 UpdateIsolatedMargin(UpdateIsolatedMarginParams),
308}
309
310#[derive(Debug, Clone, Serialize)]
312pub struct ExchangeAction {
313 #[serde(rename = "type", serialize_with = "serialize_action_type")]
314 pub action_type: ExchangeActionType,
315 #[serde(flatten)]
316 pub params: ExchangeActionParams,
317}
318
319fn serialize_action_type<S>(
320 action_type: &ExchangeActionType,
321 serializer: S,
322) -> Result<S::Ok, S::Error>
323where
324 S: serde::Serializer,
325{
326 serializer.serialize_str(action_type.as_ref())
327}
328
329impl ExchangeAction {
330 pub fn order(
332 orders: Vec<HyperliquidExecPlaceOrderRequest>,
333 builder: Option<HyperliquidExecBuilderFee>,
334 ) -> Self {
335 Self {
336 action_type: ExchangeActionType::Order,
337 params: ExchangeActionParams::Order(OrderParams {
338 orders,
339 grouping: HyperliquidExecGrouping::Na,
340 builder,
341 }),
342 }
343 }
344
345 pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
347 Self {
348 action_type: ExchangeActionType::Cancel,
349 params: ExchangeActionParams::Cancel(CancelParams { cancels }),
350 }
351 }
352
353 pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
355 Self {
356 action_type: ExchangeActionType::CancelByCloid,
357 params: ExchangeActionParams::Cancel(CancelParams { cancels }),
358 }
359 }
360
361 pub fn modify(request: HyperliquidExecModifyOrderRequest) -> Self {
363 Self {
364 action_type: ExchangeActionType::Modify,
365 params: ExchangeActionParams::Modify(ModifyParams { request }),
366 }
367 }
368
369 pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
371 Self {
372 action_type: ExchangeActionType::UpdateLeverage,
373 params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
374 asset,
375 is_cross,
376 leverage,
377 }),
378 }
379 }
380
381 pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
383 Self {
384 action_type: ExchangeActionType::UpdateIsolatedMargin,
385 params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
386 asset,
387 is_buy,
388 ntli,
389 }),
390 }
391 }
392}
393
394#[cfg(test)]
395mod tests {
396 use rstest::rstest;
397 use rust_decimal::Decimal;
398
399 use super::*;
400 use crate::http::models::{
401 Cloid, HyperliquidExecCancelByCloidRequest, HyperliquidExecLimitParams,
402 HyperliquidExecModifyOrderRequest, HyperliquidExecOrderKind,
403 HyperliquidExecPlaceOrderRequest, HyperliquidExecTif,
404 };
405
406 #[rstest]
407 fn test_info_request_meta() {
408 let req = InfoRequest::meta();
409
410 assert_eq!(req.request_type, HyperliquidInfoRequestType::Meta);
411 assert!(matches!(req.params, InfoRequestParams::None));
412 }
413
414 #[rstest]
415 fn test_info_request_all_perp_metas() {
416 let req = InfoRequest::all_perp_metas();
417
418 assert_eq!(req.request_type, HyperliquidInfoRequestType::AllPerpMetas);
419 let json = serde_json::to_string(&req).unwrap();
420 assert!(json.contains(r#""type":"allPerpMetas""#));
421 }
422
423 #[rstest]
424 fn test_info_request_l2_book() {
425 let req = InfoRequest::l2_book("BTC");
426
427 assert_eq!(req.request_type, HyperliquidInfoRequestType::L2Book);
428 let json = serde_json::to_string(&req).unwrap();
429 assert!(json.contains("\"coin\":\"BTC\""));
430 }
431
432 #[rstest]
433 fn test_exchange_action_order() {
434 let order = HyperliquidExecPlaceOrderRequest {
435 asset: 0,
436 is_buy: true,
437 price: Decimal::new(50000, 0),
438 size: Decimal::new(1, 0),
439 reduce_only: false,
440 kind: HyperliquidExecOrderKind::Limit {
441 limit: HyperliquidExecLimitParams {
442 tif: HyperliquidExecTif::Gtc,
443 },
444 },
445 cloid: None,
446 };
447
448 let action = ExchangeAction::order(vec![order], None);
449
450 assert_eq!(action.action_type, ExchangeActionType::Order);
451 let json = serde_json::to_string(&action).unwrap();
452 assert!(json.contains("\"orders\""));
453 }
454
455 #[rstest]
456 fn test_exchange_action_cancel() {
457 let cancel = HyperliquidExecCancelByCloidRequest {
458 asset: 0,
459 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
460 };
461
462 let action = ExchangeAction::cancel(vec![cancel]);
463
464 assert_eq!(action.action_type, ExchangeActionType::Cancel);
465 }
466
467 #[rstest]
468 fn test_exchange_action_serialization() {
469 let order = HyperliquidExecPlaceOrderRequest {
470 asset: 0,
471 is_buy: true,
472 price: Decimal::new(50000, 0),
473 size: Decimal::new(1, 0),
474 reduce_only: false,
475 kind: HyperliquidExecOrderKind::Limit {
476 limit: HyperliquidExecLimitParams {
477 tif: HyperliquidExecTif::Gtc,
478 },
479 },
480 cloid: None,
481 };
482
483 let action = ExchangeAction::order(vec![order], None);
484
485 let json = serde_json::to_string(&action).unwrap();
486 assert!(json.contains(r#""type":"order""#));
488 assert!(json.contains(r#""orders""#));
489 assert!(json.contains(r#""grouping":"na""#));
490 }
491
492 #[rstest]
493 fn test_exchange_action_type_as_ref() {
494 assert_eq!(ExchangeActionType::Order.as_ref(), "order");
495 assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
496 assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
497 assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
498 assert_eq!(
499 ExchangeActionType::UpdateLeverage.as_ref(),
500 "updateLeverage"
501 );
502 assert_eq!(
503 ExchangeActionType::UpdateIsolatedMargin.as_ref(),
504 "updateIsolatedMargin"
505 );
506 }
507
508 #[rstest]
509 fn test_update_leverage_serialization() {
510 let action = ExchangeAction::update_leverage(1, true, 10);
511 let json = serde_json::to_string(&action).unwrap();
512
513 assert!(json.contains(r#""type":"updateLeverage""#));
514 assert!(json.contains(r#""asset":1"#));
515 assert!(json.contains(r#""isCross":true"#));
516 assert!(json.contains(r#""leverage":10"#));
517 }
518
519 #[rstest]
520 fn test_update_isolated_margin_serialization() {
521 let action = ExchangeAction::update_isolated_margin(2, false, 1000);
522 let json = serde_json::to_string(&action).unwrap();
523
524 assert!(json.contains(r#""type":"updateIsolatedMargin""#));
525 assert!(json.contains(r#""asset":2"#));
526 assert!(json.contains(r#""isBuy":false"#));
527 assert!(json.contains(r#""ntli":1000"#));
528 }
529
530 #[rstest]
531 fn test_cancel_by_cloid_serialization() {
532 let cancel_request = HyperliquidExecCancelByCloidRequest {
533 asset: 0,
534 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
535 };
536 let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
537 let json = serde_json::to_string(&action).unwrap();
538
539 assert!(json.contains(r#""type":"cancelByCloid""#));
540 assert!(json.contains(r#""cancels""#));
541 }
542
543 #[rstest]
544 fn test_modify_serialization() {
545 let modify_request = HyperliquidExecModifyOrderRequest {
546 oid: 12345,
547 order: HyperliquidExecPlaceOrderRequest {
548 asset: 0,
549 is_buy: true,
550 price: Decimal::new(51000, 0),
551 size: Decimal::new(2, 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::modify(modify_request);
562 let json = serde_json::to_string(&action).unwrap();
563
564 assert!(json.contains(r#""type":"modify""#));
565 assert!(json.contains(r#""oid":12345"#));
566 assert!(json.contains(r#""order""#));
567 }
568}