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