1use std::fmt::Display;
24
25use crate::{defi::PoolIdentifier, identifiers::InstrumentId};
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum PoolEventKind {
30 Initialize,
31 Swap,
32 Mint,
33 Burn,
34 Collect,
35 Flash,
36}
37
38impl Display for PoolEventKind {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 let name = match self {
41 Self::Initialize => "Initialize",
42 Self::Swap => "Swap",
43 Self::Mint => "Mint",
44 Self::Burn => "Burn",
45 Self::Collect => "Collect",
46 Self::Flash => "Flash",
47 };
48 f.write_str(name)
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct PoolEventLocation {
55 pub instrument_id: InstrumentId,
56 pub pool_identifier: PoolIdentifier,
57 pub block: u64,
58 pub transaction_index: u32,
59 pub log_index: u32,
60 pub event_kind: PoolEventKind,
61}
62
63impl Display for PoolEventLocation {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 write!(
66 f,
67 "pool={} ({}) block={} tx_index={} log_index={} event={}",
68 self.instrument_id,
69 self.pool_identifier,
70 self.block,
71 self.transaction_index,
72 self.log_index,
73 self.event_kind,
74 )
75 }
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
81pub enum LiquidityMathError {
82 #[error("Liquidity addition overflow: current={current}, delta={delta}")]
83 Overflow { current: u128, delta: u128 },
84 #[error("Liquidity subtraction underflow: current={current}, delta={delta}")]
85 Underflow { current: u128, delta: u128 },
86}
87
88#[derive(Debug, thiserror::Error)]
94pub enum PoolProfilerError {
95 #[error("Pool {instrument_id} ({pool_identifier}) already initialized")]
96 AlreadyInitialized {
97 instrument_id: InstrumentId,
98 pool_identifier: PoolIdentifier,
99 },
100
101 #[error(
102 "Pool {instrument_id} ({pool_identifier}) is not initialized while processing {event_kind}"
103 )]
104 NotInitialized {
105 instrument_id: InstrumentId,
106 pool_identifier: PoolIdentifier,
107 event_kind: PoolEventKind,
108 },
109
110 #[error(
111 "Initial tick mismatch for pool {instrument_id} ({pool_identifier}): pool.initial_tick={initial_tick}, computed_from_sqrt_price={calculated_tick}"
112 )]
113 InitialTickMismatch {
114 instrument_id: InstrumentId,
115 pool_identifier: PoolIdentifier,
116 initial_tick: i32,
117 calculated_tick: i32,
118 },
119
120 #[error(
121 "Swap replay mismatch at {location}: simulated_tick={simulated_tick}, event_tick={event_tick}, simulated_liquidity={simulated_liquidity}, event_liquidity={event_liquidity}, simulated_crossed_tick_count={simulated_crossed_tick_count}, anchoring_crossed_tick={anchoring_crossed_tick:?}"
122 )]
123 SwapReplayMismatch {
124 location: PoolEventLocation,
125 simulated_tick: i32,
126 event_tick: i32,
127 simulated_liquidity: u128,
128 event_liquidity: u128,
129 simulated_crossed_tick_count: usize,
130 anchoring_crossed_tick: Option<i32>,
131 },
132
133 #[error("Liquidity overflow at {location}: current={current}, delta={delta}")]
134 LiquidityOverflow {
135 location: PoolEventLocation,
136 current: u128,
137 delta: u128,
138 },
139
140 #[error("Liquidity underflow at {location}: current={current}, delta={delta}")]
141 LiquidityUnderflow {
142 location: PoolEventLocation,
143 current: u128,
144 delta: u128,
145 },
146
147 #[error(
148 "No events processed yet for pool {instrument_id} ({pool_identifier}); cannot extract snapshot"
149 )]
150 NoEventsProcessed {
151 instrument_id: InstrumentId,
152 pool_identifier: PoolIdentifier,
153 },
154}
155
156impl PoolProfilerError {
157 #[must_use]
159 pub fn location(&self) -> Option<&PoolEventLocation> {
160 match self {
161 Self::LiquidityOverflow { location, .. }
162 | Self::LiquidityUnderflow { location, .. }
163 | Self::SwapReplayMismatch { location, .. } => Some(location),
164 _ => None,
165 }
166 }
167}
168
169#[must_use]
171pub fn liquidity_error_with_location(
172 err: LiquidityMathError,
173 location: PoolEventLocation,
174) -> PoolProfilerError {
175 match err {
176 LiquidityMathError::Overflow { current, delta } => PoolProfilerError::LiquidityOverflow {
177 location,
178 current,
179 delta,
180 },
181 LiquidityMathError::Underflow { current, delta } => PoolProfilerError::LiquidityUnderflow {
182 location,
183 current,
184 delta,
185 },
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use alloy_primitives::address;
192 use rstest::{fixture, rstest};
193
194 use super::*;
195
196 #[fixture]
197 fn location() -> PoolEventLocation {
198 PoolEventLocation {
199 instrument_id: InstrumentId::from(
200 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
201 ),
202 pool_identifier: PoolIdentifier::from_address(address!(
203 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
204 )),
205 block: 12_345,
206 transaction_index: 7,
207 log_index: 42,
208 event_kind: PoolEventKind::Burn,
209 }
210 }
211
212 #[rstest]
213 fn test_liquidity_error_with_location_maps_overflow(location: PoolEventLocation) {
214 let err = liquidity_error_with_location(
215 LiquidityMathError::Overflow {
216 current: 10,
217 delta: 20,
218 },
219 location.clone(),
220 );
221
222 match err {
223 PoolProfilerError::LiquidityOverflow {
224 location: out_loc,
225 current,
226 delta,
227 } => {
228 assert_eq!(current, 10);
229 assert_eq!(delta, 20);
230 assert_eq!(out_loc.instrument_id, location.instrument_id);
231 assert_eq!(out_loc.pool_identifier, location.pool_identifier);
232 assert_eq!(out_loc.block, location.block);
233 assert_eq!(out_loc.transaction_index, location.transaction_index);
234 assert_eq!(out_loc.log_index, location.log_index);
235 assert_eq!(out_loc.event_kind, location.event_kind);
236 }
237 other => panic!("expected LiquidityOverflow, was {other:?}"),
238 }
239 }
240
241 #[rstest]
242 fn test_liquidity_error_with_location_maps_underflow(location: PoolEventLocation) {
243 let err = liquidity_error_with_location(
244 LiquidityMathError::Underflow {
245 current: 5,
246 delta: 9,
247 },
248 location,
249 );
250
251 match err {
252 PoolProfilerError::LiquidityUnderflow { current, delta, .. } => {
253 assert_eq!(current, 5);
254 assert_eq!(delta, 9);
255 }
256 other => panic!("expected LiquidityUnderflow, was {other:?}"),
257 }
258 }
259
260 #[rstest]
261 fn test_pool_profiler_error_location_accessor(location: PoolEventLocation) {
262 let overflow = PoolProfilerError::LiquidityOverflow {
263 location: location.clone(),
264 current: 1,
265 delta: 2,
266 };
267 assert!(overflow.location().is_some());
268
269 let underflow = PoolProfilerError::LiquidityUnderflow {
270 location,
271 current: 3,
272 delta: 4,
273 };
274 assert!(underflow.location().is_some());
275
276 let not_init = PoolProfilerError::NotInitialized {
277 instrument_id: InstrumentId::from(
278 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
279 ),
280 pool_identifier: PoolIdentifier::from_address(address!(
281 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
282 )),
283 event_kind: PoolEventKind::Swap,
284 };
285 assert!(not_init.location().is_none());
286 }
287
288 #[rstest]
289 #[case(PoolEventKind::Initialize, "Initialize")]
290 #[case(PoolEventKind::Swap, "Swap")]
291 #[case(PoolEventKind::Mint, "Mint")]
292 #[case(PoolEventKind::Burn, "Burn")]
293 #[case(PoolEventKind::Collect, "Collect")]
294 #[case(PoolEventKind::Flash, "Flash")]
295 fn test_pool_event_kind_display(#[case] kind: PoolEventKind, #[case] expected: &str) {
296 assert_eq!(kind.to_string(), expected);
297 }
298
299 #[rstest]
300 fn test_pool_event_location_display_contains_required_fields(location: PoolEventLocation) {
301 let s = location.to_string();
302 assert!(s.contains("0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"));
303 assert!(s.contains("Arbitrum:UniswapV3"));
304 assert!(s.contains("block=12345"));
305 assert!(s.contains("tx_index=7"));
306 assert!(s.contains("log_index=42"));
307 assert!(s.contains("event=Burn"));
308 }
309
310 #[rstest]
311 fn test_pool_profiler_error_display_carries_full_context(location: PoolEventLocation) {
312 let underflow = PoolProfilerError::LiquidityUnderflow {
314 location: location.clone(),
315 current: 10,
316 delta: 99,
317 };
318 let s = underflow.to_string();
319 assert!(s.contains("0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"));
320 assert!(s.contains("block=12345"));
321 assert!(s.contains("tx_index=7"));
322 assert!(s.contains("log_index=42"));
323 assert!(s.contains("event=Burn"));
324 assert!(s.contains("current=10"));
325 assert!(s.contains("delta=99"));
326
327 let overflow = PoolProfilerError::LiquidityOverflow {
328 location,
329 current: 100,
330 delta: 200,
331 };
332 let s = overflow.to_string();
333 assert!(s.contains("current=100"));
334 assert!(s.contains("delta=200"));
335
336 let not_init = PoolProfilerError::NotInitialized {
337 instrument_id: InstrumentId::from(
338 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
339 ),
340 pool_identifier: PoolIdentifier::from_address(address!(
341 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
342 )),
343 event_kind: PoolEventKind::Mint,
344 };
345 let s = not_init.to_string();
346 assert!(s.contains("Arbitrum:UniswapV3"));
347 assert!(s.contains("Mint"));
348 assert!(s.contains("not initialized"));
349
350 let mismatch = PoolProfilerError::InitialTickMismatch {
351 instrument_id: InstrumentId::from(
352 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
353 ),
354 pool_identifier: PoolIdentifier::from_address(address!(
355 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
356 )),
357 initial_tick: -100,
358 calculated_tick: 200,
359 };
360 let s = mismatch.to_string();
361 assert!(s.contains("initial_tick=-100"));
362 assert!(s.contains("computed_from_sqrt_price=200"));
363 }
364}