1use std::collections::HashMap;
2
3use rust_decimal::Decimal;
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9#[non_exhaustive]
10pub struct HlPosition {
11 pub coin: String,
13 pub size: Decimal,
15 pub entry_px: Decimal,
17 pub unrealized_pnl: Decimal,
19 pub leverage: Decimal,
21 pub liquidation_px: Option<Decimal>,
23}
24
25impl HlPosition {
26 pub fn new(
28 coin: String,
29 size: Decimal,
30 entry_px: Decimal,
31 unrealized_pnl: Decimal,
32 leverage: Decimal,
33 liquidation_px: Option<Decimal>,
34 ) -> Self {
35 Self {
36 coin,
37 size,
38 entry_px,
39 unrealized_pnl,
40 leverage,
41 liquidation_px,
42 }
43 }
44}
45
46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48#[serde(rename_all = "camelCase")]
49#[non_exhaustive]
50pub struct HlFill {
51 pub coin: String,
53 pub px: Decimal,
55 pub sz: Decimal,
57 pub is_buy: bool,
59 pub timestamp: u64,
61 pub fee: Decimal,
63 pub closed_pnl: Decimal,
65}
66
67impl HlFill {
68 pub fn new(
70 coin: String,
71 px: Decimal,
72 sz: Decimal,
73 is_buy: bool,
74 timestamp: u64,
75 fee: Decimal,
76 closed_pnl: Decimal,
77 ) -> Self {
78 Self {
79 coin,
80 px,
81 sz,
82 is_buy,
83 timestamp,
84 fee,
85 closed_pnl,
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93#[non_exhaustive]
94pub struct HlAccountState {
95 pub equity: Decimal,
97 pub margin_available: Decimal,
99 pub positions: Vec<HlPosition>,
101}
102
103impl HlAccountState {
104 pub fn new(equity: Decimal, margin_available: Decimal, positions: Vec<HlPosition>) -> Self {
106 Self {
107 equity,
108 margin_available,
109 positions,
110 }
111 }
112}
113
114#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
119#[serde(rename_all = "camelCase")]
120pub struct HlVaultSummary {
121 pub vault_address: String,
123 pub name: String,
125 #[serde(default)]
127 pub leader_equity: Option<Decimal>,
128 #[serde(default)]
130 pub follower_equity: Option<Decimal>,
131 #[serde(default)]
133 pub all_time_pnl: Option<Decimal>,
134 #[serde(flatten)]
136 pub extra: HashMap<String, serde_json::Value>,
137}
138
139#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct HlVaultDetails {
145 pub name: String,
147 pub vault_address: String,
149 #[serde(default)]
151 pub leader: Option<String>,
152 #[serde(default)]
154 pub portfolio: Option<serde_json::Value>,
155 #[serde(default)]
157 pub follower_count: Option<u64>,
158 #[serde(flatten)]
160 pub extra: HashMap<String, serde_json::Value>,
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165#[serde(rename_all = "camelCase")]
166#[non_exhaustive]
167pub struct HlUserFees {
168 pub fee_tier: String,
170 pub maker_rate: Decimal,
172 pub taker_rate: Decimal,
174}
175
176impl HlUserFees {
177 pub fn new(fee_tier: String, maker_rate: Decimal, taker_rate: Decimal) -> Self {
179 Self {
180 fee_tier,
181 maker_rate,
182 taker_rate,
183 }
184 }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
189#[serde(rename_all = "camelCase")]
190#[non_exhaustive]
191pub struct HlRateLimitStatus {
192 pub used: u64,
194 pub limit: u64,
196 pub window_ms: u64,
198}
199
200impl HlRateLimitStatus {
201 pub fn new(used: u64, limit: u64, window_ms: u64) -> Self {
203 Self {
204 used,
205 limit,
206 window_ms,
207 }
208 }
209}
210
211#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
215#[serde(rename_all = "camelCase")]
216pub struct HlExtraAgent {
217 pub address: String,
219 #[serde(default)]
221 pub name: Option<String>,
222 #[serde(flatten)]
224 pub extra: HashMap<String, serde_json::Value>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230#[non_exhaustive]
231pub struct HlStakingDelegation {
232 pub validator: String,
234 pub amount: Decimal,
236 #[serde(default)]
238 pub locked_until_timestamp: u64,
239 #[serde(default)]
242 pub rewards: Decimal,
243}
244
245impl HlStakingDelegation {
246 pub fn new(
248 validator: String,
249 amount: Decimal,
250 locked_until_timestamp: u64,
251 rewards: Decimal,
252 ) -> Self {
253 Self {
254 validator,
255 amount,
256 locked_until_timestamp,
257 rewards,
258 }
259 }
260}
261
262#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
264#[serde(rename_all = "camelCase")]
265#[non_exhaustive]
266pub struct HlStakingSummary {
267 pub delegated: Decimal,
269 pub undelegated: Decimal,
271 pub total_pending_withdrawal: Decimal,
273 pub n_pending_withdrawals: u64,
275}
276
277impl HlStakingSummary {
278 pub fn new(
280 delegated: Decimal,
281 undelegated: Decimal,
282 total_pending_withdrawal: Decimal,
283 n_pending_withdrawals: u64,
284 ) -> Self {
285 Self {
286 delegated,
287 undelegated,
288 total_pending_withdrawal,
289 n_pending_withdrawals,
290 }
291 }
292}
293
294#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
296#[serde(rename_all = "camelCase")]
297#[non_exhaustive]
298pub struct HlStakingReward {
299 pub time: u64,
301 pub source: String,
303 pub total_amount: Decimal,
305}
306
307impl HlStakingReward {
308 pub fn new(time: u64, source: String, total_amount: Decimal) -> Self {
310 Self {
311 time,
312 source,
313 total_amount,
314 }
315 }
316}
317
318#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "camelCase")]
321#[non_exhaustive]
322pub struct HlBorrowLendState {
323 pub coin: String,
325 pub supply: Decimal,
327 pub borrow: Decimal,
329 pub apy: Decimal,
331}
332
333impl HlBorrowLendState {
334 pub fn new(coin: String, supply: Decimal, borrow: Decimal, apy: Decimal) -> Self {
336 Self {
337 coin,
338 supply,
339 borrow,
340 apy,
341 }
342 }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
347#[serde(rename_all = "camelCase")]
348#[non_exhaustive]
349pub struct HlOpenOrder {
350 pub oid: u64,
352 pub coin: String,
354 pub side: crate::market::TradeSide,
356 pub limit_px: Decimal,
358 pub sz: Decimal,
360 pub timestamp: u64,
362 pub order_type: String,
364 pub cloid: Option<String>,
366}
367
368impl HlOpenOrder {
369 #[allow(clippy::too_many_arguments)]
371 pub fn new(
372 oid: u64,
373 coin: String,
374 side: crate::market::TradeSide,
375 limit_px: Decimal,
376 sz: Decimal,
377 timestamp: u64,
378 order_type: String,
379 cloid: Option<String>,
380 ) -> Self {
381 Self {
382 oid,
383 coin,
384 side,
385 limit_px,
386 sz,
387 timestamp,
388 order_type,
389 cloid,
390 }
391 }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
398#[serde(rename_all = "camelCase")]
399#[non_exhaustive]
400pub struct HlFrontendOpenOrder {
401 pub oid: u64,
403 pub coin: String,
405 pub side: crate::market::TradeSide,
407 pub limit_px: Decimal,
409 pub sz: Decimal,
411 pub orig_sz: Decimal,
413 pub timestamp: u64,
415 pub order_type: String,
417 pub tif: Option<String>,
419 pub reduce_only: bool,
421 pub is_trigger: bool,
423 pub is_position_tpsl: bool,
425 pub trigger_condition: String,
427 pub trigger_px: Decimal,
429 pub cloid: Option<String>,
431 pub children: Vec<HlFrontendOpenOrder>,
433}
434
435impl HlFrontendOpenOrder {
436 #[allow(clippy::too_many_arguments)]
438 pub fn new(
439 oid: u64,
440 coin: String,
441 side: crate::market::TradeSide,
442 limit_px: Decimal,
443 sz: Decimal,
444 orig_sz: Decimal,
445 timestamp: u64,
446 order_type: String,
447 tif: Option<String>,
448 reduce_only: bool,
449 is_trigger: bool,
450 is_position_tpsl: bool,
451 trigger_condition: String,
452 trigger_px: Decimal,
453 cloid: Option<String>,
454 children: Vec<HlFrontendOpenOrder>,
455 ) -> Self {
456 Self {
457 oid,
458 coin,
459 side,
460 limit_px,
461 sz,
462 orig_sz,
463 timestamp,
464 order_type,
465 tif,
466 reduce_only,
467 is_trigger,
468 is_position_tpsl,
469 trigger_condition,
470 trigger_px,
471 cloid,
472 children,
473 }
474 }
475}
476
477#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479#[serde(rename_all = "camelCase")]
480#[non_exhaustive]
481pub struct HlOrderDetail {
482 pub oid: u64,
484 pub coin: String,
486 pub side: crate::market::TradeSide,
488 pub limit_px: Decimal,
490 pub sz: Decimal,
492 pub timestamp: u64,
494 pub order_type: String,
496 pub cloid: Option<String>,
498 pub status: String,
500}
501
502impl HlOrderDetail {
503 #[allow(clippy::too_many_arguments)]
505 pub fn new(
506 oid: u64,
507 coin: String,
508 side: crate::market::TradeSide,
509 limit_px: Decimal,
510 sz: Decimal,
511 timestamp: u64,
512 order_type: String,
513 cloid: Option<String>,
514 status: String,
515 ) -> Self {
516 Self {
517 oid,
518 coin,
519 side,
520 limit_px,
521 sz,
522 timestamp,
523 order_type,
524 cloid,
525 status,
526 }
527 }
528}
529
530#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(rename_all = "camelCase")]
533#[non_exhaustive]
534pub struct HlFundingEntry {
535 pub coin: String,
537 pub funding_rate: Decimal,
539 pub premium: Decimal,
541 pub time: u64,
543}
544
545impl HlFundingEntry {
546 pub fn new(coin: String, funding_rate: Decimal, premium: Decimal, time: u64) -> Self {
548 Self {
549 coin,
550 funding_rate,
551 premium,
552 time,
553 }
554 }
555}
556
557#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
559#[serde(rename_all = "camelCase")]
560#[non_exhaustive]
561pub struct HlUserFundingEntry {
562 pub coin: String,
564 pub usdc: Decimal,
566 pub szi: Decimal,
568 pub funding_rate: Decimal,
570 pub time: u64,
572}
573
574impl HlUserFundingEntry {
575 pub fn new(
577 coin: String,
578 usdc: Decimal,
579 szi: Decimal,
580 funding_rate: Decimal,
581 time: u64,
582 ) -> Self {
583 Self {
584 coin,
585 usdc,
586 szi,
587 funding_rate,
588 time,
589 }
590 }
591}
592
593#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
595#[serde(rename_all = "camelCase")]
596#[non_exhaustive]
597pub struct HlHistoricalOrder {
598 pub oid: u64,
600 pub coin: String,
602 pub side: crate::market::TradeSide,
604 pub limit_px: Decimal,
606 pub sz: Decimal,
608 pub timestamp: u64,
610 pub order_type: String,
612 pub cloid: Option<String>,
614 pub status: String,
616}
617
618impl HlHistoricalOrder {
619 #[allow(clippy::too_many_arguments)]
621 pub fn new(
622 oid: u64,
623 coin: String,
624 side: crate::market::TradeSide,
625 limit_px: Decimal,
626 sz: Decimal,
627 timestamp: u64,
628 order_type: String,
629 cloid: Option<String>,
630 status: String,
631 ) -> Self {
632 Self {
633 oid,
634 coin,
635 side,
636 limit_px,
637 sz,
638 timestamp,
639 order_type,
640 cloid,
641 status,
642 }
643 }
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(rename_all = "camelCase")]
649#[non_exhaustive]
650pub struct HlReferralState {
651 pub referrer: Option<String>,
653 pub referral_code: Option<String>,
655 pub cum_vlm: Decimal,
657 pub rewards: Decimal,
659}
660
661impl HlReferralState {
662 pub fn new(
664 referrer: Option<String>,
665 referral_code: Option<String>,
666 cum_vlm: Decimal,
667 rewards: Decimal,
668 ) -> Self {
669 Self {
670 referrer,
671 referral_code,
672 cum_vlm,
673 rewards,
674 }
675 }
676}
677
678#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
680#[serde(rename_all = "camelCase")]
681#[non_exhaustive]
682pub struct HlActiveAssetData {
683 pub coin: String,
685 pub leverage: Decimal,
687 pub max_trade_szs: Vec<Decimal>,
689 pub available_to_trade: Vec<Decimal>,
691 pub mark_px: Decimal,
693}
694
695impl HlActiveAssetData {
696 pub fn new(
698 coin: String,
699 leverage: Decimal,
700 max_trade_szs: Vec<Decimal>,
701 available_to_trade: Vec<Decimal>,
702 mark_px: Decimal,
703 ) -> Self {
704 Self {
705 coin,
706 leverage,
707 max_trade_szs,
708 available_to_trade,
709 mark_px,
710 }
711 }
712}
713
714#[cfg(test)]
715mod tests {
716 use super::*;
717 use crate::market::TradeSide;
718 use std::str::FromStr;
719
720 #[test]
721 fn position_serde_roundtrip() {
722 let pos = HlPosition {
723 coin: "BTC".into(),
724 size: Decimal::from_str("0.5").unwrap(),
725 entry_px: Decimal::from_str("60000.0").unwrap(),
726 unrealized_pnl: Decimal::from_str("150.0").unwrap(),
727 leverage: Decimal::from_str("10.0").unwrap(),
728 liquidation_px: Some(Decimal::from_str("54000.0").unwrap()),
729 };
730 let json = serde_json::to_string(&pos).unwrap();
731 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
732 assert_eq!(parsed.coin, "BTC");
733 assert_eq!(parsed.size, Decimal::from_str("0.5").unwrap());
734 assert_eq!(parsed.entry_px, Decimal::from_str("60000.0").unwrap());
735 assert_eq!(parsed.unrealized_pnl, Decimal::from_str("150.0").unwrap());
736 assert_eq!(parsed.leverage, Decimal::from_str("10.0").unwrap());
737 assert_eq!(
738 parsed.liquidation_px,
739 Some(Decimal::from_str("54000.0").unwrap())
740 );
741 }
742
743 #[test]
744 fn position_no_liquidation_px_roundtrip() {
745 let pos = HlPosition {
746 coin: "ETH".into(),
747 size: Decimal::from_str("-2.0").unwrap(),
748 entry_px: Decimal::from_str("3000.0").unwrap(),
749 unrealized_pnl: Decimal::from_str("-50.0").unwrap(),
750 leverage: Decimal::from_str("5.0").unwrap(),
751 liquidation_px: None,
752 };
753 let json = serde_json::to_string(&pos).unwrap();
754 let parsed: HlPosition = serde_json::from_str(&json).unwrap();
755 assert!(parsed.liquidation_px.is_none());
756 assert!(parsed.size < Decimal::ZERO);
757 }
758
759 #[test]
760 fn position_camel_case_keys() {
761 let pos = HlPosition {
762 coin: "X".into(),
763 size: Decimal::ONE,
764 entry_px: Decimal::ONE,
765 unrealized_pnl: Decimal::ZERO,
766 leverage: Decimal::ONE,
767 liquidation_px: None,
768 };
769 let json = serde_json::to_string(&pos).unwrap();
770 assert!(json.contains("entryPx"));
771 assert!(json.contains("unrealizedPnl"));
772 assert!(json.contains("liquidationPx"));
773 }
774
775 #[test]
776 fn fill_serde_roundtrip() {
777 let fill = HlFill {
778 coin: "ETH".into(),
779 px: Decimal::from_str("3000.0").unwrap(),
780 sz: Decimal::from_str("1.5").unwrap(),
781 is_buy: true,
782 timestamp: 1700000000000,
783 fee: Decimal::from_str("0.75").unwrap(),
784 closed_pnl: Decimal::ZERO,
785 };
786 let json = serde_json::to_string(&fill).unwrap();
787 let parsed: HlFill = serde_json::from_str(&json).unwrap();
788 assert_eq!(parsed.coin, "ETH");
789 assert_eq!(parsed.px, Decimal::from_str("3000.0").unwrap());
790 assert_eq!(parsed.sz, Decimal::from_str("1.5").unwrap());
791 assert!(parsed.is_buy);
792 assert_eq!(parsed.timestamp, 1700000000000);
793 assert_eq!(parsed.fee, Decimal::from_str("0.75").unwrap());
794 assert_eq!(parsed.closed_pnl, Decimal::ZERO);
795 }
796
797 #[test]
798 fn fill_camel_case_keys() {
799 let fill = HlFill {
800 coin: "X".into(),
801 px: Decimal::ONE,
802 sz: Decimal::ONE,
803 is_buy: false,
804 timestamp: 0,
805 fee: Decimal::ZERO,
806 closed_pnl: Decimal::from_str("100.0").unwrap(),
807 };
808 let json = serde_json::to_string(&fill).unwrap();
809 assert!(json.contains("isBuy"));
810 assert!(json.contains("closedPnl"));
811 }
812
813 #[test]
814 fn account_state_serde_roundtrip() {
815 let state = HlAccountState {
816 equity: Decimal::from_str("100000.0").unwrap(),
817 margin_available: Decimal::from_str("50000.0").unwrap(),
818 positions: vec![HlPosition {
819 coin: "BTC".into(),
820 size: Decimal::from_str("0.1").unwrap(),
821 entry_px: Decimal::from_str("60000.0").unwrap(),
822 unrealized_pnl: Decimal::ZERO,
823 leverage: Decimal::from_str("10.0").unwrap(),
824 liquidation_px: None,
825 }],
826 };
827 let json = serde_json::to_string(&state).unwrap();
828 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
829 assert_eq!(parsed.equity, Decimal::from_str("100000.0").unwrap());
830 assert_eq!(
831 parsed.margin_available,
832 Decimal::from_str("50000.0").unwrap()
833 );
834 assert_eq!(parsed.positions.len(), 1);
835 assert_eq!(parsed.positions[0].coin, "BTC");
836 }
837
838 #[test]
839 fn account_state_empty_positions_roundtrip() {
840 let state = HlAccountState {
841 equity: Decimal::ZERO,
842 margin_available: Decimal::ZERO,
843 positions: vec![],
844 };
845 let json = serde_json::to_string(&state).unwrap();
846 let parsed: HlAccountState = serde_json::from_str(&json).unwrap();
847 assert!(parsed.positions.is_empty());
848 }
849
850 #[test]
851 fn account_state_camel_case_keys() {
852 let state = HlAccountState {
853 equity: Decimal::ONE,
854 margin_available: Decimal::ONE,
855 positions: vec![],
856 };
857 let json = serde_json::to_string(&state).unwrap();
858 assert!(json.contains("marginAvailable"));
859 }
860
861 #[test]
862 fn vault_summary_serde_roundtrip() {
863 let json = serde_json::json!({
864 "vaultAddress": "0xabc123",
865 "name": "My Vault",
866 "leaderEquity": "10000.0",
867 "followerEquity": "50000.0",
868 "allTimePnl": "2500.0"
869 });
870 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
871 assert_eq!(parsed.vault_address, "0xabc123");
872 assert_eq!(parsed.name, "My Vault");
873 assert_eq!(
874 parsed.leader_equity,
875 Some(Decimal::from_str("10000.0").unwrap())
876 );
877 assert_eq!(
878 parsed.follower_equity,
879 Some(Decimal::from_str("50000.0").unwrap())
880 );
881 assert_eq!(
882 parsed.all_time_pnl,
883 Some(Decimal::from_str("2500.0").unwrap())
884 );
885 }
886
887 #[test]
888 fn vault_summary_minimal_fields() {
889 let json = serde_json::json!({
890 "vaultAddress": "0xdef456",
891 "name": "Minimal Vault"
892 });
893 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
894 assert_eq!(parsed.vault_address, "0xdef456");
895 assert_eq!(parsed.name, "Minimal Vault");
896 assert!(parsed.leader_equity.is_none());
897 assert!(parsed.follower_equity.is_none());
898 assert!(parsed.all_time_pnl.is_none());
899 }
900
901 #[test]
902 fn vault_summary_extra_fields_captured() {
903 let json = serde_json::json!({
904 "vaultAddress": "0x111",
905 "name": "V",
906 "someNewField": 42
907 });
908 let parsed: HlVaultSummary = serde_json::from_value(json).unwrap();
909 assert_eq!(
910 parsed.extra.get("someNewField").unwrap(),
911 &serde_json::json!(42)
912 );
913 }
914
915 #[test]
916 fn vault_summary_camel_case_keys() {
917 let summary = HlVaultSummary {
918 vault_address: "0x1".into(),
919 name: "V".into(),
920 leader_equity: Some(Decimal::ONE),
921 follower_equity: None,
922 all_time_pnl: None,
923 extra: HashMap::new(),
924 };
925 let json = serde_json::to_string(&summary).unwrap();
926 assert!(json.contains("vaultAddress"));
927 assert!(json.contains("leaderEquity"));
928 }
929
930 #[test]
931 fn vault_details_serde_roundtrip() {
932 let json = serde_json::json!({
933 "name": "Alpha Vault",
934 "vaultAddress": "0xvault",
935 "leader": "0xleader",
936 "portfolio": {"equity": "100000"},
937 "followerCount": 25
938 });
939 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
940 assert_eq!(parsed.name, "Alpha Vault");
941 assert_eq!(parsed.vault_address, "0xvault");
942 assert_eq!(parsed.leader.as_deref(), Some("0xleader"));
943 assert!(parsed.portfolio.is_some());
944 assert_eq!(parsed.follower_count, Some(25));
945 }
946
947 #[test]
948 fn vault_details_minimal_fields() {
949 let json = serde_json::json!({
950 "name": "Min",
951 "vaultAddress": "0xmin"
952 });
953 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
954 assert_eq!(parsed.name, "Min");
955 assert!(parsed.leader.is_none());
956 assert!(parsed.portfolio.is_none());
957 assert!(parsed.follower_count.is_none());
958 }
959
960 #[test]
961 fn vault_details_extra_fields_captured() {
962 let json = serde_json::json!({
963 "name": "V",
964 "vaultAddress": "0x1",
965 "customMetric": "hello"
966 });
967 let parsed: HlVaultDetails = serde_json::from_value(json).unwrap();
968 assert_eq!(
969 parsed.extra.get("customMetric").unwrap(),
970 &serde_json::json!("hello")
971 );
972 }
973
974 #[test]
975 fn extra_agent_serde_roundtrip() {
976 let json = serde_json::json!({
977 "address": "0xagent1",
978 "name": "Trading Bot"
979 });
980 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
981 assert_eq!(parsed.address, "0xagent1");
982 assert_eq!(parsed.name.as_deref(), Some("Trading Bot"));
983 }
984
985 #[test]
986 fn extra_agent_minimal_fields() {
987 let json = serde_json::json!({
988 "address": "0xagent2"
989 });
990 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
991 assert_eq!(parsed.address, "0xagent2");
992 assert!(parsed.name.is_none());
993 }
994
995 #[test]
996 fn extra_agent_extra_fields_captured() {
997 let json = serde_json::json!({
998 "address": "0xagent3",
999 "permissions": ["trade", "withdraw"]
1000 });
1001 let parsed: HlExtraAgent = serde_json::from_value(json).unwrap();
1002 assert!(parsed.extra.contains_key("permissions"));
1003 }
1004
1005 #[test]
1006 fn staking_delegation_serde_roundtrip() {
1007 let delegation = HlStakingDelegation {
1008 validator: "0xval1".into(),
1009 amount: Decimal::from_str("1000.0").unwrap(),
1010 locked_until_timestamp: 1_700_000_000_000,
1011 rewards: Decimal::from_str("5.25").unwrap(),
1012 };
1013 let json = serde_json::to_string(&delegation).unwrap();
1014 let parsed: HlStakingDelegation = serde_json::from_str(&json).unwrap();
1015 assert_eq!(parsed.validator, "0xval1");
1016 assert_eq!(parsed.amount, Decimal::from_str("1000.0").unwrap());
1017 assert_eq!(parsed.locked_until_timestamp, 1_700_000_000_000);
1018 assert_eq!(parsed.rewards, Decimal::from_str("5.25").unwrap());
1019 }
1020
1021 #[test]
1022 fn staking_delegation_from_json() {
1023 let json = serde_json::json!({
1024 "validator": "0xabc",
1025 "amount": "500.0",
1026 "rewards": "2.5"
1027 });
1028 let parsed: HlStakingDelegation = serde_json::from_value(json).unwrap();
1029 assert_eq!(parsed.validator, "0xabc");
1030 assert_eq!(parsed.amount, Decimal::from_str("500.0").unwrap());
1031 assert_eq!(parsed.rewards, Decimal::from_str("2.5").unwrap());
1032 }
1033
1034 #[test]
1035 fn borrow_lend_state_serde_roundtrip() {
1036 let state = HlBorrowLendState {
1037 coin: "USDC".into(),
1038 supply: Decimal::from_str("10000.0").unwrap(),
1039 borrow: Decimal::from_str("5000.0").unwrap(),
1040 apy: Decimal::from_str("0.05").unwrap(),
1041 };
1042 let json = serde_json::to_string(&state).unwrap();
1043 let parsed: HlBorrowLendState = serde_json::from_str(&json).unwrap();
1044 assert_eq!(parsed.coin, "USDC");
1045 assert_eq!(parsed.supply, Decimal::from_str("10000.0").unwrap());
1046 assert_eq!(parsed.borrow, Decimal::from_str("5000.0").unwrap());
1047 assert_eq!(parsed.apy, Decimal::from_str("0.05").unwrap());
1048 }
1049
1050 #[test]
1051 fn borrow_lend_state_from_json() {
1052 let json = serde_json::json!({
1053 "coin": "ETH",
1054 "supply": "100.0",
1055 "borrow": "0.0",
1056 "apy": "0.03"
1057 });
1058 let parsed: HlBorrowLendState = serde_json::from_value(json).unwrap();
1059 assert_eq!(parsed.coin, "ETH");
1060 assert_eq!(parsed.supply, Decimal::from_str("100.0").unwrap());
1061 assert_eq!(parsed.borrow, Decimal::ZERO);
1062 assert_eq!(parsed.apy, Decimal::from_str("0.03").unwrap());
1063 }
1064
1065 #[test]
1066 fn borrow_lend_state_camel_case_keys() {
1067 let state = HlBorrowLendState {
1068 coin: "X".into(),
1069 supply: Decimal::ONE,
1070 borrow: Decimal::ZERO,
1071 apy: Decimal::ZERO,
1072 };
1073 let json = serde_json::to_string(&state).unwrap();
1074 assert!(json.contains("supply"));
1076 assert!(json.contains("borrow"));
1077 assert!(json.contains("apy"));
1078 }
1079
1080 #[test]
1081 fn extra_agent_camel_case_keys() {
1082 let agent = HlExtraAgent {
1083 address: "0x1".into(),
1084 name: Some("Bot".into()),
1085 extra: HashMap::new(),
1086 };
1087 let json = serde_json::to_string(&agent).unwrap();
1088 assert!(json.contains("address"));
1089 assert!(json.contains("name"));
1090 }
1091
1092 #[test]
1093 fn user_fees_serde_roundtrip() {
1094 let fees = HlUserFees {
1095 fee_tier: "VIP2".into(),
1096 maker_rate: Decimal::from_str("0.0001").unwrap(),
1097 taker_rate: Decimal::from_str("0.0003").unwrap(),
1098 };
1099 let json = serde_json::to_string(&fees).unwrap();
1100 let parsed: HlUserFees = serde_json::from_str(&json).unwrap();
1101 assert_eq!(parsed.fee_tier, "VIP2");
1102 assert_eq!(parsed.maker_rate, Decimal::from_str("0.0001").unwrap());
1103 assert_eq!(parsed.taker_rate, Decimal::from_str("0.0003").unwrap());
1104 }
1105
1106 #[test]
1107 fn user_fees_camel_case_keys() {
1108 let fees = HlUserFees {
1109 fee_tier: "T1".into(),
1110 maker_rate: Decimal::ZERO,
1111 taker_rate: Decimal::ONE,
1112 };
1113 let json = serde_json::to_string(&fees).unwrap();
1114 assert!(json.contains("feeTier"));
1115 assert!(json.contains("makerRate"));
1116 assert!(json.contains("takerRate"));
1117 }
1118
1119 #[test]
1120 fn user_fees_constructor() {
1121 let fees = HlUserFees::new(
1122 "VIP1".into(),
1123 Decimal::from_str("0.0002").unwrap(),
1124 Decimal::from_str("0.0005").unwrap(),
1125 );
1126 assert_eq!(fees.fee_tier, "VIP1");
1127 assert_eq!(fees.maker_rate, Decimal::from_str("0.0002").unwrap());
1128 assert_eq!(fees.taker_rate, Decimal::from_str("0.0005").unwrap());
1129 }
1130
1131 #[test]
1132 fn rate_limit_status_serde_roundtrip() {
1133 let status = HlRateLimitStatus {
1134 used: 42,
1135 limit: 1200,
1136 window_ms: 60000,
1137 };
1138 let json = serde_json::to_string(&status).unwrap();
1139 let parsed: HlRateLimitStatus = serde_json::from_str(&json).unwrap();
1140 assert_eq!(parsed.used, 42);
1141 assert_eq!(parsed.limit, 1200);
1142 assert_eq!(parsed.window_ms, 60000);
1143 }
1144
1145 #[test]
1146 fn rate_limit_status_camel_case_keys() {
1147 let status = HlRateLimitStatus {
1148 used: 0,
1149 limit: 100,
1150 window_ms: 30000,
1151 };
1152 let json = serde_json::to_string(&status).unwrap();
1153 assert!(json.contains("windowMs"));
1154 }
1155
1156 #[test]
1157 fn rate_limit_status_constructor() {
1158 let status = HlRateLimitStatus::new(10, 500, 60000);
1159 assert_eq!(status.used, 10);
1160 assert_eq!(status.limit, 500);
1161 assert_eq!(status.window_ms, 60000);
1162 }
1163
1164 #[test]
1165 fn open_order_serde_roundtrip() {
1166 let order = HlOpenOrder {
1167 oid: 12345,
1168 coin: "BTC".into(),
1169 side: TradeSide::Buy,
1170 limit_px: Decimal::from_str("60000.0").unwrap(),
1171 sz: Decimal::from_str("0.5").unwrap(),
1172 timestamp: 1700000000000,
1173 order_type: "Limit".into(),
1174 cloid: Some("my-order-1".into()),
1175 };
1176 let json = serde_json::to_string(&order).unwrap();
1177 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1178 assert_eq!(parsed.oid, 12345);
1179 assert_eq!(parsed.coin, "BTC");
1180 assert_eq!(parsed.side, TradeSide::Buy);
1181 assert_eq!(parsed.limit_px, Decimal::from_str("60000.0").unwrap());
1182 assert_eq!(parsed.sz, Decimal::from_str("0.5").unwrap());
1183 assert_eq!(parsed.timestamp, 1700000000000);
1184 assert_eq!(parsed.order_type, "Limit");
1185 assert_eq!(parsed.cloid.as_deref(), Some("my-order-1"));
1186 }
1187
1188 #[test]
1189 fn open_order_no_cloid_roundtrip() {
1190 let order = HlOpenOrder {
1191 oid: 99,
1192 coin: "ETH".into(),
1193 side: TradeSide::Sell,
1194 limit_px: Decimal::from_str("3000.0").unwrap(),
1195 sz: Decimal::ONE,
1196 timestamp: 0,
1197 order_type: "Limit".into(),
1198 cloid: None,
1199 };
1200 let json = serde_json::to_string(&order).unwrap();
1201 let parsed: HlOpenOrder = serde_json::from_str(&json).unwrap();
1202 assert!(parsed.cloid.is_none());
1203 }
1204
1205 #[test]
1206 fn open_order_camel_case_keys() {
1207 let order = HlOpenOrder {
1208 oid: 1,
1209 coin: "X".into(),
1210 side: TradeSide::Buy,
1211 limit_px: Decimal::ONE,
1212 sz: Decimal::ONE,
1213 timestamp: 0,
1214 order_type: "Limit".into(),
1215 cloid: None,
1216 };
1217 let json = serde_json::to_string(&order).unwrap();
1218 assert!(json.contains("limitPx"));
1219 assert!(json.contains("orderType"));
1220 }
1221
1222 #[test]
1223 fn order_detail_serde_roundtrip() {
1224 let detail = HlOrderDetail {
1225 oid: 555,
1226 coin: "SOL".into(),
1227 side: TradeSide::Buy,
1228 limit_px: Decimal::from_str("150.0").unwrap(),
1229 sz: Decimal::from_str("10.0").unwrap(),
1230 timestamp: 1700000000000,
1231 order_type: "Limit".into(),
1232 cloid: None,
1233 status: "filled".into(),
1234 };
1235 let json = serde_json::to_string(&detail).unwrap();
1236 let parsed: HlOrderDetail = serde_json::from_str(&json).unwrap();
1237 assert_eq!(parsed.oid, 555);
1238 assert_eq!(parsed.status, "filled");
1239 assert_eq!(parsed.coin, "SOL");
1240 }
1241
1242 #[test]
1243 fn order_detail_camel_case_keys() {
1244 let detail = HlOrderDetail {
1245 oid: 1,
1246 coin: "X".into(),
1247 side: TradeSide::Buy,
1248 limit_px: Decimal::ONE,
1249 sz: Decimal::ONE,
1250 timestamp: 0,
1251 order_type: "Limit".into(),
1252 cloid: Some("c1".into()),
1253 status: "open".into(),
1254 };
1255 let json = serde_json::to_string(&detail).unwrap();
1256 assert!(json.contains("limitPx"));
1257 assert!(json.contains("orderType"));
1258 }
1259
1260 #[test]
1261 fn funding_entry_serde_roundtrip() {
1262 let entry = HlFundingEntry {
1263 coin: "BTC".into(),
1264 funding_rate: Decimal::from_str("0.0001").unwrap(),
1265 premium: Decimal::from_str("0.00005").unwrap(),
1266 time: 1700000000000,
1267 };
1268 let json = serde_json::to_string(&entry).unwrap();
1269 let parsed: HlFundingEntry = serde_json::from_str(&json).unwrap();
1270 assert_eq!(parsed.coin, "BTC");
1271 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0001").unwrap());
1272 assert_eq!(parsed.premium, Decimal::from_str("0.00005").unwrap());
1273 assert_eq!(parsed.time, 1700000000000);
1274 }
1275
1276 #[test]
1277 fn funding_entry_camel_case_keys() {
1278 let entry = HlFundingEntry {
1279 coin: "X".into(),
1280 funding_rate: Decimal::ONE,
1281 premium: Decimal::ZERO,
1282 time: 0,
1283 };
1284 let json = serde_json::to_string(&entry).unwrap();
1285 assert!(json.contains("fundingRate"));
1286 }
1287
1288 #[test]
1289 fn user_funding_entry_serde_roundtrip() {
1290 let entry = HlUserFundingEntry {
1291 coin: "ETH".into(),
1292 usdc: Decimal::from_str("-1.5").unwrap(),
1293 szi: Decimal::from_str("2.0").unwrap(),
1294 funding_rate: Decimal::from_str("0.0002").unwrap(),
1295 time: 1700000000000,
1296 };
1297 let json = serde_json::to_string(&entry).unwrap();
1298 let parsed: HlUserFundingEntry = serde_json::from_str(&json).unwrap();
1299 assert_eq!(parsed.coin, "ETH");
1300 assert_eq!(parsed.usdc, Decimal::from_str("-1.5").unwrap());
1301 assert_eq!(parsed.szi, Decimal::from_str("2.0").unwrap());
1302 assert_eq!(parsed.funding_rate, Decimal::from_str("0.0002").unwrap());
1303 assert_eq!(parsed.time, 1700000000000);
1304 }
1305
1306 #[test]
1307 fn user_funding_entry_camel_case_keys() {
1308 let entry = HlUserFundingEntry {
1309 coin: "X".into(),
1310 usdc: Decimal::ONE,
1311 szi: Decimal::ONE,
1312 funding_rate: Decimal::ZERO,
1313 time: 0,
1314 };
1315 let json = serde_json::to_string(&entry).unwrap();
1316 assert!(json.contains("fundingRate"));
1317 }
1318
1319 #[test]
1320 fn historical_order_serde_roundtrip() {
1321 let order = HlHistoricalOrder {
1322 oid: 777,
1323 coin: "BTC".into(),
1324 side: TradeSide::Sell,
1325 limit_px: Decimal::from_str("65000.0").unwrap(),
1326 sz: Decimal::from_str("0.1").unwrap(),
1327 timestamp: 1700000000000,
1328 order_type: "Limit".into(),
1329 cloid: Some("hist-1".into()),
1330 status: "filled".into(),
1331 };
1332 let json = serde_json::to_string(&order).unwrap();
1333 let parsed: HlHistoricalOrder = serde_json::from_str(&json).unwrap();
1334 assert_eq!(parsed.oid, 777);
1335 assert_eq!(parsed.status, "filled");
1336 assert_eq!(parsed.coin, "BTC");
1337 assert_eq!(parsed.cloid.as_deref(), Some("hist-1"));
1338 }
1339
1340 #[test]
1341 fn historical_order_camel_case_keys() {
1342 let order = HlHistoricalOrder {
1343 oid: 1,
1344 coin: "X".into(),
1345 side: TradeSide::Buy,
1346 limit_px: Decimal::ONE,
1347 sz: Decimal::ONE,
1348 timestamp: 0,
1349 order_type: "Limit".into(),
1350 cloid: None,
1351 status: "canceled".into(),
1352 };
1353 let json = serde_json::to_string(&order).unwrap();
1354 assert!(json.contains("limitPx"));
1355 assert!(json.contains("orderType"));
1356 }
1357
1358 #[test]
1359 fn referral_state_serde_roundtrip() {
1360 let state = HlReferralState::new(
1361 Some("0xabc".into()),
1362 Some("CODE123".into()),
1363 Decimal::from_str("50000.0").unwrap(),
1364 Decimal::from_str("100.5").unwrap(),
1365 );
1366 let json = serde_json::to_string(&state).unwrap();
1367 let parsed: HlReferralState = serde_json::from_str(&json).unwrap();
1368 assert_eq!(parsed.referrer.as_deref(), Some("0xabc"));
1369 assert_eq!(parsed.referral_code.as_deref(), Some("CODE123"));
1370 assert_eq!(parsed.cum_vlm, Decimal::from_str("50000.0").unwrap());
1371 assert_eq!(parsed.rewards, Decimal::from_str("100.5").unwrap());
1372 }
1373
1374 #[test]
1375 fn referral_state_camel_case_keys() {
1376 let state = HlReferralState::new(None, None, Decimal::ZERO, Decimal::ZERO);
1377 let json = serde_json::to_string(&state).unwrap();
1378 assert!(json.contains("cumVlm"));
1379 assert!(json.contains("referralCode"));
1380 }
1381}