1use std::fmt::Display;
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_core::hex;
20use nautilus_model::identifiers::{ClientOrderId, VenueOrderId};
21use rust_decimal::Decimal;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23use ustr::Ustr;
24
25use crate::common::{
26 enums::{
27 HyperliquidFillDirection, HyperliquidLeverageType,
28 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidPositionType,
29 HyperliquidSide, HyperliquidTimeInForce,
30 },
31 parse::{
32 deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
33 serialize_decimal_as_str, serialize_optional_decimal_as_str,
34 },
35};
36
37pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;
39
40#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
42pub struct Cloid(pub [u8; 16]);
43
44impl Cloid {
45 pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
51 let hex_str = s.as_ref();
52 let without_prefix = hex_str
53 .strip_prefix("0x")
54 .ok_or("CLOID must start with '0x'")?;
55
56 if without_prefix.len() != 32 {
57 return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
58 }
59
60 let bytes = hex::decode_array(without_prefix)
61 .map_err(|_| "Invalid hex character in CLOID".to_string())?;
62
63 Ok(Self(bytes))
64 }
65
66 #[must_use]
68 pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
69 let hash = keccak256(client_order_id.as_str().as_bytes());
70 let mut bytes = [0u8; 16];
71 bytes.copy_from_slice(&hash[..16]);
72 Self(bytes)
73 }
74
75 #[must_use]
77 pub fn is_uuid_v4(&self) -> bool {
78 self.0[6] >> 4 == 4 && matches!(self.0[8] >> 4, 8..=11)
79 }
80
81 pub fn to_hex(&self) -> String {
83 hex::encode_prefixed(self.0)
84 }
85}
86
87impl Display for Cloid {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", self.to_hex())
90 }
91}
92
93impl Serialize for Cloid {
94 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95 where
96 S: Serializer,
97 {
98 serializer.serialize_str(&self.to_hex())
99 }
100}
101
102impl<'de> Deserialize<'de> for Cloid {
103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104 where
105 D: Deserializer<'de>,
106 {
107 let s = String::deserialize(deserializer)?;
108 Self::from_hex(&s).map_err(serde::de::Error::custom)
109 }
110}
111
112pub type AssetId = u32;
117
118pub type OrderId = u64;
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct HyperliquidAssetInfo {
125 pub name: Ustr,
127 pub sz_decimals: u32,
129 #[serde(default)]
131 pub max_leverage: Option<u32>,
132 #[serde(default)]
134 pub only_isolated: Option<bool>,
135 #[serde(default)]
137 pub is_delisted: Option<bool>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct PerpMeta {
144 pub universe: Vec<PerpAsset>,
146 #[serde(default)]
148 pub margin_tables: Vec<(u32, MarginTable)>,
149 #[serde(default)]
151 pub collateral_token: Option<u32>,
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct PerpAsset {
158 pub name: String,
160 pub sz_decimals: u32,
162 #[serde(default)]
164 pub max_leverage: Option<u32>,
165 #[serde(default)]
167 pub only_isolated: Option<bool>,
168 #[serde(default)]
170 pub is_delisted: Option<bool>,
171 #[serde(default)]
173 pub growth_mode: Option<String>,
174 #[serde(default)]
176 pub margin_mode: Option<String>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct MarginTable {
183 pub description: String,
185 #[serde(default)]
187 pub margin_tiers: Vec<MarginTier>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct MarginTier {
194 #[serde(
196 serialize_with = "serialize_decimal_as_str",
197 deserialize_with = "deserialize_decimal_from_str"
198 )]
199 pub lower_bound: Decimal,
200 pub max_leverage: u32,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct PerpDex {
209 pub name: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct SpotMeta {
217 pub tokens: Vec<SpotToken>,
219 pub universe: Vec<SpotPair>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub struct EvmContract {
227 pub address: Address,
229 pub evm_extra_wei_decimals: i32,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct SpotToken {
237 pub name: String,
239 pub sz_decimals: u32,
241 pub wei_decimals: u32,
243 pub index: u32,
245 pub token_id: String,
247 pub is_canonical: bool,
249 #[serde(default)]
251 pub evm_contract: Option<EvmContract>,
252 #[serde(default)]
254 pub full_name: Option<String>,
255 #[serde(default)]
257 pub deployer_trading_fee_share: Option<String>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct SpotPair {
264 pub name: String,
266 pub tokens: [u32; 2],
268 pub index: u32,
270 pub is_canonical: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct OutcomeMeta {
278 pub outcomes: Vec<OutcomeMarket>,
280 #[serde(default)]
284 pub questions: Vec<OutcomeQuestion>,
285}
286
287impl OutcomeMeta {
288 #[must_use]
291 pub fn parent_question(&self, outcome_index: u32) -> Option<&OutcomeQuestion> {
292 self.questions.iter().find(|q| {
293 q.fallback_outcome == Some(outcome_index) || q.named_outcomes.contains(&outcome_index)
294 })
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct OutcomeMarket {
302 pub outcome: u32,
304 pub name: String,
306 pub description: String,
308 #[serde(default)]
310 pub side_specs: Vec<OutcomeSideSpec>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct OutcomeSideSpec {
317 pub name: String,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct OutcomeQuestion {
329 pub question: u32,
331 pub name: String,
333 pub description: String,
335 #[serde(default)]
337 pub fallback_outcome: Option<u32>,
338 #[serde(default)]
340 pub named_outcomes: Vec<u32>,
341 #[serde(default)]
343 pub settled_named_outcomes: Vec<u32>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(untagged)]
350pub enum PerpMetaAndCtxs {
351 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct PerpAssetCtx {
359 #[serde(
361 default,
362 serialize_with = "serialize_optional_decimal_as_str",
363 deserialize_with = "deserialize_optional_decimal_from_str"
364 )]
365 pub mark_px: Option<Decimal>,
366 #[serde(
368 default,
369 serialize_with = "serialize_optional_decimal_as_str",
370 deserialize_with = "deserialize_optional_decimal_from_str"
371 )]
372 pub mid_px: Option<Decimal>,
373 #[serde(
375 default,
376 serialize_with = "serialize_optional_decimal_as_str",
377 deserialize_with = "deserialize_optional_decimal_from_str"
378 )]
379 pub funding: Option<Decimal>,
380 #[serde(
382 default,
383 serialize_with = "serialize_optional_decimal_as_str",
384 deserialize_with = "deserialize_optional_decimal_from_str"
385 )]
386 pub open_interest: Option<Decimal>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(untagged)]
393pub enum SpotMetaAndCtxs {
394 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct SpotAssetCtx {
402 #[serde(
404 default,
405 serialize_with = "serialize_optional_decimal_as_str",
406 deserialize_with = "deserialize_optional_decimal_from_str"
407 )]
408 pub mark_px: Option<Decimal>,
409 #[serde(
411 default,
412 serialize_with = "serialize_optional_decimal_as_str",
413 deserialize_with = "deserialize_optional_decimal_from_str"
414 )]
415 pub mid_px: Option<Decimal>,
416 #[serde(
418 default,
419 serialize_with = "serialize_optional_decimal_as_str",
420 deserialize_with = "deserialize_optional_decimal_from_str"
421 )]
422 pub day_volume: Option<Decimal>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct HyperliquidL2Book {
428 pub coin: Ustr,
430 pub levels: Vec<Vec<HyperliquidLevel>>,
432 pub time: u64,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct HyperliquidLevel {
439 #[serde(
441 serialize_with = "serialize_decimal_as_str",
442 deserialize_with = "deserialize_decimal_from_str"
443 )]
444 pub px: Decimal,
445 #[serde(
447 serialize_with = "serialize_decimal_as_str",
448 deserialize_with = "deserialize_decimal_from_str"
449 )]
450 pub sz: Decimal,
451}
452
453pub type HyperliquidFills = Vec<HyperliquidFill>;
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct HyperliquidMeta {
461 #[serde(default)]
462 pub universe: Vec<HyperliquidAssetInfo>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct HyperliquidCandle {
469 #[serde(rename = "t")]
471 pub timestamp: u64,
472 #[serde(rename = "T")]
474 pub end_timestamp: u64,
475 #[serde(
477 rename = "o",
478 serialize_with = "serialize_decimal_as_str",
479 deserialize_with = "deserialize_decimal_from_str"
480 )]
481 pub open: Decimal,
482 #[serde(
484 rename = "h",
485 serialize_with = "serialize_decimal_as_str",
486 deserialize_with = "deserialize_decimal_from_str"
487 )]
488 pub high: Decimal,
489 #[serde(
491 rename = "l",
492 serialize_with = "serialize_decimal_as_str",
493 deserialize_with = "deserialize_decimal_from_str"
494 )]
495 pub low: Decimal,
496 #[serde(
498 rename = "c",
499 serialize_with = "serialize_decimal_as_str",
500 deserialize_with = "deserialize_decimal_from_str"
501 )]
502 pub close: Decimal,
503 #[serde(
505 rename = "v",
506 serialize_with = "serialize_decimal_as_str",
507 deserialize_with = "deserialize_decimal_from_str"
508 )]
509 pub volume: Decimal,
510 #[serde(rename = "n", default)]
512 pub num_trades: Option<u64>,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct HyperliquidFundingHistoryEntry {
518 pub coin: Ustr,
520 #[serde(
522 rename = "fundingRate",
523 serialize_with = "serialize_decimal_as_str",
524 deserialize_with = "deserialize_decimal_from_str"
525 )]
526 pub funding_rate: Decimal,
527 #[serde(
529 default,
530 serialize_with = "serialize_optional_decimal_as_str",
531 deserialize_with = "deserialize_optional_decimal_from_str"
532 )]
533 pub premium: Option<Decimal>,
534 pub time: u64,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct HyperliquidRecentTrade {
544 pub coin: Ustr,
546 pub side: HyperliquidSide,
548 #[serde(
550 serialize_with = "serialize_decimal_as_str",
551 deserialize_with = "deserialize_decimal_from_str"
552 )]
553 pub px: Decimal,
554 #[serde(
556 serialize_with = "serialize_decimal_as_str",
557 deserialize_with = "deserialize_decimal_from_str"
558 )]
559 pub sz: Decimal,
560 pub hash: String,
562 pub time: u64,
564 pub tid: u64,
566 pub users: [String; 2],
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct HyperliquidFill {
573 pub coin: Ustr,
575 #[serde(
577 serialize_with = "serialize_decimal_as_str",
578 deserialize_with = "deserialize_decimal_from_str"
579 )]
580 pub px: Decimal,
581 #[serde(
583 serialize_with = "serialize_decimal_as_str",
584 deserialize_with = "deserialize_decimal_from_str"
585 )]
586 pub sz: Decimal,
587 pub side: HyperliquidSide,
589 pub time: u64,
591 #[serde(
593 rename = "startPosition",
594 serialize_with = "serialize_decimal_as_str",
595 deserialize_with = "deserialize_decimal_from_str"
596 )]
597 pub start_position: Decimal,
598 pub dir: HyperliquidFillDirection,
600 #[serde(
602 rename = "closedPnl",
603 serialize_with = "serialize_decimal_as_str",
604 deserialize_with = "deserialize_decimal_from_str"
605 )]
606 pub closed_pnl: Decimal,
607 pub hash: String,
609 pub oid: u64,
611 pub crossed: bool,
613 #[serde(
615 serialize_with = "serialize_decimal_as_str",
616 deserialize_with = "deserialize_decimal_from_str"
617 )]
618 pub fee: Decimal,
619 #[serde(default)]
621 pub tid: u64,
622 #[serde(rename = "feeToken")]
624 pub fee_token: Ustr,
625 #[serde(
627 rename = "builderFee",
628 default,
629 skip_serializing_if = "Option::is_none",
630 serialize_with = "serialize_optional_decimal_as_str",
631 deserialize_with = "deserialize_optional_decimal_from_str"
632 )]
633 pub builder_fee: Option<Decimal>,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(tag = "status", rename_all = "camelCase")]
642pub enum HyperliquidOrderStatus {
643 Order { order: HyperliquidOrderStatusEntry },
644 UnknownOid,
645}
646
647impl HyperliquidOrderStatus {
648 #[must_use]
650 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
651 match self {
652 Self::Order { order } => Some(order),
653 Self::UnknownOid => None,
654 }
655 }
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct HyperliquidOrderStatusEntry {
661 pub order: HyperliquidOrderInfo,
663 pub status: HyperliquidOrderStatusEnum,
665 #[serde(rename = "statusTimestamp")]
667 pub status_timestamp: u64,
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct HyperliquidOrderInfo {
673 pub coin: Ustr,
675 pub side: HyperliquidSide,
677 #[serde(
679 rename = "limitPx",
680 serialize_with = "serialize_decimal_as_str",
681 deserialize_with = "deserialize_decimal_from_str"
682 )]
683 pub limit_px: Decimal,
684 #[serde(
686 serialize_with = "serialize_decimal_as_str",
687 deserialize_with = "deserialize_decimal_from_str"
688 )]
689 pub sz: Decimal,
690 pub oid: u64,
692 pub timestamp: u64,
694 #[serde(
696 rename = "origSz",
697 serialize_with = "serialize_decimal_as_str",
698 deserialize_with = "deserialize_decimal_from_str"
699 )]
700 pub orig_sz: Decimal,
701 #[serde(default)]
703 pub cloid: Option<String>,
704 #[serde(default)]
706 pub tif: Option<HyperliquidTimeInForce>,
707 #[serde(rename = "reduceOnly", default)]
709 pub reduce_only: Option<bool>,
710 #[serde(
712 rename = "triggerPx",
713 default,
714 deserialize_with = "deserialize_optional_decimal_from_str"
715 )]
716 pub trigger_px: Option<Decimal>,
717 #[serde(rename = "orderType", default)]
719 pub order_type: Option<String>,
720}
721
722#[derive(Debug, Clone, Serialize)]
724pub struct HyperliquidSignature {
725 pub r: String,
727 pub s: String,
729 pub v: u64,
731}
732
733impl HyperliquidSignature {
734 #[must_use]
736 pub fn new(r: String, s: String, v: u64) -> Self {
737 Self { r, s, v }
738 }
739
740 #[must_use]
742 pub fn to_hex(&self) -> String {
743 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
744 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
745 format!("0x{r}{s}{:02x}", self.v)
746 }
747
748 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
750 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
751
752 if sig_hex.len() != 130 {
753 return Err(format!(
754 "Invalid signature length: expected 130 hex chars, was {}",
755 sig_hex.len()
756 ));
757 }
758
759 let r = format!("0x{}", &sig_hex[0..64]);
760 let s = format!("0x{}", &sig_hex[64..128]);
761 let v = u64::from_str_radix(&sig_hex[128..130], 16)
762 .map_err(|e| format!("Failed to parse v component: {e}"))?;
763
764 Ok(Self { r, s, v })
765 }
766}
767
768#[derive(Debug, Clone, Serialize)]
770pub struct HyperliquidExchangeRequest<T> {
771 #[serde(rename = "action")]
773 pub action: T,
774 #[serde(rename = "nonce")]
776 pub nonce: u64,
777 #[serde(rename = "signature")]
779 pub signature: HyperliquidSignature,
780 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
782 pub vault_address: Option<String>,
783 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
785 pub expires_after: Option<u64>,
786}
787
788impl<T> HyperliquidExchangeRequest<T>
789where
790 T: Serialize,
791{
792 #[must_use]
794 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
795 Self {
796 action,
797 nonce,
798 signature,
799 vault_address: None,
800 expires_after: None,
801 }
802 }
803
804 #[must_use]
806 pub fn with_vault(
807 action: T,
808 nonce: u64,
809 signature: HyperliquidSignature,
810 vault_address: String,
811 ) -> Self {
812 Self {
813 action,
814 nonce,
815 signature,
816 vault_address: Some(vault_address),
817 expires_after: None,
818 }
819 }
820
821 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
823 serde_json::to_value(self)
824 }
825}
826
827#[derive(Debug, Clone, Serialize, Deserialize)]
829#[serde(untagged)]
830pub enum HyperliquidExchangeResponse {
831 Status {
833 status: String,
835 response: serde_json::Value,
837 },
838 Error {
840 error: String,
842 },
843}
844
845impl HyperliquidExchangeResponse {
846 pub fn is_ok(&self) -> bool {
847 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
848 }
849}
850
851pub const RESPONSE_STATUS_OK: &str = "ok";
853
854#[cfg(test)]
855mod tests {
856 use rstest::rstest;
857 use rust_decimal_macros::dec;
858 use serde_json::json;
859
860 use super::*;
861
862 #[rstest]
863 fn test_meta_deserialization() {
864 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
865
866 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
867
868 assert_eq!(meta.universe.len(), 1);
869 assert_eq!(meta.universe[0].name, "BTC");
870 assert_eq!(meta.universe[0].sz_decimals, 5);
871 }
872
873 #[rstest]
874 fn test_funding_history_entry_with_premium() {
875 let json = r#"{
876 "coin": "BTC",
877 "fundingRate": "0.0000125",
878 "premium": "0.00029005",
879 "time": 1769908800000
880 }"#;
881
882 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
883
884 assert_eq!(entry.coin.as_str(), "BTC");
885 assert_eq!(entry.funding_rate, dec!(0.0000125));
886 assert_eq!(entry.premium, Some(dec!(0.00029005)));
887 assert_eq!(entry.time, 1769908800000);
888 }
889
890 #[rstest]
891 fn test_funding_history_entry_without_premium() {
892 let json = r#"{
895 "coin": "BTC",
896 "fundingRate": "0.0000033",
897 "time": 1769916000000
898 }"#;
899
900 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
901
902 assert!(entry.premium.is_none());
903 assert_eq!(entry.funding_rate, dec!(0.0000033));
904 }
905
906 #[rstest]
907 fn test_recent_trade_deserializes() {
908 let json = r#"{
910 "coin": "BTC",
911 "side": "B",
912 "px": "104250.0",
913 "sz": "0.0123",
914 "hash": "0xabc",
915 "time": 1769916000000,
916 "tid": 987654321,
917 "users": ["0xbuyer", "0xseller"]
918 }"#;
919
920 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
921
922 assert_eq!(trade.coin.as_str(), "BTC");
923 assert_eq!(trade.side, HyperliquidSide::Buy);
924 assert_eq!(trade.px, dec!(104250.0));
925 assert_eq!(trade.sz, dec!(0.0123));
926 assert_eq!(trade.time, 1769916000000);
927 assert_eq!(trade.tid, 987654321);
928 }
929
930 #[rstest]
931 fn test_order_status_deserializes_frontend_market_tif() {
932 let status: HyperliquidOrderStatus =
933 crate::common::testing::load_test_data("http_order_status_frontend_market.json");
934 let entry = status.into_order().expect("order status entry");
935
936 assert_eq!(entry.order.oid, 1);
937 assert_eq!(
938 entry.order.tif,
939 Some(HyperliquidTimeInForce::FrontendMarket)
940 );
941 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
942 }
943
944 #[rstest]
945 fn test_historical_order_deserializes_liquidation_market_tif() {
946 let entry: HyperliquidOrderStatusEntry =
947 crate::common::testing::load_test_data("http_historical_order_liquidation_market.json");
948
949 assert_eq!(entry.order.oid, 42);
950 assert_eq!(
951 entry.order.tif,
952 Some(HyperliquidTimeInForce::LiquidationMarket)
953 );
954 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
955 }
956
957 #[rstest]
958 fn test_user_fill_deserializes_tid_and_builder_fee() {
959 let json = r#"{
960 "coin": "BTC",
961 "px": "60000.5",
962 "sz": "0.001",
963 "side": "B",
964 "time": 1704470400000,
965 "startPosition": "0",
966 "dir": "Open Long",
967 "closedPnl": "1.25",
968 "hash": "0xabc",
969 "oid": 7001,
970 "crossed": true,
971 "fee": "0.02",
972 "feeToken": "USDC",
973 "tid": 9001,
974 "builderFee": "0.001"
975 }"#;
976
977 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
978
979 assert_eq!(fill.coin.as_str(), "BTC");
980 assert_eq!(fill.oid, 7001);
981 assert_eq!(fill.tid, 9001);
982 assert_eq!(fill.builder_fee, Some(dec!(0.001)));
983 assert_eq!(fill.fee, dec!(0.02));
984 }
985
986 #[rstest]
987 fn test_user_fill_defaults_missing_tid_and_builder_fee() {
988 let json = r#"{
989 "coin": "ETH",
990 "px": "2500.25",
991 "sz": "0.5",
992 "side": "A",
993 "time": 1704470401000,
994 "startPosition": "1.0",
995 "dir": "Close Long",
996 "closedPnl": "2.5",
997 "hash": "0xdef",
998 "oid": 8002,
999 "crossed": false,
1000 "fee": "0.01",
1001 "feeToken": "USDC"
1002 }"#;
1003
1004 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1005
1006 assert_eq!(fill.oid, 8002);
1007 assert_eq!(fill.tid, 0);
1008 assert_eq!(fill.builder_fee, None);
1009 assert_eq!(fill.fee, dec!(0.01));
1010 assert!(!fill.crossed);
1011 }
1012
1013 #[rstest]
1014 fn test_perp_asset_hip3_fields() {
1015 let json = r#"{
1016 "name": "xyz:TSLA",
1017 "szDecimals": 3,
1018 "maxLeverage": 10,
1019 "onlyIsolated": true,
1020 "growthMode": "enabled",
1021 "marginMode": "strictIsolated"
1022 }"#;
1023
1024 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1025
1026 assert_eq!(asset.name, "xyz:TSLA");
1027 assert_eq!(asset.sz_decimals, 3);
1028 assert_eq!(asset.max_leverage, Some(10));
1029 assert_eq!(asset.only_isolated, Some(true));
1030 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
1031 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
1032 }
1033
1034 #[rstest]
1035 fn test_perp_asset_hip3_fields_absent() {
1036 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
1037
1038 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1039
1040 assert_eq!(asset.growth_mode, None);
1041 assert_eq!(asset.margin_mode, None);
1042 }
1043
1044 #[rstest]
1045 fn test_outcome_meta_defaults_missing_side_specs() {
1046 let json = r#"{
1047 "outcomes": [
1048 {
1049 "outcome": 123,
1050 "name": "Recurring",
1051 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
1052 }
1053 ]
1054 }"#;
1055
1056 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
1057
1058 assert_eq!(meta.outcomes.len(), 1);
1059 assert_eq!(meta.outcomes[0].outcome, 123);
1060 assert!(meta.outcomes[0].side_specs.is_empty());
1061 }
1062
1063 #[rstest]
1064 fn test_l2_book_deserialization() {
1065 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
1066
1067 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
1068
1069 assert_eq!(book.coin, "BTC");
1070 assert_eq!(book.levels.len(), 2);
1071 assert_eq!(book.time, 1234567890);
1072 }
1073
1074 #[rstest]
1075 fn test_exchange_response_deserialization() {
1076 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
1077
1078 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
1079 assert!(response.is_ok());
1080 }
1081
1082 #[rstest]
1083 fn test_spot_clearinghouse_state_deserialization() {
1084 let json = r#"{
1085 "balances": [
1086 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
1087 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
1088 ]
1089 }"#;
1090
1091 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1092
1093 assert_eq!(state.balances.len(), 2);
1094 let usdc = &state.balances[0];
1095 assert_eq!(usdc.coin.as_str(), "USDC");
1096 assert_eq!(usdc.token, Some(0));
1097 assert_eq!(usdc.total.to_string(), "14.625485");
1098 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
1099 assert_eq!(usdc.free().to_string(), "14.625485");
1100 assert_eq!(usdc.avg_entry_px(), None);
1101
1102 let purr = &state.balances[1];
1103 assert_eq!(purr.coin.as_str(), "PURR");
1104 assert_eq!(purr.token, Some(1));
1105 assert_eq!(purr.free().to_string(), "1900");
1106 assert_eq!(
1107 purr.avg_entry_px().unwrap(),
1108 rust_decimal_macros::dec!(0.61728)
1109 );
1110 }
1111
1112 #[rstest]
1113 fn test_spot_balance_outcome_side_token_lacks_token_field() {
1114 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1116 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1117 assert_eq!(balance.coin.as_str(), "+250");
1118 assert_eq!(balance.token, None);
1119 }
1120
1121 #[rstest]
1122 fn test_spot_clearinghouse_state_empty() {
1123 let json = r#"{"balances": []}"#;
1124 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1125 assert!(state.balances.is_empty());
1126 }
1127
1128 #[rstest]
1129 fn test_spot_balance_handles_missing_entry_ntl() {
1130 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1131 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1132 assert_eq!(balance.entry_ntl, None);
1133 assert_eq!(balance.avg_entry_px(), None);
1134 }
1135
1136 #[rstest]
1137 fn test_msgpack_serialization_matches_python() {
1138 let action = HyperliquidExchangeAction::Order {
1143 orders: vec![],
1144 grouping: HyperliquidExchangeGrouping::Na,
1145 builder: None,
1146 };
1147
1148 let json = serde_json::to_string(&action).unwrap();
1150 assert!(
1151 json.contains(r#""type":"order""#),
1152 "JSON should have type tag: {json}"
1153 );
1154
1155 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1157
1158 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1160
1161 assert!(
1163 decoded.get("type").is_some(),
1164 "MsgPack should have type tag. Decoded: {decoded:?}"
1165 );
1166 assert_eq!(
1167 decoded.get("type").unwrap().as_str().unwrap(),
1168 "order",
1169 "Type should be 'order'"
1170 );
1171 assert!(decoded.get("orders").is_some(), "Should have orders field");
1172 assert!(
1173 decoded.get("grouping").is_some(),
1174 "Should have grouping field"
1175 );
1176 }
1177
1178 #[rstest]
1179 fn test_cancel_action_serializes_fast_flag() {
1180 let action = HyperliquidExchangeAction::Cancel {
1181 cancels: vec![HyperliquidExchangeCancelOrderRequest {
1182 asset: 0,
1183 oid: 12345,
1184 }],
1185 fast: Some(true),
1186 };
1187
1188 let value = serde_json::to_value(action).unwrap();
1189
1190 assert_eq!(
1191 value,
1192 json!({
1193 "type": "cancel",
1194 "cancels": [{"a": 0, "o": 12345}],
1195 "f": true,
1196 })
1197 );
1198 }
1199
1200 #[rstest]
1201 fn test_cancel_by_cloid_action_serializes_fast_flag() {
1202 let action = HyperliquidExchangeAction::CancelByCloid {
1203 cancels: vec![HyperliquidExchangeCancelByCloidRequest {
1204 asset: 0,
1205 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
1206 }],
1207 fast: Some(true),
1208 };
1209
1210 let value = serde_json::to_value(action).unwrap();
1211
1212 assert_eq!(
1213 value,
1214 json!({
1215 "type": "cancelByCloid",
1216 "cancels": [{
1217 "asset": 0,
1218 "cloid": "0x00000000000000000000000000000000",
1219 }],
1220 "f": true,
1221 })
1222 );
1223 }
1224
1225 #[rstest]
1226 fn test_order_response_normal_tpsl_with_waiting_children() {
1227 let json = r#"{
1231 "statuses": [
1232 {"resting": {"oid": 446050656712}},
1233 "waitingForFill",
1234 "waitingForTrigger"
1235 ]
1236 }"#;
1237
1238 let data: HyperliquidExchangeOrderResponseData = serde_json::from_str(json).unwrap();
1239 assert_eq!(data.statuses.len(), 3);
1240
1241 assert!(matches!(
1242 data.statuses[0],
1243 HyperliquidExchangeOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1244 ));
1245 assert!(matches!(
1246 data.statuses[1],
1247 HyperliquidExchangeOrderStatus::Tag(HyperliquidExchangeOrderStatusTag::WaitingForFill)
1248 ));
1249 assert!(matches!(
1250 data.statuses[2],
1251 HyperliquidExchangeOrderStatus::Tag(
1252 HyperliquidExchangeOrderStatusTag::WaitingForTrigger
1253 )
1254 ));
1255 }
1256
1257 #[rstest]
1258 fn test_user_outcome_split_serialization() {
1259 let action = HyperliquidExchangeAction::UserOutcome {
1260 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1261 HyperliquidExchangeSplitOutcomeParams {
1262 outcome: 1,
1263 amount: dec!(123.0),
1264 },
1265 ),
1266 };
1267
1268 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1269 assert_eq!(
1270 value,
1271 json!({
1272 "type": "userOutcome",
1273 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1274 })
1275 );
1276 }
1277
1278 #[rstest]
1279 fn test_user_outcome_split_msgpack_roundtrip() {
1280 let action = HyperliquidExchangeAction::UserOutcome {
1281 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1282 HyperliquidExchangeSplitOutcomeParams {
1283 outcome: 4,
1284 amount: dec!(10),
1285 },
1286 ),
1287 };
1288
1289 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1290 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1291 assert_eq!(
1292 decoded,
1293 json!({
1294 "type": "userOutcome",
1295 "splitOutcome": { "outcome": 4, "amount": "10" }
1296 })
1297 );
1298 }
1299
1300 #[rstest]
1301 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1302 let level = HyperliquidLevel {
1305 px: dec!(98450.5),
1306 sz: dec!(2.5),
1307 };
1308 let value = serde_json::to_value(&level).unwrap();
1309 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1310 }
1311
1312 #[rstest]
1313 fn test_user_outcome_merge_outcome_serialization() {
1314 let action = HyperliquidExchangeAction::UserOutcome {
1315 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1316 HyperliquidExchangeMergeOutcomeParams {
1317 outcome: 1,
1318 amount: Some(dec!(5.0)),
1319 },
1320 ),
1321 };
1322 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1323 assert_eq!(
1324 value,
1325 json!({
1326 "type": "userOutcome",
1327 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1328 })
1329 );
1330 }
1331
1332 #[rstest]
1333 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1334 let action = HyperliquidExchangeAction::UserOutcome {
1335 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1336 HyperliquidExchangeMergeOutcomeParams {
1337 outcome: 7,
1338 amount: None,
1339 },
1340 ),
1341 };
1342 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1343 assert_eq!(
1344 value,
1345 json!({
1346 "type": "userOutcome",
1347 "mergeOutcome": { "outcome": 7, "amount": null }
1348 })
1349 );
1350 }
1351
1352 #[rstest]
1353 fn test_user_outcome_merge_question_serialization() {
1354 let action = HyperliquidExchangeAction::UserOutcome {
1355 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1356 HyperliquidExchangeMergeQuestionParams {
1357 question: 9,
1358 amount: Some(dec!(2.0)),
1359 },
1360 ),
1361 };
1362 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1363 assert_eq!(
1364 value,
1365 json!({
1366 "type": "userOutcome",
1367 "mergeQuestion": { "question": 9, "amount": "2.0" }
1368 })
1369 );
1370 }
1371
1372 #[rstest]
1373 fn test_user_outcome_merge_question_null_amount_means_max() {
1374 let action = HyperliquidExchangeAction::UserOutcome {
1375 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1376 HyperliquidExchangeMergeQuestionParams {
1377 question: 9,
1378 amount: None,
1379 },
1380 ),
1381 };
1382 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1383 assert_eq!(
1384 value,
1385 json!({
1386 "type": "userOutcome",
1387 "mergeQuestion": { "question": 9, "amount": null }
1388 })
1389 );
1390 }
1391
1392 #[rstest]
1393 fn test_user_outcome_negate_outcome_serialization() {
1394 let action = HyperliquidExchangeAction::UserOutcome {
1395 op: HyperliquidExchangeUserOutcomeOp::NegateOutcome(
1396 HyperliquidExchangeNegateOutcomeParams {
1397 question: 9,
1398 outcome: 52,
1399 amount: dec!(1.5),
1400 },
1401 ),
1402 };
1403 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1404 assert_eq!(
1405 value,
1406 json!({
1407 "type": "userOutcome",
1408 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1409 })
1410 );
1411 }
1412
1413 #[rstest]
1414 fn test_modify_target_serializes_numeric_oid() {
1415 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Oid(12345));
1416 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1417
1418 assert_eq!(value["oid"], json!(12345));
1419 }
1420
1421 #[rstest]
1422 fn test_modify_target_serializes_cloid() {
1423 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
1424 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Cloid(cloid));
1425 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1426
1427 assert_eq!(value["oid"], json!("0x1234567890abcdef1234567890abcdef"));
1428 }
1429
1430 fn modify_request_with_target(
1431 oid: HyperliquidExchangeModifyTarget,
1432 ) -> HyperliquidExchangeModifyOrderRequest {
1433 HyperliquidExchangeModifyOrderRequest {
1434 oid,
1435 order: HyperliquidExchangePlaceOrderRequest {
1436 asset: 0,
1437 is_buy: true,
1438 price: dec!(51000),
1439 size: dec!(0.2),
1440 reduce_only: false,
1441 kind: HyperliquidExchangeOrderKind::Limit {
1442 limit: HyperliquidExchangeLimitParams {
1443 tif: HyperliquidExchangeTif::Gtc,
1444 },
1445 },
1446 cloid: None,
1447 },
1448 }
1449 }
1450}
1451
1452#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1456pub enum HyperliquidExchangeTif {
1457 #[serde(rename = "Alo")]
1459 Alo,
1460 #[serde(rename = "Ioc")]
1462 Ioc,
1463 #[serde(rename = "Gtc")]
1465 Gtc,
1466}
1467
1468#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1470pub enum HyperliquidExchangeTpSl {
1471 #[serde(rename = "tp")]
1473 Tp,
1474 #[serde(rename = "sl")]
1476 Sl,
1477}
1478
1479#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1481pub enum HyperliquidExchangeGrouping {
1482 #[serde(rename = "na")]
1484 #[default]
1485 Na,
1486 #[serde(rename = "normalTpsl")]
1488 NormalTpsl,
1489 #[serde(rename = "positionTpsl")]
1491 PositionTpsl,
1492}
1493
1494#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1496#[serde(untagged)]
1497pub enum HyperliquidExchangeOrderKind {
1498 Limit {
1500 limit: HyperliquidExchangeLimitParams,
1502 },
1503 Trigger {
1505 trigger: HyperliquidExchangeTriggerParams,
1507 },
1508}
1509
1510#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1512pub struct HyperliquidExchangeLimitParams {
1513 pub tif: HyperliquidExchangeTif,
1515}
1516
1517#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1519#[serde(rename_all = "camelCase")]
1520pub struct HyperliquidExchangeTriggerParams {
1521 pub is_market: bool,
1523 #[serde(
1525 serialize_with = "serialize_decimal_as_str",
1526 deserialize_with = "deserialize_decimal_from_str"
1527 )]
1528 pub trigger_px: Decimal,
1529 pub tpsl: HyperliquidExchangeTpSl,
1531}
1532
1533#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1538pub struct HyperliquidExchangeBuilderFee {
1539 #[serde(rename = "b")]
1541 pub address: String,
1542 #[serde(rename = "f")]
1544 pub fee_tenths_bp: u32,
1545}
1546
1547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1552pub struct HyperliquidExchangePlaceOrderRequest {
1553 #[serde(rename = "a")]
1555 pub asset: AssetId,
1556 #[serde(rename = "b")]
1558 pub is_buy: bool,
1559 #[serde(
1561 rename = "p",
1562 serialize_with = "serialize_decimal_as_str",
1563 deserialize_with = "deserialize_decimal_from_str"
1564 )]
1565 pub price: Decimal,
1566 #[serde(
1568 rename = "s",
1569 serialize_with = "serialize_decimal_as_str",
1570 deserialize_with = "deserialize_decimal_from_str"
1571 )]
1572 pub size: Decimal,
1573 #[serde(rename = "r")]
1575 pub reduce_only: bool,
1576 #[serde(rename = "t")]
1578 pub kind: HyperliquidExchangeOrderKind,
1579 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1581 pub cloid: Option<Cloid>,
1582}
1583
1584#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1586pub struct HyperliquidExchangeCancelOrderRequest {
1587 #[serde(rename = "a")]
1589 pub asset: AssetId,
1590 #[serde(rename = "o")]
1592 pub oid: OrderId,
1593}
1594
1595#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1600pub struct HyperliquidExchangeCancelByCloidRequest {
1601 pub asset: AssetId,
1603 pub cloid: Cloid,
1605}
1606
1607#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1612#[serde(untagged)]
1613pub enum HyperliquidExchangeModifyTarget {
1614 Oid(OrderId),
1616 Cloid(Cloid),
1618}
1619
1620impl HyperliquidExchangeModifyTarget {
1621 pub fn from_venue_order_id(
1627 venue_order_id: &VenueOrderId,
1628 ) -> Result<Self, std::num::ParseIntError> {
1629 venue_order_id.as_str().parse::<OrderId>().map(Self::Oid)
1630 }
1631}
1632
1633impl From<OrderId> for HyperliquidExchangeModifyTarget {
1634 fn from(value: OrderId) -> Self {
1635 Self::Oid(value)
1636 }
1637}
1638
1639impl From<Cloid> for HyperliquidExchangeModifyTarget {
1640 fn from(value: Cloid) -> Self {
1641 Self::Cloid(value)
1642 }
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1650pub struct HyperliquidExchangeModifyOrderRequest {
1651 pub oid: HyperliquidExchangeModifyTarget,
1653 pub order: HyperliquidExchangePlaceOrderRequest,
1655}
1656
1657#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1662pub struct HyperliquidExchangeSplitOutcomeParams {
1663 pub outcome: u32,
1665 #[serde(
1667 serialize_with = "serialize_decimal_as_str",
1668 deserialize_with = "deserialize_decimal_from_str"
1669 )]
1670 pub amount: Decimal,
1671}
1672
1673#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1679pub struct HyperliquidExchangeMergeOutcomeParams {
1680 pub outcome: u32,
1682 #[serde(
1684 default,
1685 serialize_with = "serialize_optional_decimal_as_str",
1686 deserialize_with = "deserialize_optional_decimal_from_str"
1687 )]
1688 pub amount: Option<Decimal>,
1689}
1690
1691#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1697pub struct HyperliquidExchangeMergeQuestionParams {
1698 pub question: u32,
1700 #[serde(
1702 default,
1703 serialize_with = "serialize_optional_decimal_as_str",
1704 deserialize_with = "deserialize_optional_decimal_from_str"
1705 )]
1706 pub amount: Option<Decimal>,
1707}
1708
1709#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1714pub struct HyperliquidExchangeNegateOutcomeParams {
1715 pub question: u32,
1717 pub outcome: u32,
1719 #[serde(
1721 serialize_with = "serialize_decimal_as_str",
1722 deserialize_with = "deserialize_decimal_from_str"
1723 )]
1724 pub amount: Decimal,
1725}
1726
1727#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1734pub enum HyperliquidExchangeUserOutcomeOp {
1735 #[serde(rename = "splitOutcome")]
1737 SplitOutcome(HyperliquidExchangeSplitOutcomeParams),
1738 #[serde(rename = "mergeOutcome")]
1741 MergeOutcome(HyperliquidExchangeMergeOutcomeParams),
1742 #[serde(rename = "mergeQuestion")]
1745 MergeQuestion(HyperliquidExchangeMergeQuestionParams),
1746 #[serde(rename = "negateOutcome")]
1749 NegateOutcome(HyperliquidExchangeNegateOutcomeParams),
1750}
1751
1752#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1754pub struct HyperliquidExchangeTwapRequest {
1755 #[serde(rename = "a")]
1757 pub asset: AssetId,
1758 #[serde(rename = "b")]
1760 pub is_buy: bool,
1761 #[serde(
1763 rename = "s",
1764 serialize_with = "serialize_decimal_as_str",
1765 deserialize_with = "deserialize_decimal_from_str"
1766 )]
1767 pub size: Decimal,
1768 #[serde(rename = "m")]
1770 pub duration_ms: u64,
1771}
1772
1773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1779#[serde(tag = "type")]
1780pub enum HyperliquidExchangeAction {
1781 #[serde(rename = "order")]
1783 Order {
1784 orders: Vec<HyperliquidExchangePlaceOrderRequest>,
1786 #[serde(default)]
1788 grouping: HyperliquidExchangeGrouping,
1789 #[serde(skip_serializing_if = "Option::is_none")]
1791 builder: Option<HyperliquidExchangeBuilderFee>,
1792 },
1793
1794 #[serde(rename = "cancel")]
1796 Cancel {
1797 cancels: Vec<HyperliquidExchangeCancelOrderRequest>,
1799 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1801 fast: Option<bool>,
1802 },
1803
1804 #[serde(rename = "cancelByCloid")]
1806 CancelByCloid {
1807 cancels: Vec<HyperliquidExchangeCancelByCloidRequest>,
1809 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1811 fast: Option<bool>,
1812 },
1813
1814 #[serde(rename = "modify")]
1816 Modify {
1817 #[serde(flatten)]
1819 modify: HyperliquidExchangeModifyOrderRequest,
1820 },
1821
1822 #[serde(rename = "batchModify")]
1824 BatchModify {
1825 modifies: Vec<HyperliquidExchangeModifyOrderRequest>,
1827 },
1828
1829 #[serde(rename = "scheduleCancel")]
1831 ScheduleCancel {
1832 #[serde(skip_serializing_if = "Option::is_none")]
1835 time: Option<u64>,
1836 },
1837
1838 #[serde(rename = "updateLeverage")]
1840 UpdateLeverage {
1841 #[serde(rename = "a")]
1843 asset: AssetId,
1844 #[serde(rename = "isCross")]
1846 is_cross: bool,
1847 #[serde(rename = "leverage")]
1849 leverage: u32,
1850 },
1851
1852 #[serde(rename = "updateIsolatedMargin")]
1854 UpdateIsolatedMargin {
1855 #[serde(rename = "a")]
1857 asset: AssetId,
1858 #[serde(
1860 rename = "delta",
1861 serialize_with = "serialize_decimal_as_str",
1862 deserialize_with = "deserialize_decimal_from_str"
1863 )]
1864 delta: Decimal,
1865 },
1866
1867 #[serde(rename = "usdClassTransfer")]
1869 UsdClassTransfer {
1870 from: String,
1872 to: String,
1874 #[serde(
1876 serialize_with = "serialize_decimal_as_str",
1877 deserialize_with = "deserialize_decimal_from_str"
1878 )]
1879 amount: Decimal,
1880 },
1881
1882 #[serde(rename = "userOutcome")]
1888 UserOutcome {
1889 #[serde(flatten)]
1891 op: HyperliquidExchangeUserOutcomeOp,
1892 },
1893
1894 #[serde(rename = "twapPlace")]
1896 TwapPlace {
1897 #[serde(flatten)]
1899 twap: HyperliquidExchangeTwapRequest,
1900 },
1901
1902 #[serde(rename = "twapCancel")]
1904 TwapCancel {
1905 #[serde(rename = "a")]
1907 asset: AssetId,
1908 #[serde(rename = "t")]
1910 twap_id: u64,
1911 },
1912
1913 #[serde(rename = "noop")]
1915 Noop,
1916}
1917
1918#[derive(Debug, Clone, Serialize)]
1923#[serde(rename_all = "camelCase")]
1924pub struct HyperliquidExchangeActionRequest {
1925 pub action: HyperliquidExchangeAction,
1927 pub nonce: u64,
1929 pub signature: String,
1931 #[serde(skip_serializing_if = "Option::is_none")]
1933 pub vault_address: Option<String>,
1934 #[serde(skip_serializing_if = "Option::is_none")]
1937 pub expires_after: Option<u64>,
1938}
1939
1940#[derive(Debug, Clone, Serialize, Deserialize)]
1942pub struct HyperliquidExchangeActionResponse {
1943 pub status: String,
1945 pub response: HyperliquidExchangeResponseData,
1947}
1948
1949#[derive(Debug, Clone, Serialize, Deserialize)]
1951#[serde(tag = "type")]
1952pub enum HyperliquidExchangeResponseData {
1953 #[serde(rename = "order")]
1955 Order {
1956 data: HyperliquidExchangeOrderResponseData,
1958 },
1959 #[serde(rename = "cancel")]
1961 Cancel {
1962 data: HyperliquidExchangeCancelResponseData,
1964 },
1965 #[serde(rename = "modify")]
1967 Modify {
1968 data: HyperliquidExchangeModifyResponseData,
1970 },
1971 #[serde(rename = "default")]
1973 Default,
1974 #[serde(other)]
1976 Unknown,
1977}
1978
1979#[derive(Debug, Clone, Serialize, Deserialize)]
1981pub struct HyperliquidExchangeOrderResponseData {
1982 pub statuses: Vec<HyperliquidExchangeOrderStatus>,
1984}
1985
1986#[derive(Debug, Clone, Serialize, Deserialize)]
1988pub struct HyperliquidExchangeCancelResponseData {
1989 pub statuses: Vec<HyperliquidExchangeCancelStatus>,
1991}
1992
1993#[derive(Debug, Clone, Serialize, Deserialize)]
1995pub struct HyperliquidExchangeModifyResponseData {
1996 pub statuses: Vec<HyperliquidExchangeModifyStatus>,
1998}
1999
2000#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2002#[serde(untagged)]
2003pub enum HyperliquidExchangeOrderStatus {
2004 Resting {
2006 resting: HyperliquidExchangeRestingInfo,
2008 },
2009 Filled {
2011 filled: HyperliquidExchangeFilledInfo,
2013 },
2014 Error {
2016 error: String,
2018 },
2019 Tag(HyperliquidExchangeOrderStatusTag),
2023}
2024
2025#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2031pub enum HyperliquidExchangeOrderStatusTag {
2032 #[serde(rename = "waitingForFill")]
2034 WaitingForFill,
2035 #[serde(rename = "waitingForTrigger")]
2037 WaitingForTrigger,
2038}
2039
2040#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2042pub struct HyperliquidExchangeRestingInfo {
2043 pub oid: OrderId,
2045}
2046
2047#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2049pub struct HyperliquidExchangeFilledInfo {
2050 #[serde(
2052 rename = "totalSz",
2053 serialize_with = "serialize_decimal_as_str",
2054 deserialize_with = "deserialize_decimal_from_str"
2055 )]
2056 pub total_sz: Decimal,
2057 #[serde(
2059 rename = "avgPx",
2060 serialize_with = "serialize_decimal_as_str",
2061 deserialize_with = "deserialize_decimal_from_str"
2062 )]
2063 pub avg_px: Decimal,
2064 pub oid: OrderId,
2066}
2067
2068#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2070#[serde(untagged)]
2071pub enum HyperliquidExchangeCancelStatus {
2072 Success(String), Error {
2076 error: String,
2078 },
2079}
2080
2081#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2083#[serde(untagged)]
2084pub enum HyperliquidExchangeModifyStatus {
2085 Success(String), Error {
2089 error: String,
2091 },
2092}
2093
2094#[derive(Debug, Clone, Serialize, Deserialize)]
2097#[serde(rename_all = "camelCase")]
2098pub struct ClearinghouseState {
2099 #[serde(default)]
2101 pub asset_positions: Vec<AssetPosition>,
2102 #[serde(default)]
2104 pub cross_margin_summary: Option<CrossMarginSummary>,
2105 #[serde(
2107 default,
2108 serialize_with = "serialize_optional_decimal_as_str",
2109 deserialize_with = "deserialize_optional_decimal_from_str"
2110 )]
2111 pub withdrawable: Option<Decimal>,
2112 #[serde(default)]
2114 pub time: Option<u64>,
2115}
2116
2117#[derive(Debug, Clone, Serialize, Deserialize)]
2119#[serde(rename_all = "camelCase")]
2120pub struct AssetPosition {
2121 pub position: PositionData,
2123 #[serde(rename = "type")]
2125 pub position_type: HyperliquidPositionType,
2126}
2127
2128#[derive(Debug, Clone, Serialize, Deserialize)]
2130#[serde(rename_all = "camelCase")]
2131pub struct LeverageInfo {
2132 #[serde(rename = "type")]
2133 pub leverage_type: HyperliquidLeverageType,
2134 pub value: u32,
2136}
2137
2138#[derive(Debug, Clone, Serialize, Deserialize)]
2140#[serde(rename_all = "camelCase")]
2141pub struct CumFundingInfo {
2142 #[serde(
2144 rename = "allTime",
2145 serialize_with = "serialize_decimal_as_str",
2146 deserialize_with = "deserialize_decimal_from_str"
2147 )]
2148 pub all_time: Decimal,
2149 #[serde(
2151 rename = "sinceOpen",
2152 serialize_with = "serialize_decimal_as_str",
2153 deserialize_with = "deserialize_decimal_from_str"
2154 )]
2155 pub since_open: Decimal,
2156 #[serde(
2158 rename = "sinceChange",
2159 serialize_with = "serialize_decimal_as_str",
2160 deserialize_with = "deserialize_decimal_from_str"
2161 )]
2162 pub since_change: Decimal,
2163}
2164
2165#[derive(Debug, Clone, Serialize, Deserialize)]
2167#[serde(rename_all = "camelCase")]
2168pub struct PositionData {
2169 pub coin: Ustr,
2171 #[serde(rename = "cumFunding")]
2173 pub cum_funding: CumFundingInfo,
2174 #[serde(
2176 rename = "entryPx",
2177 serialize_with = "serialize_optional_decimal_as_str",
2178 deserialize_with = "deserialize_optional_decimal_from_str",
2179 default
2180 )]
2181 pub entry_px: Option<Decimal>,
2182 pub leverage: LeverageInfo,
2184 #[serde(
2186 rename = "liquidationPx",
2187 serialize_with = "serialize_optional_decimal_as_str",
2188 deserialize_with = "deserialize_optional_decimal_from_str",
2189 default
2190 )]
2191 pub liquidation_px: Option<Decimal>,
2192 #[serde(
2194 rename = "marginUsed",
2195 serialize_with = "serialize_decimal_as_str",
2196 deserialize_with = "deserialize_decimal_from_str"
2197 )]
2198 pub margin_used: Decimal,
2199 #[serde(rename = "maxLeverage", default)]
2201 pub max_leverage: Option<u32>,
2202 #[serde(
2204 rename = "positionValue",
2205 serialize_with = "serialize_decimal_as_str",
2206 deserialize_with = "deserialize_decimal_from_str"
2207 )]
2208 pub position_value: Decimal,
2209 #[serde(
2211 rename = "returnOnEquity",
2212 serialize_with = "serialize_decimal_as_str",
2213 deserialize_with = "deserialize_decimal_from_str"
2214 )]
2215 pub return_on_equity: Decimal,
2216 #[serde(
2218 rename = "szi",
2219 serialize_with = "serialize_decimal_as_str",
2220 deserialize_with = "deserialize_decimal_from_str"
2221 )]
2222 pub szi: Decimal,
2223 #[serde(
2225 rename = "unrealizedPnl",
2226 serialize_with = "serialize_decimal_as_str",
2227 deserialize_with = "deserialize_decimal_from_str"
2228 )]
2229 pub unrealized_pnl: Decimal,
2230}
2231
2232#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2238#[serde(rename_all = "camelCase")]
2239pub struct SpotClearinghouseState {
2240 #[serde(default)]
2242 pub balances: Vec<SpotBalance>,
2243}
2244
2245#[derive(Debug, Clone, Serialize, Deserialize)]
2247#[serde(rename_all = "camelCase")]
2248pub struct SpotBalance {
2249 pub coin: Ustr,
2251 #[serde(default)]
2254 pub token: Option<u32>,
2255 #[serde(
2257 serialize_with = "serialize_decimal_as_str",
2258 deserialize_with = "deserialize_decimal_from_str"
2259 )]
2260 pub total: Decimal,
2261 #[serde(
2263 serialize_with = "serialize_decimal_as_str",
2264 deserialize_with = "deserialize_decimal_from_str"
2265 )]
2266 pub hold: Decimal,
2267 #[serde(
2269 default,
2270 serialize_with = "serialize_optional_decimal_as_str",
2271 deserialize_with = "deserialize_optional_decimal_from_str"
2272 )]
2273 pub entry_ntl: Option<Decimal>,
2274}
2275
2276impl SpotBalance {
2277 #[must_use]
2279 pub fn free(&self) -> Decimal {
2280 (self.total - self.hold).max(Decimal::ZERO)
2281 }
2282
2283 #[must_use]
2285 pub fn avg_entry_px(&self) -> Option<Decimal> {
2286 let entry_ntl = self.entry_ntl?;
2287
2288 if entry_ntl.is_zero() || self.total.is_zero() {
2289 return None;
2290 }
2291
2292 Some(entry_ntl / self.total)
2293 }
2294}
2295
2296#[derive(Debug, Clone, Serialize, Deserialize)]
2298#[serde(rename_all = "camelCase")]
2299pub struct CrossMarginSummary {
2300 #[serde(
2302 rename = "accountValue",
2303 serialize_with = "serialize_decimal_as_str",
2304 deserialize_with = "deserialize_decimal_from_str"
2305 )]
2306 pub account_value: Decimal,
2307 #[serde(
2309 rename = "totalNtlPos",
2310 serialize_with = "serialize_decimal_as_str",
2311 deserialize_with = "deserialize_decimal_from_str"
2312 )]
2313 pub total_ntl_pos: Decimal,
2314 #[serde(
2316 rename = "totalRawUsd",
2317 serialize_with = "serialize_decimal_as_str",
2318 deserialize_with = "deserialize_decimal_from_str"
2319 )]
2320 pub total_raw_usd: Decimal,
2321 #[serde(
2323 rename = "totalMarginUsed",
2324 serialize_with = "serialize_decimal_as_str",
2325 deserialize_with = "deserialize_decimal_from_str"
2326 )]
2327 pub total_margin_used: Decimal,
2328 #[serde(
2330 rename = "withdrawable",
2331 default,
2332 serialize_with = "serialize_optional_decimal_as_str",
2333 deserialize_with = "deserialize_optional_decimal_from_str"
2334 )]
2335 pub withdrawable: Option<Decimal>,
2336}