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 use crate::http::query::ExchangeAction;
882
883 #[rstest]
884 #[case(0)]
885 #[case(1)]
886 #[case(2)]
887 #[case(3)]
888 fn test_leverage_margin_wire_contract(#[case] case_index: usize) {
889 let fixture: serde_json::Value =
890 serde_json::from_str(include_str!("../../test_data/leverage_margin_actions.json"))
891 .unwrap();
892 let case = &fixture["cases"][case_index];
893 let expected_json = case["json"].as_str().unwrap();
894 let expected_bytes = hex::decode(case["msgpack"].as_str().unwrap()).unwrap();
895 let action: HyperliquidExchangeAction = serde_json::from_str(expected_json).unwrap();
896
897 let legacy = match action {
898 HyperliquidExchangeAction::UpdateLeverage {
899 asset,
900 is_cross,
901 leverage,
902 } => ExchangeAction::update_leverage(asset, is_cross, leverage),
903 HyperliquidExchangeAction::UpdateIsolatedMargin {
904 asset,
905 is_buy,
906 ntli,
907 } => ExchangeAction::update_isolated_margin(asset, is_buy, ntli),
908 _ => panic!("unexpected fixture action"),
909 };
910
911 let bytes = rmp_serde::to_vec_named(&action).unwrap();
912 let roundtrip: HyperliquidExchangeAction = rmp_serde::from_slice(&bytes).unwrap();
913
914 assert_eq!(serde_json::to_string(&action).unwrap(), expected_json);
915 assert_eq!(serde_json::to_string(&legacy).unwrap(), expected_json);
916 assert_eq!(bytes, expected_bytes);
917 assert_eq!(rmp_serde::to_vec_named(&legacy).unwrap(), expected_bytes);
918 assert_eq!(roundtrip, action);
919 }
920
921 #[rstest]
922 fn test_signature_serialization_and_debug_redaction() {
923 let signature = HyperliquidSignature::new(
924 "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
925 "0x2222222222222222222222222222222222222222222222222222222222222222".to_string(),
926 27,
927 );
928 let wire = serde_json::to_value(&signature).unwrap();
929 let debug = format!("{signature:?}");
930
931 assert_eq!(wire["r"], signature.r.expose_secret());
932 assert_eq!(wire["s"], signature.s.expose_secret());
933 assert_eq!(wire["v"], signature.v);
934 assert_eq!(debug.matches(REDACTED).count(), 2);
935 assert!(!debug.contains(signature.r.expose_secret()));
936 assert!(!debug.contains(signature.s.expose_secret()));
937 }
938
939 #[rstest]
940 fn test_exchange_action_request_debug_redacts_signature() {
941 let request = HyperliquidExchangeActionRequest {
942 action: HyperliquidExchangeAction::Noop,
943 nonce: 1_700_000_000_000,
944 signature: SecretString::from("0xsigned-action"),
945 vault_address: Some("0xvault".to_string()),
946 expires_after: Some(1_700_000_001_000),
947 };
948 let wire = serde_json::to_value(&request).unwrap();
949 let debug = format!("{request:?}");
950
951 assert_eq!(wire["signature"], "0xsigned-action");
952 assert_eq!(wire["nonce"], 1_700_000_000_000_u64);
953 assert!(debug.contains(REDACTED));
954 assert!(!debug.contains("0xsigned-action"));
955 }
956
957 #[rstest]
958 fn test_meta_deserialization() {
959 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
960
961 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
962
963 assert_eq!(meta.universe.len(), 1);
964 assert_eq!(meta.universe[0].name, "BTC");
965 assert_eq!(meta.universe[0].sz_decimals, 5);
966 }
967
968 #[rstest]
969 fn test_funding_history_entry_with_premium() {
970 let json = r#"{
971 "coin": "BTC",
972 "fundingRate": "0.0000125",
973 "premium": "0.00029005",
974 "time": 1769908800000
975 }"#;
976
977 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
978
979 assert_eq!(entry.coin, "BTC");
980 assert_eq!(entry.funding_rate, dec!(0.0000125));
981 assert_eq!(entry.premium, Some(dec!(0.00029005)));
982 assert_eq!(entry.time, 1769908800000);
983 }
984
985 #[rstest]
986 fn test_funding_history_entry_without_premium() {
987 let json = r#"{
990 "coin": "BTC",
991 "fundingRate": "0.0000033",
992 "time": 1769916000000
993 }"#;
994
995 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
996
997 assert!(entry.premium.is_none());
998 assert_eq!(entry.funding_rate, dec!(0.0000033));
999 }
1000
1001 #[rstest]
1002 fn test_recent_trade_deserializes() {
1003 let json = r#"{
1005 "coin": "BTC",
1006 "side": "B",
1007 "px": "104250.0",
1008 "sz": "0.0123",
1009 "hash": "0xabc",
1010 "time": 1769916000000,
1011 "tid": 987654321,
1012 "users": ["0xbuyer", "0xseller"]
1013 }"#;
1014
1015 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
1016
1017 assert_eq!(trade.coin, "BTC");
1018 assert_eq!(trade.side, HyperliquidSide::Buy);
1019 assert_eq!(trade.px, dec!(104250.0));
1020 assert_eq!(trade.sz, dec!(0.0123));
1021 assert_eq!(trade.time, 1769916000000);
1022 assert_eq!(trade.tid, 987654321);
1023 }
1024
1025 #[rstest]
1026 fn test_order_status_deserializes_frontend_market_tif() {
1027 let status: HyperliquidOrderStatus =
1028 crate::common::testing::load_test_data("http_order_status_frontend_market.json");
1029 let entry = status.into_order().expect("order status entry");
1030
1031 assert_eq!(entry.order.oid, 1);
1032 assert_eq!(
1033 entry.order.tif,
1034 Some(HyperliquidTimeInForce::FrontendMarket)
1035 );
1036 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
1037 }
1038
1039 #[rstest]
1040 fn test_historical_order_deserializes_liquidation_market_tif() {
1041 let entry: HyperliquidOrderStatusEntry =
1042 crate::common::testing::load_test_data("http_historical_order_liquidation_market.json");
1043
1044 assert_eq!(entry.order.oid, 42);
1045 assert_eq!(
1046 entry.order.tif,
1047 Some(HyperliquidTimeInForce::LiquidationMarket)
1048 );
1049 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
1050 }
1051
1052 #[rstest]
1053 fn test_user_fill_deserializes_tid_and_builder_fee() {
1054 let json = r#"{
1055 "coin": "BTC",
1056 "px": "60000.5",
1057 "sz": "0.001",
1058 "side": "B",
1059 "time": 1704470400000,
1060 "startPosition": "0",
1061 "dir": "Open Long",
1062 "closedPnl": "1.25",
1063 "hash": "0xabc",
1064 "oid": 7001,
1065 "crossed": true,
1066 "fee": "0.02",
1067 "feeToken": "USDC",
1068 "tid": 9001,
1069 "builderFee": "0.001"
1070 }"#;
1071
1072 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1073
1074 assert_eq!(fill.coin, "BTC");
1075 assert_eq!(fill.oid, 7001);
1076 assert_eq!(fill.tid, 9001);
1077 assert_eq!(fill.builder_fee, Some(dec!(0.001)));
1078 assert_eq!(fill.fee, dec!(0.02));
1079 }
1080
1081 #[rstest]
1082 fn test_user_fill_defaults_missing_tid_and_builder_fee() {
1083 let json = r#"{
1084 "coin": "ETH",
1085 "px": "2500.25",
1086 "sz": "0.5",
1087 "side": "A",
1088 "time": 1704470401000,
1089 "startPosition": "1.0",
1090 "dir": "Close Long",
1091 "closedPnl": "2.5",
1092 "hash": "0xdef",
1093 "oid": 8002,
1094 "crossed": false,
1095 "fee": "0.01",
1096 "feeToken": "USDC"
1097 }"#;
1098
1099 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1100
1101 assert_eq!(fill.oid, 8002);
1102 assert_eq!(fill.tid, 0);
1103 assert_eq!(fill.builder_fee, None);
1104 assert_eq!(fill.fee, dec!(0.01));
1105 assert!(!fill.crossed);
1106 }
1107
1108 #[rstest]
1109 fn test_perp_asset_hip3_fields() {
1110 let json = r#"{
1111 "name": "xyz:TSLA",
1112 "szDecimals": 3,
1113 "maxLeverage": 10,
1114 "onlyIsolated": true,
1115 "growthMode": "enabled",
1116 "marginMode": "strictIsolated"
1117 }"#;
1118
1119 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1120
1121 assert_eq!(asset.name, "xyz:TSLA");
1122 assert_eq!(asset.sz_decimals, 3);
1123 assert_eq!(asset.max_leverage, Some(10));
1124 assert_eq!(asset.only_isolated, Some(true));
1125 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
1126 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
1127 }
1128
1129 #[rstest]
1130 fn test_perp_asset_hip3_fields_absent() {
1131 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
1132
1133 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1134
1135 assert_eq!(asset.growth_mode, None);
1136 assert_eq!(asset.margin_mode, None);
1137 }
1138
1139 #[rstest]
1140 fn test_outcome_meta_defaults_missing_side_specs() {
1141 let json = r#"{
1142 "outcomes": [
1143 {
1144 "outcome": 123,
1145 "name": "Recurring",
1146 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
1147 }
1148 ]
1149 }"#;
1150
1151 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
1152
1153 assert_eq!(meta.outcomes.len(), 1);
1154 assert_eq!(meta.outcomes[0].outcome, 123);
1155 assert!(meta.outcomes[0].side_specs.is_empty());
1156 }
1157
1158 #[rstest]
1159 fn test_l2_book_deserialization() {
1160 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
1161
1162 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
1163
1164 assert_eq!(book.coin, "BTC");
1165 assert_eq!(book.levels.len(), 2);
1166 assert_eq!(book.time, 1234567890);
1167 }
1168
1169 #[rstest]
1170 fn test_exchange_response_deserialization() {
1171 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
1172
1173 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
1174 assert!(response.is_ok());
1175 }
1176
1177 #[rstest]
1178 fn test_spot_clearinghouse_state_deserialization() {
1179 let json = r#"{
1180 "balances": [
1181 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
1182 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
1183 ]
1184 }"#;
1185
1186 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1187
1188 assert_eq!(state.balances.len(), 2);
1189 let usdc = &state.balances[0];
1190 assert_eq!(usdc.coin, "USDC");
1191 assert_eq!(usdc.token, Some(0));
1192 assert_eq!(usdc.total.to_string(), "14.625485");
1193 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
1194 assert_eq!(usdc.free().to_string(), "14.625485");
1195 assert_eq!(usdc.avg_entry_px(), None);
1196
1197 let purr = &state.balances[1];
1198 assert_eq!(purr.coin, "PURR");
1199 assert_eq!(purr.token, Some(1));
1200 assert_eq!(purr.free().to_string(), "1900");
1201 assert_eq!(
1202 purr.avg_entry_px().unwrap(),
1203 rust_decimal_macros::dec!(0.61728)
1204 );
1205 }
1206
1207 #[rstest]
1208 fn test_spot_balance_outcome_side_token_lacks_token_field() {
1209 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1211 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1212 assert_eq!(balance.coin, "+250");
1213 assert_eq!(balance.token, None);
1214 }
1215
1216 #[rstest]
1217 fn test_spot_clearinghouse_state_empty() {
1218 let json = r#"{"balances": []}"#;
1219 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1220 assert!(state.balances.is_empty());
1221 }
1222
1223 #[rstest]
1224 fn test_spot_balance_handles_missing_entry_ntl() {
1225 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1226 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1227 assert_eq!(balance.entry_ntl, None);
1228 assert_eq!(balance.avg_entry_px(), None);
1229 }
1230
1231 #[rstest]
1232 fn test_msgpack_serialization_matches_python() {
1233 let action = HyperliquidExchangeAction::Order {
1238 orders: vec![],
1239 grouping: HyperliquidExchangeGrouping::Na,
1240 builder: None,
1241 };
1242
1243 let json = serde_json::to_string(&action).unwrap();
1245 assert!(
1246 json.contains(r#""type":"order""#),
1247 "JSON should have type tag: {json}"
1248 );
1249
1250 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1252
1253 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1255
1256 assert!(
1258 decoded.get("type").is_some(),
1259 "MsgPack should have type tag. Decoded: {decoded:?}"
1260 );
1261 assert_eq!(
1262 decoded.get("type").unwrap().as_str().unwrap(),
1263 "order",
1264 "Type should be 'order'"
1265 );
1266 assert!(decoded.get("orders").is_some(), "Should have orders field");
1267 assert!(
1268 decoded.get("grouping").is_some(),
1269 "Should have grouping field"
1270 );
1271 }
1272
1273 #[rstest]
1274 fn test_cancel_action_serialization(
1275 #[values(None, Some(false), Some(true))] fast: Option<bool>,
1276 #[values(false, true)] by_cloid: bool,
1277 ) {
1278 let (action, name) = if by_cloid {
1279 (
1280 HyperliquidExchangeAction::CancelByCloid {
1281 cancels: vec![HyperliquidExchangeCancelByCloidRequest {
1282 asset: 7,
1283 cloid: Cloid::from_hex("0x0123456789abcdef0123456789abcdef").unwrap(),
1284 }],
1285 fast,
1286 },
1287 "cancel_by_cloid",
1288 )
1289 } else {
1290 (
1291 HyperliquidExchangeAction::Cancel {
1292 cancels: vec![HyperliquidExchangeCancelOrderRequest {
1293 asset: 7,
1294 oid: 12345,
1295 }],
1296 fast,
1297 },
1298 "cancel",
1299 )
1300 };
1301
1302 let fixtures: serde_json::Value =
1303 serde_json::from_str(include_str!("../../test_data/cancel_actions.json")).unwrap();
1304 let expected = &fixtures[name][if fast == Some(true) { "fast" } else { "normal" }];
1305 let json = serde_json::to_value(&action).unwrap();
1306 let msgpack = rmp_serde::to_vec_named(&action).unwrap();
1307
1308 assert_eq!(json, expected["json"]);
1309 assert_eq!(hex::encode(msgpack), expected["msgpack"].as_str().unwrap());
1310 }
1311
1312 #[rstest]
1313 fn test_order_response_normal_tpsl_with_waiting_children() {
1314 let json = r#"{
1318 "statuses": [
1319 {"resting": {"oid": 446050656712}},
1320 "waitingForFill",
1321 "waitingForTrigger"
1322 ]
1323 }"#;
1324
1325 let data: HyperliquidExchangeOrderResponseData = serde_json::from_str(json).unwrap();
1326 assert_eq!(data.statuses.len(), 3);
1327
1328 assert!(matches!(
1329 data.statuses[0],
1330 HyperliquidExchangeOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1331 ));
1332 assert!(matches!(
1333 data.statuses[1],
1334 HyperliquidExchangeOrderStatus::Tag(HyperliquidExchangeOrderStatusTag::WaitingForFill)
1335 ));
1336 assert!(matches!(
1337 data.statuses[2],
1338 HyperliquidExchangeOrderStatus::Tag(
1339 HyperliquidExchangeOrderStatusTag::WaitingForTrigger
1340 )
1341 ));
1342 }
1343
1344 #[rstest]
1345 fn test_user_outcome_split_serialization() {
1346 let action = HyperliquidExchangeAction::UserOutcome {
1347 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1348 HyperliquidExchangeSplitOutcomeParams {
1349 outcome: 1,
1350 amount: dec!(123.0),
1351 },
1352 ),
1353 };
1354
1355 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1356 assert_eq!(
1357 value,
1358 json!({
1359 "type": "userOutcome",
1360 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1361 })
1362 );
1363 }
1364
1365 #[rstest]
1366 fn test_user_outcome_split_msgpack_roundtrip() {
1367 let action = HyperliquidExchangeAction::UserOutcome {
1368 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1369 HyperliquidExchangeSplitOutcomeParams {
1370 outcome: 4,
1371 amount: dec!(10),
1372 },
1373 ),
1374 };
1375
1376 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1377 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1378 assert_eq!(
1379 decoded,
1380 json!({
1381 "type": "userOutcome",
1382 "splitOutcome": { "outcome": 4, "amount": "10" }
1383 })
1384 );
1385 }
1386
1387 #[rstest]
1388 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1389 let level = HyperliquidLevel {
1392 px: dec!(98450.5),
1393 sz: dec!(2.5),
1394 };
1395 let value = serde_json::to_value(&level).unwrap();
1396 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1397 }
1398
1399 #[rstest]
1400 fn test_user_outcome_merge_outcome_serialization() {
1401 let action = HyperliquidExchangeAction::UserOutcome {
1402 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1403 HyperliquidExchangeMergeOutcomeParams {
1404 outcome: 1,
1405 amount: Some(dec!(5.0)),
1406 },
1407 ),
1408 };
1409 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1410 assert_eq!(
1411 value,
1412 json!({
1413 "type": "userOutcome",
1414 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1415 })
1416 );
1417 }
1418
1419 #[rstest]
1420 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1421 let action = HyperliquidExchangeAction::UserOutcome {
1422 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1423 HyperliquidExchangeMergeOutcomeParams {
1424 outcome: 7,
1425 amount: None,
1426 },
1427 ),
1428 };
1429 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1430 assert_eq!(
1431 value,
1432 json!({
1433 "type": "userOutcome",
1434 "mergeOutcome": { "outcome": 7, "amount": null }
1435 })
1436 );
1437 }
1438
1439 #[rstest]
1440 fn test_user_outcome_merge_question_serialization() {
1441 let action = HyperliquidExchangeAction::UserOutcome {
1442 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1443 HyperliquidExchangeMergeQuestionParams {
1444 question: 9,
1445 amount: Some(dec!(2.0)),
1446 },
1447 ),
1448 };
1449 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1450 assert_eq!(
1451 value,
1452 json!({
1453 "type": "userOutcome",
1454 "mergeQuestion": { "question": 9, "amount": "2.0" }
1455 })
1456 );
1457 }
1458
1459 #[rstest]
1460 fn test_user_outcome_merge_question_null_amount_means_max() {
1461 let action = HyperliquidExchangeAction::UserOutcome {
1462 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1463 HyperliquidExchangeMergeQuestionParams {
1464 question: 9,
1465 amount: None,
1466 },
1467 ),
1468 };
1469 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1470 assert_eq!(
1471 value,
1472 json!({
1473 "type": "userOutcome",
1474 "mergeQuestion": { "question": 9, "amount": null }
1475 })
1476 );
1477 }
1478
1479 #[rstest]
1480 fn test_user_outcome_negate_outcome_serialization() {
1481 let action = HyperliquidExchangeAction::UserOutcome {
1482 op: HyperliquidExchangeUserOutcomeOp::NegateOutcome(
1483 HyperliquidExchangeNegateOutcomeParams {
1484 question: 9,
1485 outcome: 52,
1486 amount: dec!(1.5),
1487 },
1488 ),
1489 };
1490 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1491 assert_eq!(
1492 value,
1493 json!({
1494 "type": "userOutcome",
1495 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1496 })
1497 );
1498 }
1499
1500 #[rstest]
1501 fn test_modify_target_serializes_numeric_oid() {
1502 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Oid(12345));
1503 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1504
1505 assert_eq!(value["oid"], json!(12345));
1506 }
1507
1508 #[rstest]
1509 fn test_modify_target_serializes_cloid() {
1510 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
1511 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Cloid(cloid));
1512 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1513
1514 assert_eq!(value["oid"], json!("0x1234567890abcdef1234567890abcdef"));
1515 }
1516
1517 #[rstest]
1518 #[case("51000.00", "0.2500", "52000.000", "51000", "0.25", "52000")]
1519 #[case("0.00", "0.0000", "-0.000", "0", "0", "0")]
1520 #[case("-12.3400", "2.500", "-10.0100", "-12.34", "2.5", "-10.01")]
1521 #[case(
1522 "0.0000000000000000000000000001",
1523 "1.000",
1524 "10.0100",
1525 "0.0000000000000000000000000001",
1526 "1",
1527 "10.01"
1528 )]
1529 #[case(
1530 "79228162514264337593543950335",
1531 "0.2500",
1532 "52000.00",
1533 "79228162514264337593543950335",
1534 "0.25",
1535 "52000"
1536 )]
1537 fn test_order_decimals_serialize_without_trailing_zeros(
1538 #[case] price: &str,
1539 #[case] size: &str,
1540 #[case] trigger_price: &str,
1541 #[case] expected_price: &str,
1542 #[case] expected_size: &str,
1543 #[case] expected_trigger_price: &str,
1544 #[values(false, true)] is_market: bool,
1545 ) {
1546 let request = HyperliquidExchangePlaceOrderRequest {
1547 asset: 7,
1548 is_buy: false,
1549 price: price.parse().unwrap(),
1550 size: size.parse().unwrap(),
1551 reduce_only: true,
1552 kind: HyperliquidExchangeOrderKind::Trigger {
1553 trigger: HyperliquidExchangeTriggerParams {
1554 is_market,
1555 trigger_px: trigger_price.parse().unwrap(),
1556 tpsl: HyperliquidExchangeTpSl::Tp,
1557 },
1558 },
1559 cloid: Some(Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap()),
1560 };
1561
1562 let expected = json!({
1563 "a": 7, "b": false, "p": expected_price, "s": expected_size, "r": true,
1564 "t": {"trigger": {"isMarket": is_market, "triggerPx": expected_trigger_price, "tpsl": "tp"}},
1565 "c": "0x1234567890abcdef1234567890abcdef",
1566 });
1567 let canonical: HyperliquidExchangePlaceOrderRequest =
1568 serde_json::from_value(expected.clone()).unwrap();
1569 let json = serde_json::to_value(&request).unwrap();
1570 let msgpack = rmp_serde::to_vec_named(&request).unwrap();
1571 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack).unwrap();
1572
1573 assert_eq!(json, expected);
1574 assert_eq!(decoded, expected);
1575 assert_eq!(msgpack, rmp_serde::to_vec_named(&canonical).unwrap());
1576 assert_eq!(
1577 serde_json::to_vec(&request).unwrap(),
1578 serde_json::to_vec(&canonical).unwrap()
1579 );
1580 assert_eq!(request.price.to_string(), price);
1581 assert_eq!(request.size.to_string(), size);
1582 assert_eq!(canonical, request);
1583 }
1584
1585 fn modify_request_with_target(
1586 oid: HyperliquidExchangeModifyTarget,
1587 ) -> HyperliquidExchangeModifyOrderRequest {
1588 HyperliquidExchangeModifyOrderRequest {
1589 oid,
1590 order: HyperliquidExchangePlaceOrderRequest {
1591 asset: 0,
1592 is_buy: true,
1593 price: dec!(51000),
1594 size: dec!(0.2),
1595 reduce_only: false,
1596 kind: HyperliquidExchangeOrderKind::Limit {
1597 limit: HyperliquidExchangeLimitParams {
1598 tif: HyperliquidExchangeTif::Gtc,
1599 },
1600 },
1601 cloid: None,
1602 },
1603 }
1604 }
1605}
1606
1607#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1611pub enum HyperliquidExchangeTif {
1612 #[serde(rename = "Alo")]
1614 Alo,
1615 #[serde(rename = "Ioc")]
1617 Ioc,
1618 #[serde(rename = "Gtc")]
1620 Gtc,
1621}
1622
1623#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1625pub enum HyperliquidExchangeTpSl {
1626 #[serde(rename = "tp")]
1628 Tp,
1629 #[serde(rename = "sl")]
1631 Sl,
1632}
1633
1634#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1636pub enum HyperliquidExchangeGrouping {
1637 #[serde(rename = "na")]
1639 #[default]
1640 Na,
1641 #[serde(rename = "normalTpsl")]
1643 NormalTpsl,
1644 #[serde(rename = "positionTpsl")]
1646 PositionTpsl,
1647}
1648
1649#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1651#[serde(untagged)]
1652pub enum HyperliquidExchangeOrderKind {
1653 Limit {
1655 limit: HyperliquidExchangeLimitParams,
1657 },
1658 Trigger {
1660 trigger: HyperliquidExchangeTriggerParams,
1662 },
1663}
1664
1665#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1667pub struct HyperliquidExchangeLimitParams {
1668 pub tif: HyperliquidExchangeTif,
1670}
1671
1672#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1674#[serde(rename_all = "camelCase")]
1675pub struct HyperliquidExchangeTriggerParams {
1676 pub is_market: bool,
1678 #[serde(
1680 serialize_with = "serialize_order_decimal_as_str",
1681 deserialize_with = "deserialize_decimal_from_str"
1682 )]
1683 pub trigger_px: Decimal,
1684 pub tpsl: HyperliquidExchangeTpSl,
1686}
1687
1688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1693pub struct HyperliquidExchangeBuilderFee {
1694 #[serde(rename = "b")]
1696 pub address: String,
1697 #[serde(rename = "f")]
1699 pub fee_tenths_bp: u32,
1700}
1701
1702#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1707pub struct HyperliquidExchangePlaceOrderRequest {
1708 #[serde(rename = "a")]
1710 pub asset: AssetId,
1711 #[serde(rename = "b")]
1713 pub is_buy: bool,
1714 #[serde(
1716 rename = "p",
1717 serialize_with = "serialize_order_decimal_as_str",
1718 deserialize_with = "deserialize_decimal_from_str"
1719 )]
1720 pub price: Decimal,
1721 #[serde(
1723 rename = "s",
1724 serialize_with = "serialize_order_decimal_as_str",
1725 deserialize_with = "deserialize_decimal_from_str"
1726 )]
1727 pub size: Decimal,
1728 #[serde(rename = "r")]
1730 pub reduce_only: bool,
1731 #[serde(rename = "t")]
1733 pub kind: HyperliquidExchangeOrderKind,
1734 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1736 pub cloid: Option<Cloid>,
1737}
1738
1739#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1741pub struct HyperliquidExchangeCancelOrderRequest {
1742 #[serde(rename = "a")]
1744 pub asset: AssetId,
1745 #[serde(rename = "o")]
1747 pub oid: OrderId,
1748}
1749
1750#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1755pub struct HyperliquidExchangeCancelByCloidRequest {
1756 pub asset: AssetId,
1758 pub cloid: Cloid,
1760}
1761
1762#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1767#[serde(untagged)]
1768pub enum HyperliquidExchangeModifyTarget {
1769 Oid(OrderId),
1771 Cloid(Cloid),
1773}
1774
1775impl HyperliquidExchangeModifyTarget {
1776 pub fn from_venue_order_id(
1782 venue_order_id: &VenueOrderId,
1783 ) -> Result<Self, std::num::ParseIntError> {
1784 venue_order_id.as_str().parse::<OrderId>().map(Self::Oid)
1785 }
1786}
1787
1788impl From<OrderId> for HyperliquidExchangeModifyTarget {
1789 fn from(value: OrderId) -> Self {
1790 Self::Oid(value)
1791 }
1792}
1793
1794impl From<Cloid> for HyperliquidExchangeModifyTarget {
1795 fn from(value: Cloid) -> Self {
1796 Self::Cloid(value)
1797 }
1798}
1799
1800#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1805pub struct HyperliquidExchangeModifyOrderRequest {
1806 pub oid: HyperliquidExchangeModifyTarget,
1808 pub order: HyperliquidExchangePlaceOrderRequest,
1810}
1811
1812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1817pub struct HyperliquidExchangeSplitOutcomeParams {
1818 pub outcome: u32,
1820 #[serde(
1822 serialize_with = "serialize_decimal_as_str",
1823 deserialize_with = "deserialize_decimal_from_str"
1824 )]
1825 pub amount: Decimal,
1826}
1827
1828#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1834pub struct HyperliquidExchangeMergeOutcomeParams {
1835 pub outcome: u32,
1837 #[serde(
1839 default,
1840 serialize_with = "serialize_optional_decimal_as_str",
1841 deserialize_with = "deserialize_optional_decimal_from_str"
1842 )]
1843 pub amount: Option<Decimal>,
1844}
1845
1846#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1852pub struct HyperliquidExchangeMergeQuestionParams {
1853 pub question: u32,
1855 #[serde(
1857 default,
1858 serialize_with = "serialize_optional_decimal_as_str",
1859 deserialize_with = "deserialize_optional_decimal_from_str"
1860 )]
1861 pub amount: Option<Decimal>,
1862}
1863
1864#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1869pub struct HyperliquidExchangeNegateOutcomeParams {
1870 pub question: u32,
1872 pub outcome: u32,
1874 #[serde(
1876 serialize_with = "serialize_decimal_as_str",
1877 deserialize_with = "deserialize_decimal_from_str"
1878 )]
1879 pub amount: Decimal,
1880}
1881
1882#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1889pub enum HyperliquidExchangeUserOutcomeOp {
1890 #[serde(rename = "splitOutcome")]
1892 SplitOutcome(HyperliquidExchangeSplitOutcomeParams),
1893 #[serde(rename = "mergeOutcome")]
1896 MergeOutcome(HyperliquidExchangeMergeOutcomeParams),
1897 #[serde(rename = "mergeQuestion")]
1900 MergeQuestion(HyperliquidExchangeMergeQuestionParams),
1901 #[serde(rename = "negateOutcome")]
1904 NegateOutcome(HyperliquidExchangeNegateOutcomeParams),
1905}
1906
1907#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1909pub struct HyperliquidExchangeTwapRequest {
1910 #[serde(rename = "a")]
1912 pub asset: AssetId,
1913 #[serde(rename = "b")]
1915 pub is_buy: bool,
1916 #[serde(
1918 rename = "s",
1919 serialize_with = "serialize_decimal_as_str",
1920 deserialize_with = "deserialize_decimal_from_str"
1921 )]
1922 pub size: Decimal,
1923 #[serde(rename = "m")]
1925 pub duration_ms: u64,
1926}
1927
1928#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1934#[serde(tag = "type")]
1935pub enum HyperliquidExchangeAction {
1936 #[serde(rename = "order")]
1938 Order {
1939 orders: Vec<HyperliquidExchangePlaceOrderRequest>,
1941 #[serde(default)]
1943 grouping: HyperliquidExchangeGrouping,
1944 #[serde(skip_serializing_if = "Option::is_none")]
1946 builder: Option<HyperliquidExchangeBuilderFee>,
1947 },
1948
1949 #[serde(rename = "cancel")]
1951 Cancel {
1952 cancels: Vec<HyperliquidExchangeCancelOrderRequest>,
1954 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
1956 fast: Option<bool>,
1957 },
1958
1959 #[serde(rename = "cancelByCloid")]
1961 CancelByCloid {
1962 cancels: Vec<HyperliquidExchangeCancelByCloidRequest>,
1964 #[serde(rename = "f", skip_serializing_if = "is_false_or_none")]
1966 fast: Option<bool>,
1967 },
1968
1969 #[serde(rename = "modify")]
1971 Modify {
1972 #[serde(flatten)]
1974 modify: HyperliquidExchangeModifyOrderRequest,
1975 },
1976
1977 #[serde(rename = "batchModify")]
1979 BatchModify {
1980 modifies: Vec<HyperliquidExchangeModifyOrderRequest>,
1982 },
1983
1984 #[serde(rename = "scheduleCancel")]
1986 ScheduleCancel {
1987 #[serde(skip_serializing_if = "Option::is_none")]
1990 time: Option<u64>,
1991 },
1992
1993 #[serde(rename = "updateLeverage")]
1995 UpdateLeverage {
1996 asset: AssetId,
1998 #[serde(rename = "isCross")]
2000 is_cross: bool,
2001 #[serde(rename = "leverage")]
2003 leverage: u32,
2004 },
2005
2006 #[serde(rename = "updateIsolatedMargin")]
2008 UpdateIsolatedMargin {
2009 asset: AssetId,
2011 #[serde(rename = "isBuy")]
2013 is_buy: bool,
2014 ntli: i64,
2016 },
2017
2018 #[serde(rename = "usdClassTransfer")]
2022 UsdClassTransfer {
2023 from: String,
2025 to: String,
2027 #[serde(
2029 serialize_with = "serialize_decimal_as_str",
2030 deserialize_with = "deserialize_decimal_from_str"
2031 )]
2032 amount: Decimal,
2033 },
2034
2035 #[serde(rename = "userOutcome")]
2041 UserOutcome {
2042 #[serde(flatten)]
2044 op: HyperliquidExchangeUserOutcomeOp,
2045 },
2046
2047 #[serde(rename = "twapPlace")]
2049 TwapPlace {
2050 #[serde(flatten)]
2052 twap: HyperliquidExchangeTwapRequest,
2053 },
2054
2055 #[serde(rename = "twapCancel")]
2057 TwapCancel {
2058 #[serde(rename = "a")]
2060 asset: AssetId,
2061 #[serde(rename = "t")]
2063 twap_id: u64,
2064 },
2065
2066 #[serde(rename = "noop")]
2068 Noop,
2069}
2070
2071#[derive(Debug, Clone, Serialize)]
2076#[serde(rename_all = "camelCase")]
2077pub struct HyperliquidExchangeActionRequest {
2078 pub action: HyperliquidExchangeAction,
2080 pub nonce: u64,
2082 pub signature: SecretString,
2084 #[serde(skip_serializing_if = "Option::is_none")]
2086 pub vault_address: Option<String>,
2087 #[serde(skip_serializing_if = "Option::is_none")]
2090 pub expires_after: Option<u64>,
2091}
2092
2093#[derive(Debug, Clone, Serialize, Deserialize)]
2095pub struct HyperliquidExchangeActionResponse {
2096 pub status: String,
2098 pub response: HyperliquidExchangeResponseData,
2100}
2101
2102#[derive(Debug, Clone, Serialize, Deserialize)]
2104#[serde(tag = "type")]
2105pub enum HyperliquidExchangeResponseData {
2106 #[serde(rename = "order")]
2108 Order {
2109 data: HyperliquidExchangeOrderResponseData,
2111 },
2112 #[serde(rename = "cancel")]
2114 Cancel {
2115 data: HyperliquidExchangeCancelResponseData,
2117 },
2118 #[serde(rename = "modify")]
2120 Modify {
2121 data: HyperliquidExchangeModifyResponseData,
2123 },
2124 #[serde(rename = "default")]
2126 Default,
2127 #[serde(other)]
2129 Unknown,
2130}
2131
2132#[derive(Debug, Clone, Serialize, Deserialize)]
2134pub struct HyperliquidExchangeOrderResponseData {
2135 pub statuses: Vec<HyperliquidExchangeOrderStatus>,
2137}
2138
2139#[derive(Debug, Clone, Serialize, Deserialize)]
2141pub struct HyperliquidExchangeCancelResponseData {
2142 pub statuses: Vec<HyperliquidExchangeCancelStatus>,
2144}
2145
2146#[derive(Debug, Clone, Serialize, Deserialize)]
2148pub struct HyperliquidExchangeModifyResponseData {
2149 pub statuses: Vec<HyperliquidExchangeModifyStatus>,
2151}
2152
2153#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2155#[serde(untagged)]
2156pub enum HyperliquidExchangeOrderStatus {
2157 Resting {
2159 resting: HyperliquidExchangeRestingInfo,
2161 },
2162 Filled {
2164 filled: HyperliquidExchangeFilledInfo,
2166 },
2167 Error {
2169 error: String,
2171 },
2172 Tag(HyperliquidExchangeOrderStatusTag),
2176}
2177
2178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2184pub enum HyperliquidExchangeOrderStatusTag {
2185 #[serde(rename = "waitingForFill")]
2187 WaitingForFill,
2188 #[serde(rename = "waitingForTrigger")]
2190 WaitingForTrigger,
2191}
2192
2193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2195pub struct HyperliquidExchangeRestingInfo {
2196 pub oid: OrderId,
2198}
2199
2200#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2202pub struct HyperliquidExchangeFilledInfo {
2203 #[serde(
2205 rename = "totalSz",
2206 serialize_with = "serialize_decimal_as_str",
2207 deserialize_with = "deserialize_decimal_from_str"
2208 )]
2209 pub total_sz: Decimal,
2210 #[serde(
2212 rename = "avgPx",
2213 serialize_with = "serialize_decimal_as_str",
2214 deserialize_with = "deserialize_decimal_from_str"
2215 )]
2216 pub avg_px: Decimal,
2217 pub oid: OrderId,
2219}
2220
2221#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2223#[serde(untagged)]
2224pub enum HyperliquidExchangeCancelStatus {
2225 Success(String), Error {
2229 error: String,
2231 },
2232}
2233
2234#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2236#[serde(untagged)]
2237pub enum HyperliquidExchangeModifyStatus {
2238 Success(String), Error {
2242 error: String,
2244 },
2245}
2246
2247#[derive(Debug, Clone, Serialize, Deserialize)]
2250#[serde(rename_all = "camelCase")]
2251pub struct ClearinghouseState {
2252 #[serde(default)]
2254 pub asset_positions: Vec<AssetPosition>,
2255 #[serde(default)]
2257 pub cross_margin_summary: Option<CrossMarginSummary>,
2258 #[serde(
2260 default,
2261 serialize_with = "serialize_optional_decimal_as_str",
2262 deserialize_with = "deserialize_optional_decimal_from_str"
2263 )]
2264 pub withdrawable: Option<Decimal>,
2265 #[serde(default)]
2267 pub time: Option<u64>,
2268}
2269
2270#[derive(Debug, Clone, Serialize, Deserialize)]
2272#[serde(rename_all = "camelCase")]
2273pub struct AssetPosition {
2274 pub position: PositionData,
2276 #[serde(rename = "type")]
2278 pub position_type: HyperliquidPositionType,
2279}
2280
2281#[derive(Debug, Clone, Serialize, Deserialize)]
2283#[serde(rename_all = "camelCase")]
2284pub struct LeverageInfo {
2285 #[serde(rename = "type")]
2286 pub leverage_type: HyperliquidLeverageType,
2287 pub value: u32,
2289}
2290
2291#[derive(Debug, Clone, Serialize, Deserialize)]
2293#[serde(rename_all = "camelCase")]
2294pub struct CumFundingInfo {
2295 #[serde(
2297 rename = "allTime",
2298 serialize_with = "serialize_decimal_as_str",
2299 deserialize_with = "deserialize_decimal_from_str"
2300 )]
2301 pub all_time: Decimal,
2302 #[serde(
2304 rename = "sinceOpen",
2305 serialize_with = "serialize_decimal_as_str",
2306 deserialize_with = "deserialize_decimal_from_str"
2307 )]
2308 pub since_open: Decimal,
2309 #[serde(
2311 rename = "sinceChange",
2312 serialize_with = "serialize_decimal_as_str",
2313 deserialize_with = "deserialize_decimal_from_str"
2314 )]
2315 pub since_change: Decimal,
2316}
2317
2318#[derive(Debug, Clone, Serialize, Deserialize)]
2320#[serde(rename_all = "camelCase")]
2321pub struct PositionData {
2322 pub coin: Ustr,
2324 #[serde(rename = "cumFunding")]
2326 pub cum_funding: CumFundingInfo,
2327 #[serde(
2329 rename = "entryPx",
2330 serialize_with = "serialize_optional_decimal_as_str",
2331 deserialize_with = "deserialize_optional_decimal_from_str",
2332 default
2333 )]
2334 pub entry_px: Option<Decimal>,
2335 pub leverage: LeverageInfo,
2337 #[serde(
2339 rename = "liquidationPx",
2340 serialize_with = "serialize_optional_decimal_as_str",
2341 deserialize_with = "deserialize_optional_decimal_from_str",
2342 default
2343 )]
2344 pub liquidation_px: Option<Decimal>,
2345 #[serde(
2347 rename = "marginUsed",
2348 serialize_with = "serialize_decimal_as_str",
2349 deserialize_with = "deserialize_decimal_from_str"
2350 )]
2351 pub margin_used: Decimal,
2352 #[serde(rename = "maxLeverage", default)]
2354 pub max_leverage: Option<u32>,
2355 #[serde(
2357 rename = "positionValue",
2358 serialize_with = "serialize_decimal_as_str",
2359 deserialize_with = "deserialize_decimal_from_str"
2360 )]
2361 pub position_value: Decimal,
2362 #[serde(
2364 rename = "returnOnEquity",
2365 serialize_with = "serialize_decimal_as_str",
2366 deserialize_with = "deserialize_decimal_from_str"
2367 )]
2368 pub return_on_equity: Decimal,
2369 #[serde(
2371 rename = "szi",
2372 serialize_with = "serialize_decimal_as_str",
2373 deserialize_with = "deserialize_decimal_from_str"
2374 )]
2375 pub szi: Decimal,
2376 #[serde(
2378 rename = "unrealizedPnl",
2379 serialize_with = "serialize_decimal_as_str",
2380 deserialize_with = "deserialize_decimal_from_str"
2381 )]
2382 pub unrealized_pnl: Decimal,
2383}
2384
2385#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2391#[serde(rename_all = "camelCase")]
2392pub struct SpotClearinghouseState {
2393 #[serde(default)]
2395 pub balances: Vec<SpotBalance>,
2396}
2397
2398#[derive(Debug, Clone, Serialize, Deserialize)]
2400#[serde(rename_all = "camelCase")]
2401pub struct SpotBalance {
2402 pub coin: Ustr,
2404 #[serde(default)]
2407 pub token: Option<u32>,
2408 #[serde(
2410 serialize_with = "serialize_decimal_as_str",
2411 deserialize_with = "deserialize_decimal_from_str"
2412 )]
2413 pub total: Decimal,
2414 #[serde(
2416 serialize_with = "serialize_decimal_as_str",
2417 deserialize_with = "deserialize_decimal_from_str"
2418 )]
2419 pub hold: Decimal,
2420 #[serde(
2422 default,
2423 serialize_with = "serialize_optional_decimal_as_str",
2424 deserialize_with = "deserialize_optional_decimal_from_str"
2425 )]
2426 pub entry_ntl: Option<Decimal>,
2427}
2428
2429impl SpotBalance {
2430 #[must_use]
2432 pub fn free(&self) -> Decimal {
2433 (self.total - self.hold).max(Decimal::ZERO)
2434 }
2435
2436 #[must_use]
2438 pub fn avg_entry_px(&self) -> Option<Decimal> {
2439 let entry_ntl = self.entry_ntl?;
2440
2441 if entry_ntl.is_zero() || self.total.is_zero() {
2442 return None;
2443 }
2444
2445 Some(entry_ntl / self.total)
2446 }
2447}
2448
2449#[derive(Debug, Clone, Serialize, Deserialize)]
2451#[serde(rename_all = "camelCase")]
2452pub struct CrossMarginSummary {
2453 #[serde(
2455 rename = "accountValue",
2456 serialize_with = "serialize_decimal_as_str",
2457 deserialize_with = "deserialize_decimal_from_str"
2458 )]
2459 pub account_value: Decimal,
2460 #[serde(
2462 rename = "totalNtlPos",
2463 serialize_with = "serialize_decimal_as_str",
2464 deserialize_with = "deserialize_decimal_from_str"
2465 )]
2466 pub total_ntl_pos: Decimal,
2467 #[serde(
2469 rename = "totalRawUsd",
2470 serialize_with = "serialize_decimal_as_str",
2471 deserialize_with = "deserialize_decimal_from_str"
2472 )]
2473 pub total_raw_usd: Decimal,
2474 #[serde(
2476 rename = "totalMarginUsed",
2477 serialize_with = "serialize_decimal_as_str",
2478 deserialize_with = "deserialize_decimal_from_str"
2479 )]
2480 pub total_margin_used: Decimal,
2481 #[serde(
2483 rename = "withdrawable",
2484 default,
2485 serialize_with = "serialize_optional_decimal_as_str",
2486 deserialize_with = "deserialize_optional_decimal_from_str"
2487 )]
2488 pub withdrawable: Option<Decimal>,
2489}
2490
2491fn serialize_order_decimal_as_str<S>(decimal: &Decimal, serializer: S) -> Result<S::Ok, S::Error>
2492where
2493 S: Serializer,
2494{
2495 serialize_decimal_as_str(&decimal.normalize(), serializer)
2496}