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(rename = "feeToken")]
621 pub fee_token: Ustr,
622}
623
624#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(tag = "status", rename_all = "camelCase")]
630pub enum HyperliquidOrderStatus {
631 Order { order: HyperliquidOrderStatusEntry },
632 UnknownOid,
633}
634
635impl HyperliquidOrderStatus {
636 #[must_use]
638 pub fn into_order(self) -> Option<HyperliquidOrderStatusEntry> {
639 match self {
640 Self::Order { order } => Some(order),
641 Self::UnknownOid => None,
642 }
643 }
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize)]
648pub struct HyperliquidOrderStatusEntry {
649 pub order: HyperliquidOrderInfo,
651 pub status: HyperliquidOrderStatusEnum,
653 #[serde(rename = "statusTimestamp")]
655 pub status_timestamp: u64,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct HyperliquidOrderInfo {
661 pub coin: Ustr,
663 pub side: HyperliquidSide,
665 #[serde(
667 rename = "limitPx",
668 serialize_with = "serialize_decimal_as_str",
669 deserialize_with = "deserialize_decimal_from_str"
670 )]
671 pub limit_px: Decimal,
672 #[serde(
674 serialize_with = "serialize_decimal_as_str",
675 deserialize_with = "deserialize_decimal_from_str"
676 )]
677 pub sz: Decimal,
678 pub oid: u64,
680 pub timestamp: u64,
682 #[serde(
684 rename = "origSz",
685 serialize_with = "serialize_decimal_as_str",
686 deserialize_with = "deserialize_decimal_from_str"
687 )]
688 pub orig_sz: Decimal,
689 #[serde(default)]
691 pub cloid: Option<String>,
692 #[serde(default)]
694 pub tif: Option<HyperliquidTimeInForce>,
695 #[serde(rename = "reduceOnly", default)]
697 pub reduce_only: Option<bool>,
698 #[serde(
700 rename = "triggerPx",
701 default,
702 deserialize_with = "deserialize_optional_decimal_from_str"
703 )]
704 pub trigger_px: Option<Decimal>,
705 #[serde(rename = "orderType", default)]
707 pub order_type: Option<String>,
708}
709
710#[derive(Debug, Clone, Serialize)]
712pub struct HyperliquidSignature {
713 pub r: String,
715 pub s: String,
717 pub v: u64,
719}
720
721impl HyperliquidSignature {
722 #[must_use]
724 pub fn new(r: String, s: String, v: u64) -> Self {
725 Self { r, s, v }
726 }
727
728 #[must_use]
730 pub fn to_hex(&self) -> String {
731 let r = self.r.strip_prefix("0x").unwrap_or(&self.r);
732 let s = self.s.strip_prefix("0x").unwrap_or(&self.s);
733 format!("0x{r}{s}{:02x}", self.v)
734 }
735
736 pub fn from_hex(sig_hex: &str) -> Result<Self, String> {
738 let sig_hex = sig_hex.strip_prefix("0x").unwrap_or(sig_hex);
739
740 if sig_hex.len() != 130 {
741 return Err(format!(
742 "Invalid signature length: expected 130 hex chars, was {}",
743 sig_hex.len()
744 ));
745 }
746
747 let r = format!("0x{}", &sig_hex[0..64]);
748 let s = format!("0x{}", &sig_hex[64..128]);
749 let v = u64::from_str_radix(&sig_hex[128..130], 16)
750 .map_err(|e| format!("Failed to parse v component: {e}"))?;
751
752 Ok(Self { r, s, v })
753 }
754}
755
756#[derive(Debug, Clone, Serialize)]
758pub struct HyperliquidExchangeRequest<T> {
759 #[serde(rename = "action")]
761 pub action: T,
762 #[serde(rename = "nonce")]
764 pub nonce: u64,
765 #[serde(rename = "signature")]
767 pub signature: HyperliquidSignature,
768 #[serde(rename = "vaultAddress", skip_serializing_if = "Option::is_none")]
770 pub vault_address: Option<String>,
771 #[serde(rename = "expiresAfter", skip_serializing_if = "Option::is_none")]
773 pub expires_after: Option<u64>,
774}
775
776impl<T> HyperliquidExchangeRequest<T>
777where
778 T: Serialize,
779{
780 #[must_use]
782 pub fn new(action: T, nonce: u64, signature: HyperliquidSignature) -> Self {
783 Self {
784 action,
785 nonce,
786 signature,
787 vault_address: None,
788 expires_after: None,
789 }
790 }
791
792 #[must_use]
794 pub fn with_vault(
795 action: T,
796 nonce: u64,
797 signature: HyperliquidSignature,
798 vault_address: String,
799 ) -> Self {
800 Self {
801 action,
802 nonce,
803 signature,
804 vault_address: Some(vault_address),
805 expires_after: None,
806 }
807 }
808
809 pub fn to_sign_value(&self) -> serde_json::Result<serde_json::Value> {
811 serde_json::to_value(self)
812 }
813}
814
815#[derive(Debug, Clone, Serialize, Deserialize)]
817#[serde(untagged)]
818pub enum HyperliquidExchangeResponse {
819 Status {
821 status: String,
823 response: serde_json::Value,
825 },
826 Error {
828 error: String,
830 },
831}
832
833impl HyperliquidExchangeResponse {
834 pub fn is_ok(&self) -> bool {
835 matches!(self, Self::Status { status, .. } if status == RESPONSE_STATUS_OK)
836 }
837}
838
839pub const RESPONSE_STATUS_OK: &str = "ok";
841
842#[cfg(test)]
843mod tests {
844 use rstest::rstest;
845 use rust_decimal_macros::dec;
846 use serde_json::json;
847
848 use super::*;
849
850 #[rstest]
851 fn test_meta_deserialization() {
852 let json = r#"{"universe": [{"name": "BTC", "szDecimals": 5}]}"#;
853
854 let meta: HyperliquidMeta = serde_json::from_str(json).unwrap();
855
856 assert_eq!(meta.universe.len(), 1);
857 assert_eq!(meta.universe[0].name, "BTC");
858 assert_eq!(meta.universe[0].sz_decimals, 5);
859 }
860
861 #[rstest]
862 fn test_funding_history_entry_with_premium() {
863 let json = r#"{
864 "coin": "BTC",
865 "fundingRate": "0.0000125",
866 "premium": "0.00029005",
867 "time": 1769908800000
868 }"#;
869
870 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
871
872 assert_eq!(entry.coin.as_str(), "BTC");
873 assert_eq!(entry.funding_rate, dec!(0.0000125));
874 assert_eq!(entry.premium, Some(dec!(0.00029005)));
875 assert_eq!(entry.time, 1769908800000);
876 }
877
878 #[rstest]
879 fn test_funding_history_entry_without_premium() {
880 let json = r#"{
883 "coin": "BTC",
884 "fundingRate": "0.0000033",
885 "time": 1769916000000
886 }"#;
887
888 let entry: HyperliquidFundingHistoryEntry = serde_json::from_str(json).unwrap();
889
890 assert!(entry.premium.is_none());
891 assert_eq!(entry.funding_rate, dec!(0.0000033));
892 }
893
894 #[rstest]
895 fn test_recent_trade_deserializes() {
896 let json = r#"{
898 "coin": "BTC",
899 "side": "B",
900 "px": "104250.0",
901 "sz": "0.0123",
902 "hash": "0xabc",
903 "time": 1769916000000,
904 "tid": 987654321,
905 "users": ["0xbuyer", "0xseller"]
906 }"#;
907
908 let trade: HyperliquidRecentTrade = serde_json::from_str(json).unwrap();
909
910 assert_eq!(trade.coin.as_str(), "BTC");
911 assert_eq!(trade.side, HyperliquidSide::Buy);
912 assert_eq!(trade.px, dec!(104250.0));
913 assert_eq!(trade.sz, dec!(0.0123));
914 assert_eq!(trade.time, 1769916000000);
915 assert_eq!(trade.tid, 987654321);
916 }
917
918 #[rstest]
919 fn test_perp_asset_hip3_fields() {
920 let json = r#"{
921 "name": "xyz:TSLA",
922 "szDecimals": 3,
923 "maxLeverage": 10,
924 "onlyIsolated": true,
925 "growthMode": "enabled",
926 "marginMode": "strictIsolated"
927 }"#;
928
929 let asset: PerpAsset = serde_json::from_str(json).unwrap();
930
931 assert_eq!(asset.name, "xyz:TSLA");
932 assert_eq!(asset.sz_decimals, 3);
933 assert_eq!(asset.max_leverage, Some(10));
934 assert_eq!(asset.only_isolated, Some(true));
935 assert_eq!(asset.growth_mode.as_deref(), Some("enabled"));
936 assert_eq!(asset.margin_mode.as_deref(), Some("strictIsolated"));
937 }
938
939 #[rstest]
940 fn test_perp_asset_hip3_fields_absent() {
941 let json = r#"{"name": "BTC", "szDecimals": 5}"#;
942
943 let asset: PerpAsset = serde_json::from_str(json).unwrap();
944
945 assert_eq!(asset.growth_mode, None);
946 assert_eq!(asset.margin_mode, None);
947 }
948
949 #[rstest]
950 fn test_outcome_meta_defaults_missing_side_specs() {
951 let json = r#"{
952 "outcomes": [
953 {
954 "outcome": 123,
955 "name": "Recurring",
956 "description": "class:priceBinary|underlying:HYPE|expiry:20260310-1100|targetPrice:34.5|period:3m"
957 }
958 ]
959 }"#;
960
961 let meta: OutcomeMeta = serde_json::from_str(json).unwrap();
962
963 assert_eq!(meta.outcomes.len(), 1);
964 assert_eq!(meta.outcomes[0].outcome, 123);
965 assert!(meta.outcomes[0].side_specs.is_empty());
966 }
967
968 #[rstest]
969 fn test_l2_book_deserialization() {
970 let json = r#"{"coin": "BTC", "levels": [[{"px": "50000", "sz": "1.5"}], [{"px": "50100", "sz": "2.0"}]], "time": 1234567890}"#;
971
972 let book: HyperliquidL2Book = serde_json::from_str(json).unwrap();
973
974 assert_eq!(book.coin, "BTC");
975 assert_eq!(book.levels.len(), 2);
976 assert_eq!(book.time, 1234567890);
977 }
978
979 #[rstest]
980 fn test_exchange_response_deserialization() {
981 let json = r#"{"status": "ok", "response": {"type": "order"}}"#;
982
983 let response: HyperliquidExchangeResponse = serde_json::from_str(json).unwrap();
984 assert!(response.is_ok());
985 }
986
987 #[rstest]
988 fn test_spot_clearinghouse_state_deserialization() {
989 let json = r#"{
990 "balances": [
991 {"coin": "USDC", "token": 0, "total": "14.625485", "hold": "0.0", "entryNtl": "0.0"},
992 {"coin": "PURR", "token": 1, "total": "2000", "hold": "100", "entryNtl": "1234.56"}
993 ]
994 }"#;
995
996 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
997
998 assert_eq!(state.balances.len(), 2);
999 let usdc = &state.balances[0];
1000 assert_eq!(usdc.coin.as_str(), "USDC");
1001 assert_eq!(usdc.token, Some(0));
1002 assert_eq!(usdc.total.to_string(), "14.625485");
1003 assert_eq!(usdc.hold, rust_decimal::Decimal::ZERO);
1004 assert_eq!(usdc.free().to_string(), "14.625485");
1005 assert_eq!(usdc.avg_entry_px(), None);
1006
1007 let purr = &state.balances[1];
1008 assert_eq!(purr.coin.as_str(), "PURR");
1009 assert_eq!(purr.token, Some(1));
1010 assert_eq!(purr.free().to_string(), "1900");
1011 assert_eq!(
1012 purr.avg_entry_px().unwrap(),
1013 rust_decimal_macros::dec!(0.61728)
1014 );
1015 }
1016
1017 #[rstest]
1018 fn test_spot_balance_outcome_side_token_lacks_token_field() {
1019 let json = r#"{"coin": "+250", "total": "0.0", "hold": "0.0", "entryNtl": "0.0"}"#;
1021 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1022 assert_eq!(balance.coin.as_str(), "+250");
1023 assert_eq!(balance.token, None);
1024 }
1025
1026 #[rstest]
1027 fn test_spot_clearinghouse_state_empty() {
1028 let json = r#"{"balances": []}"#;
1029 let state: SpotClearinghouseState = serde_json::from_str(json).unwrap();
1030 assert!(state.balances.is_empty());
1031 }
1032
1033 #[rstest]
1034 fn test_spot_balance_handles_missing_entry_ntl() {
1035 let json = r#"{"coin": "HYPE", "token": 150, "total": "5", "hold": "0"}"#;
1036 let balance: SpotBalance = serde_json::from_str(json).unwrap();
1037 assert_eq!(balance.entry_ntl, None);
1038 assert_eq!(balance.avg_entry_px(), None);
1039 }
1040
1041 #[rstest]
1042 fn test_msgpack_serialization_matches_python() {
1043 let action = HyperliquidExecAction::Order {
1048 orders: vec![],
1049 grouping: HyperliquidExecGrouping::Na,
1050 builder: None,
1051 };
1052
1053 let json = serde_json::to_string(&action).unwrap();
1055 assert!(
1056 json.contains(r#""type":"order""#),
1057 "JSON should have type tag: {json}"
1058 );
1059
1060 let msgpack_bytes = rmp_serde::to_vec_named(&action).unwrap();
1062
1063 let decoded: serde_json::Value = rmp_serde::from_slice(&msgpack_bytes).unwrap();
1065
1066 assert!(
1068 decoded.get("type").is_some(),
1069 "MsgPack should have type tag. Decoded: {decoded:?}"
1070 );
1071 assert_eq!(
1072 decoded.get("type").unwrap().as_str().unwrap(),
1073 "order",
1074 "Type should be 'order'"
1075 );
1076 assert!(decoded.get("orders").is_some(), "Should have orders field");
1077 assert!(
1078 decoded.get("grouping").is_some(),
1079 "Should have grouping field"
1080 );
1081 }
1082
1083 #[rstest]
1084 fn test_cancel_action_serializes_fast_flag() {
1085 let action = HyperliquidExecAction::Cancel {
1086 cancels: vec![HyperliquidExecCancelOrderRequest {
1087 asset: 0,
1088 oid: 12345,
1089 }],
1090 fast: Some(true),
1091 };
1092
1093 let value = serde_json::to_value(action).unwrap();
1094
1095 assert_eq!(
1096 value,
1097 json!({
1098 "type": "cancel",
1099 "cancels": [{"a": 0, "o": 12345}],
1100 "f": true,
1101 })
1102 );
1103 }
1104
1105 #[rstest]
1106 fn test_cancel_by_cloid_action_serializes_fast_flag() {
1107 let action = HyperliquidExecAction::CancelByCloid {
1108 cancels: vec![HyperliquidExecCancelByCloidRequest {
1109 asset: 0,
1110 cloid: Cloid::from_hex("0x00000000000000000000000000000000").unwrap(),
1111 }],
1112 fast: Some(true),
1113 };
1114
1115 let value = serde_json::to_value(action).unwrap();
1116
1117 assert_eq!(
1118 value,
1119 json!({
1120 "type": "cancelByCloid",
1121 "cancels": [{
1122 "asset": 0,
1123 "cloid": "0x00000000000000000000000000000000",
1124 }],
1125 "f": true,
1126 })
1127 );
1128 }
1129
1130 #[rstest]
1131 fn test_order_response_normal_tpsl_with_waiting_children() {
1132 let json = r#"{
1136 "statuses": [
1137 {"resting": {"oid": 446050656712}},
1138 "waitingForFill",
1139 "waitingForTrigger"
1140 ]
1141 }"#;
1142
1143 let data: HyperliquidExecOrderResponseData = serde_json::from_str(json).unwrap();
1144 assert_eq!(data.statuses.len(), 3);
1145
1146 assert!(matches!(
1147 data.statuses[0],
1148 HyperliquidExecOrderStatus::Resting { ref resting } if resting.oid == 446050656712
1149 ));
1150 assert!(matches!(
1151 data.statuses[1],
1152 HyperliquidExecOrderStatus::Tag(HyperliquidExecOrderStatusTag::WaitingForFill)
1153 ));
1154 assert!(matches!(
1155 data.statuses[2],
1156 HyperliquidExecOrderStatus::Tag(HyperliquidExecOrderStatusTag::WaitingForTrigger)
1157 ));
1158 }
1159
1160 #[rstest]
1161 fn test_user_outcome_split_serialization() {
1162 let action = HyperliquidExecAction::UserOutcome {
1163 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
1164 outcome: 1,
1165 amount: dec!(123.0),
1166 }),
1167 };
1168
1169 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1170 assert_eq!(
1171 value,
1172 json!({
1173 "type": "userOutcome",
1174 "splitOutcome": { "outcome": 1, "amount": "123.0" }
1175 })
1176 );
1177 }
1178
1179 #[rstest]
1180 fn test_user_outcome_split_msgpack_roundtrip() {
1181 let action = HyperliquidExecAction::UserOutcome {
1182 op: HyperliquidExecUserOutcomeOp::SplitOutcome(HyperliquidExecSplitOutcomeParams {
1183 outcome: 4,
1184 amount: dec!(10),
1185 }),
1186 };
1187
1188 let bytes = rmp_serde::to_vec_named(&action).unwrap();
1189 let decoded: serde_json::Value = rmp_serde::from_slice(&bytes).unwrap();
1190 assert_eq!(
1191 decoded,
1192 json!({
1193 "type": "userOutcome",
1194 "splitOutcome": { "outcome": 4, "amount": "10" }
1195 })
1196 );
1197 }
1198
1199 #[rstest]
1200 fn test_hyperliquid_level_serializes_decimals_as_strings() {
1201 let level = HyperliquidLevel {
1204 px: dec!(98450.5),
1205 sz: dec!(2.5),
1206 };
1207 let value = serde_json::to_value(&level).unwrap();
1208 assert_eq!(value, json!({ "px": "98450.5", "sz": "2.5" }));
1209 }
1210
1211 #[rstest]
1212 fn test_user_outcome_merge_outcome_serialization() {
1213 let action = HyperliquidExecAction::UserOutcome {
1214 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
1215 outcome: 1,
1216 amount: Some(dec!(5.0)),
1217 }),
1218 };
1219 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1220 assert_eq!(
1221 value,
1222 json!({
1223 "type": "userOutcome",
1224 "mergeOutcome": { "outcome": 1, "amount": "5.0" }
1225 })
1226 );
1227 }
1228
1229 #[rstest]
1230 fn test_user_outcome_merge_outcome_null_amount_means_max() {
1231 let action = HyperliquidExecAction::UserOutcome {
1232 op: HyperliquidExecUserOutcomeOp::MergeOutcome(HyperliquidExecMergeOutcomeParams {
1233 outcome: 7,
1234 amount: None,
1235 }),
1236 };
1237 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1238 assert_eq!(
1239 value,
1240 json!({
1241 "type": "userOutcome",
1242 "mergeOutcome": { "outcome": 7, "amount": null }
1243 })
1244 );
1245 }
1246
1247 #[rstest]
1248 fn test_user_outcome_merge_question_serialization() {
1249 let action = HyperliquidExecAction::UserOutcome {
1250 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1251 question: 9,
1252 amount: Some(dec!(2.0)),
1253 }),
1254 };
1255 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1256 assert_eq!(
1257 value,
1258 json!({
1259 "type": "userOutcome",
1260 "mergeQuestion": { "question": 9, "amount": "2.0" }
1261 })
1262 );
1263 }
1264
1265 #[rstest]
1266 fn test_user_outcome_merge_question_null_amount_means_max() {
1267 let action = HyperliquidExecAction::UserOutcome {
1268 op: HyperliquidExecUserOutcomeOp::MergeQuestion(HyperliquidExecMergeQuestionParams {
1269 question: 9,
1270 amount: None,
1271 }),
1272 };
1273 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1274 assert_eq!(
1275 value,
1276 json!({
1277 "type": "userOutcome",
1278 "mergeQuestion": { "question": 9, "amount": null }
1279 })
1280 );
1281 }
1282
1283 #[rstest]
1284 fn test_user_outcome_negate_outcome_serialization() {
1285 let action = HyperliquidExecAction::UserOutcome {
1286 op: HyperliquidExecUserOutcomeOp::NegateOutcome(HyperliquidExecNegateOutcomeParams {
1287 question: 9,
1288 outcome: 52,
1289 amount: dec!(1.5),
1290 }),
1291 };
1292 let value: serde_json::Value = serde_json::to_value(&action).unwrap();
1293 assert_eq!(
1294 value,
1295 json!({
1296 "type": "userOutcome",
1297 "negateOutcome": { "question": 9, "outcome": 52, "amount": "1.5" }
1298 })
1299 );
1300 }
1301
1302 #[rstest]
1303 fn test_modify_target_serializes_numeric_oid() {
1304 let request = modify_request_with_target(HyperliquidExecModifyTarget::Oid(12345));
1305 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1306
1307 assert_eq!(value["oid"], json!(12345));
1308 }
1309
1310 #[rstest]
1311 fn test_modify_target_serializes_cloid() {
1312 let cloid = Cloid::from_hex("0x1234567890abcdef1234567890abcdef").unwrap();
1313 let request = modify_request_with_target(HyperliquidExecModifyTarget::Cloid(cloid));
1314 let value: serde_json::Value = serde_json::to_value(request).unwrap();
1315
1316 assert_eq!(value["oid"], json!("0x1234567890abcdef1234567890abcdef"));
1317 }
1318
1319 fn modify_request_with_target(
1320 oid: HyperliquidExecModifyTarget,
1321 ) -> HyperliquidExecModifyOrderRequest {
1322 HyperliquidExecModifyOrderRequest {
1323 oid,
1324 order: HyperliquidExecPlaceOrderRequest {
1325 asset: 0,
1326 is_buy: true,
1327 price: dec!(51000),
1328 size: dec!(0.2),
1329 reduce_only: false,
1330 kind: HyperliquidExecOrderKind::Limit {
1331 limit: HyperliquidExecLimitParams {
1332 tif: HyperliquidExecTif::Gtc,
1333 },
1334 },
1335 cloid: None,
1336 },
1337 }
1338 }
1339}
1340
1341#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1345pub enum HyperliquidExecTif {
1346 #[serde(rename = "Alo")]
1348 Alo,
1349 #[serde(rename = "Ioc")]
1351 Ioc,
1352 #[serde(rename = "Gtc")]
1354 Gtc,
1355}
1356
1357#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1359pub enum HyperliquidExecTpSl {
1360 #[serde(rename = "tp")]
1362 Tp,
1363 #[serde(rename = "sl")]
1365 Sl,
1366}
1367
1368#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
1370pub enum HyperliquidExecGrouping {
1371 #[serde(rename = "na")]
1373 #[default]
1374 Na,
1375 #[serde(rename = "normalTpsl")]
1377 NormalTpsl,
1378 #[serde(rename = "positionTpsl")]
1380 PositionTpsl,
1381}
1382
1383#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1385#[serde(untagged)]
1386pub enum HyperliquidExecOrderKind {
1387 Limit {
1389 limit: HyperliquidExecLimitParams,
1391 },
1392 Trigger {
1394 trigger: HyperliquidExecTriggerParams,
1396 },
1397}
1398
1399#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1401pub struct HyperliquidExecLimitParams {
1402 pub tif: HyperliquidExecTif,
1404}
1405
1406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1408#[serde(rename_all = "camelCase")]
1409pub struct HyperliquidExecTriggerParams {
1410 pub is_market: bool,
1412 #[serde(
1414 serialize_with = "serialize_decimal_as_str",
1415 deserialize_with = "deserialize_decimal_from_str"
1416 )]
1417 pub trigger_px: Decimal,
1418 pub tpsl: HyperliquidExecTpSl,
1420}
1421
1422#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1427pub struct HyperliquidExecBuilderFee {
1428 #[serde(rename = "b")]
1430 pub address: String,
1431 #[serde(rename = "f")]
1433 pub fee_tenths_bp: u32,
1434}
1435
1436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1441pub struct HyperliquidExecPlaceOrderRequest {
1442 #[serde(rename = "a")]
1444 pub asset: AssetId,
1445 #[serde(rename = "b")]
1447 pub is_buy: bool,
1448 #[serde(
1450 rename = "p",
1451 serialize_with = "serialize_decimal_as_str",
1452 deserialize_with = "deserialize_decimal_from_str"
1453 )]
1454 pub price: Decimal,
1455 #[serde(
1457 rename = "s",
1458 serialize_with = "serialize_decimal_as_str",
1459 deserialize_with = "deserialize_decimal_from_str"
1460 )]
1461 pub size: Decimal,
1462 #[serde(rename = "r")]
1464 pub reduce_only: bool,
1465 #[serde(rename = "t")]
1467 pub kind: HyperliquidExecOrderKind,
1468 #[serde(rename = "c", skip_serializing_if = "Option::is_none")]
1470 pub cloid: Option<Cloid>,
1471}
1472
1473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1475pub struct HyperliquidExecCancelOrderRequest {
1476 #[serde(rename = "a")]
1478 pub asset: AssetId,
1479 #[serde(rename = "o")]
1481 pub oid: OrderId,
1482}
1483
1484#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1489pub struct HyperliquidExecCancelByCloidRequest {
1490 pub asset: AssetId,
1492 pub cloid: Cloid,
1494}
1495
1496#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1501#[serde(untagged)]
1502pub enum HyperliquidExecModifyTarget {
1503 Oid(OrderId),
1505 Cloid(Cloid),
1507}
1508
1509impl HyperliquidExecModifyTarget {
1510 pub fn from_venue_order_id(
1516 venue_order_id: &VenueOrderId,
1517 ) -> Result<Self, std::num::ParseIntError> {
1518 venue_order_id.as_str().parse::<OrderId>().map(Self::Oid)
1519 }
1520}
1521
1522impl From<OrderId> for HyperliquidExecModifyTarget {
1523 fn from(value: OrderId) -> Self {
1524 Self::Oid(value)
1525 }
1526}
1527
1528impl From<Cloid> for HyperliquidExecModifyTarget {
1529 fn from(value: Cloid) -> Self {
1530 Self::Cloid(value)
1531 }
1532}
1533
1534#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1539pub struct HyperliquidExecModifyOrderRequest {
1540 pub oid: HyperliquidExecModifyTarget,
1542 pub order: HyperliquidExecPlaceOrderRequest,
1544}
1545
1546#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1551pub struct HyperliquidExecSplitOutcomeParams {
1552 pub outcome: u32,
1554 #[serde(
1556 serialize_with = "serialize_decimal_as_str",
1557 deserialize_with = "deserialize_decimal_from_str"
1558 )]
1559 pub amount: Decimal,
1560}
1561
1562#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1568pub struct HyperliquidExecMergeOutcomeParams {
1569 pub outcome: u32,
1571 #[serde(
1573 default,
1574 serialize_with = "serialize_optional_decimal_as_str",
1575 deserialize_with = "deserialize_optional_decimal_from_str"
1576 )]
1577 pub amount: Option<Decimal>,
1578}
1579
1580#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1586pub struct HyperliquidExecMergeQuestionParams {
1587 pub question: u32,
1589 #[serde(
1591 default,
1592 serialize_with = "serialize_optional_decimal_as_str",
1593 deserialize_with = "deserialize_optional_decimal_from_str"
1594 )]
1595 pub amount: Option<Decimal>,
1596}
1597
1598#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1603pub struct HyperliquidExecNegateOutcomeParams {
1604 pub question: u32,
1606 pub outcome: u32,
1608 #[serde(
1610 serialize_with = "serialize_decimal_as_str",
1611 deserialize_with = "deserialize_decimal_from_str"
1612 )]
1613 pub amount: Decimal,
1614}
1615
1616#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1623pub enum HyperliquidExecUserOutcomeOp {
1624 #[serde(rename = "splitOutcome")]
1626 SplitOutcome(HyperliquidExecSplitOutcomeParams),
1627 #[serde(rename = "mergeOutcome")]
1630 MergeOutcome(HyperliquidExecMergeOutcomeParams),
1631 #[serde(rename = "mergeQuestion")]
1634 MergeQuestion(HyperliquidExecMergeQuestionParams),
1635 #[serde(rename = "negateOutcome")]
1638 NegateOutcome(HyperliquidExecNegateOutcomeParams),
1639}
1640
1641#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1643pub struct HyperliquidExecTwapRequest {
1644 #[serde(rename = "a")]
1646 pub asset: AssetId,
1647 #[serde(rename = "b")]
1649 pub is_buy: bool,
1650 #[serde(
1652 rename = "s",
1653 serialize_with = "serialize_decimal_as_str",
1654 deserialize_with = "deserialize_decimal_from_str"
1655 )]
1656 pub size: Decimal,
1657 #[serde(rename = "m")]
1659 pub duration_ms: u64,
1660}
1661
1662#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1668#[serde(tag = "type")]
1669pub enum HyperliquidExecAction {
1670 #[serde(rename = "order")]
1672 Order {
1673 orders: Vec<HyperliquidExecPlaceOrderRequest>,
1675 #[serde(default)]
1677 grouping: HyperliquidExecGrouping,
1678 #[serde(skip_serializing_if = "Option::is_none")]
1680 builder: Option<HyperliquidExecBuilderFee>,
1681 },
1682
1683 #[serde(rename = "cancel")]
1685 Cancel {
1686 cancels: Vec<HyperliquidExecCancelOrderRequest>,
1688 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1690 fast: Option<bool>,
1691 },
1692
1693 #[serde(rename = "cancelByCloid")]
1695 CancelByCloid {
1696 cancels: Vec<HyperliquidExecCancelByCloidRequest>,
1698 #[serde(rename = "f", skip_serializing_if = "Option::is_none")]
1700 fast: Option<bool>,
1701 },
1702
1703 #[serde(rename = "modify")]
1705 Modify {
1706 #[serde(flatten)]
1708 modify: HyperliquidExecModifyOrderRequest,
1709 },
1710
1711 #[serde(rename = "batchModify")]
1713 BatchModify {
1714 modifies: Vec<HyperliquidExecModifyOrderRequest>,
1716 },
1717
1718 #[serde(rename = "scheduleCancel")]
1720 ScheduleCancel {
1721 #[serde(skip_serializing_if = "Option::is_none")]
1724 time: Option<u64>,
1725 },
1726
1727 #[serde(rename = "updateLeverage")]
1729 UpdateLeverage {
1730 #[serde(rename = "a")]
1732 asset: AssetId,
1733 #[serde(rename = "isCross")]
1735 is_cross: bool,
1736 #[serde(rename = "leverage")]
1738 leverage: u32,
1739 },
1740
1741 #[serde(rename = "updateIsolatedMargin")]
1743 UpdateIsolatedMargin {
1744 #[serde(rename = "a")]
1746 asset: AssetId,
1747 #[serde(
1749 rename = "delta",
1750 serialize_with = "serialize_decimal_as_str",
1751 deserialize_with = "deserialize_decimal_from_str"
1752 )]
1753 delta: Decimal,
1754 },
1755
1756 #[serde(rename = "usdClassTransfer")]
1758 UsdClassTransfer {
1759 from: String,
1761 to: String,
1763 #[serde(
1765 serialize_with = "serialize_decimal_as_str",
1766 deserialize_with = "deserialize_decimal_from_str"
1767 )]
1768 amount: Decimal,
1769 },
1770
1771 #[serde(rename = "userOutcome")]
1777 UserOutcome {
1778 #[serde(flatten)]
1780 op: HyperliquidExecUserOutcomeOp,
1781 },
1782
1783 #[serde(rename = "twapPlace")]
1785 TwapPlace {
1786 #[serde(flatten)]
1788 twap: HyperliquidExecTwapRequest,
1789 },
1790
1791 #[serde(rename = "twapCancel")]
1793 TwapCancel {
1794 #[serde(rename = "a")]
1796 asset: AssetId,
1797 #[serde(rename = "t")]
1799 twap_id: u64,
1800 },
1801
1802 #[serde(rename = "noop")]
1804 Noop,
1805}
1806
1807#[derive(Debug, Clone, Serialize)]
1812#[serde(rename_all = "camelCase")]
1813pub struct HyperliquidExecRequest {
1814 pub action: HyperliquidExecAction,
1816 pub nonce: u64,
1818 pub signature: String,
1820 #[serde(skip_serializing_if = "Option::is_none")]
1822 pub vault_address: Option<String>,
1823 #[serde(skip_serializing_if = "Option::is_none")]
1826 pub expires_after: Option<u64>,
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize)]
1831pub struct HyperliquidExecResponse {
1832 pub status: String,
1834 pub response: HyperliquidExecResponseData,
1836}
1837
1838#[derive(Debug, Clone, Serialize, Deserialize)]
1840#[serde(tag = "type")]
1841pub enum HyperliquidExecResponseData {
1842 #[serde(rename = "order")]
1844 Order {
1845 data: HyperliquidExecOrderResponseData,
1847 },
1848 #[serde(rename = "cancel")]
1850 Cancel {
1851 data: HyperliquidExecCancelResponseData,
1853 },
1854 #[serde(rename = "modify")]
1856 Modify {
1857 data: HyperliquidExecModifyResponseData,
1859 },
1860 #[serde(rename = "default")]
1862 Default,
1863 #[serde(other)]
1865 Unknown,
1866}
1867
1868#[derive(Debug, Clone, Serialize, Deserialize)]
1870pub struct HyperliquidExecOrderResponseData {
1871 pub statuses: Vec<HyperliquidExecOrderStatus>,
1873}
1874
1875#[derive(Debug, Clone, Serialize, Deserialize)]
1877pub struct HyperliquidExecCancelResponseData {
1878 pub statuses: Vec<HyperliquidExecCancelStatus>,
1880}
1881
1882#[derive(Debug, Clone, Serialize, Deserialize)]
1884pub struct HyperliquidExecModifyResponseData {
1885 pub statuses: Vec<HyperliquidExecModifyStatus>,
1887}
1888
1889#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1891#[serde(untagged)]
1892pub enum HyperliquidExecOrderStatus {
1893 Resting {
1895 resting: HyperliquidExecRestingInfo,
1897 },
1898 Filled {
1900 filled: HyperliquidExecFilledInfo,
1902 },
1903 Error {
1905 error: String,
1907 },
1908 Tag(HyperliquidExecOrderStatusTag),
1912}
1913
1914#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1920pub enum HyperliquidExecOrderStatusTag {
1921 #[serde(rename = "waitingForFill")]
1923 WaitingForFill,
1924 #[serde(rename = "waitingForTrigger")]
1926 WaitingForTrigger,
1927}
1928
1929#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1931pub struct HyperliquidExecRestingInfo {
1932 pub oid: OrderId,
1934}
1935
1936#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1938pub struct HyperliquidExecFilledInfo {
1939 #[serde(
1941 rename = "totalSz",
1942 serialize_with = "serialize_decimal_as_str",
1943 deserialize_with = "deserialize_decimal_from_str"
1944 )]
1945 pub total_sz: Decimal,
1946 #[serde(
1948 rename = "avgPx",
1949 serialize_with = "serialize_decimal_as_str",
1950 deserialize_with = "deserialize_decimal_from_str"
1951 )]
1952 pub avg_px: Decimal,
1953 pub oid: OrderId,
1955}
1956
1957#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1959#[serde(untagged)]
1960pub enum HyperliquidExecCancelStatus {
1961 Success(String), Error {
1965 error: String,
1967 },
1968}
1969
1970#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1972#[serde(untagged)]
1973pub enum HyperliquidExecModifyStatus {
1974 Success(String), Error {
1978 error: String,
1980 },
1981}
1982
1983#[derive(Debug, Clone, Serialize, Deserialize)]
1986#[serde(rename_all = "camelCase")]
1987pub struct ClearinghouseState {
1988 #[serde(default)]
1990 pub asset_positions: Vec<AssetPosition>,
1991 #[serde(default)]
1993 pub cross_margin_summary: Option<CrossMarginSummary>,
1994 #[serde(
1996 default,
1997 serialize_with = "serialize_optional_decimal_as_str",
1998 deserialize_with = "deserialize_optional_decimal_from_str"
1999 )]
2000 pub withdrawable: Option<Decimal>,
2001 #[serde(default)]
2003 pub time: Option<u64>,
2004}
2005
2006#[derive(Debug, Clone, Serialize, Deserialize)]
2008#[serde(rename_all = "camelCase")]
2009pub struct AssetPosition {
2010 pub position: PositionData,
2012 #[serde(rename = "type")]
2014 pub position_type: HyperliquidPositionType,
2015}
2016
2017#[derive(Debug, Clone, Serialize, Deserialize)]
2019#[serde(rename_all = "camelCase")]
2020pub struct LeverageInfo {
2021 #[serde(rename = "type")]
2022 pub leverage_type: HyperliquidLeverageType,
2023 pub value: u32,
2025}
2026
2027#[derive(Debug, Clone, Serialize, Deserialize)]
2029#[serde(rename_all = "camelCase")]
2030pub struct CumFundingInfo {
2031 #[serde(
2033 rename = "allTime",
2034 serialize_with = "serialize_decimal_as_str",
2035 deserialize_with = "deserialize_decimal_from_str"
2036 )]
2037 pub all_time: Decimal,
2038 #[serde(
2040 rename = "sinceOpen",
2041 serialize_with = "serialize_decimal_as_str",
2042 deserialize_with = "deserialize_decimal_from_str"
2043 )]
2044 pub since_open: Decimal,
2045 #[serde(
2047 rename = "sinceChange",
2048 serialize_with = "serialize_decimal_as_str",
2049 deserialize_with = "deserialize_decimal_from_str"
2050 )]
2051 pub since_change: Decimal,
2052}
2053
2054#[derive(Debug, Clone, Serialize, Deserialize)]
2056#[serde(rename_all = "camelCase")]
2057pub struct PositionData {
2058 pub coin: Ustr,
2060 #[serde(rename = "cumFunding")]
2062 pub cum_funding: CumFundingInfo,
2063 #[serde(
2065 rename = "entryPx",
2066 serialize_with = "serialize_optional_decimal_as_str",
2067 deserialize_with = "deserialize_optional_decimal_from_str",
2068 default
2069 )]
2070 pub entry_px: Option<Decimal>,
2071 pub leverage: LeverageInfo,
2073 #[serde(
2075 rename = "liquidationPx",
2076 serialize_with = "serialize_optional_decimal_as_str",
2077 deserialize_with = "deserialize_optional_decimal_from_str",
2078 default
2079 )]
2080 pub liquidation_px: Option<Decimal>,
2081 #[serde(
2083 rename = "marginUsed",
2084 serialize_with = "serialize_decimal_as_str",
2085 deserialize_with = "deserialize_decimal_from_str"
2086 )]
2087 pub margin_used: Decimal,
2088 #[serde(rename = "maxLeverage", default)]
2090 pub max_leverage: Option<u32>,
2091 #[serde(
2093 rename = "positionValue",
2094 serialize_with = "serialize_decimal_as_str",
2095 deserialize_with = "deserialize_decimal_from_str"
2096 )]
2097 pub position_value: Decimal,
2098 #[serde(
2100 rename = "returnOnEquity",
2101 serialize_with = "serialize_decimal_as_str",
2102 deserialize_with = "deserialize_decimal_from_str"
2103 )]
2104 pub return_on_equity: Decimal,
2105 #[serde(
2107 rename = "szi",
2108 serialize_with = "serialize_decimal_as_str",
2109 deserialize_with = "deserialize_decimal_from_str"
2110 )]
2111 pub szi: Decimal,
2112 #[serde(
2114 rename = "unrealizedPnl",
2115 serialize_with = "serialize_decimal_as_str",
2116 deserialize_with = "deserialize_decimal_from_str"
2117 )]
2118 pub unrealized_pnl: Decimal,
2119}
2120
2121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
2127#[serde(rename_all = "camelCase")]
2128pub struct SpotClearinghouseState {
2129 #[serde(default)]
2131 pub balances: Vec<SpotBalance>,
2132}
2133
2134#[derive(Debug, Clone, Serialize, Deserialize)]
2136#[serde(rename_all = "camelCase")]
2137pub struct SpotBalance {
2138 pub coin: Ustr,
2140 #[serde(default)]
2143 pub token: Option<u32>,
2144 #[serde(
2146 serialize_with = "serialize_decimal_as_str",
2147 deserialize_with = "deserialize_decimal_from_str"
2148 )]
2149 pub total: Decimal,
2150 #[serde(
2152 serialize_with = "serialize_decimal_as_str",
2153 deserialize_with = "deserialize_decimal_from_str"
2154 )]
2155 pub hold: Decimal,
2156 #[serde(
2158 default,
2159 serialize_with = "serialize_optional_decimal_as_str",
2160 deserialize_with = "deserialize_optional_decimal_from_str"
2161 )]
2162 pub entry_ntl: Option<Decimal>,
2163}
2164
2165impl SpotBalance {
2166 #[must_use]
2168 pub fn free(&self) -> Decimal {
2169 (self.total - self.hold).max(Decimal::ZERO)
2170 }
2171
2172 #[must_use]
2174 pub fn avg_entry_px(&self) -> Option<Decimal> {
2175 let entry_ntl = self.entry_ntl?;
2176
2177 if entry_ntl.is_zero() || self.total.is_zero() {
2178 return None;
2179 }
2180
2181 Some(entry_ntl / self.total)
2182 }
2183}
2184
2185#[derive(Debug, Clone, Serialize, Deserialize)]
2187#[serde(rename_all = "camelCase")]
2188pub struct CrossMarginSummary {
2189 #[serde(
2191 rename = "accountValue",
2192 serialize_with = "serialize_decimal_as_str",
2193 deserialize_with = "deserialize_decimal_from_str"
2194 )]
2195 pub account_value: Decimal,
2196 #[serde(
2198 rename = "totalNtlPos",
2199 serialize_with = "serialize_decimal_as_str",
2200 deserialize_with = "deserialize_decimal_from_str"
2201 )]
2202 pub total_ntl_pos: Decimal,
2203 #[serde(
2205 rename = "totalRawUsd",
2206 serialize_with = "serialize_decimal_as_str",
2207 deserialize_with = "deserialize_decimal_from_str"
2208 )]
2209 pub total_raw_usd: Decimal,
2210 #[serde(
2212 rename = "totalMarginUsed",
2213 serialize_with = "serialize_decimal_as_str",
2214 deserialize_with = "deserialize_decimal_from_str"
2215 )]
2216 pub total_margin_used: Decimal,
2217 #[serde(
2219 rename = "withdrawable",
2220 default,
2221 serialize_with = "serialize_optional_decimal_as_str",
2222 deserialize_with = "deserialize_optional_decimal_from_str"
2223 )]
2224 pub withdrawable: Option<Decimal>,
2225}