nautilus_model/defi/pool_analysis/
error.rs1use 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("Liquidity overflow at {location}: current={current}, delta={delta}")]
121 LiquidityOverflow {
122 location: PoolEventLocation,
123 current: u128,
124 delta: u128,
125 },
126
127 #[error("Liquidity underflow at {location}: current={current}, delta={delta}")]
128 LiquidityUnderflow {
129 location: PoolEventLocation,
130 current: u128,
131 delta: u128,
132 },
133
134 #[error(
135 "No events processed yet for pool {instrument_id} ({pool_identifier}); cannot extract snapshot"
136 )]
137 NoEventsProcessed {
138 instrument_id: InstrumentId,
139 pool_identifier: PoolIdentifier,
140 },
141}
142
143impl PoolProfilerError {
144 #[must_use]
146 pub fn location(&self) -> Option<&PoolEventLocation> {
147 match self {
148 Self::LiquidityOverflow { location, .. }
149 | Self::LiquidityUnderflow { location, .. } => Some(location),
150 _ => None,
151 }
152 }
153}
154
155#[must_use]
157pub fn liquidity_error_with_location(
158 err: LiquidityMathError,
159 location: PoolEventLocation,
160) -> PoolProfilerError {
161 match err {
162 LiquidityMathError::Overflow { current, delta } => PoolProfilerError::LiquidityOverflow {
163 location,
164 current,
165 delta,
166 },
167 LiquidityMathError::Underflow { current, delta } => PoolProfilerError::LiquidityUnderflow {
168 location,
169 current,
170 delta,
171 },
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use alloy_primitives::address;
178 use rstest::{fixture, rstest};
179
180 use super::*;
181
182 #[fixture]
183 fn location() -> PoolEventLocation {
184 PoolEventLocation {
185 instrument_id: InstrumentId::from(
186 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
187 ),
188 pool_identifier: PoolIdentifier::from_address(address!(
189 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
190 )),
191 block: 12_345,
192 transaction_index: 7,
193 log_index: 42,
194 event_kind: PoolEventKind::Burn,
195 }
196 }
197
198 #[rstest]
199 fn test_liquidity_error_with_location_maps_overflow(location: PoolEventLocation) {
200 let err = liquidity_error_with_location(
201 LiquidityMathError::Overflow {
202 current: 10,
203 delta: 20,
204 },
205 location.clone(),
206 );
207
208 match err {
209 PoolProfilerError::LiquidityOverflow {
210 location: out_loc,
211 current,
212 delta,
213 } => {
214 assert_eq!(current, 10);
215 assert_eq!(delta, 20);
216 assert_eq!(out_loc.instrument_id, location.instrument_id);
217 assert_eq!(out_loc.pool_identifier, location.pool_identifier);
218 assert_eq!(out_loc.block, location.block);
219 assert_eq!(out_loc.transaction_index, location.transaction_index);
220 assert_eq!(out_loc.log_index, location.log_index);
221 assert_eq!(out_loc.event_kind, location.event_kind);
222 }
223 other => panic!("expected LiquidityOverflow, was {other:?}"),
224 }
225 }
226
227 #[rstest]
228 fn test_liquidity_error_with_location_maps_underflow(location: PoolEventLocation) {
229 let err = liquidity_error_with_location(
230 LiquidityMathError::Underflow {
231 current: 5,
232 delta: 9,
233 },
234 location,
235 );
236
237 match err {
238 PoolProfilerError::LiquidityUnderflow { current, delta, .. } => {
239 assert_eq!(current, 5);
240 assert_eq!(delta, 9);
241 }
242 other => panic!("expected LiquidityUnderflow, was {other:?}"),
243 }
244 }
245
246 #[rstest]
247 fn test_pool_profiler_error_location_accessor(location: PoolEventLocation) {
248 let overflow = PoolProfilerError::LiquidityOverflow {
249 location: location.clone(),
250 current: 1,
251 delta: 2,
252 };
253 assert!(overflow.location().is_some());
254
255 let underflow = PoolProfilerError::LiquidityUnderflow {
256 location,
257 current: 3,
258 delta: 4,
259 };
260 assert!(underflow.location().is_some());
261
262 let not_init = PoolProfilerError::NotInitialized {
263 instrument_id: InstrumentId::from(
264 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
265 ),
266 pool_identifier: PoolIdentifier::from_address(address!(
267 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
268 )),
269 event_kind: PoolEventKind::Swap,
270 };
271 assert!(not_init.location().is_none());
272 }
273
274 #[rstest]
275 #[case(PoolEventKind::Initialize, "Initialize")]
276 #[case(PoolEventKind::Swap, "Swap")]
277 #[case(PoolEventKind::Mint, "Mint")]
278 #[case(PoolEventKind::Burn, "Burn")]
279 #[case(PoolEventKind::Collect, "Collect")]
280 #[case(PoolEventKind::Flash, "Flash")]
281 fn test_pool_event_kind_display(#[case] kind: PoolEventKind, #[case] expected: &str) {
282 assert_eq!(kind.to_string(), expected);
283 }
284
285 #[rstest]
286 fn test_pool_event_location_display_contains_required_fields(location: PoolEventLocation) {
287 let s = location.to_string();
288 assert!(s.contains("0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"));
289 assert!(s.contains("Arbitrum:UniswapV3"));
290 assert!(s.contains("block=12345"));
291 assert!(s.contains("tx_index=7"));
292 assert!(s.contains("log_index=42"));
293 assert!(s.contains("event=Burn"));
294 }
295
296 #[rstest]
297 fn test_pool_profiler_error_display_carries_full_context(location: PoolEventLocation) {
298 let underflow = PoolProfilerError::LiquidityUnderflow {
300 location: location.clone(),
301 current: 10,
302 delta: 99,
303 };
304 let s = underflow.to_string();
305 assert!(s.contains("0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"));
306 assert!(s.contains("block=12345"));
307 assert!(s.contains("tx_index=7"));
308 assert!(s.contains("log_index=42"));
309 assert!(s.contains("event=Burn"));
310 assert!(s.contains("current=10"));
311 assert!(s.contains("delta=99"));
312
313 let overflow = PoolProfilerError::LiquidityOverflow {
314 location,
315 current: 100,
316 delta: 200,
317 };
318 let s = overflow.to_string();
319 assert!(s.contains("current=100"));
320 assert!(s.contains("delta=200"));
321
322 let not_init = PoolProfilerError::NotInitialized {
323 instrument_id: InstrumentId::from(
324 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
325 ),
326 pool_identifier: PoolIdentifier::from_address(address!(
327 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
328 )),
329 event_kind: PoolEventKind::Mint,
330 };
331 let s = not_init.to_string();
332 assert!(s.contains("Arbitrum:UniswapV3"));
333 assert!(s.contains("Mint"));
334 assert!(s.contains("not initialized"));
335
336 let mismatch = PoolProfilerError::InitialTickMismatch {
337 instrument_id: InstrumentId::from(
338 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234.Arbitrum:UniswapV3",
339 ),
340 pool_identifier: PoolIdentifier::from_address(address!(
341 "0xBBf3209130dF7d19356d72Eb8a193e2D9Ec5c234"
342 )),
343 initial_tick: -100,
344 calculated_tick: 200,
345 };
346 let s = mismatch.to_string();
347 assert!(s.contains("initial_tick=-100"));
348 assert!(s.contains("computed_from_sqrt_price=200"));
349 }
350}