1use serde::Serialize;
17
18use crate::http::models::{
19 HyperliquidExecCancelByCloidRequest, HyperliquidExecModifyOrderRequest,
20 HyperliquidExecPlaceOrderRequest,
21};
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
25#[serde(rename_all = "camelCase")]
26pub enum ExchangeActionType {
27 Order,
29 Cancel,
31 CancelByCloid,
33 Modify,
35 UpdateLeverage,
37 UpdateIsolatedMargin,
39}
40
41impl AsRef<str> for ExchangeActionType {
42 fn as_ref(&self) -> &str {
43 match self {
44 Self::Order => "order",
45 Self::Cancel => "cancel",
46 Self::CancelByCloid => "cancelByCloid",
47 Self::Modify => "modify",
48 Self::UpdateLeverage => "updateLeverage",
49 Self::UpdateIsolatedMargin => "updateIsolatedMargin",
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize)]
56pub struct OrderParams {
57 pub orders: Vec<HyperliquidExecPlaceOrderRequest>,
58 pub grouping: String,
59}
60
61#[derive(Debug, Clone, Serialize)]
63pub struct CancelParams {
64 pub cancels: Vec<HyperliquidExecCancelByCloidRequest>,
65}
66
67#[derive(Debug, Clone, Serialize)]
69pub struct ModifyParams {
70 pub oid: u64,
71 pub order: HyperliquidExecModifyOrderRequest,
72}
73
74#[derive(Debug, Clone, Serialize)]
76#[serde(rename_all = "camelCase")]
77pub struct UpdateLeverageParams {
78 pub asset: u32,
79 pub is_cross: bool,
80 pub leverage: u32,
81}
82
83#[derive(Debug, Clone, Serialize)]
85#[serde(rename_all = "camelCase")]
86pub struct UpdateIsolatedMarginParams {
87 pub asset: u32,
88 pub is_buy: bool,
89 pub ntli: i64,
90}
91
92#[derive(Debug, Clone, Serialize)]
94pub struct L2BookParams {
95 pub coin: String,
96}
97
98#[derive(Debug, Clone, Serialize)]
100pub struct UserFillsParams {
101 pub user: String,
102}
103
104#[derive(Debug, Clone, Serialize)]
106pub struct OrderStatusParams {
107 pub user: String,
108 pub oid: u64,
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct OpenOrdersParams {
114 pub user: String,
115}
116
117#[derive(Debug, Clone, Serialize)]
119pub struct ClearinghouseStateParams {
120 pub user: String,
121}
122
123#[derive(Debug, Clone, Serialize)]
125#[serde(rename_all = "camelCase")]
126pub struct CandleSnapshotReq {
127 pub coin: String,
128 pub interval: String,
129 pub start_time: u64,
130 pub end_time: u64,
131}
132
133#[derive(Debug, Clone, Serialize)]
135pub struct CandleSnapshotParams {
136 pub req: CandleSnapshotReq,
137}
138
139#[derive(Debug, Clone, Serialize)]
141#[serde(untagged)]
142pub enum InfoRequestParams {
143 L2Book(L2BookParams),
144 UserFills(UserFillsParams),
145 OrderStatus(OrderStatusParams),
146 OpenOrders(OpenOrdersParams),
147 ClearinghouseState(ClearinghouseStateParams),
148 CandleSnapshot(CandleSnapshotParams),
149 None,
150}
151
152#[derive(Debug, Clone, Serialize)]
154pub struct InfoRequest {
155 #[serde(rename = "type")]
156 pub request_type: String,
157 #[serde(flatten)]
158 pub params: InfoRequestParams,
159}
160
161impl InfoRequest {
162 pub fn meta() -> Self {
164 Self {
165 request_type: "meta".to_string(),
166 params: InfoRequestParams::None,
167 }
168 }
169
170 pub fn spot_meta() -> Self {
172 Self {
173 request_type: "spotMeta".to_string(),
174 params: InfoRequestParams::None,
175 }
176 }
177
178 pub fn meta_and_asset_ctxs() -> Self {
180 Self {
181 request_type: "metaAndAssetCtxs".to_string(),
182 params: InfoRequestParams::None,
183 }
184 }
185
186 pub fn spot_meta_and_asset_ctxs() -> Self {
188 Self {
189 request_type: "spotMetaAndAssetCtxs".to_string(),
190 params: InfoRequestParams::None,
191 }
192 }
193
194 pub fn l2_book(coin: &str) -> Self {
196 Self {
197 request_type: "l2Book".to_string(),
198 params: InfoRequestParams::L2Book(L2BookParams {
199 coin: coin.to_string(),
200 }),
201 }
202 }
203
204 pub fn user_fills(user: &str) -> Self {
206 Self {
207 request_type: "userFills".to_string(),
208 params: InfoRequestParams::UserFills(UserFillsParams {
209 user: user.to_string(),
210 }),
211 }
212 }
213
214 pub fn order_status(user: &str, oid: u64) -> Self {
216 Self {
217 request_type: "orderStatus".to_string(),
218 params: InfoRequestParams::OrderStatus(OrderStatusParams {
219 user: user.to_string(),
220 oid,
221 }),
222 }
223 }
224
225 pub fn open_orders(user: &str) -> Self {
227 Self {
228 request_type: "openOrders".to_string(),
229 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
230 user: user.to_string(),
231 }),
232 }
233 }
234
235 pub fn frontend_open_orders(user: &str) -> Self {
237 Self {
238 request_type: "frontendOpenOrders".to_string(),
239 params: InfoRequestParams::OpenOrders(OpenOrdersParams {
240 user: user.to_string(),
241 }),
242 }
243 }
244
245 pub fn clearinghouse_state(user: &str) -> Self {
247 Self {
248 request_type: "clearinghouseState".to_string(),
249 params: InfoRequestParams::ClearinghouseState(ClearinghouseStateParams {
250 user: user.to_string(),
251 }),
252 }
253 }
254
255 pub fn candle_snapshot(coin: &str, interval: &str, start_time: u64, end_time: u64) -> Self {
263 Self {
264 request_type: "candleSnapshot".to_string(),
265 params: InfoRequestParams::CandleSnapshot(CandleSnapshotParams {
266 req: CandleSnapshotReq {
267 coin: coin.to_string(),
268 interval: interval.to_string(),
269 start_time,
270 end_time,
271 },
272 }),
273 }
274 }
275}
276
277#[derive(Debug, Clone, Serialize)]
279#[serde(untagged)]
280pub enum ExchangeActionParams {
281 Order(OrderParams),
282 Cancel(CancelParams),
283 Modify(ModifyParams),
284 UpdateLeverage(UpdateLeverageParams),
285 UpdateIsolatedMargin(UpdateIsolatedMarginParams),
286}
287
288#[derive(Debug, Clone, Serialize)]
290pub struct ExchangeAction {
291 #[serde(rename = "type", serialize_with = "serialize_action_type")]
292 pub action_type: ExchangeActionType,
293 #[serde(flatten)]
294 pub params: ExchangeActionParams,
295}
296
297fn serialize_action_type<S>(
298 action_type: &ExchangeActionType,
299 serializer: S,
300) -> Result<S::Ok, S::Error>
301where
302 S: serde::Serializer,
303{
304 serializer.serialize_str(action_type.as_ref())
305}
306
307impl ExchangeAction {
308 pub fn order(orders: Vec<HyperliquidExecPlaceOrderRequest>) -> Self {
310 Self {
311 action_type: ExchangeActionType::Order,
312 params: ExchangeActionParams::Order(OrderParams {
313 orders,
314 grouping: "na".to_string(),
315 }),
316 }
317 }
318
319 pub fn cancel(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
321 Self {
322 action_type: ExchangeActionType::Cancel,
323 params: ExchangeActionParams::Cancel(CancelParams { cancels }),
324 }
325 }
326
327 pub fn cancel_by_cloid(cancels: Vec<HyperliquidExecCancelByCloidRequest>) -> Self {
329 Self {
330 action_type: ExchangeActionType::CancelByCloid,
331 params: ExchangeActionParams::Cancel(CancelParams { cancels }),
332 }
333 }
334
335 pub fn modify(oid: u64, order: HyperliquidExecModifyOrderRequest) -> Self {
337 Self {
338 action_type: ExchangeActionType::Modify,
339 params: ExchangeActionParams::Modify(ModifyParams { oid, order }),
340 }
341 }
342
343 pub fn update_leverage(asset: u32, is_cross: bool, leverage: u32) -> Self {
345 Self {
346 action_type: ExchangeActionType::UpdateLeverage,
347 params: ExchangeActionParams::UpdateLeverage(UpdateLeverageParams {
348 asset,
349 is_cross,
350 leverage,
351 }),
352 }
353 }
354
355 pub fn update_isolated_margin(asset: u32, is_buy: bool, ntli: i64) -> Self {
357 Self {
358 action_type: ExchangeActionType::UpdateIsolatedMargin,
359 params: ExchangeActionParams::UpdateIsolatedMargin(UpdateIsolatedMarginParams {
360 asset,
361 is_buy,
362 ntli,
363 }),
364 }
365 }
366}
367
368#[cfg(test)]
373mod tests {
374 use rstest::rstest;
375
376 use super::*;
377
378 #[rstest]
379 fn test_info_request_meta() {
380 let req = InfoRequest::meta();
381
382 assert_eq!(req.request_type, "meta");
383 assert!(matches!(req.params, InfoRequestParams::None));
384 }
385
386 #[rstest]
387 fn test_info_request_l2_book() {
388 let req = InfoRequest::l2_book("BTC");
389
390 assert_eq!(req.request_type, "l2Book");
391 let json = serde_json::to_string(&req).unwrap();
392 assert!(json.contains("\"coin\":\"BTC\""));
393 }
394
395 #[rstest]
396 fn test_exchange_action_order() {
397 use rust_decimal::Decimal;
398
399 use crate::http::models::{
400 HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
401 HyperliquidExecTif,
402 };
403
404 let order = HyperliquidExecPlaceOrderRequest {
405 asset: 0,
406 is_buy: true,
407 price: Decimal::new(50000, 0),
408 size: Decimal::new(1, 0),
409 reduce_only: false,
410 kind: HyperliquidExecOrderKind::Limit {
411 limit: HyperliquidExecLimitParams {
412 tif: HyperliquidExecTif::Gtc,
413 },
414 },
415 cloid: None,
416 };
417
418 let action = ExchangeAction::order(vec![order]);
419
420 assert_eq!(action.action_type, ExchangeActionType::Order);
421 let json = serde_json::to_string(&action).unwrap();
422 assert!(json.contains("\"orders\""));
423 }
424
425 #[rstest]
426 fn test_exchange_action_cancel() {
427 use crate::http::models::HyperliquidExecCancelByCloidRequest;
428
429 let cancel = HyperliquidExecCancelByCloidRequest {
430 asset: 0,
431 cloid: crate::http::models::Cloid::from_hex("0x00000000000000000000000000000000")
432 .unwrap(),
433 };
434
435 let action = ExchangeAction::cancel(vec![cancel]);
436
437 assert_eq!(action.action_type, ExchangeActionType::Cancel);
438 }
439
440 #[rstest]
441 fn test_exchange_action_serialization() {
442 use rust_decimal::Decimal;
443
444 use crate::http::models::{
445 HyperliquidExecLimitParams, HyperliquidExecOrderKind, HyperliquidExecPlaceOrderRequest,
446 HyperliquidExecTif,
447 };
448
449 let order = HyperliquidExecPlaceOrderRequest {
450 asset: 0,
451 is_buy: true,
452 price: Decimal::new(50000, 0),
453 size: Decimal::new(1, 0),
454 reduce_only: false,
455 kind: HyperliquidExecOrderKind::Limit {
456 limit: HyperliquidExecLimitParams {
457 tif: HyperliquidExecTif::Gtc,
458 },
459 },
460 cloid: None,
461 };
462
463 let action = ExchangeAction::order(vec![order]);
464
465 let json = serde_json::to_string(&action).unwrap();
466 assert!(json.contains(r#""type":"order""#));
468 assert!(json.contains(r#""orders""#));
469 assert!(json.contains(r#""grouping":"na""#));
470 }
471
472 #[rstest]
473 fn test_exchange_action_type_as_ref() {
474 assert_eq!(ExchangeActionType::Order.as_ref(), "order");
475 assert_eq!(ExchangeActionType::Cancel.as_ref(), "cancel");
476 assert_eq!(ExchangeActionType::CancelByCloid.as_ref(), "cancelByCloid");
477 assert_eq!(ExchangeActionType::Modify.as_ref(), "modify");
478 assert_eq!(
479 ExchangeActionType::UpdateLeverage.as_ref(),
480 "updateLeverage"
481 );
482 assert_eq!(
483 ExchangeActionType::UpdateIsolatedMargin.as_ref(),
484 "updateIsolatedMargin"
485 );
486 }
487
488 #[rstest]
489 fn test_update_leverage_serialization() {
490 let action = ExchangeAction::update_leverage(1, true, 10);
491 let json = serde_json::to_string(&action).unwrap();
492
493 assert!(json.contains(r#""type":"updateLeverage""#));
494 assert!(json.contains(r#""asset":1"#));
495 assert!(json.contains(r#""isCross":true"#));
496 assert!(json.contains(r#""leverage":10"#));
497 }
498
499 #[rstest]
500 fn test_update_isolated_margin_serialization() {
501 let action = ExchangeAction::update_isolated_margin(2, false, 1000);
502 let json = serde_json::to_string(&action).unwrap();
503
504 assert!(json.contains(r#""type":"updateIsolatedMargin""#));
505 assert!(json.contains(r#""asset":2"#));
506 assert!(json.contains(r#""isBuy":false"#));
507 assert!(json.contains(r#""ntli":1000"#));
508 }
509
510 #[rstest]
511 fn test_cancel_by_cloid_serialization() {
512 use crate::http::models::{Cloid, HyperliquidExecCancelByCloidRequest};
513
514 let cancel_request = HyperliquidExecCancelByCloidRequest {
515 asset: 0,
516 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
517 };
518 let action = ExchangeAction::cancel_by_cloid(vec![cancel_request]);
519 let json = serde_json::to_string(&action).unwrap();
520
521 assert!(json.contains(r#""type":"cancelByCloid""#));
522 assert!(json.contains(r#""cancels""#));
523 }
524
525 #[rstest]
526 fn test_modify_serialization() {
527 use rust_decimal::Decimal;
528
529 use crate::http::models::HyperliquidExecModifyOrderRequest;
530
531 let modify_request = HyperliquidExecModifyOrderRequest {
532 asset: 0,
533 oid: 12345,
534 price: Some(Decimal::new(51000, 0)),
535 size: Some(Decimal::new(2, 0)),
536 reduce_only: None,
537 kind: None,
538 };
539 let action = ExchangeAction::modify(12345, modify_request);
540 let json = serde_json::to_string(&action).unwrap();
541
542 assert!(json.contains(r#""type":"modify""#));
543 assert!(json.contains(r#""oid":12345"#));
544 }
545}