1use std::fmt::Display;
17
18use alloy_primitives::{Address, keccak256};
19use nautilus_core::hex;
20use nautilus_model::identifiers::ClientOrderId;
21use rust_decimal::Decimal;
22use serde::{Deserialize, Deserializer, Serialize, Serializer};
23use ustr::Ustr;
24
25use crate::common::{
26 enums::{
27 HyperliquidFillDirection, HyperliquidLeverageType,
28 HyperliquidOrderStatus as HyperliquidOrderStatusEnum, HyperliquidPositionType,
29 HyperliquidSide,
30 },
31 parse::{
32 deserialize_decimal_from_str, deserialize_optional_decimal_from_str,
33 serialize_decimal_as_str, serialize_optional_decimal_as_str,
34 },
35};
36
37pub type HyperliquidCandleSnapshot = Vec<HyperliquidCandle>;
39
40#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
42pub struct Cloid(pub [u8; 16]);
43
44impl Cloid {
45 pub fn from_hex<S: AsRef<str>>(s: S) -> Result<Self, String> {
51 let hex_str = s.as_ref();
52 let without_prefix = hex_str
53 .strip_prefix("0x")
54 .ok_or("CLOID must start with '0x'")?;
55
56 if without_prefix.len() != 32 {
57 return Err("CLOID must be exactly 32 hex characters (128 bits)".to_string());
58 }
59
60 let bytes = hex::decode_array(without_prefix)
61 .map_err(|_| "Invalid hex character in CLOID".to_string())?;
62
63 Ok(Self(bytes))
64 }
65
66 #[must_use]
68 pub fn from_client_order_id(client_order_id: ClientOrderId) -> Self {
69 let hash = keccak256(client_order_id.as_str().as_bytes());
70 let mut bytes = [0u8; 16];
71 bytes.copy_from_slice(&hash[..16]);
72 Self(bytes)
73 }
74
75 #[must_use]
77 pub fn is_uuid_v4(&self) -> bool {
78 self.0[6] >> 4 == 4 && matches!(self.0[8] >> 4, 8..=11)
79 }
80
81 pub fn to_hex(&self) -> String {
83 hex::encode_prefixed(self.0)
84 }
85}
86
87impl Display for Cloid {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(f, "{}", self.to_hex())
90 }
91}
92
93impl Serialize for Cloid {
94 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95 where
96 S: Serializer,
97 {
98 serializer.serialize_str(&self.to_hex())
99 }
100}
101
102impl<'de> Deserialize<'de> for Cloid {
103 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
104 where
105 D: Deserializer<'de>,
106 {
107 let s = String::deserialize(deserializer)?;
108 Self::from_hex(&s).map_err(serde::de::Error::custom)
109 }
110}
111
112pub type AssetId = u32;
117
118pub type OrderId = u64;
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124pub struct HyperliquidAssetInfo {
125 pub name: Ustr,
127 pub sz_decimals: u32,
129 #[serde(default)]
131 pub max_leverage: Option<u32>,
132 #[serde(default)]
134 pub only_isolated: Option<bool>,
135 #[serde(default)]
137 pub is_delisted: Option<bool>,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142#[serde(rename_all = "camelCase")]
143pub struct PerpMeta {
144 pub universe: Vec<PerpAsset>,
146 #[serde(default)]
148 pub margin_tables: Vec<(u32, MarginTable)>,
149 #[serde(default)]
151 pub collateral_token: Option<u32>,
152}
153
154#[derive(Debug, Clone, Default, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct PerpAsset {
158 pub name: String,
160 pub sz_decimals: u32,
162 #[serde(default)]
164 pub max_leverage: Option<u32>,
165 #[serde(default)]
167 pub only_isolated: Option<bool>,
168 #[serde(default)]
170 pub is_delisted: Option<bool>,
171 #[serde(default)]
173 pub growth_mode: Option<String>,
174 #[serde(default)]
176 pub margin_mode: Option<String>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181#[serde(rename_all = "camelCase")]
182pub struct MarginTable {
183 pub description: String,
185 #[serde(default)]
187 pub margin_tiers: Vec<MarginTier>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct MarginTier {
194 #[serde(
196 serialize_with = "serialize_decimal_as_str",
197 deserialize_with = "deserialize_decimal_from_str"
198 )]
199 pub lower_bound: Decimal,
200 pub max_leverage: u32,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
207#[serde(rename_all = "camelCase")]
208pub struct PerpDex {
209 pub name: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct SpotMeta {
217 pub tokens: Vec<SpotToken>,
219 pub universe: Vec<SpotPair>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225#[serde(rename_all = "snake_case")]
226pub struct EvmContract {
227 pub address: Address,
229 pub evm_extra_wei_decimals: i32,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235#[serde(rename_all = "camelCase")]
236pub struct SpotToken {
237 pub name: String,
239 pub sz_decimals: u32,
241 pub wei_decimals: u32,
243 pub index: u32,
245 pub token_id: String,
247 pub is_canonical: bool,
249 #[serde(default)]
251 pub evm_contract: Option<EvmContract>,
252 #[serde(default)]
254 pub full_name: Option<String>,
255 #[serde(default)]
257 pub deployer_trading_fee_share: Option<String>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
262#[serde(rename_all = "camelCase")]
263pub struct SpotPair {
264 pub name: String,
266 pub tokens: [u32; 2],
268 pub index: u32,
270 pub is_canonical: bool,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(rename_all = "camelCase")]
277pub struct OutcomeMeta {
278 pub outcomes: Vec<OutcomeMarket>,
280 #[serde(default)]
284 pub questions: Vec<OutcomeQuestion>,
285}
286
287impl OutcomeMeta {
288 #[must_use]
291 pub fn parent_question(&self, outcome_index: u32) -> Option<&OutcomeQuestion> {
292 self.questions.iter().find(|q| {
293 q.fallback_outcome == Some(outcome_index) || q.named_outcomes.contains(&outcome_index)
294 })
295 }
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
300#[serde(rename_all = "camelCase")]
301pub struct OutcomeMarket {
302 pub outcome: u32,
304 pub name: String,
306 pub description: String,
308 #[serde(default)]
310 pub side_specs: Vec<OutcomeSideSpec>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315#[serde(rename_all = "camelCase")]
316pub struct OutcomeSideSpec {
317 pub name: String,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
327#[serde(rename_all = "camelCase")]
328pub struct OutcomeQuestion {
329 pub question: u32,
331 pub name: String,
333 pub description: String,
335 #[serde(default)]
337 pub fallback_outcome: Option<u32>,
338 #[serde(default)]
340 pub named_outcomes: Vec<u32>,
341 #[serde(default)]
343 pub settled_named_outcomes: Vec<u32>,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
349#[serde(untagged)]
350pub enum PerpMetaAndCtxs {
351 Payload(Box<(PerpMeta, Vec<PerpAssetCtx>)>),
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357#[serde(rename_all = "camelCase")]
358pub struct PerpAssetCtx {
359 #[serde(
361 default,
362 serialize_with = "serialize_optional_decimal_as_str",
363 deserialize_with = "deserialize_optional_decimal_from_str"
364 )]
365 pub mark_px: Option<Decimal>,
366 #[serde(
368 default,
369 serialize_with = "serialize_optional_decimal_as_str",
370 deserialize_with = "deserialize_optional_decimal_from_str"
371 )]
372 pub mid_px: Option<Decimal>,
373 #[serde(
375 default,
376 serialize_with = "serialize_optional_decimal_as_str",
377 deserialize_with = "deserialize_optional_decimal_from_str"
378 )]
379 pub funding: Option<Decimal>,
380 #[serde(
382 default,
383 serialize_with = "serialize_optional_decimal_as_str",
384 deserialize_with = "deserialize_optional_decimal_from_str"
385 )]
386 pub open_interest: Option<Decimal>,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
392#[serde(untagged)]
393pub enum SpotMetaAndCtxs {
394 Payload(Box<(SpotMeta, Vec<SpotAssetCtx>)>),
396}
397
398#[derive(Debug, Clone, Serialize, Deserialize)]
400#[serde(rename_all = "camelCase")]
401pub struct SpotAssetCtx {
402 #[serde(
404 default,
405 serialize_with = "serialize_optional_decimal_as_str",
406 deserialize_with = "deserialize_optional_decimal_from_str"
407 )]
408 pub mark_px: Option<Decimal>,
409 #[serde(
411 default,
412 serialize_with = "serialize_optional_decimal_as_str",
413 deserialize_with = "deserialize_optional_decimal_from_str"
414 )]
415 pub mid_px: Option<Decimal>,
416 #[serde(
418 default,
419 serialize_with = "serialize_optional_decimal_as_str",
420 deserialize_with = "deserialize_optional_decimal_from_str"
421 )]
422 pub day_volume: Option<Decimal>,
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
427pub struct HyperliquidL2Book {
428 pub coin: Ustr,
430 pub levels: Vec<Vec<HyperliquidLevel>>,
432 pub time: u64,
434}
435
436#[derive(Debug, Clone, Serialize, Deserialize)]
438pub struct HyperliquidLevel {
439 #[serde(
441 serialize_with = "serialize_decimal_as_str",
442 deserialize_with = "deserialize_decimal_from_str"
443 )]
444 pub px: Decimal,
445 #[serde(
447 serialize_with = "serialize_decimal_as_str",
448 deserialize_with = "deserialize_decimal_from_str"
449 )]
450 pub sz: Decimal,
451}
452
453pub type HyperliquidFills = Vec<HyperliquidFill>;
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub struct HyperliquidMeta {
461 #[serde(default)]
462 pub universe: Vec<HyperliquidAssetInfo>,
463}
464
465#[derive(Debug, Clone, Serialize, Deserialize)]
467#[serde(rename_all = "camelCase")]
468pub struct HyperliquidCandle {
469 #[serde(rename = "t")]
471 pub timestamp: u64,
472 #[serde(rename = "T")]
474 pub end_timestamp: u64,
475 #[serde(
477 rename = "o",
478 serialize_with = "serialize_decimal_as_str",
479 deserialize_with = "deserialize_decimal_from_str"
480 )]
481 pub open: Decimal,
482 #[serde(
484 rename = "h",
485 serialize_with = "serialize_decimal_as_str",
486 deserialize_with = "deserialize_decimal_from_str"
487 )]
488 pub high: Decimal,
489 #[serde(
491 rename = "l",
492 serialize_with = "serialize_decimal_as_str",
493 deserialize_with = "deserialize_decimal_from_str"
494 )]
495 pub low: Decimal,
496 #[serde(
498 rename = "c",
499 serialize_with = "serialize_decimal_as_str",
500 deserialize_with = "deserialize_decimal_from_str"
501 )]
502 pub close: Decimal,
503 #[serde(
505 rename = "v",
506 serialize_with = "serialize_decimal_as_str",
507 deserialize_with = "deserialize_decimal_from_str"
508 )]
509 pub volume: Decimal,
510 #[serde(rename = "n", default)]
512 pub num_trades: Option<u64>,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct HyperliquidFundingHistoryEntry {
518 pub coin: Ustr,
520 #[serde(
522 rename = "fundingRate",
523 serialize_with = "serialize_decimal_as_str",
524 deserialize_with = "deserialize_decimal_from_str"
525 )]
526 pub funding_rate: Decimal,
527 #[serde(
529 default,
530 serialize_with = "serialize_optional_decimal_as_str",
531 deserialize_with = "deserialize_optional_decimal_from_str"
532 )]
533 pub premium: Option<Decimal>,
534 pub time: u64,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize)]
543pub struct HyperliquidRecentTrade {
544 pub coin: Ustr,
546 pub side: HyperliquidSide,
548 #[serde(
550 serialize_with = "serialize_decimal_as_str",
551 deserialize_with = "deserialize_decimal_from_str"
552 )]
553 pub px: Decimal,
554 #[serde(
556 serialize_with = "serialize_decimal_as_str",
557 deserialize_with = "deserialize_decimal_from_str"
558 )]
559 pub sz: Decimal,
560 pub time: u64,
562 pub tid: u64,
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
568pub struct HyperliquidFill {
569 pub coin: Ustr,
571 #[serde(
573 serialize_with = "serialize_decimal_as_str",
574 deserialize_with = "deserialize_decimal_from_str"
575 )]
576 pub px: Decimal,
577 #[serde(
579 serialize_with = "serialize_decimal_as_str",
580 deserialize_with = "deserialize_decimal_from_str"
581 )]
582 pub sz: Decimal,
583 pub side: HyperliquidSide,
585 pub time: u64,
587 #[serde(
589 rename = "startPosition",
590 serialize_with = "serialize_decimal_as_str",
591 deserialize_with = "deserialize_decimal_from_str"
592 )]
593 pub start_position: Decimal,
594 pub dir: HyperliquidFillDirection,
596 #[serde(
598 rename = "closedPnl",
599 serialize_with = "serialize_decimal_as_str",
600 deserialize_with = "deserialize_decimal_from_str"
601 )]
602 pub closed_pnl: Decimal,
603 pub hash: String,
605 pub oid: u64,
607 pub crossed: bool,
609 #[serde(
611 serialize_with = "serialize_decimal_as_str",
612 deserialize_with = "deserialize_decimal_from_str"
613 )]
614 pub fee: Decimal,
615 #[serde(rename = "feeToken")]
617 pub fee_token: Ustr,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize)]
625#[serde(tag = "status", rename_all = "camelCase")]
626pub enum HyperliquidOrderStatus {
627 Order { order: HyperliquidOrderStatusEntry },
628 UnknownOid,
629}
630
631impl HyperliquidOrderStatus {
632 #[must_use]
634 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
635 match self {
636 Self::Order { order } => Some(order),
637 Self::UnknownOid => None,
638 }
639 }
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize)]
644pub struct HyperliquidOrderStatusEntry {
645 pub order: HyperliquidOrderInfo,
647 pub status: HyperliquidOrderStatusEnum,
649 #[serde(rename = "statusTimestamp")]
651 pub status_timestamp: u64,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize)]
656pub struct HyperliquidOrderInfo {
657 pub coin: Ustr,
659 pub side: HyperliquidSide,
661 #[serde(
663 rename = "limitPx",
664 serialize_with = "serialize_decimal_as_str",
665 deserialize_with = "deserialize_decimal_from_str"
666 )]
667 pub limit_px: Decimal,
668 #[serde(
670 serialize_with = "serialize_decimal_as_str",
671 deserialize_with = "deserialize_decimal_from_str"
672 )]
673 pub sz: Decimal,
674 pub oid: u64,
676 pub timestamp: u64,
678 #[serde(
680 rename = "origSz",
681 serialize_with = "serialize_decimal_as_str",
682 deserialize_with = "deserialize_decimal_from_str"
683 )]
684 pub orig_sz: Decimal,
685 #[serde(default)]
687 pub cloid: Option<String>,
688}
689
690#[derive(Debug, Clone, Serialize)]
692pub struct HyperliquidSignature {
693 pub r: String,
695 pub s: String,
697 pub v: u64,
699}
700
701impl HyperliquidSignature {
702 #[must_use]
704 pub fn new(r: String, s: String, v: u64) -> Self {
705 Self { r, s, v }
706 }
707
708 #[must_use]
710 pub fn to_hex(&self) -> String {
711 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
712 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
713 format!("0x{r}{s}{:02x}", self.v)
714 }
715
716 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
718 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
719
720 if sig_hex.len() != 130 {
721 return Err(format!(
722 "Invalid signature length: expected 130 hex chars, was {}",
723 sig_hex.len()
724 ));
725 }
726
727 let r = format!("0x{}", &sig_hex[0..64]);
728 let s = format!("0x{}", &sig_hex[64..128]);
729 let v = u64::from_str_radix(&sig_hex[128..130], 16)
730 .map_err(|e| format!("Failed to parse v component: {e}"))?;
731
732 Ok(Self { r, s, v })
733 }
734}
735
736#[derive(Debug, Clone, Serialize)]
738pub struct HyperliquidExchangeRequest<T> {
739 #[serde(rename = "action")]
741 pub action: T,
742 #[serde(rename = "nonce")]
744 pub nonce: u64,
745 #[serde(rename = "signature")]
747 pub signature: HyperliquidSignature,
748 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
750 pub vault_address: Option<String>,
751 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
753 pub expires_after: Option<u64>,
754}
755
756impl<T> HyperliquidExchangeRequest<T>
757where
758 T: Serialize,
759{
760 #[must_use]
762 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
763 Self {
764 action,
765 nonce,
766 signature,
767 vault_address: None,
768 expires_after: None,
769 }
770 }
771
772 #[must_use]
774 pub fn with_vault(
775 action: T,
776 nonce: u64,
777 signature: HyperliquidSignature,
778 vault_address: String,
779 ) -> Self {
780 Self {
781 action,
782 nonce,
783 signature,
784 vault_address: Some(vault_address),
785 expires_after: None,
786 }
787 }
788
789 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
791 serde_json::to_value(self)
792 }
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize)]
797#[serde(untagged)]
798pub enum HyperliquidExchangeResponse {
799 Status {
801 status: String,
803 response: serde_json::Value,
805 },
806 Error {
808 error: String,
810 },
811}
812
813impl HyperliquidExchangeResponse {
814 pub fn is_ok(&self) -> bool {
815 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
816 }
817}
818
819pub const RESPONSE_STATUS_OK: &str = "ok";
821
822#[cfg(test)]
823mod tests {
824 use rstest::rstest;
825 use rust_decimal_macros::dec;
826 use serde_json::json;
827
828 use super::*;
829
830 #[rstest]
831 fn test_meta_deserialization() {
832 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
833
834 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
835
836 assert_eq!(meta.universe.len(), 1);
837 assert_eq!(meta.universe[0].name, "BTC");
838 assert_eq!(meta.universe[0].sz_decimals, 5);
839 }
840
841 #[rstest]
842 fn test_funding_history_entry_with_premium() {
843 let json = r#"{
844 "coin": "BTC",
845 "fundingRate": "0.0000125",
846 "premium": "0.00029005",
847 "time": 1769908800000
848 }"#;
849
850 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
851
852 assert_eq!(entry.coin.as_str(), "BTC");
853 assert_eq!(entry.funding_rate, dec!(0.0000125));
854 assert_eq!(entry.premium, Some(dec!(0.00029005)));
855 assert_eq!(entry.time, 1769908800000);
856 }
857
858 #[rstest]
859 fn test_funding_history_entry_without_premium() {
860 let json = r#"{
863 "coin": "BTC",
864 "fundingRate": "0.0000033",
865 "time": 1769916000000
866 }"#;
867
868 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
869
870 assert!(entry.premium.is_none());
871 assert_eq!(entry.funding_rate, dec!(0.0000033));
872 }
873
874 #[rstest]
875 fn test_recent_trade_deserializes() {
876 let json = r#"{
878 "coin": "BTC",
879 "side": "B",
880 "px": "104250.0",
881 "sz": "0.0123",
882 "hash": "0xabc",
883 "time": 1769916000000,
884 "tid": 987654321,
885 "users": ["0xbuyer", "0xseller"]
886 }"#;
887
888 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
889
890 assert_eq!(trade.coin.as_str(), "BTC");
891 assert_eq!(trade.side, HyperliquidSide::Buy);
892 assert_eq!(trade.px, dec!(104250.0));
893 assert_eq!(trade.sz, dec!(0.0123));
894 assert_eq!(trade.time, 1769916000000);
895 assert_eq!(trade.tid, 987654321);
896 }
897
898 #[rstest]
899 fn test_perp_asset_hip3_fields() {
900 let json = r#"{
901 "name": "xyz:TSLA",
902 "szDecimals": 3,
903 "maxLeverage": 10,
904 "onlyIsolated": true,
905 "growthMode": "enabled",
906 "marginMode": "strictIsolated"
907 }"#;
908
909 let asset: PerpAsset = serde_json::from_str(json).unwrap();
910
911 assert_eq!(asset.name, "xyz:TSLA");
912 assert_eq!(asset.sz_decimals, 3);
913 assert_eq!(asset.max_leverage, Some(10));
914 assert_eq!(asset.only_isolated, Some(true));
915 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
916 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
917 }
918
919 #[rstest]
920 fn test_perp_asset_hip3_fields_absent() {
921 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
922
923 let asset: PerpAsset = serde_json::from_str(json).unwrap();
924
925 assert_eq!(asset.growth_mode, None);
926 assert_eq!(asset.margin_mode, None);
927 }
928
929 #[rstest]
930 fn test_outcome_meta_defaults_missing_side_specs() {
931 let json = r#"{
932 "outcomes": [
933 {
934 "outcome": 123,
935 "name": "Recurring",
936 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
937 }
938 ]
939 }"#;
940
941 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
942
943 assert_eq!(meta.outcomes.len(), 1);
944 assert_eq!(meta.outcomes[0].outcome, 123);
945 assert!(meta.outcomes[0].side_specs.is_empty());
946 }
947
948 #[rstest]
949 fn test_l2_book_deserialization() {
950 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
951
952 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
953
954 assert_eq!(book.coin, "BTC");
955 assert_eq!(book.levels.len(), 2);
956 assert_eq!(book.time, 1234567890);
957 }
958
959 #[rstest]
960 fn test_exchange_response_deserialization() {
961 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
962
963 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
964 assert!(response.is_ok());
965 }
966
967 #[rstest]
968 fn test_spot_clearinghouse_state_deserialization() {
969 let json = r#"{
970 "balances": [
971 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
972 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
973 ]
974 }"#;
975
976 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
977
978 assert_eq!(state.balances.len(), 2);
979 let usdc = &state.balances[0];
980 assert_eq!(usdc.coin.as_str(), "USDC");
981 assert_eq!(usdc.token, Some(0));
982 assert_eq!(usdc.total.to_string(), "14.625485");
983 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
984 assert_eq!(usdc.free().to_string(), "14.625485");
985 assert_eq!(usdc.avg_entry_px(), None);
986
987 let purr = &state.balances[1];
988 assert_eq!(purr.coin.as_str(), "PURR");
989 assert_eq!(purr.token, Some(1));
990 assert_eq!(purr.free().to_string(), "1900");
991 assert_eq!(
992 purr.avg_entry_px().unwrap(),
993 rust_decimal_macros::dec!(0.61728)
994 );
995 }
996
997 #[rstest]
998 fn test_spot_balance_outcome_side_token_lacks_token_field() {
999 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1001 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1002 assert_eq!(balance.coin.as_str(), "+250");
1003 assert_eq!(balance.token, None);
1004 }
1005
1006 #[rstest]
1007 fn test_spot_clearinghouse_state_empty() {
1008 let json = r#"{"balances": []}"#;
1009 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1010 assert!(state.balances.is_empty());
1011 }
1012
1013 #[rstest]
1014 fn test_spot_balance_handles_missing_entry_ntl() {
1015 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1016 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1017 assert_eq!(balance.entry_ntl, None);
1018 assert_eq!(balance.avg_entry_px(), None);
1019 }
1020
1021 #[rstest]
1022 fn test_msgpack_serialization_matches_python() {
1023 let action = HyperliquidExecAction::Order {
1028 orders: vec![],
1029 grouping: HyperliquidExecGrouping::Na,
1030 builder: None,
1031 };
1032
1033 let json = serde_json::to_string(&action).unwrap();
1035 assert!(
1036 json.contains(r#""type":"order""#),
1037 "JSON should have type tag: {json}"
1038 );
1039
1040 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1042
1043 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1045
1046 assert!(
1048 decoded.get("type").is_some(),
1049 "MsgPack should have type tag. Decoded: {decoded:?}"
1050 );
1051 assert_eq!(
1052 decoded.get("type").unwrap().as_str().unwrap(),
1053 "order",
1054 "Type should be 'order'"
1055 );
1056 assert!(decoded.get("orders").is_some(), "Should have orders field");
1057 assert!(
1058 decoded.get("grouping").is_some(),
1059 "Should have grouping field"
1060 );
1061 }
1062
1063 #[rstest]
1064 fn test_order_response_normal_tpsl_with_waiting_children() {
1065 let json = r#"{
1069 "statuses": [
1070 {"resting": {"oid": 446050656712}},
1071 "waitingForFill",
1072 "waitingForTrigger"
1073 ]
1074 }"#;
1075
1076 let data: HyperliquidExecOrderResponseData = serde_json::from_str(json).unwrap();
1077 assert_eq!(data.statuses.len(), 3);
1078
1079 assert!(matches!(
1080 data.statuses[0],
1081 HyperliquidExecOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1082 ));
1083 assert!(matches!(
1084 data.statuses[1],
1085 HyperliquidExecOrderStatus::Tag(HyperliquidExecOrderStatusTag::WaitingForFill)
1086 ));
1087 assert!(matches!(
1088 data.statuses[2],
1089 HyperliquidExecOrderStatus::Tag(HyperliquidExecOrderStatusTag::WaitingForTrigger)
1090 ));
1091 }
1092
1093 #[rstest]
1094 fn test_user_outcome_split_serialization() {
1095 let action = HyperliquidExecAction::UserOutcome {
1096 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
1097 outcome: 1,
1098 amount: dec!(123.0),
1099 }),
1100 };
1101
1102 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1103 assert_eq!(
1104 value,
1105 json!({
1106 "type": "userOutcome",
1107 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1108 })
1109 );
1110 }
1111
1112 #[rstest]
1113 fn test_user_outcome_split_msgpack_roundtrip() {
1114 let action = HyperliquidExecAction::UserOutcome {
1115 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
1116 outcome: 4,
1117 amount: dec!(10),
1118 }),
1119 };
1120
1121 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1122 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1123 assert_eq!(
1124 decoded,
1125 json!({
1126 "type": "userOutcome",
1127 "splitOutcome": { "outcome": 4, "amount": "10" }
1128 })
1129 );
1130 }
1131
1132 #[rstest]
1133 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1134 let level = HyperliquidLevel {
1137 px: dec!(98450.5),
1138 sz: dec!(2.5),
1139 };
1140 let value = serde_json::to_value(&level).unwrap();
1141 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1142 }
1143
1144 #[rstest]
1145 fn test_user_outcome_merge_outcome_serialization() {
1146 let action = HyperliquidExecAction::UserOutcome {
1147 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
1148 outcome: 1,
1149 amount: Some(dec!(5.0)),
1150 }),
1151 };
1152 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1153 assert_eq!(
1154 value,
1155 json!({
1156 "type": "userOutcome",
1157 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1158 })
1159 );
1160 }
1161
1162 #[rstest]
1163 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1164 let action = HyperliquidExecAction::UserOutcome {
1165 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
1166 outcome: 7,
1167 amount: None,
1168 }),
1169 };
1170 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1171 assert_eq!(
1172 value,
1173 json!({
1174 "type": "userOutcome",
1175 "mergeOutcome": { "outcome": 7, "amount": null }
1176 })
1177 );
1178 }
1179
1180 #[rstest]
1181 fn test_user_outcome_merge_question_serialization() {
1182 let action = HyperliquidExecAction::UserOutcome {
1183 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1184 question: 9,
1185 amount: Some(dec!(2.0)),
1186 }),
1187 };
1188 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1189 assert_eq!(
1190 value,
1191 json!({
1192 "type": "userOutcome",
1193 "mergeQuestion": { "question": 9, "amount": "2.0" }
1194 })
1195 );
1196 }
1197
1198 #[rstest]
1199 fn test_user_outcome_merge_question_null_amount_means_max() {
1200 let action = HyperliquidExecAction::UserOutcome {
1201 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1202 question: 9,
1203 amount: None,
1204 }),
1205 };
1206 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1207 assert_eq!(
1208 value,
1209 json!({
1210 "type": "userOutcome",
1211 "mergeQuestion": { "question": 9, "amount": null }
1212 })
1213 );
1214 }
1215
1216 #[rstest]
1217 fn test_user_outcome_negate_outcome_serialization() {
1218 let action = HyperliquidExecAction::UserOutcome {
1219 op: HyperliquidExecUserOutcomeOp::NegateOutcome(HyperliquidExecNegateOutcomeParams {
1220 question: 9,
1221 outcome: 52,
1222 amount: dec!(1.5),
1223 }),
1224 };
1225 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1226 assert_eq!(
1227 value,
1228 json!({
1229 "type": "userOutcome",
1230 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1231 })
1232 );
1233 }
1234}
1235
1236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1240pub enum HyperliquidExecTif {
1241 #[serde(rename = "Alo")]
1243 Alo,
1244 #[serde(rename = "Ioc")]
1246 Ioc,
1247 #[serde(rename = "Gtc")]
1249 Gtc,
1250}
1251
1252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1254pub enum HyperliquidExecTpSl {
1255 #[serde(rename = "tp")]
1257 Tp,
1258 #[serde(rename = "sl")]
1260 Sl,
1261}
1262
1263#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1265pub enum HyperliquidExecGrouping {
1266 #[serde(rename = "na")]
1268 #[default]
1269 Na,
1270 #[serde(rename = "normalTpsl")]
1272 NormalTpsl,
1273 #[serde(rename = "positionTpsl")]
1275 PositionTpsl,
1276}
1277
1278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1280#[serde(untagged)]
1281pub enum HyperliquidExecOrderKind {
1282 Limit {
1284 limit: HyperliquidExecLimitParams,
1286 },
1287 Trigger {
1289 trigger: HyperliquidExecTriggerParams,
1291 },
1292}
1293
1294#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1296pub struct HyperliquidExecLimitParams {
1297 pub tif: HyperliquidExecTif,
1299}
1300
1301#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1303#[serde(rename_all = "camelCase")]
1304pub struct HyperliquidExecTriggerParams {
1305 pub is_market: bool,
1307 #[serde(
1309 serialize_with = "serialize_decimal_as_str",
1310 deserialize_with = "deserialize_decimal_from_str"
1311 )]
1312 pub trigger_px: Decimal,
1313 pub tpsl: HyperliquidExecTpSl,
1315}
1316
1317#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1322pub struct HyperliquidExecBuilderFee {
1323 #[serde(rename = "b")]
1325 pub address: String,
1326 #[serde(rename = "f")]
1328 pub fee_tenths_bp: u32,
1329}
1330
1331#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1336pub struct HyperliquidExecPlaceOrderRequest {
1337 #[serde(rename = "a")]
1339 pub asset: AssetId,
1340 #[serde(rename = "b")]
1342 pub is_buy: bool,
1343 #[serde(
1345 rename = "p",
1346 serialize_with = "serialize_decimal_as_str",
1347 deserialize_with = "deserialize_decimal_from_str"
1348 )]
1349 pub price: Decimal,
1350 #[serde(
1352 rename = "s",
1353 serialize_with = "serialize_decimal_as_str",
1354 deserialize_with = "deserialize_decimal_from_str"
1355 )]
1356 pub size: Decimal,
1357 #[serde(rename = "r")]
1359 pub reduce_only: bool,
1360 #[serde(rename = "t")]
1362 pub kind: HyperliquidExecOrderKind,
1363 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1365 pub cloid: Option<Cloid>,
1366}
1367
1368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1370pub struct HyperliquidExecCancelOrderRequest {
1371 #[serde(rename = "a")]
1373 pub asset: AssetId,
1374 #[serde(rename = "o")]
1376 pub oid: OrderId,
1377}
1378
1379#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1384pub struct HyperliquidExecCancelByCloidRequest {
1385 pub asset: AssetId,
1387 pub cloid: Cloid,
1389}
1390
1391#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1396pub struct HyperliquidExecModifyOrderRequest {
1397 pub oid: OrderId,
1399 pub order: HyperliquidExecPlaceOrderRequest,
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1408pub struct HyperliquidExecSplitOutcomeParams {
1409 pub outcome: u32,
1411 #[serde(
1413 serialize_with = "serialize_decimal_as_str",
1414 deserialize_with = "deserialize_decimal_from_str"
1415 )]
1416 pub amount: Decimal,
1417}
1418
1419#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1425pub struct HyperliquidExecMergeOutcomeParams {
1426 pub outcome: u32,
1428 #[serde(
1430 default,
1431 serialize_with = "serialize_optional_decimal_as_str",
1432 deserialize_with = "deserialize_optional_decimal_from_str"
1433 )]
1434 pub amount: Option<Decimal>,
1435}
1436
1437#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1443pub struct HyperliquidExecMergeQuestionParams {
1444 pub question: u32,
1446 #[serde(
1448 default,
1449 serialize_with = "serialize_optional_decimal_as_str",
1450 deserialize_with = "deserialize_optional_decimal_from_str"
1451 )]
1452 pub amount: Option<Decimal>,
1453}
1454
1455#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1460pub struct HyperliquidExecNegateOutcomeParams {
1461 pub question: u32,
1463 pub outcome: u32,
1465 #[serde(
1467 serialize_with = "serialize_decimal_as_str",
1468 deserialize_with = "deserialize_decimal_from_str"
1469 )]
1470 pub amount: Decimal,
1471}
1472
1473#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1480pub enum HyperliquidExecUserOutcomeOp {
1481 #[serde(rename = "splitOutcome")]
1483 SplitOutcome(HyperliquidExecSplitOutcomeParams),
1484 #[serde(rename = "mergeOutcome")]
1487 MergeOutcome(HyperliquidExecMergeOutcomeParams),
1488 #[serde(rename = "mergeQuestion")]
1491 MergeQuestion(HyperliquidExecMergeQuestionParams),
1492 #[serde(rename = "negateOutcome")]
1495 NegateOutcome(HyperliquidExecNegateOutcomeParams),
1496}
1497
1498#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1500pub struct HyperliquidExecTwapRequest {
1501 #[serde(rename = "a")]
1503 pub asset: AssetId,
1504 #[serde(rename = "b")]
1506 pub is_buy: bool,
1507 #[serde(
1509 rename = "s",
1510 serialize_with = "serialize_decimal_as_str",
1511 deserialize_with = "deserialize_decimal_from_str"
1512 )]
1513 pub size: Decimal,
1514 #[serde(rename = "m")]
1516 pub duration_ms: u64,
1517}
1518
1519#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1525#[serde(tag = "type")]
1526pub enum HyperliquidExecAction {
1527 #[serde(rename = "order")]
1529 Order {
1530 orders: Vec<HyperliquidExecPlaceOrderRequest>,
1532 #[serde(default)]
1534 grouping: HyperliquidExecGrouping,
1535 #[serde(skip_serializing_if = "Option::is_none")]
1537 builder: Option<HyperliquidExecBuilderFee>,
1538 },
1539
1540 #[serde(rename = "cancel")]
1542 Cancel {
1543 cancels: Vec<HyperliquidExecCancelOrderRequest>,
1545 },
1546
1547 #[serde(rename = "cancelByCloid")]
1549 CancelByCloid {
1550 cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1552 },
1553
1554 #[serde(rename = "modify")]
1556 Modify {
1557 #[serde(flatten)]
1559 modify: HyperliquidExecModifyOrderRequest,
1560 },
1561
1562 #[serde(rename = "batchModify")]
1564 BatchModify {
1565 modifies: Vec<HyperliquidExecModifyOrderRequest>,
1567 },
1568
1569 #[serde(rename = "scheduleCancel")]
1571 ScheduleCancel {
1572 #[serde(skip_serializing_if = "Option::is_none")]
1575 time: Option<u64>,
1576 },
1577
1578 #[serde(rename = "updateLeverage")]
1580 UpdateLeverage {
1581 #[serde(rename = "a")]
1583 asset: AssetId,
1584 #[serde(rename = "isCross")]
1586 is_cross: bool,
1587 #[serde(rename = "leverage")]
1589 leverage: u32,
1590 },
1591
1592 #[serde(rename = "updateIsolatedMargin")]
1594 UpdateIsolatedMargin {
1595 #[serde(rename = "a")]
1597 asset: AssetId,
1598 #[serde(
1600 rename = "delta",
1601 serialize_with = "serialize_decimal_as_str",
1602 deserialize_with = "deserialize_decimal_from_str"
1603 )]
1604 delta: Decimal,
1605 },
1606
1607 #[serde(rename = "usdClassTransfer")]
1609 UsdClassTransfer {
1610 from: String,
1612 to: String,
1614 #[serde(
1616 serialize_with = "serialize_decimal_as_str",
1617 deserialize_with = "deserialize_decimal_from_str"
1618 )]
1619 amount: Decimal,
1620 },
1621
1622 #[serde(rename = "userOutcome")]
1628 UserOutcome {
1629 #[serde(flatten)]
1631 op: HyperliquidExecUserOutcomeOp,
1632 },
1633
1634 #[serde(rename = "twapPlace")]
1636 TwapPlace {
1637 #[serde(flatten)]
1639 twap: HyperliquidExecTwapRequest,
1640 },
1641
1642 #[serde(rename = "twapCancel")]
1644 TwapCancel {
1645 #[serde(rename = "a")]
1647 asset: AssetId,
1648 #[serde(rename = "t")]
1650 twap_id: u64,
1651 },
1652
1653 #[serde(rename = "noop")]
1655 Noop,
1656}
1657
1658#[derive(Debug, Clone, Serialize)]
1663#[serde(rename_all = "camelCase")]
1664pub struct HyperliquidExecRequest {
1665 pub action: HyperliquidExecAction,
1667 pub nonce: u64,
1669 pub signature: String,
1671 #[serde(skip_serializing_if = "Option::is_none")]
1673 pub vault_address: Option<String>,
1674 #[serde(skip_serializing_if = "Option::is_none")]
1677 pub expires_after: Option<u64>,
1678}
1679
1680#[derive(Debug, Clone, Serialize, Deserialize)]
1682pub struct HyperliquidExecResponse {
1683 pub status: String,
1685 pub response: HyperliquidExecResponseData,
1687}
1688
1689#[derive(Debug, Clone, Serialize, Deserialize)]
1691#[serde(tag = "type")]
1692pub enum HyperliquidExecResponseData {
1693 #[serde(rename = "order")]
1695 Order {
1696 data: HyperliquidExecOrderResponseData,
1698 },
1699 #[serde(rename = "cancel")]
1701 Cancel {
1702 data: HyperliquidExecCancelResponseData,
1704 },
1705 #[serde(rename = "modify")]
1707 Modify {
1708 data: HyperliquidExecModifyResponseData,
1710 },
1711 #[serde(rename = "default")]
1713 Default,
1714 #[serde(other)]
1716 Unknown,
1717}
1718
1719#[derive(Debug, Clone, Serialize, Deserialize)]
1721pub struct HyperliquidExecOrderResponseData {
1722 pub statuses: Vec<HyperliquidExecOrderStatus>,
1724}
1725
1726#[derive(Debug, Clone, Serialize, Deserialize)]
1728pub struct HyperliquidExecCancelResponseData {
1729 pub statuses: Vec<HyperliquidExecCancelStatus>,
1731}
1732
1733#[derive(Debug, Clone, Serialize, Deserialize)]
1735pub struct HyperliquidExecModifyResponseData {
1736 pub statuses: Vec<HyperliquidExecModifyStatus>,
1738}
1739
1740#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1742#[serde(untagged)]
1743pub enum HyperliquidExecOrderStatus {
1744 Resting {
1746 resting: HyperliquidExecRestingInfo,
1748 },
1749 Filled {
1751 filled: HyperliquidExecFilledInfo,
1753 },
1754 Error {
1756 error: String,
1758 },
1759 Tag(HyperliquidExecOrderStatusTag),
1763}
1764
1765#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1771pub enum HyperliquidExecOrderStatusTag {
1772 #[serde(rename = "waitingForFill")]
1774 WaitingForFill,
1775 #[serde(rename = "waitingForTrigger")]
1777 WaitingForTrigger,
1778}
1779
1780#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1782pub struct HyperliquidExecRestingInfo {
1783 pub oid: OrderId,
1785}
1786
1787#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1789pub struct HyperliquidExecFilledInfo {
1790 #[serde(
1792 rename = "totalSz",
1793 serialize_with = "serialize_decimal_as_str",
1794 deserialize_with = "deserialize_decimal_from_str"
1795 )]
1796 pub total_sz: Decimal,
1797 #[serde(
1799 rename = "avgPx",
1800 serialize_with = "serialize_decimal_as_str",
1801 deserialize_with = "deserialize_decimal_from_str"
1802 )]
1803 pub avg_px: Decimal,
1804 pub oid: OrderId,
1806}
1807
1808#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1810#[serde(untagged)]
1811pub enum HyperliquidExecCancelStatus {
1812 Success(String), Error {
1816 error: String,
1818 },
1819}
1820
1821#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1823#[serde(untagged)]
1824pub enum HyperliquidExecModifyStatus {
1825 Success(String), Error {
1829 error: String,
1831 },
1832}
1833
1834#[derive(Debug, Clone, Serialize, Deserialize)]
1837#[serde(rename_all = "camelCase")]
1838pub struct ClearinghouseState {
1839 #[serde(default)]
1841 pub asset_positions: Vec<AssetPosition>,
1842 #[serde(default)]
1844 pub cross_margin_summary: Option<CrossMarginSummary>,
1845 #[serde(
1847 default,
1848 serialize_with = "serialize_optional_decimal_as_str",
1849 deserialize_with = "deserialize_optional_decimal_from_str"
1850 )]
1851 pub withdrawable: Option<Decimal>,
1852 #[serde(default)]
1854 pub time: Option<u64>,
1855}
1856
1857#[derive(Debug, Clone, Serialize, Deserialize)]
1859#[serde(rename_all = "camelCase")]
1860pub struct AssetPosition {
1861 pub position: PositionData,
1863 #[serde(rename = "type")]
1865 pub position_type: HyperliquidPositionType,
1866}
1867
1868#[derive(Debug, Clone, Serialize, Deserialize)]
1870#[serde(rename_all = "camelCase")]
1871pub struct LeverageInfo {
1872 #[serde(rename = "type")]
1873 pub leverage_type: HyperliquidLeverageType,
1874 pub value: u32,
1876}
1877
1878#[derive(Debug, Clone, Serialize, Deserialize)]
1880#[serde(rename_all = "camelCase")]
1881pub struct CumFundingInfo {
1882 #[serde(
1884 rename = "allTime",
1885 serialize_with = "serialize_decimal_as_str",
1886 deserialize_with = "deserialize_decimal_from_str"
1887 )]
1888 pub all_time: Decimal,
1889 #[serde(
1891 rename = "sinceOpen",
1892 serialize_with = "serialize_decimal_as_str",
1893 deserialize_with = "deserialize_decimal_from_str"
1894 )]
1895 pub since_open: Decimal,
1896 #[serde(
1898 rename = "sinceChange",
1899 serialize_with = "serialize_decimal_as_str",
1900 deserialize_with = "deserialize_decimal_from_str"
1901 )]
1902 pub since_change: Decimal,
1903}
1904
1905#[derive(Debug, Clone, Serialize, Deserialize)]
1907#[serde(rename_all = "camelCase")]
1908pub struct PositionData {
1909 pub coin: Ustr,
1911 #[serde(rename = "cumFunding")]
1913 pub cum_funding: CumFundingInfo,
1914 #[serde(
1916 rename = "entryPx",
1917 serialize_with = "serialize_optional_decimal_as_str",
1918 deserialize_with = "deserialize_optional_decimal_from_str",
1919 default
1920 )]
1921 pub entry_px: Option<Decimal>,
1922 pub leverage: LeverageInfo,
1924 #[serde(
1926 rename = "liquidationPx",
1927 serialize_with = "serialize_optional_decimal_as_str",
1928 deserialize_with = "deserialize_optional_decimal_from_str",
1929 default
1930 )]
1931 pub liquidation_px: Option<Decimal>,
1932 #[serde(
1934 rename = "marginUsed",
1935 serialize_with = "serialize_decimal_as_str",
1936 deserialize_with = "deserialize_decimal_from_str"
1937 )]
1938 pub margin_used: Decimal,
1939 #[serde(rename = "maxLeverage", default)]
1941 pub max_leverage: Option<u32>,
1942 #[serde(
1944 rename = "positionValue",
1945 serialize_with = "serialize_decimal_as_str",
1946 deserialize_with = "deserialize_decimal_from_str"
1947 )]
1948 pub position_value: Decimal,
1949 #[serde(
1951 rename = "returnOnEquity",
1952 serialize_with = "serialize_decimal_as_str",
1953 deserialize_with = "deserialize_decimal_from_str"
1954 )]
1955 pub return_on_equity: Decimal,
1956 #[serde(
1958 rename = "szi",
1959 serialize_with = "serialize_decimal_as_str",
1960 deserialize_with = "deserialize_decimal_from_str"
1961 )]
1962 pub szi: Decimal,
1963 #[serde(
1965 rename = "unrealizedPnl",
1966 serialize_with = "serialize_decimal_as_str",
1967 deserialize_with = "deserialize_decimal_from_str"
1968 )]
1969 pub unrealized_pnl: Decimal,
1970}
1971
1972#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1978#[serde(rename_all = "camelCase")]
1979pub struct SpotClearinghouseState {
1980 #[serde(default)]
1982 pub balances: Vec<SpotBalance>,
1983}
1984
1985#[derive(Debug, Clone, Serialize, Deserialize)]
1987#[serde(rename_all = "camelCase")]
1988pub struct SpotBalance {
1989 pub coin: Ustr,
1991 #[serde(default)]
1994 pub token: Option<u32>,
1995 #[serde(
1997 serialize_with = "serialize_decimal_as_str",
1998 deserialize_with = "deserialize_decimal_from_str"
1999 )]
2000 pub total: Decimal,
2001 #[serde(
2003 serialize_with = "serialize_decimal_as_str",
2004 deserialize_with = "deserialize_decimal_from_str"
2005 )]
2006 pub hold: Decimal,
2007 #[serde(
2009 default,
2010 serialize_with = "serialize_optional_decimal_as_str",
2011 deserialize_with = "deserialize_optional_decimal_from_str"
2012 )]
2013 pub entry_ntl: Option<Decimal>,
2014}
2015
2016impl SpotBalance {
2017 #[must_use]
2019 pub fn free(&self) -> Decimal {
2020 (self.total - self.hold).max(Decimal::ZERO)
2021 }
2022
2023 #[must_use]
2025 pub fn avg_entry_px(&self) -> Option<Decimal> {
2026 let entry_ntl = self.entry_ntl?;
2027
2028 if entry_ntl.is_zero() || self.total.is_zero() {
2029 return None;
2030 }
2031
2032 Some(entry_ntl / self.total)
2033 }
2034}
2035
2036#[derive(Debug, Clone, Serialize, Deserialize)]
2038#[serde(rename_all = "camelCase")]
2039pub struct CrossMarginSummary {
2040 #[serde(
2042 rename = "accountValue",
2043 serialize_with = "serialize_decimal_as_str",
2044 deserialize_with = "deserialize_decimal_from_str"
2045 )]
2046 pub account_value: Decimal,
2047 #[serde(
2049 rename = "totalNtlPos",
2050 serialize_with = "serialize_decimal_as_str",
2051 deserialize_with = "deserialize_decimal_from_str"
2052 )]
2053 pub total_ntl_pos: Decimal,
2054 #[serde(
2056 rename = "totalRawUsd",
2057 serialize_with = "serialize_decimal_as_str",
2058 deserialize_with = "deserialize_decimal_from_str"
2059 )]
2060 pub total_raw_usd: Decimal,
2061 #[serde(
2063 rename = "totalMarginUsed",
2064 serialize_with = "serialize_decimal_as_str",
2065 deserialize_with = "deserialize_decimal_from_str"
2066 )]
2067 pub total_margin_used: Decimal,
2068 #[serde(
2070 rename = "withdrawable",
2071 default,
2072 serialize_with = "serialize_optional_decimal_as_str",
2073 deserialize_with = "deserialize_optional_decimal_from_str"
2074 )]
2075 pub withdrawable: Option<Decimal>,
2076}