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,
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 side_specs: Vec<OutcomeSideSpec>,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(rename_all = "camelCase")]
318pub struct OutcomeSideSpec {
319 pub name: String,
321}
322
323#[derive(Debug, Clone, Serialize, Deserialize)]
329#[serde(rename_all = "camelCase")]
330pub struct OutcomeQuestion {
331 pub question: u32,
333 pub name: String,
335 pub description: String,
337 #[serde(default)]
339 pub fallback_outcome: Option<u32>,
340 #[serde(default)]
342 pub named_outcomes: Vec<u32>,
343 #[serde(default)]
345 pub settled_named_outcomes: Vec<u32>,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize)]
351#[serde(untagged)]
352pub enum PerpMetaAndCtxs {
353 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
355}
356
357#[derive(Debug, Clone, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct PerpAssetCtx {
361 #[serde(
363 default,
364 serialize_with = "serialize_optional_decimal_as_str",
365 deserialize_with = "deserialize_optional_decimal_from_str"
366 )]
367 pub mark_px: Option<Decimal>,
368 #[serde(
370 default,
371 serialize_with = "serialize_optional_decimal_as_str",
372 deserialize_with = "deserialize_optional_decimal_from_str"
373 )]
374 pub mid_px: Option<Decimal>,
375 #[serde(
377 default,
378 serialize_with = "serialize_optional_decimal_as_str",
379 deserialize_with = "deserialize_optional_decimal_from_str"
380 )]
381 pub funding: Option<Decimal>,
382 #[serde(
384 default,
385 serialize_with = "serialize_optional_decimal_as_str",
386 deserialize_with = "deserialize_optional_decimal_from_str"
387 )]
388 pub open_interest: Option<Decimal>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
394#[serde(untagged)]
395pub enum SpotMetaAndCtxs {
396 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402#[serde(rename_all = "camelCase")]
403pub struct SpotAssetCtx {
404 #[serde(
406 default,
407 serialize_with = "serialize_optional_decimal_as_str",
408 deserialize_with = "deserialize_optional_decimal_from_str"
409 )]
410 pub mark_px: Option<Decimal>,
411 #[serde(
413 default,
414 serialize_with = "serialize_optional_decimal_as_str",
415 deserialize_with = "deserialize_optional_decimal_from_str"
416 )]
417 pub mid_px: Option<Decimal>,
418 #[serde(
420 default,
421 serialize_with = "serialize_optional_decimal_as_str",
422 deserialize_with = "deserialize_optional_decimal_from_str"
423 )]
424 pub day_volume: Option<Decimal>,
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct HyperliquidL2Book {
430 pub coin: Ustr,
432 pub levels: Vec<Vec<HyperliquidLevel>>,
434 pub time: u64,
436}
437
438#[derive(Debug, Clone, Serialize, Deserialize)]
440pub struct HyperliquidLevel {
441 #[serde(
443 serialize_with = "serialize_decimal_as_str",
444 deserialize_with = "deserialize_decimal_from_str"
445 )]
446 pub px: Decimal,
447 #[serde(
449 serialize_with = "serialize_decimal_as_str",
450 deserialize_with = "deserialize_decimal_from_str"
451 )]
452 pub sz: Decimal,
453}
454
455pub type HyperliquidFills = Vec<HyperliquidFill>;
459
460#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct HyperliquidMeta {
463 #[serde(default)]
464 pub universe: Vec<HyperliquidAssetInfo>,
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize)]
469#[serde(rename_all = "camelCase")]
470pub struct HyperliquidCandle {
471 #[serde(rename = "t")]
473 pub timestamp: u64,
474 #[serde(rename = "T")]
476 pub end_timestamp: u64,
477 #[serde(
479 rename = "o",
480 serialize_with = "serialize_decimal_as_str",
481 deserialize_with = "deserialize_decimal_from_str"
482 )]
483 pub open: Decimal,
484 #[serde(
486 rename = "h",
487 serialize_with = "serialize_decimal_as_str",
488 deserialize_with = "deserialize_decimal_from_str"
489 )]
490 pub high: Decimal,
491 #[serde(
493 rename = "l",
494 serialize_with = "serialize_decimal_as_str",
495 deserialize_with = "deserialize_decimal_from_str"
496 )]
497 pub low: Decimal,
498 #[serde(
500 rename = "c",
501 serialize_with = "serialize_decimal_as_str",
502 deserialize_with = "deserialize_decimal_from_str"
503 )]
504 pub close: Decimal,
505 #[serde(
507 rename = "v",
508 serialize_with = "serialize_decimal_as_str",
509 deserialize_with = "deserialize_decimal_from_str"
510 )]
511 pub volume: Decimal,
512 #[serde(rename = "n", default)]
514 pub num_trades: Option<u64>,
515}
516
517#[derive(Debug, Clone, Serialize, Deserialize)]
519pub struct HyperliquidFundingHistoryEntry {
520 pub coin: Ustr,
522 #[serde(
524 rename = "fundingRate",
525 serialize_with = "serialize_decimal_as_str",
526 deserialize_with = "deserialize_decimal_from_str"
527 )]
528 pub funding_rate: Decimal,
529 #[serde(
531 default,
532 serialize_with = "serialize_optional_decimal_as_str",
533 deserialize_with = "deserialize_optional_decimal_from_str"
534 )]
535 pub premium: Option<Decimal>,
536 pub time: u64,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct HyperliquidRecentTrade {
546 pub coin: Ustr,
548 pub side: HyperliquidSide,
550 #[serde(
552 serialize_with = "serialize_decimal_as_str",
553 deserialize_with = "deserialize_decimal_from_str"
554 )]
555 pub px: Decimal,
556 #[serde(
558 serialize_with = "serialize_decimal_as_str",
559 deserialize_with = "deserialize_decimal_from_str"
560 )]
561 pub sz: Decimal,
562 pub hash: String,
564 pub time: u64,
566 pub tid: u64,
568 pub users: [String; 2],
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
574pub struct HyperliquidFill {
575 pub coin: Ustr,
577 #[serde(
579 serialize_with = "serialize_decimal_as_str",
580 deserialize_with = "deserialize_decimal_from_str"
581 )]
582 pub px: Decimal,
583 #[serde(
585 serialize_with = "serialize_decimal_as_str",
586 deserialize_with = "deserialize_decimal_from_str"
587 )]
588 pub sz: Decimal,
589 pub side: HyperliquidSide,
591 pub time: u64,
593 #[serde(
595 rename = "startPosition",
596 serialize_with = "serialize_decimal_as_str",
597 deserialize_with = "deserialize_decimal_from_str"
598 )]
599 pub start_position: Decimal,
600 pub dir: HyperliquidFillDirection,
602 #[serde(
604 rename = "closedPnl",
605 serialize_with = "serialize_decimal_as_str",
606 deserialize_with = "deserialize_decimal_from_str"
607 )]
608 pub closed_pnl: Decimal,
609 pub hash: String,
611 pub oid: u64,
613 pub crossed: bool,
615 #[serde(
617 serialize_with = "serialize_decimal_as_str",
618 deserialize_with = "deserialize_decimal_from_str"
619 )]
620 pub fee: Decimal,
621 #[serde(default)]
623 pub tid: u64,
624 #[serde(rename = "feeToken")]
626 pub fee_token: Ustr,
627 #[serde(
629 rename = "builderFee",
630 default,
631 skip_serializing_if = "Option::is_none",
632 serialize_with = "serialize_optional_decimal_as_str",
633 deserialize_with = "deserialize_optional_decimal_from_str"
634 )]
635 pub builder_fee: Option<Decimal>,
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
643#[serde(tag = "status", rename_all = "camelCase")]
644pub enum HyperliquidOrderStatus {
645 Order { order: HyperliquidOrderStatusEntry },
646 UnknownOid,
647}
648
649impl HyperliquidOrderStatus {
650 #[must_use]
652 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
653 match self {
654 Self::Order { order } => Some(order),
655 Self::UnknownOid => None,
656 }
657 }
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct HyperliquidOrderStatusEntry {
663 pub order: HyperliquidOrderInfo,
665 pub status: HyperliquidOrderStatusEnum,
667 #[serde(rename = "statusTimestamp")]
669 pub status_timestamp: u64,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct HyperliquidOrderInfo {
675 pub coin: Ustr,
677 pub side: HyperliquidSide,
679 #[serde(
681 rename = "limitPx",
682 serialize_with = "serialize_decimal_as_str",
683 deserialize_with = "deserialize_decimal_from_str"
684 )]
685 pub limit_px: Decimal,
686 #[serde(
688 serialize_with = "serialize_decimal_as_str",
689 deserialize_with = "deserialize_decimal_from_str"
690 )]
691 pub sz: Decimal,
692 pub oid: u64,
694 pub timestamp: u64,
696 #[serde(
698 rename = "origSz",
699 serialize_with = "serialize_decimal_as_str",
700 deserialize_with = "deserialize_decimal_from_str"
701 )]
702 pub orig_sz: Decimal,
703 #[serde(default)]
705 pub cloid: Option<String>,
706 #[serde(default)]
708 pub tif: Option<HyperliquidTimeInForce>,
709 #[serde(rename = "reduceOnly", default)]
711 pub reduce_only: Option<bool>,
712 #[serde(
714 rename = "triggerPx",
715 default,
716 deserialize_with = "deserialize_optional_decimal_from_str"
717 )]
718 pub trigger_px: Option<Decimal>,
719 #[serde(rename = "orderType", default)]
721 pub order_type: Option<String>,
722}
723
724#[derive(Debug, Clone, Serialize)]
726pub struct HyperliquidSignature {
727 pub r: SecretString,
729 pub s: SecretString,
731 pub v: u64,
733}
734
735impl HyperliquidSignature {
736 #[must_use]
738 pub fn new(r: impl Into<SecretString>, s: impl Into<SecretString>, v: u64) -> Self {
739 Self {
740 r: r.into(),
741 s: s.into(),
742 v,
743 }
744 }
745
746 #[must_use]
748 pub fn to_hex(&self) -> SecretString {
749 let r = self
750 .r
751 .expose_secret()
752 .strip_prefix("0x")
753 .unwrap_or(self.r.expose_secret());
754 let s = self
755 .s
756 .expose_secret()
757 .strip_prefix("0x")
758 .unwrap_or(self.s.expose_secret());
759 SecretString::from(format!("0x{r}{s}{:02x}", self.v))
760 }
761
762 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
764 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
765
766 if sig_hex.len() != 130 {
767 return Err(format!(
768 "Invalid signature length: expected 130 hex chars, was {}",
769 sig_hex.len()
770 ));
771 }
772
773 let r = format!("0x{}", &sig_hex[0..64]);
774 let s = format!("0x{}", &sig_hex[64..128]);
775 let v = u64::from_str_radix(&sig_hex[128..130], 16)
776 .map_err(|e| format!("Failed to parse v component: {e}"))?;
777
778 Ok(Self::new(r, s, v))
779 }
780}
781
782#[derive(Debug, Clone, Serialize)]
784pub struct HyperliquidExchangeRequest<T> {
785 #[serde(rename = "action")]
787 pub action: T,
788 #[serde(rename = "nonce")]
790 pub nonce: u64,
791 #[serde(rename = "signature")]
793 pub signature: HyperliquidSignature,
794 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
796 pub vault_address: Option<String>,
797 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
799 pub expires_after: Option<u64>,
800}
801
802impl<T> HyperliquidExchangeRequest<T>
803where
804 T: Serialize,
805{
806 #[must_use]
808 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
809 Self {
810 action,
811 nonce,
812 signature,
813 vault_address: None,
814 expires_after: None,
815 }
816 }
817
818 #[must_use]
820 pub fn with_vault(
821 action: T,
822 nonce: u64,
823 signature: HyperliquidSignature,
824 vault_address: String,
825 ) -> Self {
826 Self {
827 action,
828 nonce,
829 signature,
830 vault_address: Some(vault_address),
831 expires_after: None,
832 }
833 }
834
835 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
837 serde_json::to_value(self)
838 }
839}
840
841#[derive(Debug, Clone, Serialize, Deserialize)]
843#[serde(untagged)]
844pub enum HyperliquidExchangeResponse {
845 Status {
847 status: String,
849 response: serde_json::Value,
851 },
852 Error {
854 error: String,
856 },
857}
858
859impl HyperliquidExchangeResponse {
860 pub fn is_ok(&self) -> bool {
861 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
862 }
863}
864
865pub const RESPONSE_STATUS_OK: &str = "ok";
867
868#[cfg(test)]
869mod tests {
870 use rstest::rstest;
871 use rust_decimal_macros::dec;
872 use serde_json::json;
873
874 use super::*;
875
876 #[rstest]
877 fn test_signature_serialization_and_debug_redaction() {
878 let signature = HyperliquidSignature::new(
879 "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
880 "0x2222222222222222222222222222222222222222222222222222222222222222".to_string(),
881 27,
882 );
883 let wire = serde_json::to_value(&signature).unwrap();
884 let debug = format!("{signature:?}");
885
886 assert_eq!(wire["r"], signature.r.expose_secret());
887 assert_eq!(wire["s"], signature.s.expose_secret());
888 assert_eq!(wire["v"], signature.v);
889 assert_eq!(debug.matches(REDACTED).count(), 2);
890 assert!(!debug.contains(signature.r.expose_secret()));
891 assert!(!debug.contains(signature.s.expose_secret()));
892 }
893
894 #[rstest]
895 fn test_exchange_action_request_debug_redacts_signature() {
896 let request = HyperliquidExchangeActionRequest {
897 action: HyperliquidExchangeAction::Noop,
898 nonce: 1_700_000_000_000,
899 signature: SecretString::from("0xsigned-action"),
900 vault_address: Some("0xvault".to_string()),
901 expires_after: Some(1_700_000_001_000),
902 };
903 let wire = serde_json::to_value(&request).unwrap();
904 let debug = format!("{request:?}");
905
906 assert_eq!(wire["signature"], "0xsigned-action");
907 assert_eq!(wire["nonce"], 1_700_000_000_000_u64);
908 assert!(debug.contains(REDACTED));
909 assert!(!debug.contains("0xsigned-action"));
910 }
911
912 #[rstest]
913 fn test_meta_deserialization() {
914 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
915
916 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
917
918 assert_eq!(meta.universe.len(), 1);
919 assert_eq!(meta.universe[0].name, "BTC");
920 assert_eq!(meta.universe[0].sz_decimals, 5);
921 }
922
923 #[rstest]
924 fn test_funding_history_entry_with_premium() {
925 let json = r#"{
926 "coin": "BTC",
927 "fundingRate": "0.0000125",
928 "premium": "0.00029005",
929 "time": 1769908800000
930 }"#;
931
932 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
933
934 assert_eq!(entry.coin, "BTC");
935 assert_eq!(entry.funding_rate, dec!(0.0000125));
936 assert_eq!(entry.premium, Some(dec!(0.00029005)));
937 assert_eq!(entry.time, 1769908800000);
938 }
939
940 #[rstest]
941 fn test_funding_history_entry_without_premium() {
942 let json = r#"{
945 "coin": "BTC",
946 "fundingRate": "0.0000033",
947 "time": 1769916000000
948 }"#;
949
950 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
951
952 assert!(entry.premium.is_none());
953 assert_eq!(entry.funding_rate, dec!(0.0000033));
954 }
955
956 #[rstest]
957 fn test_recent_trade_deserializes() {
958 let json = r#"{
960 "coin": "BTC",
961 "side": "B",
962 "px": "104250.0",
963 "sz": "0.0123",
964 "hash": "0xabc",
965 "time": 1769916000000,
966 "tid": 987654321,
967 "users": ["0xbuyer", "0xseller"]
968 }"#;
969
970 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
971
972 assert_eq!(trade.coin, "BTC");
973 assert_eq!(trade.side, HyperliquidSide::Buy);
974 assert_eq!(trade.px, dec!(104250.0));
975 assert_eq!(trade.sz, dec!(0.0123));
976 assert_eq!(trade.time, 1769916000000);
977 assert_eq!(trade.tid, 987654321);
978 }
979
980 #[rstest]
981 fn test_order_status_deserializes_frontend_market_tif() {
982 let status: HyperliquidOrderStatus =
983 crate::common::testing::load_test_data("http_order_status_frontend_market.json");
984 let entry = status.into_order().expect("order status entry");
985
986 assert_eq!(entry.order.oid, 1);
987 assert_eq!(
988 entry.order.tif,
989 Some(HyperliquidTimeInForce::FrontendMarket)
990 );
991 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
992 }
993
994 #[rstest]
995 fn test_historical_order_deserializes_liquidation_market_tif() {
996 let entry: HyperliquidOrderStatusEntry =
997 crate::common::testing::load_test_data("http_historical_order_liquidation_market.json");
998
999 assert_eq!(entry.order.oid, 42);
1000 assert_eq!(
1001 entry.order.tif,
1002 Some(HyperliquidTimeInForce::LiquidationMarket)
1003 );
1004 assert_eq!(entry.status, HyperliquidOrderStatusEnum::Filled);
1005 }
1006
1007 #[rstest]
1008 fn test_user_fill_deserializes_tid_and_builder_fee() {
1009 let json = r#"{
1010 "coin": "BTC",
1011 "px": "60000.5",
1012 "sz": "0.001",
1013 "side": "B",
1014 "time": 1704470400000,
1015 "startPosition": "0",
1016 "dir": "Open Long",
1017 "closedPnl": "1.25",
1018 "hash": "0xabc",
1019 "oid": 7001,
1020 "crossed": true,
1021 "fee": "0.02",
1022 "feeToken": "USDC",
1023 "tid": 9001,
1024 "builderFee": "0.001"
1025 }"#;
1026
1027 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1028
1029 assert_eq!(fill.coin, "BTC");
1030 assert_eq!(fill.oid, 7001);
1031 assert_eq!(fill.tid, 9001);
1032 assert_eq!(fill.builder_fee, Some(dec!(0.001)));
1033 assert_eq!(fill.fee, dec!(0.02));
1034 }
1035
1036 #[rstest]
1037 fn test_user_fill_defaults_missing_tid_and_builder_fee() {
1038 let json = r#"{
1039 "coin": "ETH",
1040 "px": "2500.25",
1041 "sz": "0.5",
1042 "side": "A",
1043 "time": 1704470401000,
1044 "startPosition": "1.0",
1045 "dir": "Close Long",
1046 "closedPnl": "2.5",
1047 "hash": "0xdef",
1048 "oid": 8002,
1049 "crossed": false,
1050 "fee": "0.01",
1051 "feeToken": "USDC"
1052 }"#;
1053
1054 let fill: HyperliquidFill = serde_json::from_str(json).unwrap();
1055
1056 assert_eq!(fill.oid, 8002);
1057 assert_eq!(fill.tid, 0);
1058 assert_eq!(fill.builder_fee, None);
1059 assert_eq!(fill.fee, dec!(0.01));
1060 assert!(!fill.crossed);
1061 }
1062
1063 #[rstest]
1064 fn test_perp_asset_hip3_fields() {
1065 let json = r#"{
1066 "name": "xyz:TSLA",
1067 "szDecimals": 3,
1068 "maxLeverage": 10,
1069 "onlyIsolated": true,
1070 "growthMode": "enabled",
1071 "marginMode": "strictIsolated"
1072 }"#;
1073
1074 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1075
1076 assert_eq!(asset.name, "xyz:TSLA");
1077 assert_eq!(asset.sz_decimals, 3);
1078 assert_eq!(asset.max_leverage, Some(10));
1079 assert_eq!(asset.only_isolated, Some(true));
1080 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
1081 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
1082 }
1083
1084 #[rstest]
1085 fn test_perp_asset_hip3_fields_absent() {
1086 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
1087
1088 let asset: PerpAsset = serde_json::from_str(json).unwrap();
1089
1090 assert_eq!(asset.growth_mode, None);
1091 assert_eq!(asset.margin_mode, None);
1092 }
1093
1094 #[rstest]
1095 fn test_outcome_meta_defaults_missing_side_specs() {
1096 let json = r#"{
1097 "outcomes": [
1098 {
1099 "outcome": 123,
1100 "name": "Recurring",
1101 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
1102 }
1103 ]
1104 }"#;
1105
1106 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
1107
1108 assert_eq!(meta.outcomes.len(), 1);
1109 assert_eq!(meta.outcomes[0].outcome, 123);
1110 assert!(meta.outcomes[0].side_specs.is_empty());
1111 }
1112
1113 #[rstest]
1114 fn test_l2_book_deserialization() {
1115 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
1116
1117 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
1118
1119 assert_eq!(book.coin, "BTC");
1120 assert_eq!(book.levels.len(), 2);
1121 assert_eq!(book.time, 1234567890);
1122 }
1123
1124 #[rstest]
1125 fn test_exchange_response_deserialization() {
1126 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
1127
1128 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
1129 assert!(response.is_ok());
1130 }
1131
1132 #[rstest]
1133 fn test_spot_clearinghouse_state_deserialization() {
1134 let json = r#"{
1135 "balances": [
1136 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
1137 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
1138 ]
1139 }"#;
1140
1141 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1142
1143 assert_eq!(state.balances.len(), 2);
1144 let usdc = &state.balances[0];
1145 assert_eq!(usdc.coin, "USDC");
1146 assert_eq!(usdc.token, Some(0));
1147 assert_eq!(usdc.total.to_string(), "14.625485");
1148 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
1149 assert_eq!(usdc.free().to_string(), "14.625485");
1150 assert_eq!(usdc.avg_entry_px(), None);
1151
1152 let purr = &state.balances[1];
1153 assert_eq!(purr.coin, "PURR");
1154 assert_eq!(purr.token, Some(1));
1155 assert_eq!(purr.free().to_string(), "1900");
1156 assert_eq!(
1157 purr.avg_entry_px().unwrap(),
1158 rust_decimal_macros::dec!(0.61728)
1159 );
1160 }
1161
1162 #[rstest]
1163 fn test_spot_balance_outcome_side_token_lacks_token_field() {
1164 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1166 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1167 assert_eq!(balance.coin, "+250");
1168 assert_eq!(balance.token, None);
1169 }
1170
1171 #[rstest]
1172 fn test_spot_clearinghouse_state_empty() {
1173 let json = r#"{"balances": []}"#;
1174 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1175 assert!(state.balances.is_empty());
1176 }
1177
1178 #[rstest]
1179 fn test_spot_balance_handles_missing_entry_ntl() {
1180 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1181 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1182 assert_eq!(balance.entry_ntl, None);
1183 assert_eq!(balance.avg_entry_px(), None);
1184 }
1185
1186 #[rstest]
1187 fn test_msgpack_serialization_matches_python() {
1188 let action = HyperliquidExchangeAction::Order {
1193 orders: vec![],
1194 grouping: HyperliquidExchangeGrouping::Na,
1195 builder: None,
1196 };
1197
1198 let json = serde_json::to_string(&action).unwrap();
1200 assert!(
1201 json.contains(r#""type":"order""#),
1202 "JSON should have type tag: {json}"
1203 );
1204
1205 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1207
1208 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1210
1211 assert!(
1213 decoded.get("type").is_some(),
1214 "MsgPack should have type tag. Decoded: {decoded:?}"
1215 );
1216 assert_eq!(
1217 decoded.get("type").unwrap().as_str().unwrap(),
1218 "order",
1219 "Type should be 'order'"
1220 );
1221 assert!(decoded.get("orders").is_some(), "Should have orders field");
1222 assert!(
1223 decoded.get("grouping").is_some(),
1224 "Should have grouping field"
1225 );
1226 }
1227
1228 #[rstest]
1229 fn test_cancel_action_serializes_fast_flag() {
1230 let action = HyperliquidExchangeAction::Cancel {
1231 cancels: vec![HyperliquidExchangeCancelOrderRequest {
1232 asset: 0,
1233 oid: 12345,
1234 }],
1235 fast: Some(true),
1236 };
1237
1238 let value = serde_json::to_value(action).unwrap();
1239
1240 assert_eq!(
1241 value,
1242 json!({
1243 "type": "cancel",
1244 "cancels": [{"a": 0, "o": 12345}],
1245 "f": true,
1246 })
1247 );
1248 }
1249
1250 #[rstest]
1251 fn test_cancel_by_cloid_action_serializes_fast_flag() {
1252 let action = HyperliquidExchangeAction::CancelByCloid {
1253 cancels: vec![HyperliquidExchangeCancelByCloidRequest {
1254 asset: 0,
1255 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
1256 }],
1257 fast: Some(true),
1258 };
1259
1260 let value = serde_json::to_value(action).unwrap();
1261
1262 assert_eq!(
1263 value,
1264 json!({
1265 "type": "cancelByCloid",
1266 "cancels": [{
1267 "asset": 0,
1268 "cloid": "0x00000000000000000000000000000000",
1269 }],
1270 "f": true,
1271 })
1272 );
1273 }
1274
1275 #[rstest]
1276 fn test_order_response_normal_tpsl_with_waiting_children() {
1277 let json = r#"{
1281 "statuses": [
1282 {"resting": {"oid": 446050656712}},
1283 "waitingForFill",
1284 "waitingForTrigger"
1285 ]
1286 }"#;
1287
1288 let data: HyperliquidExchangeOrderResponseData = serde_json::from_str(json).unwrap();
1289 assert_eq!(data.statuses.len(), 3);
1290
1291 assert!(matches!(
1292 data.statuses[0],
1293 HyperliquidExchangeOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1294 ));
1295 assert!(matches!(
1296 data.statuses[1],
1297 HyperliquidExchangeOrderStatus::Tag(HyperliquidExchangeOrderStatusTag::WaitingForFill)
1298 ));
1299 assert!(matches!(
1300 data.statuses[2],
1301 HyperliquidExchangeOrderStatus::Tag(
1302 HyperliquidExchangeOrderStatusTag::WaitingForTrigger
1303 )
1304 ));
1305 }
1306
1307 #[rstest]
1308 fn test_user_outcome_split_serialization() {
1309 let action = HyperliquidExchangeAction::UserOutcome {
1310 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1311 HyperliquidExchangeSplitOutcomeParams {
1312 outcome: 1,
1313 amount: dec!(123.0),
1314 },
1315 ),
1316 };
1317
1318 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1319 assert_eq!(
1320 value,
1321 json!({
1322 "type": "userOutcome",
1323 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1324 })
1325 );
1326 }
1327
1328 #[rstest]
1329 fn test_user_outcome_split_msgpack_roundtrip() {
1330 let action = HyperliquidExchangeAction::UserOutcome {
1331 op: HyperliquidExchangeUserOutcomeOp::SplitOutcome(
1332 HyperliquidExchangeSplitOutcomeParams {
1333 outcome: 4,
1334 amount: dec!(10),
1335 },
1336 ),
1337 };
1338
1339 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1340 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1341 assert_eq!(
1342 decoded,
1343 json!({
1344 "type": "userOutcome",
1345 "splitOutcome": { "outcome": 4, "amount": "10" }
1346 })
1347 );
1348 }
1349
1350 #[rstest]
1351 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1352 let level = HyperliquidLevel {
1355 px: dec!(98450.5),
1356 sz: dec!(2.5),
1357 };
1358 let value = serde_json::to_value(&level).unwrap();
1359 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1360 }
1361
1362 #[rstest]
1363 fn test_user_outcome_merge_outcome_serialization() {
1364 let action = HyperliquidExchangeAction::UserOutcome {
1365 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1366 HyperliquidExchangeMergeOutcomeParams {
1367 outcome: 1,
1368 amount: Some(dec!(5.0)),
1369 },
1370 ),
1371 };
1372 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1373 assert_eq!(
1374 value,
1375 json!({
1376 "type": "userOutcome",
1377 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1378 })
1379 );
1380 }
1381
1382 #[rstest]
1383 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1384 let action = HyperliquidExchangeAction::UserOutcome {
1385 op: HyperliquidExchangeUserOutcomeOp::MergeOutcome(
1386 HyperliquidExchangeMergeOutcomeParams {
1387 outcome: 7,
1388 amount: None,
1389 },
1390 ),
1391 };
1392 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1393 assert_eq!(
1394 value,
1395 json!({
1396 "type": "userOutcome",
1397 "mergeOutcome": { "outcome": 7, "amount": null }
1398 })
1399 );
1400 }
1401
1402 #[rstest]
1403 fn test_user_outcome_merge_question_serialization() {
1404 let action = HyperliquidExchangeAction::UserOutcome {
1405 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1406 HyperliquidExchangeMergeQuestionParams {
1407 question: 9,
1408 amount: Some(dec!(2.0)),
1409 },
1410 ),
1411 };
1412 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1413 assert_eq!(
1414 value,
1415 json!({
1416 "type": "userOutcome",
1417 "mergeQuestion": { "question": 9, "amount": "2.0" }
1418 })
1419 );
1420 }
1421
1422 #[rstest]
1423 fn test_user_outcome_merge_question_null_amount_means_max() {
1424 let action = HyperliquidExchangeAction::UserOutcome {
1425 op: HyperliquidExchangeUserOutcomeOp::MergeQuestion(
1426 HyperliquidExchangeMergeQuestionParams {
1427 question: 9,
1428 amount: None,
1429 },
1430 ),
1431 };
1432 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1433 assert_eq!(
1434 value,
1435 json!({
1436 "type": "userOutcome",
1437 "mergeQuestion": { "question": 9, "amount": null }
1438 })
1439 );
1440 }
1441
1442 #[rstest]
1443 fn test_user_outcome_negate_outcome_serialization() {
1444 let action = HyperliquidExchangeAction::UserOutcome {
1445 op: HyperliquidExchangeUserOutcomeOp::NegateOutcome(
1446 HyperliquidExchangeNegateOutcomeParams {
1447 question: 9,
1448 outcome: 52,
1449 amount: dec!(1.5),
1450 },
1451 ),
1452 };
1453 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1454 assert_eq!(
1455 value,
1456 json!({
1457 "type": "userOutcome",
1458 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1459 })
1460 );
1461 }
1462
1463 #[rstest]
1464 fn test_modify_target_serializes_numeric_oid() {
1465 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Oid(12345));
1466 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1467
1468 assert_eq!(value["oid"], json!(12345));
1469 }
1470
1471 #[rstest]
1472 fn test_modify_target_serializes_cloid() {
1473 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
1474 let request = modify_request_with_target(HyperliquidExchangeModifyTarget::Cloid(cloid));
1475 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1476
1477 assert_eq!(value["oid"], json!("0x1234567890abcdef1234567890abcdef"));
1478 }
1479
1480 fn modify_request_with_target(
1481 oid: HyperliquidExchangeModifyTarget,
1482 ) -> HyperliquidExchangeModifyOrderRequest {
1483 HyperliquidExchangeModifyOrderRequest {
1484 oid,
1485 order: HyperliquidExchangePlaceOrderRequest {
1486 asset: 0,
1487 is_buy: true,
1488 price: dec!(51000),
1489 size: dec!(0.2),
1490 reduce_only: false,
1491 kind: HyperliquidExchangeOrderKind::Limit {
1492 limit: HyperliquidExchangeLimitParams {
1493 tif: HyperliquidExchangeTif::Gtc,
1494 },
1495 },
1496 cloid: None,
1497 },
1498 }
1499 }
1500}
1501
1502#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1506pub enum HyperliquidExchangeTif {
1507 #[serde(rename = "Alo")]
1509 Alo,
1510 #[serde(rename = "Ioc")]
1512 Ioc,
1513 #[serde(rename = "Gtc")]
1515 Gtc,
1516}
1517
1518#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1520pub enum HyperliquidExchangeTpSl {
1521 #[serde(rename = "tp")]
1523 Tp,
1524 #[serde(rename = "sl")]
1526 Sl,
1527}
1528
1529#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1531pub enum HyperliquidExchangeGrouping {
1532 #[serde(rename = "na")]
1534 #[default]
1535 Na,
1536 #[serde(rename = "normalTpsl")]
1538 NormalTpsl,
1539 #[serde(rename = "positionTpsl")]
1541 PositionTpsl,
1542}
1543
1544#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1546#[serde(untagged)]
1547pub enum HyperliquidExchangeOrderKind {
1548 Limit {
1550 limit: HyperliquidExchangeLimitParams,
1552 },
1553 Trigger {
1555 trigger: HyperliquidExchangeTriggerParams,
1557 },
1558}
1559
1560#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1562pub struct HyperliquidExchangeLimitParams {
1563 pub tif: HyperliquidExchangeTif,
1565}
1566
1567#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1569#[serde(rename_all = "camelCase")]
1570pub struct HyperliquidExchangeTriggerParams {
1571 pub is_market: bool,
1573 #[serde(
1575 serialize_with = "serialize_decimal_as_str",
1576 deserialize_with = "deserialize_decimal_from_str"
1577 )]
1578 pub trigger_px: Decimal,
1579 pub tpsl: HyperliquidExchangeTpSl,
1581}
1582
1583#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1588pub struct HyperliquidExchangeBuilderFee {
1589 #[serde(rename = "b")]
1591 pub address: String,
1592 #[serde(rename = "f")]
1594 pub fee_tenths_bp: u32,
1595}
1596
1597#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1602pub struct HyperliquidExchangePlaceOrderRequest {
1603 #[serde(rename = "a")]
1605 pub asset: AssetId,
1606 #[serde(rename = "b")]
1608 pub is_buy: bool,
1609 #[serde(
1611 rename = "p",
1612 serialize_with = "serialize_decimal_as_str",
1613 deserialize_with = "deserialize_decimal_from_str"
1614 )]
1615 pub price: Decimal,
1616 #[serde(
1618 rename = "s",
1619 serialize_with = "serialize_decimal_as_str",
1620 deserialize_with = "deserialize_decimal_from_str"
1621 )]
1622 pub size: Decimal,
1623 #[serde(rename = "r")]
1625 pub reduce_only: bool,
1626 #[serde(rename = "t")]
1628 pub kind: HyperliquidExchangeOrderKind,
1629 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1631 pub cloid: Option<Cloid>,
1632}
1633
1634#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1636pub struct HyperliquidExchangeCancelOrderRequest {
1637 #[serde(rename = "a")]
1639 pub asset: AssetId,
1640 #[serde(rename = "o")]
1642 pub oid: OrderId,
1643}
1644
1645#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1650pub struct HyperliquidExchangeCancelByCloidRequest {
1651 pub asset: AssetId,
1653 pub cloid: Cloid,
1655}
1656
1657#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1662#[serde(untagged)]
1663pub enum HyperliquidExchangeModifyTarget {
1664 Oid(OrderId),
1666 Cloid(Cloid),
1668}
1669
1670impl HyperliquidExchangeModifyTarget {
1671 pub fn from_venue_order_id(
1677 venue_order_id: &VenueOrderId,
1678 ) -> Result<Self, std::num::ParseIntError> {
1679 venue_order_id.as_str().parse::<OrderId>().map(Self::Oid)
1680 }
1681}
1682
1683impl From<OrderId> for HyperliquidExchangeModifyTarget {
1684 fn from(value: OrderId) -> Self {
1685 Self::Oid(value)
1686 }
1687}
1688
1689impl From<Cloid> for HyperliquidExchangeModifyTarget {
1690 fn from(value: Cloid) -> Self {
1691 Self::Cloid(value)
1692 }
1693}
1694
1695#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1700pub struct HyperliquidExchangeModifyOrderRequest {
1701 pub oid: HyperliquidExchangeModifyTarget,
1703 pub order: HyperliquidExchangePlaceOrderRequest,
1705}
1706
1707#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1712pub struct HyperliquidExchangeSplitOutcomeParams {
1713 pub outcome: u32,
1715 #[serde(
1717 serialize_with = "serialize_decimal_as_str",
1718 deserialize_with = "deserialize_decimal_from_str"
1719 )]
1720 pub amount: Decimal,
1721}
1722
1723#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1729pub struct HyperliquidExchangeMergeOutcomeParams {
1730 pub outcome: u32,
1732 #[serde(
1734 default,
1735 serialize_with = "serialize_optional_decimal_as_str",
1736 deserialize_with = "deserialize_optional_decimal_from_str"
1737 )]
1738 pub amount: Option<Decimal>,
1739}
1740
1741#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1747pub struct HyperliquidExchangeMergeQuestionParams {
1748 pub question: u32,
1750 #[serde(
1752 default,
1753 serialize_with = "serialize_optional_decimal_as_str",
1754 deserialize_with = "deserialize_optional_decimal_from_str"
1755 )]
1756 pub amount: Option<Decimal>,
1757}
1758
1759#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1764pub struct HyperliquidExchangeNegateOutcomeParams {
1765 pub question: u32,
1767 pub outcome: u32,
1769 #[serde(
1771 serialize_with = "serialize_decimal_as_str",
1772 deserialize_with = "deserialize_decimal_from_str"
1773 )]
1774 pub amount: Decimal,
1775}
1776
1777#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1784pub enum HyperliquidExchangeUserOutcomeOp {
1785 #[serde(rename = "splitOutcome")]
1787 SplitOutcome(HyperliquidExchangeSplitOutcomeParams),
1788 #[serde(rename = "mergeOutcome")]
1791 MergeOutcome(HyperliquidExchangeMergeOutcomeParams),
1792 #[serde(rename = "mergeQuestion")]
1795 MergeQuestion(HyperliquidExchangeMergeQuestionParams),
1796 #[serde(rename = "negateOutcome")]
1799 NegateOutcome(HyperliquidExchangeNegateOutcomeParams),
1800}
1801
1802#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1804pub struct HyperliquidExchangeTwapRequest {
1805 #[serde(rename = "a")]
1807 pub asset: AssetId,
1808 #[serde(rename = "b")]
1810 pub is_buy: bool,
1811 #[serde(
1813 rename = "s",
1814 serialize_with = "serialize_decimal_as_str",
1815 deserialize_with = "deserialize_decimal_from_str"
1816 )]
1817 pub size: Decimal,
1818 #[serde(rename = "m")]
1820 pub duration_ms: u64,
1821}
1822
1823#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1829#[serde(tag = "type")]
1830pub enum HyperliquidExchangeAction {
1831 #[serde(rename = "order")]
1833 Order {
1834 orders: Vec<HyperliquidExchangePlaceOrderRequest>,
1836 #[serde(default)]
1838 grouping: HyperliquidExchangeGrouping,
1839 #[serde(skip_serializing_if = "Option::is_none")]
1841 builder: Option<HyperliquidExchangeBuilderFee>,
1842 },
1843
1844 #[serde(rename = "cancel")]
1846 Cancel {
1847 cancels: Vec<HyperliquidExchangeCancelOrderRequest>,
1849 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1851 fast: Option<bool>,
1852 },
1853
1854 #[serde(rename = "cancelByCloid")]
1856 CancelByCloid {
1857 cancels: Vec<HyperliquidExchangeCancelByCloidRequest>,
1859 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1861 fast: Option<bool>,
1862 },
1863
1864 #[serde(rename = "modify")]
1866 Modify {
1867 #[serde(flatten)]
1869 modify: HyperliquidExchangeModifyOrderRequest,
1870 },
1871
1872 #[serde(rename = "batchModify")]
1874 BatchModify {
1875 modifies: Vec<HyperliquidExchangeModifyOrderRequest>,
1877 },
1878
1879 #[serde(rename = "scheduleCancel")]
1881 ScheduleCancel {
1882 #[serde(skip_serializing_if = "Option::is_none")]
1885 time: Option<u64>,
1886 },
1887
1888 #[serde(rename = "updateLeverage")]
1890 UpdateLeverage {
1891 #[serde(rename = "a")]
1893 asset: AssetId,
1894 #[serde(rename = "isCross")]
1896 is_cross: bool,
1897 #[serde(rename = "leverage")]
1899 leverage: u32,
1900 },
1901
1902 #[serde(rename = "updateIsolatedMargin")]
1904 UpdateIsolatedMargin {
1905 #[serde(rename = "a")]
1907 asset: AssetId,
1908 #[serde(
1910 rename = "delta",
1911 serialize_with = "serialize_decimal_as_str",
1912 deserialize_with = "deserialize_decimal_from_str"
1913 )]
1914 delta: Decimal,
1915 },
1916
1917 #[serde(rename = "usdClassTransfer")]
1919 UsdClassTransfer {
1920 from: String,
1922 to: String,
1924 #[serde(
1926 serialize_with = "serialize_decimal_as_str",
1927 deserialize_with = "deserialize_decimal_from_str"
1928 )]
1929 amount: Decimal,
1930 },
1931
1932 #[serde(rename = "userOutcome")]
1938 UserOutcome {
1939 #[serde(flatten)]
1941 op: HyperliquidExchangeUserOutcomeOp,
1942 },
1943
1944 #[serde(rename = "twapPlace")]
1946 TwapPlace {
1947 #[serde(flatten)]
1949 twap: HyperliquidExchangeTwapRequest,
1950 },
1951
1952 #[serde(rename = "twapCancel")]
1954 TwapCancel {
1955 #[serde(rename = "a")]
1957 asset: AssetId,
1958 #[serde(rename = "t")]
1960 twap_id: u64,
1961 },
1962
1963 #[serde(rename = "noop")]
1965 Noop,
1966}
1967
1968#[derive(Debug, Clone, Serialize)]
1973#[serde(rename_all = "camelCase")]
1974pub struct HyperliquidExchangeActionRequest {
1975 pub action: HyperliquidExchangeAction,
1977 pub nonce: u64,
1979 pub signature: SecretString,
1981 #[serde(skip_serializing_if = "Option::is_none")]
1983 pub vault_address: Option<String>,
1984 #[serde(skip_serializing_if = "Option::is_none")]
1987 pub expires_after: Option<u64>,
1988}
1989
1990#[derive(Debug, Clone, Serialize, Deserialize)]
1992pub struct HyperliquidExchangeActionResponse {
1993 pub status: String,
1995 pub response: HyperliquidExchangeResponseData,
1997}
1998
1999#[derive(Debug, Clone, Serialize, Deserialize)]
2001#[serde(tag = "type")]
2002pub enum HyperliquidExchangeResponseData {
2003 #[serde(rename = "order")]
2005 Order {
2006 data: HyperliquidExchangeOrderResponseData,
2008 },
2009 #[serde(rename = "cancel")]
2011 Cancel {
2012 data: HyperliquidExchangeCancelResponseData,
2014 },
2015 #[serde(rename = "modify")]
2017 Modify {
2018 data: HyperliquidExchangeModifyResponseData,
2020 },
2021 #[serde(rename = "default")]
2023 Default,
2024 #[serde(other)]
2026 Unknown,
2027}
2028
2029#[derive(Debug, Clone, Serialize, Deserialize)]
2031pub struct HyperliquidExchangeOrderResponseData {
2032 pub statuses: Vec<HyperliquidExchangeOrderStatus>,
2034}
2035
2036#[derive(Debug, Clone, Serialize, Deserialize)]
2038pub struct HyperliquidExchangeCancelResponseData {
2039 pub statuses: Vec<HyperliquidExchangeCancelStatus>,
2041}
2042
2043#[derive(Debug, Clone, Serialize, Deserialize)]
2045pub struct HyperliquidExchangeModifyResponseData {
2046 pub statuses: Vec<HyperliquidExchangeModifyStatus>,
2048}
2049
2050#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2052#[serde(untagged)]
2053pub enum HyperliquidExchangeOrderStatus {
2054 Resting {
2056 resting: HyperliquidExchangeRestingInfo,
2058 },
2059 Filled {
2061 filled: HyperliquidExchangeFilledInfo,
2063 },
2064 Error {
2066 error: String,
2068 },
2069 Tag(HyperliquidExchangeOrderStatusTag),
2073}
2074
2075#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
2081pub enum HyperliquidExchangeOrderStatusTag {
2082 #[serde(rename = "waitingForFill")]
2084 WaitingForFill,
2085 #[serde(rename = "waitingForTrigger")]
2087 WaitingForTrigger,
2088}
2089
2090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
2092pub struct HyperliquidExchangeRestingInfo {
2093 pub oid: OrderId,
2095}
2096
2097#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2099pub struct HyperliquidExchangeFilledInfo {
2100 #[serde(
2102 rename = "totalSz",
2103 serialize_with = "serialize_decimal_as_str",
2104 deserialize_with = "deserialize_decimal_from_str"
2105 )]
2106 pub total_sz: Decimal,
2107 #[serde(
2109 rename = "avgPx",
2110 serialize_with = "serialize_decimal_as_str",
2111 deserialize_with = "deserialize_decimal_from_str"
2112 )]
2113 pub avg_px: Decimal,
2114 pub oid: OrderId,
2116}
2117
2118#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2120#[serde(untagged)]
2121pub enum HyperliquidExchangeCancelStatus {
2122 Success(String), Error {
2126 error: String,
2128 },
2129}
2130
2131#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
2133#[serde(untagged)]
2134pub enum HyperliquidExchangeModifyStatus {
2135 Success(String), Error {
2139 error: String,
2141 },
2142}
2143
2144#[derive(Debug, Clone, Serialize, Deserialize)]
2147#[serde(rename_all = "camelCase")]
2148pub struct ClearinghouseState {
2149 #[serde(default)]
2151 pub asset_positions: Vec<AssetPosition>,
2152 #[serde(default)]
2154 pub cross_margin_summary: Option<CrossMarginSummary>,
2155 #[serde(
2157 default,
2158 serialize_with = "serialize_optional_decimal_as_str",
2159 deserialize_with = "deserialize_optional_decimal_from_str"
2160 )]
2161 pub withdrawable: Option<Decimal>,
2162 #[serde(default)]
2164 pub time: Option<u64>,
2165}
2166
2167#[derive(Debug, Clone, Serialize, Deserialize)]
2169#[serde(rename_all = "camelCase")]
2170pub struct AssetPosition {
2171 pub position: PositionData,
2173 #[serde(rename = "type")]
2175 pub position_type: HyperliquidPositionType,
2176}
2177
2178#[derive(Debug, Clone, Serialize, Deserialize)]
2180#[serde(rename_all = "camelCase")]
2181pub struct LeverageInfo {
2182 #[serde(rename = "type")]
2183 pub leverage_type: HyperliquidLeverageType,
2184 pub value: u32,
2186}
2187
2188#[derive(Debug, Clone, Serialize, Deserialize)]
2190#[serde(rename_all = "camelCase")]
2191pub struct CumFundingInfo {
2192 #[serde(
2194 rename = "allTime",
2195 serialize_with = "serialize_decimal_as_str",
2196 deserialize_with = "deserialize_decimal_from_str"
2197 )]
2198 pub all_time: Decimal,
2199 #[serde(
2201 rename = "sinceOpen",
2202 serialize_with = "serialize_decimal_as_str",
2203 deserialize_with = "deserialize_decimal_from_str"
2204 )]
2205 pub since_open: Decimal,
2206 #[serde(
2208 rename = "sinceChange",
2209 serialize_with = "serialize_decimal_as_str",
2210 deserialize_with = "deserialize_decimal_from_str"
2211 )]
2212 pub since_change: Decimal,
2213}
2214
2215#[derive(Debug, Clone, Serialize, Deserialize)]
2217#[serde(rename_all = "camelCase")]
2218pub struct PositionData {
2219 pub coin: Ustr,
2221 #[serde(rename = "cumFunding")]
2223 pub cum_funding: CumFundingInfo,
2224 #[serde(
2226 rename = "entryPx",
2227 serialize_with = "serialize_optional_decimal_as_str",
2228 deserialize_with = "deserialize_optional_decimal_from_str",
2229 default
2230 )]
2231 pub entry_px: Option<Decimal>,
2232 pub leverage: LeverageInfo,
2234 #[serde(
2236 rename = "liquidationPx",
2237 serialize_with = "serialize_optional_decimal_as_str",
2238 deserialize_with = "deserialize_optional_decimal_from_str",
2239 default
2240 )]
2241 pub liquidation_px: Option<Decimal>,
2242 #[serde(
2244 rename = "marginUsed",
2245 serialize_with = "serialize_decimal_as_str",
2246 deserialize_with = "deserialize_decimal_from_str"
2247 )]
2248 pub margin_used: Decimal,
2249 #[serde(rename = "maxLeverage", default)]
2251 pub max_leverage: Option<u32>,
2252 #[serde(
2254 rename = "positionValue",
2255 serialize_with = "serialize_decimal_as_str",
2256 deserialize_with = "deserialize_decimal_from_str"
2257 )]
2258 pub position_value: Decimal,
2259 #[serde(
2261 rename = "returnOnEquity",
2262 serialize_with = "serialize_decimal_as_str",
2263 deserialize_with = "deserialize_decimal_from_str"
2264 )]
2265 pub return_on_equity: Decimal,
2266 #[serde(
2268 rename = "szi",
2269 serialize_with = "serialize_decimal_as_str",
2270 deserialize_with = "deserialize_decimal_from_str"
2271 )]
2272 pub szi: Decimal,
2273 #[serde(
2275 rename = "unrealizedPnl",
2276 serialize_with = "serialize_decimal_as_str",
2277 deserialize_with = "deserialize_decimal_from_str"
2278 )]
2279 pub unrealized_pnl: Decimal,
2280}
2281
2282#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2288#[serde(rename_all = "camelCase")]
2289pub struct SpotClearinghouseState {
2290 #[serde(default)]
2292 pub balances: Vec<SpotBalance>,
2293}
2294
2295#[derive(Debug, Clone, Serialize, Deserialize)]
2297#[serde(rename_all = "camelCase")]
2298pub struct SpotBalance {
2299 pub coin: Ustr,
2301 #[serde(default)]
2304 pub token: Option<u32>,
2305 #[serde(
2307 serialize_with = "serialize_decimal_as_str",
2308 deserialize_with = "deserialize_decimal_from_str"
2309 )]
2310 pub total: Decimal,
2311 #[serde(
2313 serialize_with = "serialize_decimal_as_str",
2314 deserialize_with = "deserialize_decimal_from_str"
2315 )]
2316 pub hold: Decimal,
2317 #[serde(
2319 default,
2320 serialize_with = "serialize_optional_decimal_as_str",
2321 deserialize_with = "deserialize_optional_decimal_from_str"
2322 )]
2323 pub entry_ntl: Option<Decimal>,
2324}
2325
2326impl SpotBalance {
2327 #[must_use]
2329 pub fn free(&self) -> Decimal {
2330 (self.total - self.hold).max(Decimal::ZERO)
2331 }
2332
2333 #[must_use]
2335 pub fn avg_entry_px(&self) -> Option<Decimal> {
2336 let entry_ntl = self.entry_ntl?;
2337
2338 if entry_ntl.is_zero() || self.total.is_zero() {
2339 return None;
2340 }
2341
2342 Some(entry_ntl / self.total)
2343 }
2344}
2345
2346#[derive(Debug, Clone, Serialize, Deserialize)]
2348#[serde(rename_all = "camelCase")]
2349pub struct CrossMarginSummary {
2350 #[serde(
2352 rename = "accountValue",
2353 serialize_with = "serialize_decimal_as_str",
2354 deserialize_with = "deserialize_decimal_from_str"
2355 )]
2356 pub account_value: Decimal,
2357 #[serde(
2359 rename = "totalNtlPos",
2360 serialize_with = "serialize_decimal_as_str",
2361 deserialize_with = "deserialize_decimal_from_str"
2362 )]
2363 pub total_ntl_pos: Decimal,
2364 #[serde(
2366 rename = "totalRawUsd",
2367 serialize_with = "serialize_decimal_as_str",
2368 deserialize_with = "deserialize_decimal_from_str"
2369 )]
2370 pub total_raw_usd: Decimal,
2371 #[serde(
2373 rename = "totalMarginUsed",
2374 serialize_with = "serialize_decimal_as_str",
2375 deserialize_with = "deserialize_decimal_from_str"
2376 )]
2377 pub total_margin_used: Decimal,
2378 #[serde(
2380 rename = "withdrawable",
2381 default,
2382 serialize_with = "serialize_optional_decimal_as_str",
2383 deserialize_with = "deserialize_optional_decimal_from_str"
2384 )]
2385 pub withdrawable: Option<Decimal>,
2386}