1use std::fmt::{Debug, Display};
17
18use alloy_primitives::{Address, keccak256};
19#[cfg(test)]
20use nautilus_core::string::secret::REDACTED;
21use nautilus_core::{hex, string::secret::SecretString};
22use nautilus_model::identifiers::{ClientOrderId, VenueOrderId};
23use rust_decimal::Decimal;
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use ustr::Ustr;
26
27use crate::common::{
28 enums::{
29 HyperliquidFillDirection, HyperliquidLeverageType,
30 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidPositionType,
31 HyperliquidSide, HyperliquidTimeInForce,
32 },
33 parse::{
34 deserialize_decimal_from_str, deserialize_optional_decimal_from_str, is_false_or_none,
35 serialize_decimal_as_str, serialize_optional_decimal_as_str,
36 },
37};
38
39pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;
41
42#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
44pub struct Cloid(pub [u8; 16]);
45
46impl Cloid {
47 pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
53 let hex_str = s.as_ref();
54 let without_prefix = hex_str
55 .strip_prefix("0x")
56 .ok_or("CLOID must start with '0x'")?;
57
58 if without_prefix.len() != 32 {
59 return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
60 }
61
62 let bytes = hex::decode_array(without_prefix)
63 .map_err(|_| "Invalid hex character in CLOID".to_string())?;
64
65 Ok(Self(bytes))
66 }
67
68 #[must_use]
70 pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
71 let hash = keccak256(client_order_id.as_str().as_bytes());
72 let mut bytes = [0u8; 16];
73 bytes.copy_from_slice(&hash[..16]);
74 Self(bytes)
75 }
76
77 #[must_use]
79 pub fn is_uuid_v4(&self) -> bool {
80 self.0[6] >> 4 == 4 && matches!(self.0[8] >> 4, 8..=11)
81 }
82
83 pub fn to_hex(&self) -> String {
85 hex::encode_prefixed(self.0)
86 }
87}
88
89impl Display for Cloid {
90 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91 write!(f, "{}", self.to_hex())
92 }
93}
94
95impl Serialize for Cloid {
96 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97 where
98 S: Serializer,
99 {
100 serializer.serialize_str(&self.to_hex())
101 }
102}
103
104impl<'de> Deserialize<'de> for Cloid {
105 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106 where
107 D: Deserializer<'de>,
108 {
109 let s = String::deserialize(deserializer)?;
110 Self::from_hex(&s).map_err(serde::de::Error::custom)
111 }
112}
113
114pub type AssetId = u32;
119
120pub type OrderId = u64;
122
123#[derive(Debug, Clone, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct HyperliquidAssetInfo {
127 pub name: Ustr,
129 pub sz_decimals: u32,
131 #[serde(default)]
133 pub max_leverage: Option<u32>,
134 #[serde(default)]
136 pub only_isolated: Option<bool>,
137 #[serde(default)]
139 pub is_delisted: Option<bool>,
140}
141
142#[derive(Debug, Clone, Serialize, Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct PerpMeta {
146 pub universe: Vec<PerpAsset>,
148 #[serde(default)]
150 pub margin_tables: Vec<(u32, MarginTable)>,
151 #[serde(default)]
153 pub collateral_token: Option<u32>,
154}
155
156#[derive(Debug, Clone, Default, Serialize, Deserialize)]
158#[serde(rename_all = "camelCase")]
159pub struct PerpAsset {
160 pub name: String,
162 pub sz_decimals: u32,
164 #[serde(default)]
166 pub max_leverage: Option<u32>,
167 #[serde(default)]
169 pub only_isolated: Option<bool>,
170 #[serde(default)]
172 pub is_delisted: Option<bool>,
173 #[serde(default)]
175 pub growth_mode: Option<String>,
176 #[serde(default)]
178 pub margin_mode: Option<String>,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[serde(rename_all = "camelCase")]
184pub struct MarginTable {
185 pub description: String,
187 #[serde(default)]
189 pub margin_tiers: Vec<MarginTier>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194#[serde(rename_all = "camelCase")]
195pub struct MarginTier {
196 #[serde(
198 serialize_with = "serialize_decimal_as_str",
199 deserialize_with = "deserialize_decimal_from_str"
200 )]
201 pub lower_bound: Decimal,
202 pub max_leverage: u32,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(rename_all = "camelCase")]
210pub struct PerpDex {
211 pub name: String,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217#[serde(rename_all = "camelCase")]
218pub struct SpotMeta {
219 pub tokens: Vec<SpotToken>,
221 pub universe: Vec<SpotPair>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(rename_all = "snake_case")]
228pub struct EvmContract {
229 pub address: Address,
231 pub evm_extra_wei_decimals: i32,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237#[serde(rename_all = "camelCase")]
238pub struct SpotToken {
239 pub name: String,
241 pub sz_decimals: u32,
243 pub wei_decimals: u32,
245 pub index: u32,
247 pub token_id: String,
249 pub is_canonical: bool,
251 #[serde(default)]
253 pub evm_contract: Option<EvmContract>,
254 #[serde(default)]
256 pub full_name: Option<String>,
257 #[serde(default)]
259 pub deployer_trading_fee_share: Option<String>,
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265pub struct SpotPair {
266 pub name: String,
268 pub tokens: [u32; 2],
270 pub index: u32,
272 pub is_canonical: bool,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278#[serde(rename_all = "camelCase")]
279pub struct OutcomeMeta {
280 pub outcomes: Vec<OutcomeMarket>,
282 #[serde(default)]
286 pub questions: Vec<OutcomeQuestion>,
287}
288
289impl OutcomeMeta {
290 #[must_use]
293 pub fn parent_question(&self, outcome_index: u32) -> Option<&OutcomeQuestion> {
294 self.questions.iter().find(|q| {
295 q.fallback_outcome == Some(outcome_index) || q.named_outcomes.contains(&outcome_index)
296 })
297 }
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302#[serde(rename_all = "camelCase")]
303pub struct OutcomeMarket {
304 pub outcome: u32,
306 pub name: String,
308 pub description: String,
310 #[serde(default)]
312 pub quote_token: Option<String>,
313 #[serde(default)]
315 pub side_specs: Vec<OutcomeSideSpec>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321pub struct OutcomeSideSpec {
322 pub name: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
332#[serde(rename_all = "camelCase")]
333pub struct OutcomeQuestion {
334 pub question: u32,
336 pub name: String,
338 pub description: String,
340 #[serde(default)]
342 pub fallback_outcome: Option<u32>,
343 #[serde(default)]
345 pub named_outcomes: Vec<u32>,
346 #[serde(default)]
348 pub settled_named_outcomes: Vec<u32>,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(untagged)]
355pub enum PerpMetaAndCtxs {
356 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362#[serde(rename_all = "camelCase")]
363pub struct PerpAssetCtx {
364 #[serde(
366 default,
367 serialize_with = "serialize_optional_decimal_as_str",
368 deserialize_with = "deserialize_optional_decimal_from_str"
369 )]
370 pub mark_px: Option<Decimal>,
371 #[serde(
373 default,
374 serialize_with = "serialize_optional_decimal_as_str",
375 deserialize_with = "deserialize_optional_decimal_from_str"
376 )]
377 pub mid_px: Option<Decimal>,
378 #[serde(
380 default,
381 serialize_with = "serialize_optional_decimal_as_str",
382 deserialize_with = "deserialize_optional_decimal_from_str"
383 )]
384 pub funding: Option<Decimal>,
385 #[serde(
387 default,
388 serialize_with = "serialize_optional_decimal_as_str",
389 deserialize_with = "deserialize_optional_decimal_from_str"
390 )]
391 pub open_interest: Option<Decimal>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize)]
397#[serde(untagged)]
398pub enum SpotMetaAndCtxs {
399 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize)]
405#[serde(rename_all = "camelCase")]
406pub struct SpotAssetCtx {
407 #[serde(
409 default,
410 serialize_with = "serialize_optional_decimal_as_str",
411 deserialize_with = "deserialize_optional_decimal_from_str"
412 )]
413 pub mark_px: Option<Decimal>,
414 #[serde(
416 default,
417 serialize_with = "serialize_optional_decimal_as_str",
418 deserialize_with = "deserialize_optional_decimal_from_str"
419 )]
420 pub mid_px: Option<Decimal>,
421 #[serde(
423 default,
424 serialize_with = "serialize_optional_decimal_as_str",
425 deserialize_with = "deserialize_optional_decimal_from_str"
426 )]
427 pub day_volume: Option<Decimal>,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize)]
432pub struct HyperliquidL2Book {
433 pub coin: Ustr,
435 pub levels: Vec<Vec<HyperliquidLevel>>,
437 pub time: u64,
439}
440
441#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct HyperliquidLevel {
444 #[serde(
446 serialize_with = "serialize_decimal_as_str",
447 deserialize_with = "deserialize_decimal_from_str"
448 )]
449 pub px: Decimal,
450 #[serde(
452 serialize_with = "serialize_decimal_as_str",
453 deserialize_with = "deserialize_decimal_from_str"
454 )]
455 pub sz: Decimal,
456}
457
458pub type HyperliquidFills = Vec<HyperliquidFill>;
462
463#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct HyperliquidMeta {
466 #[serde(default)]
467 pub universe: Vec<HyperliquidAssetInfo>,
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct HyperliquidCandle {
474 #[serde(rename = "t")]
476 pub timestamp: u64,
477 #[serde(rename = "T")]
479 pub end_timestamp: u64,
480 #[serde(
482 rename = "o",
483 serialize_with = "serialize_decimal_as_str",
484 deserialize_with = "deserialize_decimal_from_str"
485 )]
486 pub open: Decimal,
487 #[serde(
489 rename = "h",
490 serialize_with = "serialize_decimal_as_str",
491 deserialize_with = "deserialize_decimal_from_str"
492 )]
493 pub high: Decimal,
494 #[serde(
496 rename = "l",
497 serialize_with = "serialize_decimal_as_str",
498 deserialize_with = "deserialize_decimal_from_str"
499 )]
500 pub low: Decimal,
501 #[serde(
503 rename = "c",
504 serialize_with = "serialize_decimal_as_str",
505 deserialize_with = "deserialize_decimal_from_str"
506 )]
507 pub close: Decimal,
508 #[serde(
510 rename = "v",
511 serialize_with = "serialize_decimal_as_str",
512 deserialize_with = "deserialize_decimal_from_str"
513 )]
514 pub volume: Decimal,
515 #[serde(rename = "n", default)]
517 pub num_trades: Option<u64>,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
522pub struct HyperliquidFundingHistoryEntry {
523 pub coin: Ustr,
525 #[serde(
527 rename = "fundingRate",
528 serialize_with = "serialize_decimal_as_str",
529 deserialize_with = "deserialize_decimal_from_str"
530 )]
531 pub funding_rate: Decimal,
532 #[serde(
534 default,
535 serialize_with = "serialize_optional_decimal_as_str",
536 deserialize_with = "deserialize_optional_decimal_from_str"
537 )]
538 pub premium: Option<Decimal>,
539 pub time: u64,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize)]
548pub struct HyperliquidRecentTrade {
549 pub coin: Ustr,
551 pub side: HyperliquidSide,
553 #[serde(
555 serialize_with = "serialize_decimal_as_str",
556 deserialize_with = "deserialize_decimal_from_str"
557 )]
558 pub px: Decimal,
559 #[serde(
561 serialize_with = "serialize_decimal_as_str",
562 deserialize_with = "deserialize_decimal_from_str"
563 )]
564 pub sz: Decimal,
565 pub hash: String,
567 pub time: u64,
569 pub tid: u64,
571 pub users: [String; 2],
573}
574
575#[derive(Debug, Clone, Serialize, Deserialize)]
577pub struct HyperliquidFill {
578 pub coin: Ustr,
580 #[serde(
582 serialize_with = "serialize_decimal_as_str",
583 deserialize_with = "deserialize_decimal_from_str"
584 )]
585 pub px: Decimal,
586 #[serde(
588 serialize_with = "serialize_decimal_as_str",
589 deserialize_with = "deserialize_decimal_from_str"
590 )]
591 pub sz: Decimal,
592 pub side: HyperliquidSide,
594 pub time: u64,
596 #[serde(
598 rename = "startPosition",
599 serialize_with = "serialize_decimal_as_str",
600 deserialize_with = "deserialize_decimal_from_str"
601 )]
602 pub start_position: Decimal,
603 pub dir: HyperliquidFillDirection,
605 #[serde(
607 rename = "closedPnl",
608 serialize_with = "serialize_decimal_as_str",
609 deserialize_with = "deserialize_decimal_from_str"
610 )]
611 pub closed_pnl: Decimal,
612 pub hash: String,
614 pub oid: u64,
616 pub crossed: bool,
618 #[serde(
620 serialize_with = "serialize_decimal_as_str",
621 deserialize_with = "deserialize_decimal_from_str"
622 )]
623 pub fee: Decimal,
624 #[serde(default)]
626 pub tid: u64,
627 #[serde(rename = "feeToken")]
629 pub fee_token: Ustr,
630 #[serde(
632 rename = "builderFee",
633 default,
634 skip_serializing_if = "Option::is_none",
635 serialize_with = "serialize_optional_decimal_as_str",
636 deserialize_with = "deserialize_optional_decimal_from_str"
637 )]
638 pub builder_fee: Option<Decimal>,
639 #[serde(default)]
641 pub cloid: Option<String>,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize)]
649#[serde(tag = "status", rename_all = "camelCase")]
650pub enum HyperliquidOrderStatus {
651 Order { order: HyperliquidOrderStatusEntry },
652 UnknownOid,
653}
654
655impl HyperliquidOrderStatus {
656 #[must_use]
658 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
659 match self {
660 Self::Order { order } => Some(order),
661 Self::UnknownOid => None,
662 }
663 }
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize)]
668pub struct HyperliquidOrderStatusEntry {
669 pub order: HyperliquidOrderInfo,
671 pub status: HyperliquidOrderStatusEnum,
673 #[serde(rename = "statusTimestamp")]
675 pub status_timestamp: u64,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
680pub struct HyperliquidOrderInfo {
681 pub coin: Ustr,
683 pub side: HyperliquidSide,
685 #[serde(
687 rename = "limitPx",
688 serialize_with = "serialize_decimal_as_str",
689 deserialize_with = "deserialize_decimal_from_str"
690 )]
691 pub limit_px: Decimal,
692 #[serde(
694 serialize_with = "serialize_decimal_as_str",
695 deserialize_with = "deserialize_decimal_from_str"
696 )]
697 pub sz: Decimal,
698 pub oid: u64,
700 pub timestamp: u64,
702 #[serde(
704 rename = "origSz",
705 serialize_with = "serialize_decimal_as_str",
706 deserialize_with = "deserialize_decimal_from_str"
707 )]
708 pub orig_sz: Decimal,
709 #[serde(default)]
711 pub cloid: Option<String>,
712 #[serde(default)]
714 pub tif: Option<HyperliquidTimeInForce>,
715 #[serde(rename = "reduceOnly", default)]
717 pub reduce_only: Option<bool>,
718 #[serde(
720 rename = "triggerPx",
721 default,
722 deserialize_with = "deserialize_optional_decimal_from_str"
723 )]
724 pub trigger_px: Option<Decimal>,
725 #[serde(rename = "orderType", default)]
727 pub order_type: Option<String>,
728}
729
730#[derive(Debug, Clone, Serialize)]
732pub struct HyperliquidSignature {
733 pub r: SecretString,
735 pub s: SecretString,
737 pub v: u64,
739}
740
741impl HyperliquidSignature {
742 #[must_use]
744 pub fn new(r: impl Into<SecretString>, s: impl Into<SecretString>, v: u64) -> Self {
745 Self {
746 r: r.into(),
747 s: s.into(),
748 v,
749 }
750 }
751
752 #[must_use]
754 pub fn to_hex(&self) -> SecretString {
755 let r = self
756 .r
757 .expose_secret()
758 .strip_prefix("0x")
759 .unwrap_or(self.r.expose_secret());
760 let s = self
761 .s
762 .expose_secret()
763 .strip_prefix("0x")
764 .unwrap_or(self.s.expose_secret());
765 SecretString::from(format!("0x{r}{s}{:02x}", self.v))
766 }
767
768 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
770 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
771
772 if sig_hex.len() != 130 {
773 return Err(format!(
774 "Invalid signature length: expected 130 hex chars, was {}",
775 sig_hex.len()
776 ));
777 }
778
779 let r = format!("0x{}", &sig_hex[0..64]);
780 let s = format!("0x{}", &sig_hex[64..128]);
781 let v = u64::from_str_radix(&sig_hex[128..130], 16)
782 .map_err(|e| format!("Failed to parse v component: {e}"))?;
783
784 Ok(Self::new(r, s, v))
785 }
786}
787
788#[derive(Debug, Clone, Serialize)]
790pub struct HyperliquidExchangeRequest<T> {
791 #[serde(rename = "action")]
793 pub action: T,
794 #[serde(rename = "nonce")]
796 pub nonce: u64,
797 #[serde(rename = "signature")]
799 pub signature: HyperliquidSignature,
800 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
802 pub vault_address: Option<String>,
803 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
805 pub expires_after: Option<u64>,
806}
807
808impl<T> HyperliquidExchangeRequest<T>
809where
810 T: Serialize,
811{
812 #[must_use]
814 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
815 Self {
816 action,
817 nonce,
818 signature,
819 vault_address: None,
820 expires_after: None,
821 }
822 }
823
824 #[must_use]
826 pub fn with_vault(
827 action: T,
828 nonce: u64,
829 signature: HyperliquidSignature,
830 vault_address: String,
831 ) -> Self {
832 Self {
833 action,
834 nonce,
835 signature,
836 vault_address: Some(vault_address),
837 expires_after: None,
838 }
839 }
840
841 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
843 serde_json::to_value(self)
844 }
845}
846
847#[derive(Debug, Clone, Serialize, Deserialize)]
849#[serde(untagged)]
850pub enum HyperliquidExchangeResponse {
851 Status {
853 status: String,
855 response: serde_json::Value,
857 },
858 Error {
860 error: String,
862 },
863}
864
865impl HyperliquidExchangeResponse {
866 pub fn is_ok(&self) -> bool {
867 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
868 }
869}
870
871pub const RESPONSE_STATUS_OK: &str = "ok";
873
874#[cfg(test)]
875mod tests {
876 use rstest::rstest;
877 use rust_decimal_macros::dec;
878 use serde_json::json;
879
880 use super::*;
881
882 #[rstest]
883 fn test_signature_serialization_and_debug_redaction() {
884 let signature = HyperliquidSignature::new(
885 "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
886 "0x2222222222222222222222222222222222222222222222222222222222222222".to_string(),
887 27,
888 );
889 let wire = serde_json::to_value(&signature).unwrap();
890 let debug = format!("{signature:?}");
891
892 assert_eq!(wire["r"], signature.r.expose_secret());
893 assert_eq!(wire["s"], signature.s.expose_secret());
894 assert_eq!(wire["v"], signature.v);
895 assert_eq!(debug.matches(REDACTED).count(), 2);
896 assert!(!debug.contains(signature.r.expose_secret()));
897 assert!(!debug.contains(signature.s.expose_secret()));
898 }
899
900 #[rstest]
901 fn test_exchange_action_request_debug_redacts_signature() {
902 let request = HyperliquidExchangeActionRequest {
903 action: HyperliquidExchangeAction::Noop,
904 nonce: 1_700_000_000_000,
905 signature: SecretString::from("0xsigned-action"),
906 vault_address: Some("0xvault".to_string()),
907 expires_after: Some(1_700_000_001_000),
908 };
909 let wire = serde_json::to_value(&request).unwrap();
910 let debug = format!("{request:?}");
911
912 assert_eq!(wire["signature"], "0xsigned-action");
913 assert_eq!(wire["nonce"], 1_700_000_000_000_u64);
914 assert!(debug.contains(REDACTED));
915 assert!(!debug.contains("0xsigned-action"));
916 }
917
918 #[rstest]
919 fn test_meta_deserialization() {
920 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
921
922 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
923
924 assert_eq!(meta.universe.len(), 1);
925 assert_eq!(meta.universe[0].name, "BTC");
926 assert_eq!(meta.universe[0].sz_decimals, 5);
927 }
928
929 #[rstest]
930 fn test_funding_history_entry_with_premium() {
931 let json = r#"{
932 "coin": "BTC",
933 "fundingRate": "0.0000125",
934 "premium": "0.00029005",
935 "time": 1769908800000
936 }"#;
937
938 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
939
940 assert_eq!(entry.coin, "BTC");
941 assert_eq!(entry.funding_rate, dec!(0.0000125));
942 assert_eq!(entry.premium, Some(dec!(0.00029005)));
943 assert_eq!(entry.time, 1769908800000);
944 }
945
946 #[rstest]
947 fn test_funding_history_entry_without_premium() {
948 let json = r#"{
951 "coin": "BTC",
952 "fundingRate": "0.0000033",
953 "time": 1769916000000
954 }"#;
955
956 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
957
958 assert!(entry.premium.is_none());
959 assert_eq!(entry.funding_rate, dec!(0.0000033));
960 }
961
962 #[rstest]
963 fn test_recent_trade_deserializes() {
964 let json = r#"{
966 "coin": "BTC",
967 "side": "B",
968 "px": "104250.0",
969 "sz": "0.0123",
970 "hash": "0xabc",
971 "time": 1769916000000,
972 "tid": 987654321,
973 "users": ["0xbuyer", "0xseller"]
974 }"#;
975
976 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
977
978 assert_eq!(trade.coin, "BTC");
979 assert_eq!(trade.side, HyperliquidSide::Buy);
980 assert_eq!(trade.px, dec!(104250.0));
981 assert_eq!(trade.sz, dec!(0.0123));
982 assert_eq!(trade.time, 1769916000000);
983 assert_eq!(trade.tid, 987654321);
984 }
985
986 #[rstest]
987 fn test_order_status_deserializes_frontend_market_tif() {
988 let status: HyperliquidOrderStatus =
989 crate::common::testing::load_test_data("http_order_status_frontend_market.json");
990 let entry = status.into_order().expect("order status entry");
991
992 assert_eq!(entry.order.oid, 1);
993 assert_eq!(
994 entry.order.tif,
995 Some(HyperliquidTimeInForce::FrontendMarket)
996 );
997 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
998 }
999
1000 #[rstest]
1001 fn test_historical_order_deserializes_liquidation_market_tif() {
1002 let entry: HyperliquidOrderStatusEntry =
1003 crate::common::testing::load_test_data("http_historical_order_liquidation_market.json");
1004
1005 assert_eq!(entry.order.oid, 42);
1006 assert_eq!(
1007 entry.order.tif,
1008 Some(HyperliquidTimeInForce::LiquidationMarket)
1009 );
1010 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
1011 }
1012
1013 #[rstest]
1014 fn test_user_fill_deserializes_tid_and_builder_fee() {
1015 let json = r#"{
1016 "coin": "BTC",
1017 "px": "60000.5",
1018 "sz": "0.001",
1019 "side": "B",
1020 "time": 1704470400000,
1021 "startPosition": "0",
1022 "dir": "Open Long",
1023 "closedPnl": "1.25",
1024 "hash": "0xabc",
1025 "oid": 7001,
1026 "crossed": true,
1027 "fee": "0.02",
1028 "feeToken": "USDC",
1029 "tid": 9001,
1030 "builderFee": "0.001"
1031 }"#;
1032
1033 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1034
1035 assert_eq!(fill.coin, "BTC");
1036 assert_eq!(fill.oid, 7001);
1037 assert_eq!(fill.tid, 9001);
1038 assert_eq!(fill.builder_fee, Some(dec!(0.001)));
1039 assert_eq!(fill.fee, dec!(0.02));
1040 }
1041
1042 #[rstest]
1043 fn test_user_fill_defaults_missing_tid_and_builder_fee() {
1044 let json = r#"{
1045 "coin": "ETH",
1046 "px": "2500.25",
1047 "sz": "0.5",
1048 "side": "A",
1049 "time": 1704470401000,
1050 "startPosition": "1.0",
1051 "dir": "Close Long",
1052 "closedPnl": "2.5",
1053 "hash": "0xdef",
1054 "oid": 8002,
1055 "crossed": false,
1056 "fee": "0.01",
1057 "feeToken": "USDC"
1058 }"#;
1059
1060 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1061
1062 assert_eq!(fill.oid, 8002);
1063 assert_eq!(fill.tid, 0);
1064 assert_eq!(fill.builder_fee, None);
1065 assert_eq!(fill.fee, dec!(0.01));
1066 assert!(!fill.crossed);
1067 }
1068
1069 #[rstest]
1070 fn test_perp_asset_hip3_fields() {
1071 let json = r#"{
1072 "name": "xyz:TSLA",
1073 "szDecimals": 3,
1074 "maxLeverage": 10,
1075 "onlyIsolated": true,
1076 "growthMode": "enabled",
1077 "marginMode": "strictIsolated"
1078 }"#;
1079
1080 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1081
1082 assert_eq!(asset.name, "xyz:TSLA");
1083 assert_eq!(asset.sz_decimals, 3);
1084 assert_eq!(asset.max_leverage, Some(10));
1085 assert_eq!(asset.only_isolated, Some(true));
1086 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
1087 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
1088 }
1089
1090 #[rstest]
1091 fn test_perp_asset_hip3_fields_absent() {
1092 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
1093
1094 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1095
1096 assert_eq!(asset.growth_mode, None);
1097 assert_eq!(asset.margin_mode, None);
1098 }
1099
1100 #[rstest]
1101 fn test_outcome_meta_defaults_missing_side_specs() {
1102 let json = r#"{
1103 "outcomes": [
1104 {
1105 "outcome": 123,
1106 "name": "Recurring",
1107 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
1108 }
1109 ]
1110 }"#;
1111
1112 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
1113
1114 assert_eq!(meta.outcomes.len(), 1);
1115 assert_eq!(meta.outcomes[0].outcome, 123);
1116 assert!(meta.outcomes[0].side_specs.is_empty());
1117 }
1118
1119 #[rstest]
1120 fn test_l2_book_deserialization() {
1121 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
1122
1123 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
1124
1125 assert_eq!(book.coin, "BTC");
1126 assert_eq!(book.levels.len(), 2);
1127 assert_eq!(book.time, 1234567890);
1128 }
1129
1130 #[rstest]
1131 fn test_exchange_response_deserialization() {
1132 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
1133
1134 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
1135 assert!(response.is_ok());
1136 }
1137
1138 #[rstest]
1139 fn test_spot_clearinghouse_state_deserialization() {
1140 let json = r#"{
1141 "balances": [
1142 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
1143 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
1144 ]
1145 }"#;
1146
1147 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1148
1149 assert_eq!(state.balances.len(), 2);
1150 let usdc = &state.balances[0];
1151 assert_eq!(usdc.coin, "USDC");
1152 assert_eq!(usdc.token, Some(0));
1153 assert_eq!(usdc.total.to_string(), "14.625485");
1154 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
1155 assert_eq!(usdc.free().to_string(), "14.625485");
1156 assert_eq!(usdc.avg_entry_px(), None);
1157
1158 let purr = &state.balances[1];
1159 assert_eq!(purr.coin, "PURR");
1160 assert_eq!(purr.token, Some(1));
1161 assert_eq!(purr.free().to_string(), "1900");
1162 assert_eq!(
1163 purr.avg_entry_px().unwrap(),
1164 rust_decimal_macros::dec!(0.61728)
1165 );
1166 }
1167
1168 #[rstest]
1169 fn test_spot_balance_outcome_side_token_lacks_token_field() {
1170 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1172 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1173 assert_eq!(balance.coin, "+250");
1174 assert_eq!(balance.token, None);
1175 }
1176
1177 #[rstest]
1178 fn test_spot_clearinghouse_state_empty() {
1179 let json = r#"{"balances": []}"#;
1180 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1181 assert!(state.balances.is_empty());
1182 }
1183
1184 #[rstest]
1185 fn test_spot_balance_handles_missing_entry_ntl() {
1186 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1187 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1188 assert_eq!(balance.entry_ntl, None);
1189 assert_eq!(balance.avg_entry_px(), None);
1190 }
1191
1192 #[rstest]
1193 fn test_msgpack_serialization_matches_python() {
1194 let action = HyperliquidExchangeAction::Order {
1199 orders: vec![],
1200 grouping: HyperliquidExchangeGrouping::Na,
1201 builder: None,
1202 };
1203
1204 let json = serde_json::to_string(&action).unwrap();
1206 assert!(
1207 json.contains(r#""type":"order""#),
1208 "JSON should have type tag: {json}"
1209 );
1210
1211 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1213
1214 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1216
1217 assert!(
1219 decoded.get("type").is_some(),
1220 "MsgPack should have type tag. Decoded: {decoded:?}"
1221 );
1222 assert_eq!(
1223 decoded.get("type").unwrap().as_str().unwrap(),
1224 "order",
1225 "Type should be 'order'"
1226 );
1227 assert!(decoded.get("orders").is_some(), "Should have orders field");
1228 assert!(
1229 decoded.get("grouping").is_some(),
1230 "Should have grouping field"
1231 );
1232 }
1233
1234 #[rstest]
1235 fn test_cancel_action_serialization(
1236 #[values(None, Some(false), Some(true))] fast: Option<bool>,
1237 #[values(false, true)] by_cloid: bool,
1238 ) {
1239 let (action, name) = if by_cloid {
1240 (
1241 HyperliquidExchangeAction::CancelByCloid {
1242 cancels: vec![HyperliquidExchangeCancelByCloidRequest {
1243 asset: 7,
1244 cloid: Cloid::from_hex("0x0123456789abcdef0123456789abcdef").unwrap(),
1245 }],
1246 fast,
1247 },
1248 "cancel_by_cloid",
1249 )
1250 } else {
1251 (
1252 HyperliquidExchangeAction::Cancel {
1253 cancels: vec![HyperliquidExchangeCancelOrderRequest {
1254 asset: 7,
1255 oid: 12345,
1256 }],
1257 fast,
1258 },
1259 "cancel",
1260 )
1261 };
1262
1263 let fixtures: serde_json::Value =
1264 serde_json::from_str(include_str!("../../test_data/cancel_actions.json")).unwrap();
1265 let expected = &fixtures[name][if fast == Some(true) { "fast" } else { "normal" }];
1266 let json = serde_json::to_value(&action).unwrap();
1267 let msgpack = rmp_serde::to_vec_named(&action).unwrap();
1268
1269 assert_eq!(json, expected["json"]);
1270 assert_eq!(hex::encode(msgpack), expected["msgpack"].as_str().unwrap());
1271 }
1272
1273 #[rstest]
1274 fn test_order_response_normal_tpsl_with_waiting_children() {
1275 let json = r#"{
1279 "statuses": [
1280 {"resting": {"oid": 446050656712}},
1281 "waitingForFill",
1282 "waitingForTrigger"
1283 ]
1284 }"#;
1285
1286 let data: HyperliquidExchangeOrderResponseData = serde_json::from_str(json).unwrap();
1287 assert_eq!(data.statuses.len(), 3);
1288
1289 assert!(matches!(
1290 data.statuses[0],
1291 HyperliquidExchangeOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1292 ));
1293 assert!(matches!(
1294 data.statuses[1],
1295 HyperliquidExchangeOrderStatus::Tag(HyperliquidExchangeOrderStatusTag::WaitingForFill)
1296 ));
1297 assert!(matches!(
1298 data.statuses[2],
1299 HyperliquidExchangeOrderStatus::Tag(
1300 HyperliquidExchangeOrderStatusTag::WaitingForTrigger
1301 )
1302 ));
1303 }
1304
1305 #[rstest]
1306 fn test_user_outcome_split_serialization() {
1307 let action = HyperliquidExchangeAction::UserOutcome {
1308 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1309 HyperliquidExchangeSplitOutcomeParams {
1310 outcome: 1,
1311 amount: dec!(123.0),
1312 },
1313 ),
1314 };
1315
1316 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1317 assert_eq!(
1318 value,
1319 json!({
1320 "type": "userOutcome",
1321 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1322 })
1323 );
1324 }
1325
1326 #[rstest]
1327 fn test_user_outcome_split_msgpack_roundtrip() {
1328 let action = HyperliquidExchangeAction::UserOutcome {
1329 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1330 HyperliquidExchangeSplitOutcomeParams {
1331 outcome: 4,
1332 amount: dec!(10),
1333 },
1334 ),
1335 };
1336
1337 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1338 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1339 assert_eq!(
1340 decoded,
1341 json!({
1342 "type": "userOutcome",
1343 "splitOutcome": { "outcome": 4, "amount": "10" }
1344 })
1345 );
1346 }
1347
1348 #[rstest]
1349 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1350 let level = HyperliquidLevel {
1353 px: dec!(98450.5),
1354 sz: dec!(2.5),
1355 };
1356 let value = serde_json::to_value(&level).unwrap();
1357 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1358 }
1359
1360 #[rstest]
1361 fn test_user_outcome_merge_outcome_serialization() {
1362 let action = HyperliquidExchangeAction::UserOutcome {
1363 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1364 HyperliquidExchangeMergeOutcomeParams {
1365 outcome: 1,
1366 amount: Some(dec!(5.0)),
1367 },
1368 ),
1369 };
1370 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1371 assert_eq!(
1372 value,
1373 json!({
1374 "type": "userOutcome",
1375 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1376 })
1377 );
1378 }
1379
1380 #[rstest]
1381 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1382 let action = HyperliquidExchangeAction::UserOutcome {
1383 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1384 HyperliquidExchangeMergeOutcomeParams {
1385 outcome: 7,
1386 amount: None,
1387 },
1388 ),
1389 };
1390 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1391 assert_eq!(
1392 value,
1393 json!({
1394 "type": "userOutcome",
1395 "mergeOutcome": { "outcome": 7, "amount": null }
1396 })
1397 );
1398 }
1399
1400 #[rstest]
1401 fn test_user_outcome_merge_question_serialization() {
1402 let action = HyperliquidExchangeAction::UserOutcome {
1403 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1404 HyperliquidExchangeMergeQuestionParams {
1405 question: 9,
1406 amount: Some(dec!(2.0)),
1407 },
1408 ),
1409 };
1410 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1411 assert_eq!(
1412 value,
1413 json!({
1414 "type": "userOutcome",
1415 "mergeQuestion": { "question": 9, "amount": "2.0" }
1416 })
1417 );
1418 }
1419
1420 #[rstest]
1421 fn test_user_outcome_merge_question_null_amount_means_max() {
1422 let action = HyperliquidExchangeAction::UserOutcome {
1423 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1424 HyperliquidExchangeMergeQuestionParams {
1425 question: 9,
1426 amount: None,
1427 },
1428 ),
1429 };
1430 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1431 assert_eq!(
1432 value,
1433 json!({
1434 "type": "userOutcome",
1435 "mergeQuestion": { "question": 9, "amount": null }
1436 })
1437 );
1438 }
1439
1440 #[rstest]
1441 fn test_user_outcome_negate_outcome_serialization() {
1442 let action = HyperliquidExchangeAction::UserOutcome {
1443 op: HyperliquidExchangeUserOutcomeOp::NegateOutcome(
1444 HyperliquidExchangeNegateOutcomeParams {
1445 question: 9,
1446 outcome: 52,
1447 amount: dec!(1.5),
1448 },
1449 ),
1450 };
1451 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1452 assert_eq!(
1453 value,
1454 json!({
1455 "type": "userOutcome",
1456 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1457 })
1458 );
1459 }
1460
1461 #[rstest]
1462 fn test_modify_target_serializes_numeric_oid() {
1463 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Oid(12345));
1464 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1465
1466 assert_eq!(value["oid"], json!(12345));
1467 }
1468
1469 #[rstest]
1470 fn test_modify_target_serializes_cloid() {
1471 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
1472 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Cloid(cloid));
1473 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1474
1475 assert_eq!(value["oid"], json!("0x1234567890abcdef1234567890abcdef"));
1476 }
1477
1478 #[rstest]
1479 #[case("51000.00", "0.2500", "52000.000", "51000", "0.25", "52000")]
1480 #[case("0.00", "0.0000", "-0.000", "0", "0", "0")]
1481 #[case("-12.3400", "2.500", "-10.0100", "-12.34", "2.5", "-10.01")]
1482 #[case(
1483 "0.0000000000000000000000000001",
1484 "1.000",
1485 "10.0100",
1486 "0.0000000000000000000000000001",
1487 "1",
1488 "10.01"
1489 )]
1490 #[case(
1491 "79228162514264337593543950335",
1492 "0.2500",
1493 "52000.00",
1494 "79228162514264337593543950335",
1495 "0.25",
1496 "52000"
1497 )]
1498 fn test_order_decimals_serialize_without_trailing_zeros(
1499 #[case] price: &str,
1500 #[case] size: &str,
1501 #[case] trigger_price: &str,
1502 #[case] expected_price: &str,
1503 #[case] expected_size: &str,
1504 #[case] expected_trigger_price: &str,
1505 #[values(false, true)] is_market: bool,
1506 ) {
1507 let request = HyperliquidExchangePlaceOrderRequest {
1508 asset: 7,
1509 is_buy: false,
1510 price: price.parse().unwrap(),
1511 size: size.parse().unwrap(),
1512 reduce_only: true,
1513 kind: HyperliquidExchangeOrderKind::Trigger {
1514 trigger: HyperliquidExchangeTriggerParams {
1515 is_market,
1516 trigger_px: trigger_price.parse().unwrap(),
1517 tpsl: HyperliquidExchangeTpSl::Tp,
1518 },
1519 },
1520 cloid: Some(Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap()),
1521 };
1522
1523 let expected = json!({
1524 "a": 7, "b": false, "p": expected_price, "s": expected_size, "r": true,
1525 "t": {"trigger": {"isMarket": is_market, "triggerPx": expected_trigger_price, "tpsl": "tp"}},
1526 "c": "0x1234567890abcdef1234567890abcdef",
1527 });
1528 let canonical: HyperliquidExchangePlaceOrderRequest =
1529 serde_json::from_value(expected.clone()).unwrap();
1530 let json = serde_json::to_value(&request).unwrap();
1531 let msgpack = rmp_serde::to_vec_named(&request).unwrap();
1532 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack).unwrap();
1533
1534 assert_eq!(json, expected);
1535 assert_eq!(decoded, expected);
1536 assert_eq!(msgpack, rmp_serde::to_vec_named(&canonical).unwrap());
1537 assert_eq!(
1538 serde_json::to_vec(&request).unwrap(),
1539 serde_json::to_vec(&canonical).unwrap()
1540 );
1541 assert_eq!(request.price.to_string(), price);
1542 assert_eq!(request.size.to_string(), size);
1543 assert_eq!(canonical, request);
1544 }
1545
1546 fn modify_request_with_target(
1547 oid: HyperliquidExchangeModifyTarget,
1548 ) -> HyperliquidExchangeModifyOrderRequest {
1549 HyperliquidExchangeModifyOrderRequest {
1550 oid,
1551 order: HyperliquidExchangePlaceOrderRequest {
1552 asset: 0,
1553 is_buy: true,
1554 price: dec!(51000),
1555 size: dec!(0.2),
1556 reduce_only: false,
1557 kind: HyperliquidExchangeOrderKind::Limit {
1558 limit: HyperliquidExchangeLimitParams {
1559 tif: HyperliquidExchangeTif::Gtc,
1560 },
1561 },
1562 cloid: None,
1563 },
1564 }
1565 }
1566}
1567
1568#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1572pub enum HyperliquidExchangeTif {
1573 #[serde(rename = "Alo")]
1575 Alo,
1576 #[serde(rename = "Ioc")]
1578 Ioc,
1579 #[serde(rename = "Gtc")]
1581 Gtc,
1582}
1583
1584#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1586pub enum HyperliquidExchangeTpSl {
1587 #[serde(rename = "tp")]
1589 Tp,
1590 #[serde(rename = "sl")]
1592 Sl,
1593}
1594
1595#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1597pub enum HyperliquidExchangeGrouping {
1598 #[serde(rename = "na")]
1600 #[default]
1601 Na,
1602 #[serde(rename = "normalTpsl")]
1604 NormalTpsl,
1605 #[serde(rename = "positionTpsl")]
1607 PositionTpsl,
1608}
1609
1610#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1612#[serde(untagged)]
1613pub enum HyperliquidExchangeOrderKind {
1614 Limit {
1616 limit: HyperliquidExchangeLimitParams,
1618 },
1619 Trigger {
1621 trigger: HyperliquidExchangeTriggerParams,
1623 },
1624}
1625
1626#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1628pub struct HyperliquidExchangeLimitParams {
1629 pub tif: HyperliquidExchangeTif,
1631}
1632
1633#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1635#[serde(rename_all = "camelCase")]
1636pub struct HyperliquidExchangeTriggerParams {
1637 pub is_market: bool,
1639 #[serde(
1641 serialize_with = "serialize_order_decimal_as_str",
1642 deserialize_with = "deserialize_decimal_from_str"
1643 )]
1644 pub trigger_px: Decimal,
1645 pub tpsl: HyperliquidExchangeTpSl,
1647}
1648
1649#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1654pub struct HyperliquidExchangeBuilderFee {
1655 #[serde(rename = "b")]
1657 pub address: String,
1658 #[serde(rename = "f")]
1660 pub fee_tenths_bp: u32,
1661}
1662
1663#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1668pub struct HyperliquidExchangePlaceOrderRequest {
1669 #[serde(rename = "a")]
1671 pub asset: AssetId,
1672 #[serde(rename = "b")]
1674 pub is_buy: bool,
1675 #[serde(
1677 rename = "p",
1678 serialize_with = "serialize_order_decimal_as_str",
1679 deserialize_with = "deserialize_decimal_from_str"
1680 )]
1681 pub price: Decimal,
1682 #[serde(
1684 rename = "s",
1685 serialize_with = "serialize_order_decimal_as_str",
1686 deserialize_with = "deserialize_decimal_from_str"
1687 )]
1688 pub size: Decimal,
1689 #[serde(rename = "r")]
1691 pub reduce_only: bool,
1692 #[serde(rename = "t")]
1694 pub kind: HyperliquidExchangeOrderKind,
1695 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1697 pub cloid: Option<Cloid>,
1698}
1699
1700#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1702pub struct HyperliquidExchangeCancelOrderRequest {
1703 #[serde(rename = "a")]
1705 pub asset: AssetId,
1706 #[serde(rename = "o")]
1708 pub oid: OrderId,
1709}
1710
1711#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1716pub struct HyperliquidExchangeCancelByCloidRequest {
1717 pub asset: AssetId,
1719 pub cloid: Cloid,
1721}
1722
1723#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1728#[serde(untagged)]
1729pub enum HyperliquidExchangeModifyTarget {
1730 Oid(OrderId),
1732 Cloid(Cloid),
1734}
1735
1736impl HyperliquidExchangeModifyTarget {
1737 pub fn from_venue_order_id(
1743 venue_order_id: &VenueOrderId,
1744 ) -> Result<Self, std::num::ParseIntError> {
1745 venue_order_id.as_str().parse::<OrderId>().map(Self::Oid)
1746 }
1747}
1748
1749impl From<OrderId> for HyperliquidExchangeModifyTarget {
1750 fn from(value: OrderId) -> Self {
1751 Self::Oid(value)
1752 }
1753}
1754
1755impl From<Cloid> for HyperliquidExchangeModifyTarget {
1756 fn from(value: Cloid) -> Self {
1757 Self::Cloid(value)
1758 }
1759}
1760
1761#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1766pub struct HyperliquidExchangeModifyOrderRequest {
1767 pub oid: HyperliquidExchangeModifyTarget,
1769 pub order: HyperliquidExchangePlaceOrderRequest,
1771}
1772
1773#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1778pub struct HyperliquidExchangeSplitOutcomeParams {
1779 pub outcome: u32,
1781 #[serde(
1783 serialize_with = "serialize_decimal_as_str",
1784 deserialize_with = "deserialize_decimal_from_str"
1785 )]
1786 pub amount: Decimal,
1787}
1788
1789#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1795pub struct HyperliquidExchangeMergeOutcomeParams {
1796 pub outcome: u32,
1798 #[serde(
1800 default,
1801 serialize_with = "serialize_optional_decimal_as_str",
1802 deserialize_with = "deserialize_optional_decimal_from_str"
1803 )]
1804 pub amount: Option<Decimal>,
1805}
1806
1807#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1813pub struct HyperliquidExchangeMergeQuestionParams {
1814 pub question: u32,
1816 #[serde(
1818 default,
1819 serialize_with = "serialize_optional_decimal_as_str",
1820 deserialize_with = "deserialize_optional_decimal_from_str"
1821 )]
1822 pub amount: Option<Decimal>,
1823}
1824
1825#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1830pub struct HyperliquidExchangeNegateOutcomeParams {
1831 pub question: u32,
1833 pub outcome: u32,
1835 #[serde(
1837 serialize_with = "serialize_decimal_as_str",
1838 deserialize_with = "deserialize_decimal_from_str"
1839 )]
1840 pub amount: Decimal,
1841}
1842
1843#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1850pub enum HyperliquidExchangeUserOutcomeOp {
1851 #[serde(rename = "splitOutcome")]
1853 SplitOutcome(HyperliquidExchangeSplitOutcomeParams),
1854 #[serde(rename = "mergeOutcome")]
1857 MergeOutcome(HyperliquidExchangeMergeOutcomeParams),
1858 #[serde(rename = "mergeQuestion")]
1861 MergeQuestion(HyperliquidExchangeMergeQuestionParams),
1862 #[serde(rename = "negateOutcome")]
1865 NegateOutcome(HyperliquidExchangeNegateOutcomeParams),
1866}
1867
1868#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1870pub struct HyperliquidExchangeTwapRequest {
1871 #[serde(rename = "a")]
1873 pub asset: AssetId,
1874 #[serde(rename = "b")]
1876 pub is_buy: bool,
1877 #[serde(
1879 rename = "s",
1880 serialize_with = "serialize_decimal_as_str",
1881 deserialize_with = "deserialize_decimal_from_str"
1882 )]
1883 pub size: Decimal,
1884 #[serde(rename = "m")]
1886 pub duration_ms: u64,
1887}
1888
1889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1895#[serde(tag = "type")]
1896pub enum HyperliquidExchangeAction {
1897 #[serde(rename = "order")]
1899 Order {
1900 orders: Vec<HyperliquidExchangePlaceOrderRequest>,
1902 #[serde(default)]
1904 grouping: HyperliquidExchangeGrouping,
1905 #[serde(skip_serializing_if = "Option::is_none")]
1907 builder: Option<HyperliquidExchangeBuilderFee>,
1908 },
1909
1910 #[serde(rename = "cancel")]
1912 Cancel {
1913 cancels: Vec<HyperliquidExchangeCancelOrderRequest>,
1915 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
1917 fast: Option<bool>,
1918 },
1919
1920 #[serde(rename = "cancelByCloid")]
1922 CancelByCloid {
1923 cancels: Vec<HyperliquidExchangeCancelByCloidRequest>,
1925 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
1927 fast: Option<bool>,
1928 },
1929
1930 #[serde(rename = "modify")]
1932 Modify {
1933 #[serde(flatten)]
1935 modify: HyperliquidExchangeModifyOrderRequest,
1936 },
1937
1938 #[serde(rename = "batchModify")]
1940 BatchModify {
1941 modifies: Vec<HyperliquidExchangeModifyOrderRequest>,
1943 },
1944
1945 #[serde(rename = "scheduleCancel")]
1947 ScheduleCancel {
1948 #[serde(skip_serializing_if = "Option::is_none")]
1951 time: Option<u64>,
1952 },
1953
1954 #[serde(rename = "updateLeverage")]
1956 UpdateLeverage {
1957 #[serde(rename = "a")]
1959 asset: AssetId,
1960 #[serde(rename = "isCross")]
1962 is_cross: bool,
1963 #[serde(rename = "leverage")]
1965 leverage: u32,
1966 },
1967
1968 #[serde(rename = "updateIsolatedMargin")]
1970 UpdateIsolatedMargin {
1971 #[serde(rename = "a")]
1973 asset: AssetId,
1974 #[serde(
1976 rename = "delta",
1977 serialize_with = "serialize_decimal_as_str",
1978 deserialize_with = "deserialize_decimal_from_str"
1979 )]
1980 delta: Decimal,
1981 },
1982
1983 #[serde(rename = "usdClassTransfer")]
1985 UsdClassTransfer {
1986 from: String,
1988 to: String,
1990 #[serde(
1992 serialize_with = "serialize_decimal_as_str",
1993 deserialize_with = "deserialize_decimal_from_str"
1994 )]
1995 amount: Decimal,
1996 },
1997
1998 #[serde(rename = "userOutcome")]
2004 UserOutcome {
2005 #[serde(flatten)]
2007 op: HyperliquidExchangeUserOutcomeOp,
2008 },
2009
2010 #[serde(rename = "twapPlace")]
2012 TwapPlace {
2013 #[serde(flatten)]
2015 twap: HyperliquidExchangeTwapRequest,
2016 },
2017
2018 #[serde(rename = "twapCancel")]
2020 TwapCancel {
2021 #[serde(rename = "a")]
2023 asset: AssetId,
2024 #[serde(rename = "t")]
2026 twap_id: u64,
2027 },
2028
2029 #[serde(rename = "noop")]
2031 Noop,
2032}
2033
2034#[derive(Debug, Clone, Serialize)]
2039#[serde(rename_all = "camelCase")]
2040pub struct HyperliquidExchangeActionRequest {
2041 pub action: HyperliquidExchangeAction,
2043 pub nonce: u64,
2045 pub signature: SecretString,
2047 #[serde(skip_serializing_if = "Option::is_none")]
2049 pub vault_address: Option<String>,
2050 #[serde(skip_serializing_if = "Option::is_none")]
2053 pub expires_after: Option<u64>,
2054}
2055
2056#[derive(Debug, Clone, Serialize, Deserialize)]
2058pub struct HyperliquidExchangeActionResponse {
2059 pub status: String,
2061 pub response: HyperliquidExchangeResponseData,
2063}
2064
2065#[derive(Debug, Clone, Serialize, Deserialize)]
2067#[serde(tag = "type")]
2068pub enum HyperliquidExchangeResponseData {
2069 #[serde(rename = "order")]
2071 Order {
2072 data: HyperliquidExchangeOrderResponseData,
2074 },
2075 #[serde(rename = "cancel")]
2077 Cancel {
2078 data: HyperliquidExchangeCancelResponseData,
2080 },
2081 #[serde(rename = "modify")]
2083 Modify {
2084 data: HyperliquidExchangeModifyResponseData,
2086 },
2087 #[serde(rename = "default")]
2089 Default,
2090 #[serde(other)]
2092 Unknown,
2093}
2094
2095#[derive(Debug, Clone, Serialize, Deserialize)]
2097pub struct HyperliquidExchangeOrderResponseData {
2098 pub statuses: Vec<HyperliquidExchangeOrderStatus>,
2100}
2101
2102#[derive(Debug, Clone, Serialize, Deserialize)]
2104pub struct HyperliquidExchangeCancelResponseData {
2105 pub statuses: Vec<HyperliquidExchangeCancelStatus>,
2107}
2108
2109#[derive(Debug, Clone, Serialize, Deserialize)]
2111pub struct HyperliquidExchangeModifyResponseData {
2112 pub statuses: Vec<HyperliquidExchangeModifyStatus>,
2114}
2115
2116#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2118#[serde(untagged)]
2119pub enum HyperliquidExchangeOrderStatus {
2120 Resting {
2122 resting: HyperliquidExchangeRestingInfo,
2124 },
2125 Filled {
2127 filled: HyperliquidExchangeFilledInfo,
2129 },
2130 Error {
2132 error: String,
2134 },
2135 Tag(HyperliquidExchangeOrderStatusTag),
2139}
2140
2141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2147pub enum HyperliquidExchangeOrderStatusTag {
2148 #[serde(rename = "waitingForFill")]
2150 WaitingForFill,
2151 #[serde(rename = "waitingForTrigger")]
2153 WaitingForTrigger,
2154}
2155
2156#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2158pub struct HyperliquidExchangeRestingInfo {
2159 pub oid: OrderId,
2161}
2162
2163#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2165pub struct HyperliquidExchangeFilledInfo {
2166 #[serde(
2168 rename = "totalSz",
2169 serialize_with = "serialize_decimal_as_str",
2170 deserialize_with = "deserialize_decimal_from_str"
2171 )]
2172 pub total_sz: Decimal,
2173 #[serde(
2175 rename = "avgPx",
2176 serialize_with = "serialize_decimal_as_str",
2177 deserialize_with = "deserialize_decimal_from_str"
2178 )]
2179 pub avg_px: Decimal,
2180 pub oid: OrderId,
2182}
2183
2184#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2186#[serde(untagged)]
2187pub enum HyperliquidExchangeCancelStatus {
2188 Success(String), Error {
2192 error: String,
2194 },
2195}
2196
2197#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2199#[serde(untagged)]
2200pub enum HyperliquidExchangeModifyStatus {
2201 Success(String), Error {
2205 error: String,
2207 },
2208}
2209
2210#[derive(Debug, Clone, Serialize, Deserialize)]
2213#[serde(rename_all = "camelCase")]
2214pub struct ClearinghouseState {
2215 #[serde(default)]
2217 pub asset_positions: Vec<AssetPosition>,
2218 #[serde(default)]
2220 pub cross_margin_summary: Option<CrossMarginSummary>,
2221 #[serde(
2223 default,
2224 serialize_with = "serialize_optional_decimal_as_str",
2225 deserialize_with = "deserialize_optional_decimal_from_str"
2226 )]
2227 pub withdrawable: Option<Decimal>,
2228 #[serde(default)]
2230 pub time: Option<u64>,
2231}
2232
2233#[derive(Debug, Clone, Serialize, Deserialize)]
2235#[serde(rename_all = "camelCase")]
2236pub struct AssetPosition {
2237 pub position: PositionData,
2239 #[serde(rename = "type")]
2241 pub position_type: HyperliquidPositionType,
2242}
2243
2244#[derive(Debug, Clone, Serialize, Deserialize)]
2246#[serde(rename_all = "camelCase")]
2247pub struct LeverageInfo {
2248 #[serde(rename = "type")]
2249 pub leverage_type: HyperliquidLeverageType,
2250 pub value: u32,
2252}
2253
2254#[derive(Debug, Clone, Serialize, Deserialize)]
2256#[serde(rename_all = "camelCase")]
2257pub struct CumFundingInfo {
2258 #[serde(
2260 rename = "allTime",
2261 serialize_with = "serialize_decimal_as_str",
2262 deserialize_with = "deserialize_decimal_from_str"
2263 )]
2264 pub all_time: Decimal,
2265 #[serde(
2267 rename = "sinceOpen",
2268 serialize_with = "serialize_decimal_as_str",
2269 deserialize_with = "deserialize_decimal_from_str"
2270 )]
2271 pub since_open: Decimal,
2272 #[serde(
2274 rename = "sinceChange",
2275 serialize_with = "serialize_decimal_as_str",
2276 deserialize_with = "deserialize_decimal_from_str"
2277 )]
2278 pub since_change: Decimal,
2279}
2280
2281#[derive(Debug, Clone, Serialize, Deserialize)]
2283#[serde(rename_all = "camelCase")]
2284pub struct PositionData {
2285 pub coin: Ustr,
2287 #[serde(rename = "cumFunding")]
2289 pub cum_funding: CumFundingInfo,
2290 #[serde(
2292 rename = "entryPx",
2293 serialize_with = "serialize_optional_decimal_as_str",
2294 deserialize_with = "deserialize_optional_decimal_from_str",
2295 default
2296 )]
2297 pub entry_px: Option<Decimal>,
2298 pub leverage: LeverageInfo,
2300 #[serde(
2302 rename = "liquidationPx",
2303 serialize_with = "serialize_optional_decimal_as_str",
2304 deserialize_with = "deserialize_optional_decimal_from_str",
2305 default
2306 )]
2307 pub liquidation_px: Option<Decimal>,
2308 #[serde(
2310 rename = "marginUsed",
2311 serialize_with = "serialize_decimal_as_str",
2312 deserialize_with = "deserialize_decimal_from_str"
2313 )]
2314 pub margin_used: Decimal,
2315 #[serde(rename = "maxLeverage", default)]
2317 pub max_leverage: Option<u32>,
2318 #[serde(
2320 rename = "positionValue",
2321 serialize_with = "serialize_decimal_as_str",
2322 deserialize_with = "deserialize_decimal_from_str"
2323 )]
2324 pub position_value: Decimal,
2325 #[serde(
2327 rename = "returnOnEquity",
2328 serialize_with = "serialize_decimal_as_str",
2329 deserialize_with = "deserialize_decimal_from_str"
2330 )]
2331 pub return_on_equity: Decimal,
2332 #[serde(
2334 rename = "szi",
2335 serialize_with = "serialize_decimal_as_str",
2336 deserialize_with = "deserialize_decimal_from_str"
2337 )]
2338 pub szi: Decimal,
2339 #[serde(
2341 rename = "unrealizedPnl",
2342 serialize_with = "serialize_decimal_as_str",
2343 deserialize_with = "deserialize_decimal_from_str"
2344 )]
2345 pub unrealized_pnl: Decimal,
2346}
2347
2348#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2354#[serde(rename_all = "camelCase")]
2355pub struct SpotClearinghouseState {
2356 #[serde(default)]
2358 pub balances: Vec<SpotBalance>,
2359}
2360
2361#[derive(Debug, Clone, Serialize, Deserialize)]
2363#[serde(rename_all = "camelCase")]
2364pub struct SpotBalance {
2365 pub coin: Ustr,
2367 #[serde(default)]
2370 pub token: Option<u32>,
2371 #[serde(
2373 serialize_with = "serialize_decimal_as_str",
2374 deserialize_with = "deserialize_decimal_from_str"
2375 )]
2376 pub total: Decimal,
2377 #[serde(
2379 serialize_with = "serialize_decimal_as_str",
2380 deserialize_with = "deserialize_decimal_from_str"
2381 )]
2382 pub hold: Decimal,
2383 #[serde(
2385 default,
2386 serialize_with = "serialize_optional_decimal_as_str",
2387 deserialize_with = "deserialize_optional_decimal_from_str"
2388 )]
2389 pub entry_ntl: Option<Decimal>,
2390}
2391
2392impl SpotBalance {
2393 #[must_use]
2395 pub fn free(&self) -> Decimal {
2396 (self.total - self.hold).max(Decimal::ZERO)
2397 }
2398
2399 #[must_use]
2401 pub fn avg_entry_px(&self) -> Option<Decimal> {
2402 let entry_ntl = self.entry_ntl?;
2403
2404 if entry_ntl.is_zero() || self.total.is_zero() {
2405 return None;
2406 }
2407
2408 Some(entry_ntl / self.total)
2409 }
2410}
2411
2412#[derive(Debug, Clone, Serialize, Deserialize)]
2414#[serde(rename_all = "camelCase")]
2415pub struct CrossMarginSummary {
2416 #[serde(
2418 rename = "accountValue",
2419 serialize_with = "serialize_decimal_as_str",
2420 deserialize_with = "deserialize_decimal_from_str"
2421 )]
2422 pub account_value: Decimal,
2423 #[serde(
2425 rename = "totalNtlPos",
2426 serialize_with = "serialize_decimal_as_str",
2427 deserialize_with = "deserialize_decimal_from_str"
2428 )]
2429 pub total_ntl_pos: Decimal,
2430 #[serde(
2432 rename = "totalRawUsd",
2433 serialize_with = "serialize_decimal_as_str",
2434 deserialize_with = "deserialize_decimal_from_str"
2435 )]
2436 pub total_raw_usd: Decimal,
2437 #[serde(
2439 rename = "totalMarginUsed",
2440 serialize_with = "serialize_decimal_as_str",
2441 deserialize_with = "deserialize_decimal_from_str"
2442 )]
2443 pub total_margin_used: Decimal,
2444 #[serde(
2446 rename = "withdrawable",
2447 default,
2448 serialize_with = "serialize_optional_decimal_as_str",
2449 deserialize_with = "deserialize_optional_decimal_from_str"
2450 )]
2451 pub withdrawable: Option<Decimal>,
2452}
2453
2454fn serialize_order_decimal_as_str<S>(decimal: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
2455where
2456 S: Serializer,
2457{
2458 serialize_decimal_as_str(&decimal.normalize(), serializer)
2459}