hypercall/liquidator/
state.rs1pub use hypercall_types::liquidation_state::*;
12
13#[cfg(test)]
14mod tests {
15 use super::*;
16 use crate::rsm::MarginMode;
17 use hypercall_types::WalletAddress;
18 use rust_decimal::Decimal;
19 use rust_decimal_macros::dec;
20
21 fn test_wallet() -> WalletAddress {
22 "0x1234567890123456789012345678901234567890"
23 .parse()
24 .unwrap()
25 }
26
27 fn partial_metadata() -> PartialLiquidationMetadata {
28 PartialLiquidationMetadata {
29 entered_at: 2_000,
30 mm_shortfall: dec!(1000),
31 target_equity: dec!(6000),
32 escalation_deadline: 62_000,
33 last_reprice_at: Some(3_000),
34 active_order_request_ids: vec!["req-1".to_string()],
35 active_order_client_ids: vec!["liq-1".to_string()],
36 bonus_bps: 125,
37 pending_full_auction_id: None,
38 pending_full_request_id: None,
39 pending_full_tx_hash: None,
40 pending_full_margin_needed: None,
41 }
42 }
43
44 #[test]
45 fn test_healthy_status() {
46 let status = AccountLiquidationStatus::healthy(
47 test_wallet(),
48 MarginMode::Standard,
49 dec!(10000),
50 dec!(5000),
51 1000,
52 );
53
54 assert!(status.state.is_healthy());
55 assert!(!status.needs_liquidation());
56 assert_eq!(status.shortfall(), Decimal::ZERO);
57 assert_eq!(status.maintenance_margin, dec!(5000));
58 }
59
60 #[test]
61 fn test_pre_liquidation_transition() {
62 let mut status = AccountLiquidationStatus::healthy(
63 test_wallet(),
64 MarginMode::Standard,
65 dec!(4000),
66 dec!(5000),
67 1000,
68 );
69
70 assert!(status.needs_liquidation());
71 status.enter_pre_liquidation(partial_metadata(), 2_000);
72
73 assert!(status.state.is_pre_liquidation());
74 assert!(status.state.should_block_risk_increasing());
75
76 match &status.state {
77 LiquidationState::PreLiquidation(metadata) => {
78 assert_eq!(metadata.mm_shortfall, dec!(1000));
79 assert_eq!(metadata.target_equity, dec!(6000));
80 assert_eq!(metadata.active_order_client_ids, vec!["liq-1".to_string()]);
81 }
82 _ => panic!("expected pre-liquidation"),
83 }
84 }
85
86 #[test]
87 fn test_recovery() {
88 let mut status = AccountLiquidationStatus::healthy(
89 test_wallet(),
90 MarginMode::Standard,
91 dec!(4000),
92 dec!(5000),
93 1000,
94 );
95
96 status.enter_pre_liquidation(partial_metadata(), 2_000);
97 status.update_health(dec!(10000), dec!(5000), 3_000);
98 status.recover_to_healthy(3_000);
99
100 assert!(status.state.is_healthy());
101 assert!(!status.needs_liquidation());
102 }
103
104 #[test]
105 fn test_liquidation_flow() {
106 let mut status = AccountLiquidationStatus::healthy(
107 test_wallet(),
108 MarginMode::Standard,
109 dec!(4000),
110 dec!(5000),
111 1000,
112 );
113
114 status.enter_pre_liquidation(partial_metadata(), 2_000);
115 assert!(status.state.is_pre_liquidation());
116
117 status.enter_liquidation(
118 FullLiquidationMetadata {
119 auction_id: "auction-123".to_string(),
120 request_id: Some("full-req-1".to_string()),
121 tx_hash: None,
122 started_at: 3_000,
123 chain_start_time: None,
124 margin_needed: dec!(2500),
125 stop_request_id: None,
126 stop_tx_hash: None,
127 },
128 3_000,
129 );
130 assert!(status.state.is_in_liquidation());
131 assert_eq!(status.state.auction_id(), Some("auction-123"));
132
133 status.complete_liquidation(
134 LiquidatedMetadata {
135 auction_id: "auction-123".to_string(),
136 completed_at: 4_000,
137 winner: Some(test_wallet()),
138 bonus: dec!(250),
139 tx_hash: Some("0xdead".to_string()),
140 },
141 4_000,
142 );
143 assert!(status.state.is_liquidated());
144 assert_eq!(status.state.auction_id(), Some("auction-123"));
145 }
146
147 #[test]
148 fn test_state_display_and_mode() {
149 assert_eq!(LiquidationState::Healthy.to_string(), "Healthy");
150 assert_eq!(LiquidationState::Healthy.as_str(), "Healthy");
151 assert_eq!(LiquidationState::Healthy.liquidation_mode(), None);
152
153 let pre_liq = LiquidationState::PreLiquidation(partial_metadata());
154 assert_eq!(pre_liq.as_str(), "PreLiquidation");
155 assert_eq!(pre_liq.liquidation_mode(), Some(LiquidationMode::Partial));
156 assert!(pre_liq.to_string().contains("target_equity=6000"));
157
158 let mut pending_full = partial_metadata();
159 pending_full.pending_full_auction_id = Some("auction-1".to_string());
160 pending_full.pending_full_request_id = Some("req-1".to_string());
161 let pending_state = LiquidationState::PreLiquidation(pending_full);
162 assert_eq!(
163 pending_state.liquidation_mode(),
164 Some(LiquidationMode::Full)
165 );
166 assert_eq!(pending_state.auction_id(), Some("auction-1"));
167 }
168
169 #[test]
170 fn test_serde() {
171 let state = LiquidationState::PreLiquidation(partial_metadata());
172 let json = sonic_rs::to_string(&state).unwrap();
173 assert!(json.contains("pre_liquidation"));
174
175 let parsed: LiquidationState = sonic_rs::from_str(&json).unwrap();
176 assert_eq!(parsed, state);
177 }
178
179 #[test]
180 fn test_partial_projection_change_detects_live_liquidation_deltas() {
181 let previous = LiquidationState::PreLiquidation(partial_metadata());
182 let mut current_metadata = partial_metadata();
183 current_metadata.bonus_bps = 250;
184 current_metadata.active_order_client_ids = vec!["liq-2".to_string()];
185 current_metadata.mm_shortfall = dec!(1250);
186 let current = LiquidationState::PreLiquidation(current_metadata);
187
188 assert!(has_material_projection_change(&previous, ¤t));
189 }
190}