1use std::collections::HashMap;
17
18use serde::{Deserialize, Serialize};
19
20#[derive(Debug, Clone, Serialize)]
22#[serde(tag = "method")]
23#[serde(rename_all = "lowercase")]
24pub enum HyperliquidWsRequest {
25 Subscribe {
27 subscription: SubscriptionRequest,
29 },
30 Unsubscribe {
32 subscription: SubscriptionRequest,
34 },
35 Post {
37 id: u64,
39 request: PostRequest,
41 },
42 Ping,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
48#[serde(tag = "type")]
49#[serde(rename_all = "camelCase")]
50pub enum SubscriptionRequest {
51 AllMids {
53 #[serde(skip_serializing_if = "Option::is_none")]
54 dex: Option<String>,
55 },
56 Notification { user: String },
58 WebData2 { user: String },
60 Candle { coin: String, interval: String },
62 L2Book {
64 coin: String,
65 #[serde(skip_serializing_if = "Option::is_none")]
66 #[serde(rename = "nSigFigs")]
67 n_sig_figs: Option<u32>,
68 #[serde(skip_serializing_if = "Option::is_none")]
69 mantissa: Option<u32>,
70 },
71 Trades { coin: String },
73 OrderUpdates { user: String },
75 UserEvents { user: String },
77 UserFills {
79 user: String,
80 #[serde(skip_serializing_if = "Option::is_none")]
81 #[serde(rename = "aggregateByTime")]
82 aggregate_by_time: Option<bool>,
83 },
84 UserFundings { user: String },
86 UserNonFundingLedgerUpdates { user: String },
88 ActiveAssetCtx { coin: String },
90 ActiveAssetData { user: String, coin: String },
92 UserTwapSliceFills { user: String },
94 UserTwapHistory { user: String },
96 Bbo { coin: String },
98}
99
100#[derive(Debug, Clone, Serialize)]
102#[serde(tag = "type")]
103#[serde(rename_all = "lowercase")]
104pub enum PostRequest {
105 Info { payload: serde_json::Value },
107 Action { payload: ActionPayload },
109}
110
111#[derive(Debug, Clone, Serialize)]
113pub struct ActionPayload {
114 pub action: ActionRequest,
115 pub nonce: u64,
116 pub signature: SignatureData,
117 #[serde(skip_serializing_if = "Option::is_none")]
118 #[serde(rename = "vaultAddress")]
119 pub vault_address: Option<String>,
120}
121
122#[derive(Debug, Clone, Serialize)]
124pub struct SignatureData {
125 pub r: String,
126 pub s: String,
127 pub v: String,
128}
129
130#[derive(Debug, Clone, Serialize)]
132#[serde(tag = "type")]
133#[serde(rename_all = "lowercase")]
134pub enum ActionRequest {
135 Order {
137 orders: Vec<OrderRequest>,
138 grouping: String,
139 },
140 Cancel { cancels: Vec<CancelRequest> },
142 CancelByCloid { cancels: Vec<CancelByCloidRequest> },
144 Modify { modifies: Vec<ModifyRequest> },
146}
147
148#[derive(Debug, Clone, Serialize)]
150pub struct OrderRequest {
151 pub a: u32,
153 pub b: bool,
155 pub p: String,
157 pub s: String,
159 pub r: bool,
161 pub t: OrderTypeRequest,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub c: Option<String>,
166}
167
168#[derive(Debug, Clone, Serialize)]
170#[serde(tag = "type")]
171#[serde(rename_all = "lowercase")]
172pub enum OrderTypeRequest {
173 Limit {
174 tif: TimeInForceRequest,
175 },
176 Trigger {
177 #[serde(rename = "isMarket")]
178 is_market: bool,
179 #[serde(rename = "triggerPx")]
180 trigger_px: String,
181 tpsl: TpSlRequest,
182 },
183}
184
185#[derive(Debug, Clone, Serialize)]
187#[serde(rename_all = "PascalCase")]
188pub enum TimeInForceRequest {
189 Alo,
190 Ioc,
191 Gtc,
192}
193
194#[derive(Debug, Clone, Serialize)]
196#[serde(rename_all = "lowercase")]
197pub enum TpSlRequest {
198 Tp,
199 Sl,
200}
201
202#[derive(Debug, Clone, Serialize)]
204pub struct CancelRequest {
205 pub a: u32,
207 pub o: u64,
209}
210
211#[derive(Debug, Clone, Serialize)]
213pub struct CancelByCloidRequest {
214 pub asset: u32,
216 pub cloid: String,
218}
219
220#[derive(Debug, Clone, Serialize)]
222pub struct ModifyRequest {
223 pub oid: u64,
225 pub order: OrderRequest,
227}
228
229#[derive(Debug, Clone, Deserialize)]
231#[serde(tag = "channel")]
232#[serde(rename_all = "camelCase")]
233pub enum HyperliquidWsMessage {
234 SubscriptionResponse { data: SubscriptionRequest },
236 Post { data: PostResponse },
238 AllMids { data: AllMidsData },
240 Notification { data: NotificationData },
242 WebData2 { data: serde_json::Value },
244 Candle { data: Vec<CandleData> },
246 L2Book { data: WsBookData },
248 Trades { data: Vec<WsTradeData> },
250 OrderUpdates { data: Vec<WsOrderData> },
252 UserEvents { data: WsUserEventData },
254 UserFills { data: WsUserFillsData },
256 UserFundings { data: WsUserFundingsData },
258 UserNonFundingLedgerUpdates { data: serde_json::Value },
260 ActiveAssetCtx { data: WsActiveAssetCtxData },
262 ActiveAssetData { data: WsActiveAssetData },
264 UserTwapSliceFills { data: WsUserTwapSliceFillsData },
266 UserTwapHistory { data: WsUserTwapHistoryData },
268 Bbo { data: WsBboData },
270 Pong,
272}
273
274#[derive(Debug, Clone, Deserialize)]
276pub struct PostResponse {
277 pub id: u64,
278 pub response: PostResponsePayload,
279}
280
281#[derive(Debug, Clone, Deserialize)]
283#[serde(tag = "type")]
284#[serde(rename_all = "lowercase")]
285pub enum PostResponsePayload {
286 Info { payload: serde_json::Value },
287 Action { payload: serde_json::Value },
288 Error { payload: String },
289}
290
291#[derive(Debug, Clone, Deserialize)]
293pub struct AllMidsData {
294 pub mids: HashMap<String, String>,
295}
296
297#[derive(Debug, Clone, Deserialize)]
299pub struct NotificationData {
300 pub notification: String,
301}
302
303#[derive(Debug, Clone, Deserialize)]
305pub struct CandleData {
306 pub t: u64,
308 #[serde(rename = "T")]
310 pub close_time: u64,
311 pub s: String,
313 pub i: String,
315 pub o: f64,
317 pub c: f64,
319 pub h: f64,
321 pub l: f64,
323 pub v: f64,
325 pub n: u32,
327}
328
329#[derive(Debug, Clone, Deserialize)]
331pub struct WsBookData {
332 pub coin: String,
333 pub levels: [Vec<WsLevelData>; 2], pub time: u64,
335}
336
337#[derive(Debug, Clone, Deserialize)]
339pub struct WsLevelData {
340 pub px: String,
342 pub sz: String,
344 pub n: u32,
346}
347
348#[derive(Debug, Clone, Deserialize)]
350pub struct WsTradeData {
351 pub coin: String,
352 pub side: String,
353 pub px: String,
354 pub sz: String,
355 pub hash: String,
356 pub time: u64,
357 pub tid: u64,
358 pub users: [String; 2], }
360
361#[derive(Debug, Clone, Deserialize)]
363pub struct WsOrderData {
364 pub order: WsBasicOrderData,
365 pub status: String,
366 #[serde(rename = "statusTimestamp")]
367 pub status_timestamp: u64,
368}
369
370#[derive(Debug, Clone, Deserialize)]
372pub struct WsBasicOrderData {
373 pub coin: String,
374 pub side: String,
375 #[serde(rename = "limitPx")]
376 pub limit_px: String,
377 pub sz: String,
378 pub oid: u64,
379 pub timestamp: u64,
380 #[serde(rename = "origSz")]
381 pub orig_sz: String,
382 pub cloid: Option<String>,
383}
384
385#[derive(Debug, Clone, Deserialize)]
387#[serde(untagged)]
388pub enum WsUserEventData {
389 Fills {
390 fills: Vec<WsFillData>,
391 },
392 Funding {
393 funding: WsUserFundingData,
394 },
395 Liquidation {
396 liquidation: WsLiquidationData,
397 },
398 NonUserCancel {
399 #[serde(rename = "nonUserCancel")]
400 non_user_cancel: Vec<WsNonUserCancelData>,
401 },
402}
403
404#[derive(Debug, Clone, Deserialize)]
406pub struct WsFillData {
407 pub coin: String,
408 pub px: String,
409 pub sz: String,
410 pub side: String,
411 pub time: u64,
412 #[serde(rename = "startPosition")]
413 pub start_position: String,
414 pub dir: String,
415 #[serde(rename = "closedPnl")]
416 pub closed_pnl: String,
417 pub hash: String,
418 pub oid: u64,
419 pub crossed: bool,
420 pub fee: String,
421 pub tid: u64,
422 pub liquidation: Option<FillLiquidationData>,
423 #[serde(rename = "feeToken")]
424 pub fee_token: String,
425 #[serde(rename = "builderFee")]
426 pub builder_fee: Option<String>,
427}
428
429#[derive(Debug, Clone, Deserialize)]
431pub struct FillLiquidationData {
432 #[serde(rename = "liquidatedUser")]
433 pub liquidated_user: Option<String>,
434 #[serde(rename = "markPx")]
435 pub mark_px: f64,
436 pub method: String, }
438
439#[derive(Debug, Clone, Deserialize)]
441pub struct WsUserFundingData {
442 pub time: u64,
443 pub coin: String,
444 pub usdc: String,
445 pub szi: String,
446 #[serde(rename = "fundingRate")]
447 pub funding_rate: String,
448}
449
450#[derive(Debug, Clone, Deserialize)]
452pub struct WsLiquidationData {
453 pub lid: u64,
454 pub liquidator: String,
455 pub liquidated_user: String,
456 pub liquidated_ntl_pos: String,
457 pub liquidated_account_value: String,
458}
459
460#[derive(Debug, Clone, Deserialize)]
462pub struct WsNonUserCancelData {
463 pub coin: String,
464 pub oid: u64,
465}
466
467#[derive(Debug, Clone, Deserialize)]
469pub struct WsUserFillsData {
470 #[serde(rename = "isSnapshot")]
471 pub is_snapshot: Option<bool>,
472 pub user: String,
473 pub fills: Vec<WsFillData>,
474}
475
476#[derive(Debug, Clone, Deserialize)]
478pub struct WsUserFundingsData {
479 #[serde(rename = "isSnapshot")]
480 pub is_snapshot: Option<bool>,
481 pub user: String,
482 pub fundings: Vec<WsUserFundingData>,
483}
484
485#[derive(Debug, Clone, Deserialize)]
487#[serde(untagged)]
488pub enum WsActiveAssetCtxData {
489 Perp { coin: String, ctx: PerpsAssetCtx },
490 Spot { coin: String, ctx: SpotAssetCtx },
491}
492
493#[derive(Debug, Clone, Deserialize)]
495pub struct SharedAssetCtx {
496 #[serde(rename = "dayNtlVlm")]
497 pub day_ntl_vlm: f64,
498 #[serde(rename = "prevDayPx")]
499 pub prev_day_px: f64,
500 #[serde(rename = "markPx")]
501 pub mark_px: f64,
502 #[serde(rename = "midPx")]
503 pub mid_px: Option<f64>,
504}
505
506#[derive(Debug, Clone, Deserialize)]
508pub struct PerpsAssetCtx {
509 #[serde(flatten)]
510 pub shared: SharedAssetCtx,
511 pub funding: f64,
512 #[serde(rename = "openInterest")]
513 pub open_interest: f64,
514 #[serde(rename = "oraclePx")]
515 pub oracle_px: f64,
516}
517
518#[derive(Debug, Clone, Deserialize)]
520pub struct SpotAssetCtx {
521 #[serde(flatten)]
522 pub shared: SharedAssetCtx,
523 #[serde(rename = "circulatingSupply")]
524 pub circulating_supply: f64,
525}
526
527#[derive(Debug, Clone, Deserialize)]
529pub struct WsActiveAssetData {
530 pub user: String,
531 pub coin: String,
532 pub leverage: LeverageData,
533 #[serde(rename = "maxTradeSzs")]
534 pub max_trade_szs: [f64; 2],
535 #[serde(rename = "availableToTrade")]
536 pub available_to_trade: [f64; 2],
537}
538
539#[derive(Debug, Clone, Deserialize)]
541pub struct LeverageData {
542 pub value: f64,
543 pub type_: String,
544}
545
546#[derive(Debug, Clone, Deserialize)]
548pub struct WsUserTwapSliceFillsData {
549 #[serde(rename = "isSnapshot")]
550 pub is_snapshot: Option<bool>,
551 pub user: String,
552 #[serde(rename = "twapSliceFills")]
553 pub twap_slice_fills: Vec<WsTwapSliceFillData>,
554}
555
556#[derive(Debug, Clone, Deserialize)]
558pub struct WsTwapSliceFillData {
559 pub fill: WsFillData,
560 #[serde(rename = "twapId")]
561 pub twap_id: u64,
562}
563
564#[derive(Debug, Clone, Deserialize)]
566pub struct WsUserTwapHistoryData {
567 #[serde(rename = "isSnapshot")]
568 pub is_snapshot: Option<bool>,
569 pub user: String,
570 pub history: Vec<WsTwapHistoryData>,
571}
572
573#[derive(Debug, Clone, Deserialize)]
575pub struct WsTwapHistoryData {
576 pub state: TwapStateData,
577 pub status: TwapStatusData,
578 pub time: u64,
579}
580
581#[derive(Debug, Clone, Deserialize)]
583pub struct TwapStateData {
584 pub coin: String,
585 pub user: String,
586 pub side: String,
587 pub sz: f64,
588 #[serde(rename = "executedSz")]
589 pub executed_sz: f64,
590 #[serde(rename = "executedNtl")]
591 pub executed_ntl: f64,
592 pub minutes: u32,
593 #[serde(rename = "reduceOnly")]
594 pub reduce_only: bool,
595 pub randomize: bool,
596 pub timestamp: u64,
597}
598
599#[derive(Debug, Clone, Deserialize)]
601pub struct TwapStatusData {
602 pub status: String, pub description: String,
604}
605
606#[derive(Debug, Clone, Deserialize)]
608pub struct WsBboData {
609 pub coin: String,
610 pub time: u64,
611 pub bbo: [Option<WsLevelData>; 2], }
613
614#[cfg(test)]
619mod tests {
620 use rstest::rstest;
621 use serde_json;
622
623 use super::*;
624
625 #[rstest]
626 fn test_subscription_request_serialization() {
627 let sub = SubscriptionRequest::L2Book {
628 coin: "BTC".to_string(),
629 n_sig_figs: Some(5),
630 mantissa: None,
631 };
632
633 let json = serde_json::to_string(&sub).unwrap();
634 assert!(json.contains(r#""type":"l2Book""#));
635 assert!(json.contains(r#""coin":"BTC""#));
636 }
637
638 #[rstest]
639 fn test_hyperliquid_ws_request_serialization() {
640 let req = HyperliquidWsRequest::Subscribe {
641 subscription: SubscriptionRequest::Trades {
642 coin: "ETH".to_string(),
643 },
644 };
645
646 let json = serde_json::to_string(&req).unwrap();
647 assert!(json.contains(r#""method":"subscribe""#));
648 assert!(json.contains(r#""type":"trades""#));
649 }
650
651 #[rstest]
652 fn test_order_request_serialization() {
653 let order = OrderRequest {
654 a: 0, b: true, p: "50000.0".to_string(),
657 s: "0.1".to_string(),
658 r: false,
659 t: OrderTypeRequest::Limit {
660 tif: TimeInForceRequest::Gtc,
661 },
662 c: Some("client-123".to_string()),
663 };
664
665 let json = serde_json::to_string(&order).unwrap();
666 assert!(json.contains(r#""a":0"#));
667 assert!(json.contains(r#""b":true"#));
668 assert!(json.contains(r#""p":"50000.0""#));
669 }
670
671 #[rstest]
672 fn test_ws_trade_data_deserialization() {
673 let json = r#"{
674 "coin": "BTC",
675 "side": "B",
676 "px": "50000.0",
677 "sz": "0.1",
678 "hash": "0x123",
679 "time": 1234567890,
680 "tid": 12345,
681 "users": ["0xabc", "0xdef"]
682 }"#;
683
684 let trade: WsTradeData = serde_json::from_str(json).unwrap();
685 assert_eq!(trade.coin, "BTC");
686 assert_eq!(trade.side, "B");
687 assert_eq!(trade.px, "50000.0");
688 }
689
690 #[rstest]
691 fn test_ws_book_data_deserialization() {
692 let json = r#"{
693 "coin": "ETH",
694 "levels": [
695 [{"px": "3000.0", "sz": "1.0", "n": 1}],
696 [{"px": "3001.0", "sz": "2.0", "n": 2}]
697 ],
698 "time": 1234567890
699 }"#;
700
701 let book: WsBookData = serde_json::from_str(json).unwrap();
702 assert_eq!(book.coin, "ETH");
703 assert_eq!(book.levels[0].len(), 1);
704 assert_eq!(book.levels[1].len(), 1);
705 }
706}