Skip to main content

nautilus_model/defi/pool_analysis/
profiler.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Pool profiling utilities for analyzing DeFi pool event data.
17
18use ahash::AHashMap;
19use alloy_primitives::{Address, I256, U160, U256};
20use nautilus_core::UnixNanos;
21
22use crate::defi::{
23    PoolLiquidityUpdate, PoolSwap, SharedPool,
24    data::{
25        DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate,
26        PoolLiquidityUpdateType, block::BlockPosition, flash::PoolFlash,
27    },
28    pool_analysis::{
29        error::{
30            PoolEventKind, PoolEventLocation, PoolProfilerError, liquidity_error_with_location,
31        },
32        position::PoolPosition,
33        quote::SwapQuote,
34        size_estimator,
35        snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
36        swap_math::compute_swap_step,
37    },
38    reporting::{BlockchainSyncReportItems, BlockchainSyncReporter},
39    tick_map::{
40        TickMap,
41        full_math::{FullMath, Q128},
42        liquidity_math::{liquidity_math_add, try_liquidity_math_add},
43        sqrt_price_math::{get_amount0_delta, get_amount1_delta, get_amounts_for_liquidity},
44        tick::{CrossedTick, PoolTick},
45        tick_math::{
46            MAX_SQRT_RATIO, MIN_SQRT_RATIO, get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio,
47        },
48    },
49};
50
51/// A DeFi pool state tracker and event processor for UniswapV3-style AMM pools.
52///
53/// The `PoolProfiler` provides complete pool state management including:
54/// - Liquidity position tracking and management.
55/// - Tick crossing and price movement simulation.
56/// - Fee accumulation and distribution tracking.
57/// - Protocol fee calculation.
58/// - Pool state validation and maintenance.
59///
60/// This profiler can both process historical events and execute new operations,
61/// making it suitable for both backtesting and simulation scenarios.
62///
63/// # Usage
64///
65/// Create a new profiler with a pool definition, initialize it with a starting price,
66/// then either process historical events or execute new pool operations to simulate
67/// trading activity and analyze pool behavior.
68#[derive(Debug, Clone)]
69#[cfg_attr(
70    feature = "python",
71    pyo3::pyclass(module = "nautilus_trader.core.nautilus_pyo3.model", from_py_object)
72)]
73#[cfg_attr(
74    feature = "python",
75    pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
76)]
77pub struct PoolProfiler {
78    /// Pool definition.
79    pub pool: SharedPool,
80    /// Position tracking by position key (`owner:tick_lower:tick_upper`).
81    positions: AHashMap<String, PoolPosition>,
82    /// Tick map managing liquidity distribution across price ranges.
83    pub tick_map: TickMap,
84    /// Global pool state including current price, tick, and cumulative flows with fees.
85    pub state: PoolState,
86    /// Analytics counters tracking pool operations and performance metrics.
87    pub analytics: PoolAnalytics,
88    /// The block position of the last processed event.
89    pub last_processed_event: Option<BlockPosition>,
90    /// The event timestamp of the last processed event.
91    pub last_processed_ts: Option<UnixNanos>,
92    /// Flag indicating whether the pool has been initialized with a starting price.
93    pub is_initialized: bool,
94    /// Optional progress reporter for tracking event processing.
95    reporter: Option<BlockchainSyncReporter>,
96    /// The last block number that was reported (used for progress tracking).
97    last_reported_block: u64,
98}
99
100impl PoolProfiler {
101    /// Creates a new [`PoolProfiler`] instance for tracking pool state and events.
102    ///
103    /// # Panics
104    ///
105    /// Panics if the pool's tick spacing is not set.
106    #[must_use]
107    pub fn new(pool: SharedPool) -> Self {
108        let tick_spacing = pool.tick_spacing.expect("Pool tick spacing must be set");
109        Self {
110            pool,
111            positions: AHashMap::new(),
112            tick_map: TickMap::new(tick_spacing),
113            state: PoolState::default(),
114            analytics: PoolAnalytics::default(),
115            last_processed_event: None,
116            last_processed_ts: None,
117            is_initialized: false,
118            reporter: None,
119            last_reported_block: 0,
120        }
121    }
122
123    /// Initializes the pool with a starting price and activates the profiler.
124    ///
125    /// # Errors
126    ///
127    /// Returns [`PoolProfilerError::AlreadyInitialized`] if the profiler has already been
128    /// initialized, or [`PoolProfilerError::InitialTickMismatch`] if the pool config carries
129    /// an `initial_tick` that disagrees with the tick derived from `price_sqrt_ratio_x96`.
130    pub fn initialize(&mut self, price_sqrt_ratio_x96: U160) -> Result<(), PoolProfilerError> {
131        if self.is_initialized {
132            return Err(PoolProfilerError::AlreadyInitialized {
133                instrument_id: self.pool.instrument_id,
134                pool_identifier: self.pool.pool_identifier,
135            });
136        }
137
138        let calculated_tick = get_tick_at_sqrt_ratio(price_sqrt_ratio_x96);
139
140        if let Some(initial_tick) = self.pool.initial_tick
141            && initial_tick != calculated_tick
142        {
143            return Err(PoolProfilerError::InitialTickMismatch {
144                instrument_id: self.pool.instrument_id,
145                pool_identifier: self.pool.pool_identifier,
146                initial_tick,
147                calculated_tick,
148            });
149        }
150
151        log::info!(
152            "Initializing pool profiler with tick {calculated_tick} and price sqrt ratio {price_sqrt_ratio_x96}"
153        );
154
155        self.state.current_tick = calculated_tick;
156        self.state.price_sqrt_ratio_x96 = price_sqrt_ratio_x96;
157        self.is_initialized = true;
158        Ok(())
159    }
160
161    /// Returns an error if the pool has not been initialized.
162    ///
163    /// # Errors
164    ///
165    /// Returns [`PoolProfilerError::NotInitialized`] when [`Self::initialize`] or
166    /// [`Self::restore_from_snapshot`] has not been called yet.
167    pub fn check_if_initialized(&self, event_kind: PoolEventKind) -> Result<(), PoolProfilerError> {
168        if !self.is_initialized {
169            return Err(PoolProfilerError::NotInitialized {
170                instrument_id: self.pool.instrument_id,
171                pool_identifier: self.pool.pool_identifier,
172                event_kind,
173            });
174        }
175        Ok(())
176    }
177
178    fn event_location(
179        &self,
180        event_kind: PoolEventKind,
181        block: u64,
182        transaction_index: u32,
183        log_index: u32,
184    ) -> PoolEventLocation {
185        PoolEventLocation {
186            instrument_id: self.pool.instrument_id,
187            pool_identifier: self.pool.pool_identifier,
188            block,
189            transaction_index,
190            log_index,
191            event_kind,
192        }
193    }
194
195    /// Processes a historical pool event and updates internal state.
196    ///
197    /// Handles all types of pool events (swaps, mints, burns, fee collections),
198    /// and updates the profiler's internal state accordingly. This is the main
199    /// entry point for processing historical blockchain events.
200    ///
201    /// # Errors
202    ///
203    /// This function returns an error if:
204    /// - Pool is not initialized.
205    /// - Event contains invalid data (tick ranges, amounts).
206    /// - Mathematical operations overflow.
207    pub fn process(&mut self, event: &DexPoolData) -> anyhow::Result<()> {
208        if self.check_if_already_processed(
209            event.block_number(),
210            event.transaction_index(),
211            event.log_index(),
212        ) {
213            return Ok(());
214        }
215
216        match event {
217            DexPoolData::Swap(swap) => self.process_swap(swap)?,
218            DexPoolData::LiquidityUpdate(update) => match update.kind {
219                PoolLiquidityUpdateType::Mint => self.process_mint(update)?,
220                PoolLiquidityUpdateType::Burn => self.process_burn(update)?,
221            },
222            DexPoolData::FeeCollect(collect) => self.process_collect(collect)?,
223            DexPoolData::FeeProtocolUpdate(update) => self.process_fee_protocol_update(update)?,
224            DexPoolData::FeeProtocolCollect(collect) => {
225                self.process_fee_protocol_collect(collect)?;
226            }
227            DexPoolData::Flash(flash) => self.process_flash(flash)?,
228        }
229
230        self.update_reporter_if_enabled(event.block_number());
231
232        Ok(())
233    }
234
235    // Checks if we need to skip events at or before the last processed event to prevent double-processing.
236    fn check_if_already_processed(&self, block: u64, tx_idx: u32, log_idx: u32) -> bool {
237        if let Some(last_event) = &self.last_processed_event {
238            let should_skip = block < last_event.number
239                || (block == last_event.number && tx_idx < last_event.transaction_index)
240                || (block == last_event.number
241                    && tx_idx == last_event.transaction_index
242                    && log_idx <= last_event.log_index);
243
244            if should_skip {
245                log::debug!(
246                    "Skipping already processed event at block {block} tx {tx_idx} log {log_idx}"
247                );
248            }
249            return should_skip;
250        }
251
252        false
253    }
254
255    /// Auto-updates reporter if it's enabled.
256    fn update_reporter_if_enabled(&mut self, current_block: u64) {
257        // Auto-update reporter if enabled
258        if let Some(reporter) = &mut self.reporter {
259            let blocks_processed = current_block.saturating_sub(self.last_reported_block);
260
261            if blocks_processed > 0 {
262                reporter.update(blocks_processed as usize);
263                self.last_reported_block = current_block;
264
265                if reporter.should_log_progress(current_block, current_block) {
266                    reporter.log_progress(current_block);
267                }
268            }
269        }
270    }
271
272    /// Processes a historical swap event from blockchain data.
273    ///
274    /// Replays the swap by simulating it through [`Self::simulate_swap_through_ticks`],
275    /// then verifies the simulation results against the actual event data. If mismatches
276    /// are detected (tick or liquidity), the pool state is corrected to match the event
277    /// values and warnings are logged.
278    ///
279    /// This self-healing approach ensures pool state stays synchronized with on-chain
280    /// reality even if simulation logic differs slightly from actual contract behavior.
281    ///
282    /// # Use Case
283    ///
284    /// Historical event processing when rebuilding pool state from blockchain events.
285    ///
286    /// # Errors
287    ///
288    /// This function returns an error if:
289    /// - Pool initialization checks fail.
290    /// - Swap simulation fails (see [`Self::simulate_swap_through_ticks`] errors).
291    pub fn process_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
292        self.check_if_initialized(PoolEventKind::Swap)?;
293
294        if self.check_if_already_processed(swap.block, swap.transaction_index, swap.log_index) {
295            return Ok(());
296        }
297
298        let zero_for_one = swap.amount0.is_positive();
299        let amount_specified = if zero_for_one {
300            swap.amount0
301        } else {
302            swap.amount1
303        };
304
305        // For price limit use the final sqrt price from swap, which is a
306        // good proxy to price limit
307        let sqrt_price_limit_x96 = swap.sqrt_price_x96;
308        let location = self.event_location(
309            PoolEventKind::Swap,
310            swap.block,
311            swap.transaction_index,
312            swap.log_index,
313        );
314        let swap_quote = self
315            .simulate_swap_through_ticks(amount_specified, zero_for_one, sqrt_price_limit_x96, true)
316            .map_err(|e| Self::wrap_liquidity_error(e, location))?;
317
318        self.apply_swap_quote(&swap_quote);
319
320        // Verify simulation against event data - correct with event values if mismatch detected
321        if swap.tick != self.state.current_tick {
322            log::warn!(
323                "Inconsistency in swap processing: Current tick mismatch: simulated {}, event {} on block {}",
324                self.state.current_tick,
325                swap.tick,
326                swap.block
327            );
328            self.state.current_tick = swap.tick;
329        }
330
331        if swap.liquidity != self.tick_map.liquidity {
332            log::warn!(
333                "Inconsistency in swap processing: Active liquidity mismatch: simulated {}, event {} on block {}",
334                self.tick_map.liquidity,
335                swap.liquidity,
336                swap.block
337            );
338            self.tick_map.liquidity = swap.liquidity;
339        }
340
341        if swap.sqrt_price_x96 != self.state.price_sqrt_ratio_x96 {
342            log::warn!(
343                "Inconsistency in swap processing: Sqrt price mismatch: simulated {}, event {} on block {}",
344                self.state.price_sqrt_ratio_x96,
345                swap.sqrt_price_x96,
346                swap.block
347            );
348            self.state.price_sqrt_ratio_x96 = swap.sqrt_price_x96;
349        }
350
351        self.last_processed_event = Some(BlockPosition::new(
352            swap.block,
353            swap.transaction_hash.clone(),
354            swap.transaction_index,
355            swap.log_index,
356        ));
357        self.last_processed_ts = Some(swap.ts_event);
358        self.update_reporter_if_enabled(swap.block);
359        self.update_liquidity_analytics();
360
361        Ok(())
362    }
363
364    /// Executes a new simulated swap and returns the resulting event.
365    ///
366    /// This is the public API for forward simulation of swap operations. It delegates
367    /// the core swap mathematics to [`Self::simulate_swap_through_ticks`], then wraps
368    /// the results in a [`PoolSwap`] event structure with full metadata.
369    ///
370    /// # Errors
371    ///
372    /// Returns errors from [`Self::simulate_swap_through_ticks`]:
373    /// - Pool metadata missing or invalid
374    /// - Price limit violations
375    /// - Arithmetic overflow in fee or liquidity calculations
376    pub fn execute_swap(
377        &mut self,
378        sender: Address,
379        recipient: Address,
380        block: BlockPosition,
381        zero_for_one: bool,
382        amount_specified: I256,
383        sqrt_price_limit_x96: U160,
384    ) -> anyhow::Result<PoolSwap> {
385        self.check_if_initialized(PoolEventKind::Swap)?;
386
387        let swap_quote = self.simulate_swap_through_ticks(
388            amount_specified,
389            zero_for_one,
390            sqrt_price_limit_x96,
391            false,
392        )?;
393
394        self.apply_swap_quote(&swap_quote);
395
396        let swap_event = PoolSwap::new(
397            self.pool.chain.clone(),
398            self.pool.dex.clone(),
399            self.pool.instrument_id,
400            self.pool.pool_identifier,
401            block.number,
402            block.transaction_hash,
403            block.transaction_index,
404            block.log_index,
405            self.pool.ts_init, // ts_event (simulated; pool init time)
406            self.pool.ts_init, // ts_init
407            sender,
408            recipient,
409            swap_quote.amount0,
410            swap_quote.amount1,
411            self.state.price_sqrt_ratio_x96,
412            self.tick_map.liquidity,
413            self.state.current_tick,
414        );
415        Ok(swap_event)
416    }
417
418    /// Core **read-only** swap simulation engine implementing `UniswapV3` mathematics.
419    ///
420    /// This method performs a complete swap simulation without modifying pool state,
421    /// working entirely on stack-allocated local copies of state variables. It returns
422    /// a [`SwapQuote`] containing all swap results and profiling data,
423    /// including a complete audit trail of crossed ticks.
424    ///
425    ///
426    /// # Algorithm Overview
427    ///
428    /// 1. **Iterative price curve traversal**: Walks through liquidity ranges until
429    ///    the input/output amount is exhausted or the price limit is reached
430    /// 2. **Tick crossing tracking**: When crossing initialized tick boundaries, records
431    ///    the crossing in `crossed_ticks` vector with complete state snapshot (tick, direction, fee growth)
432    /// 3. **Local liquidity updates**: Tracks liquidity changes in local variables by reading
433    ///    `liquidity_net` from tick map (read-only, no mutations)
434    /// 4. **Fee calculation**: Splits fees between LPs and protocol, accumulates in local variables
435    /// 5. **Quote assembly**: Returns [`SwapQuote`] with amounts, prices, fees, and crossed tick data
436    ///
437    /// When `traverse_empty_ranges` is set, the walk continues across zero-liquidity ranges
438    /// to `sqrt_price_limit_x96` even after the amount is exhausted. This reproduces a
439    /// historical swap whose recorded amount (the on-chain consumed amount) runs out at the
440    /// last liquid tick before an empty range to the boundary; forward simulation leaves it
441    /// unset so the swap stops where the amount is spent, matching `UniswapV3`.
442    ///
443    /// # Errors
444    ///
445    /// Returns error if:
446    /// - Pool fee is not configured
447    /// - Fee growth arithmetic overflows when scaling by liquidity
448    /// - Swap step calculations fail
449    ///
450    /// # Panics
451    ///
452    /// Panics if the pool fee has not been initialized.
453    pub fn simulate_swap_through_ticks(
454        &self,
455        amount_specified: I256,
456        zero_for_one: bool,
457        sqrt_price_limit_x96: U160,
458        traverse_empty_ranges: bool,
459    ) -> anyhow::Result<SwapQuote> {
460        let exact_input = amount_specified.is_positive();
461        let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
462
463        let mut current_sqrt_price = self.state.price_sqrt_ratio_x96;
464        let mut current_tick = self.state.current_tick;
465        let mut current_active_liquidity = self.tick_map.liquidity;
466        let mut amount_specified_remaining = amount_specified;
467        let mut amount_calculated = I256::ZERO;
468        let mut protocol_fee = U256::ZERO;
469        let mut lp_fee = U256::ZERO;
470        let mut crossed_ticks = Vec::new();
471
472        // Swapping cache variables
473        let fee_protocol = if zero_for_one {
474            // Extract lower 4 bits for token0 protocol fee
475            self.state.fee_protocol % 16
476        } else {
477            // Extract upper 4 bits for token1 protocol fee
478            self.state.fee_protocol >> 4
479        };
480
481        // Track current fee growth during swap
482        let mut current_fee_growth_global = if zero_for_one {
483            self.state.fee_growth_global_0
484        } else {
485            self.state.fee_growth_global_1
486        };
487
488        // The replay clause keeps crossing empty ranges to the limit after the amount runs out
489        while (amount_specified_remaining != I256::ZERO
490            || (traverse_empty_ranges && current_active_liquidity == 0))
491            && sqrt_price_limit_x96 != current_sqrt_price
492        {
493            let sqrt_price_start_x96 = current_sqrt_price;
494
495            let (mut tick_next, initialized) = self
496                .tick_map
497                .next_initialized_tick(current_tick, zero_for_one);
498
499            // Make sure we do not overshoot MIN/MAX tick
500            tick_next = tick_next.clamp(PoolTick::MIN_TICK, PoolTick::MAX_TICK);
501
502            // Get the price for the next tick
503            let sqrt_price_next = get_sqrt_ratio_at_tick(tick_next);
504
505            // Compute values to swap to the target tick, price limit, or point where input/output amount is exhausted
506            let sqrt_price_target = if (zero_for_one && sqrt_price_next < sqrt_price_limit_x96)
507                || (!zero_for_one && sqrt_price_next > sqrt_price_limit_x96)
508            {
509                sqrt_price_limit_x96
510            } else {
511                sqrt_price_next
512            };
513            let swap_step_result = compute_swap_step(
514                current_sqrt_price,
515                sqrt_price_target,
516                current_active_liquidity,
517                amount_specified_remaining,
518                fee_tier,
519            )?;
520
521            // Update current price to the new price after this swap step (BEFORE amount updates, matching Solidity)
522            current_sqrt_price = swap_step_result.sqrt_ratio_next_x96;
523
524            // Update amounts based on swap direction and type
525            if exact_input {
526                // For exact input swaps: subtract input amount and fees from remaining, subtract output from calculated
527                amount_specified_remaining -= FullMath::truncate_to_i256(
528                    swap_step_result.amount_in + swap_step_result.fee_amount,
529                );
530                amount_calculated -= FullMath::truncate_to_i256(swap_step_result.amount_out);
531            } else {
532                // For exact output swaps: add output to remaining, add input and fees to calculated
533                amount_specified_remaining +=
534                    FullMath::truncate_to_i256(swap_step_result.amount_out);
535                amount_calculated += FullMath::truncate_to_i256(
536                    swap_step_result.amount_in + swap_step_result.fee_amount,
537                );
538            }
539
540            // Calculate protocol fee if enabled
541            let mut step_fee_amount = swap_step_result.fee_amount;
542
543            if fee_protocol > 0 {
544                let protocol_fee_delta = swap_step_result.fee_amount / U256::from(fee_protocol);
545                step_fee_amount -= protocol_fee_delta;
546                protocol_fee += protocol_fee_delta;
547            }
548
549            // Accumulate LP fee (protocol fee is already deducted if it exists).
550            lp_fee += step_fee_amount;
551
552            // Update global fee tracker
553            if current_active_liquidity > 0 {
554                let fee_growth_delta =
555                    FullMath::mul_div(step_fee_amount, Q128, U256::from(current_active_liquidity))?;
556                current_fee_growth_global += fee_growth_delta;
557            }
558
559            // Shift tick if we reached the next price
560            if swap_step_result.sqrt_ratio_next_x96 == sqrt_price_next {
561                // We have swapped all the way to the boundary of the next tick.
562                // Time to handle crossing into the next tick, which may change liquidity.
563                // If the tick is initialized, run the tick transition logic (liquidity changes, fee accumulators, etc.).
564                if initialized {
565                    crossed_ticks.push(CrossedTick::new(
566                        tick_next,
567                        zero_for_one,
568                        if zero_for_one {
569                            current_fee_growth_global
570                        } else {
571                            self.state.fee_growth_global_0
572                        },
573                        if zero_for_one {
574                            self.state.fee_growth_global_1
575                        } else {
576                            current_fee_growth_global
577                        },
578                    ));
579
580                    // Update local liquidity tracking when crossing ticks
581                    if let Some(tick_data) = self.tick_map.get_tick(tick_next) {
582                        let liquidity_net = tick_data.liquidity_net;
583                        current_active_liquidity = if zero_for_one {
584                            try_liquidity_math_add(current_active_liquidity, -liquidity_net)?
585                        } else {
586                            try_liquidity_math_add(current_active_liquidity, liquidity_net)?
587                        };
588                    }
589                }
590
591                current_tick = if zero_for_one {
592                    tick_next - 1
593                } else {
594                    tick_next
595                };
596            } else if swap_step_result.sqrt_ratio_next_x96 != sqrt_price_start_x96 {
597                // The price moved during this swap step, but didn't reach a tick boundary.
598                // So, update the tick to match the new price.
599                current_tick = get_tick_at_sqrt_ratio(current_sqrt_price);
600            }
601        }
602
603        // Calculate final amounts
604        let (amount0, amount1) = if zero_for_one == exact_input {
605            (
606                amount_specified - amount_specified_remaining,
607                amount_calculated,
608            )
609        } else {
610            (
611                amount_calculated,
612                amount_specified - amount_specified_remaining,
613            )
614        };
615
616        let quote = SwapQuote::new(
617            self.pool.instrument_id,
618            amount0,
619            amount1,
620            self.state.price_sqrt_ratio_x96,
621            current_sqrt_price,
622            self.state.current_tick,
623            current_tick,
624            current_active_liquidity,
625            current_fee_growth_global,
626            lp_fee,
627            protocol_fee,
628            crossed_ticks,
629        );
630        Ok(quote)
631    }
632
633    /// Applies a swap quote to the pool state (mutations only, no simulation).
634    ///
635    /// # Panics
636    ///
637    /// Panics if applying a tick-crossing liquidity delta overflows or underflows,
638    /// which indicates internal tick-map inconsistency rather than a recoverable
639    /// replay error.
640    pub fn apply_swap_quote(&mut self, swap_quote: &SwapQuote) {
641        self.state.current_tick = swap_quote.tick_after;
642        self.state.price_sqrt_ratio_x96 = swap_quote.sqrt_price_after_x96;
643
644        if swap_quote.zero_for_one() {
645            self.state.fee_growth_global_0 = swap_quote.fee_growth_global_after;
646            self.state.protocol_fees_token0 += swap_quote.protocol_fee;
647        } else {
648            self.state.fee_growth_global_1 = swap_quote.fee_growth_global_after;
649            self.state.protocol_fees_token1 += swap_quote.protocol_fee;
650        }
651
652        for crossed in &swap_quote.crossed_ticks {
653            let liquidity_net =
654                self.tick_map
655                    .cross_tick(crossed.tick, crossed.fee_growth_0, crossed.fee_growth_1);
656
657            self.tick_map.liquidity = if crossed.zero_for_one {
658                liquidity_math_add(self.tick_map.liquidity, -liquidity_net)
659            } else {
660                liquidity_math_add(self.tick_map.liquidity, liquidity_net)
661            };
662        }
663        self.analytics.total_swaps += 1;
664
665        debug_assert_eq!(
666            self.tick_map.liquidity, swap_quote.liquidity_after,
667            "Liquidity mismatch in apply_swap_quote: computed={}, quote={}",
668            self.tick_map.liquidity, swap_quote.liquidity_after
669        );
670    }
671
672    /// Wraps a low-level [`LiquidityMathError`](super::error::LiquidityMathError) into a
673    /// [`PoolProfilerError`] carrying the supplied event location, leaving non-liquidity
674    /// errors untouched.
675    #[must_use]
676    pub fn wrap_liquidity_error(err: anyhow::Error, location: PoolEventLocation) -> anyhow::Error {
677        match err.downcast::<super::error::LiquidityMathError>() {
678            Ok(math_err) => anyhow::Error::from(liquidity_error_with_location(math_err, location)),
679            Err(other) => other,
680        }
681    }
682
683    /// Returns a swap quote without modifying pool state.
684    ///
685    /// This method simulates a swap and provides detailed profiling metrics including:
686    /// - Amounts of tokens that would be exchanged
687    /// - Price before and after the swap
688    /// - Fee breakdown (LP fees and protocol fees)
689    /// - List of crossed ticks with state snapshots
690    ///
691    /// # Errors
692    ///
693    /// Returns error if:
694    /// - Pool fee is not configured
695    /// - Fee growth arithmetic overflows when scaling by liquidity
696    /// - Swap step calculations fail
697    pub fn quote_swap(
698        &self,
699        amount_specified: I256,
700        zero_for_one: bool,
701        sqrt_price_limit_x96: Option<U160>,
702    ) -> anyhow::Result<SwapQuote> {
703        self.check_if_initialized(PoolEventKind::Swap)?;
704
705        if amount_specified.is_zero() {
706            anyhow::bail!("Cannot quote swap with zero amount");
707        }
708
709        if let Some(price_limit) = sqrt_price_limit_x96 {
710            self.validate_price_limit(price_limit, zero_for_one)?;
711        }
712
713        let limit = sqrt_price_limit_x96.unwrap_or_else(|| {
714            if zero_for_one {
715                MIN_SQRT_RATIO + U160::from(1)
716            } else {
717                MAX_SQRT_RATIO - U160::from(1)
718            }
719        });
720
721        self.simulate_swap_through_ticks(amount_specified, zero_for_one, limit, false)
722    }
723
724    /// Simulates an exact input swap (know input amount, calculate output amount).
725    ///
726    /// # Errors
727    /// Returns error if pool is not initialized, input is zero, or price limit is invalid
728    pub fn swap_exact_in(
729        &self,
730        amount_in: U256,
731        zero_for_one: bool,
732        sqrt_price_limit_x96: Option<U160>,
733    ) -> anyhow::Result<SwapQuote> {
734        // Positive = exact input.
735        let amount_specified = I256::from(amount_in);
736        let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
737
738        Ok(quote)
739    }
740
741    /// Simulates an exact output swap (know output amount, calculate required input amount).
742    ///
743    /// # Errors
744    /// Returns error if pool is not initialized, output is zero, price limit is invalid,
745    /// or insufficient liquidity exists to fulfill the exact output amount
746    pub fn swap_exact_out(
747        &self,
748        amount_out: U256,
749        zero_for_one: bool,
750        sqrt_price_limit_x96: Option<U160>,
751    ) -> anyhow::Result<SwapQuote> {
752        // Negative = exact output.
753        let amount_specified = -I256::from(amount_out);
754        let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
755        quote.validate_exact_output(amount_out)?;
756
757        Ok(quote)
758    }
759
760    /// Simulates a swap to move the pool price down to a target price.
761    ///
762    /// # Errors
763    /// Returns error if pool is not initialized or price limit is invalid.
764    pub fn swap_to_lower_sqrt_price(
765        &self,
766        sqrt_price_limit_x96: U160,
767    ) -> anyhow::Result<SwapQuote> {
768        self.quote_swap(I256::MAX, true, Some(sqrt_price_limit_x96))
769    }
770
771    /// Simulates a swap to move the pool price up to a target price.
772    ///
773    /// # Errors
774    /// Returns error if pool is not initialized or price limit is invalid.
775    pub fn swap_to_higher_sqrt_price(
776        &self,
777        sqrt_price_limit_x96: U160,
778    ) -> anyhow::Result<SwapQuote> {
779        self.quote_swap(I256::MAX, false, Some(sqrt_price_limit_x96))
780    }
781
782    /// Finds the maximum trade size that produces a target slippage (including fees).
783    ///
784    /// Uses binary search to find the largest trade size that results in slippage
785    /// at or below the target. The method iteratively simulates swaps at different
786    /// sizes until it converges to the optimal size within the specified tolerance.
787    ///
788    /// # Returns
789    /// The maximum trade size (U256) that produces the target slippage
790    ///
791    /// # Errors
792    /// Returns error if:
793    /// - Impact is zero or exceeds 100% (10000 bps)
794    /// - Pool is not initialized
795    /// - Swap simulations fail
796    pub fn size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> anyhow::Result<U256> {
797        let config = size_estimator::EstimationConfig::default();
798        size_estimator::size_for_impact_bps(self, impact_bps, zero_for_one, &config)
799    }
800
801    /// Finds the maximum trade size with search diagnostics.
802    /// This is the detailed version of [`Self::size_for_impact_bps`] that returns
803    /// extensive information about the search process.It is useful for debugging,
804    /// monitoring, and analyzing search behavior in production.
805    ///
806    /// # Returns
807    /// Detailed result with size and search diagnostics
808    ///
809    /// # Errors
810    /// Returns error if:
811    /// - Impact is zero or exceeds 100% (10000 bps)
812    /// - Pool is not initialized
813    /// - Swap simulations fail
814    pub fn size_for_impact_bps_detailed(
815        &self,
816        impact_bps: u32,
817        zero_for_one: bool,
818    ) -> anyhow::Result<size_estimator::SizeForImpactResult> {
819        let config = size_estimator::EstimationConfig::default();
820        size_estimator::size_for_impact_bps_detailed(self, impact_bps, zero_for_one, &config)
821    }
822
823    /// Validates that the price limit is in the correct direction for the swap.
824    ///
825    /// # Errors
826    /// Returns error if price limit violates swap direction constraints.
827    fn validate_price_limit(
828        &self,
829        limit_price_sqrt: U160,
830        zero_for_one: bool,
831    ) -> anyhow::Result<()> {
832        if zero_for_one {
833            // Swapping token0 for token1: price must decrease
834            if limit_price_sqrt >= self.state.price_sqrt_ratio_x96 {
835                anyhow::bail!("Price limit must be less than current price for zero_for_one swaps");
836            }
837        } else {
838            // Swapping token1 for token0: price must increase
839            if limit_price_sqrt <= self.state.price_sqrt_ratio_x96 {
840                anyhow::bail!(
841                    "Price limit must be greater than current price for one_for_zero swaps"
842                );
843            }
844        }
845
846        Ok(())
847    }
848
849    /// Processes a mint (liquidity add) event from historical data.
850    ///
851    /// Updates pool state when liquidity is added to a position, validates ticks,
852    /// and delegates to internal liquidity management methods.
853    ///
854    /// # Errors
855    ///
856    /// This function returns an error if:
857    /// - Pool is not initialized.
858    /// - Tick range is invalid or not properly spaced.
859    /// - Position updates fail.
860    pub fn process_mint(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
861        self.check_if_initialized(PoolEventKind::Mint)?;
862
863        if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
864        {
865            return Ok(());
866        }
867
868        self.validate_ticks(update.tick_lower, update.tick_upper)?;
869        let location = self.event_location(
870            PoolEventKind::Mint,
871            update.block,
872            update.transaction_index,
873            update.log_index,
874        );
875        self.add_liquidity(
876            &update.owner,
877            update.tick_lower,
878            update.tick_upper,
879            update.position_liquidity,
880            update.amount0,
881            update.amount1,
882        )
883        .map_err(|e| Self::wrap_liquidity_error(e, location))?;
884
885        self.analytics.total_mints += 1;
886        self.last_processed_event = Some(BlockPosition::new(
887            update.block,
888            update.transaction_hash.clone(),
889            update.transaction_index,
890            update.log_index,
891        ));
892        self.last_processed_ts = Some(update.ts_event);
893        self.update_reporter_if_enabled(update.block);
894        self.update_liquidity_analytics();
895
896        Ok(())
897    }
898
899    /// Internal helper to add liquidity to a position.
900    ///
901    /// Updates position state, tracks deposited amounts, and manages tick maps.
902    /// Called by both historical event processing and simulated operations.
903    fn add_liquidity(
904        &mut self,
905        owner: &Address,
906        tick_lower: i32,
907        tick_upper: i32,
908        liquidity: u128,
909        amount0: U256,
910        amount1: U256,
911    ) -> anyhow::Result<()> {
912        let liquidity_delta = i128::try_from(liquidity)
913            .map_err(|_| anyhow::anyhow!("Liquidity {liquidity} exceeds i128::MAX"))?;
914        self.update_position(
915            owner,
916            tick_lower,
917            tick_upper,
918            liquidity_delta,
919            amount0,
920            amount1,
921        )?;
922
923        // Track deposited amounts
924        self.analytics.total_amount0_deposited += amount0;
925        self.analytics.total_amount1_deposited += amount1;
926
927        Ok(())
928    }
929
930    /// Executes a simulated mint (liquidity addition) operation.
931    ///
932    /// Calculates required token amounts for the specified liquidity amount,
933    /// updates pool state, and returns the resulting mint event.
934    ///
935    /// # Errors
936    ///
937    /// This function returns an error if:
938    /// - Pool is not initialized.
939    /// - Tick range is invalid.
940    /// - Amount calculations fail.
941    pub fn execute_mint(
942        &mut self,
943        recipient: Address,
944        block: BlockPosition,
945        tick_lower: i32,
946        tick_upper: i32,
947        liquidity: u128,
948    ) -> anyhow::Result<PoolLiquidityUpdate> {
949        self.check_if_initialized(PoolEventKind::Mint)?;
950
951        self.validate_ticks(tick_lower, tick_upper)?;
952        let (amount0, amount1) = get_amounts_for_liquidity(
953            self.state.price_sqrt_ratio_x96,
954            tick_lower,
955            tick_upper,
956            liquidity,
957            true,
958        );
959        self.add_liquidity(
960            &recipient, tick_lower, tick_upper, liquidity, amount0, amount1,
961        )?;
962
963        self.analytics.total_mints += 1;
964
965        let event = PoolLiquidityUpdate::new(
966            self.pool.chain.clone(),
967            self.pool.dex.clone(),
968            self.pool.instrument_id,
969            self.pool.pool_identifier,
970            PoolLiquidityUpdateType::Mint,
971            block.number,
972            block.transaction_hash,
973            block.transaction_index,
974            block.log_index,
975            None,
976            recipient,
977            liquidity,
978            amount0,
979            amount1,
980            tick_lower,
981            tick_upper,
982            self.pool.ts_init, // ts_event (simulated; pool init time)
983            self.pool.ts_init, // ts_init
984        );
985
986        Ok(event)
987    }
988
989    /// Processes a burn (liquidity removal) event from historical data.
990    ///
991    /// Updates pool state when liquidity is removed from a position. Uses negative
992    /// liquidity delta to reduce the position size and tracks withdrawn amounts.
993    ///
994    /// # Errors
995    ///
996    /// This function returns an error if:
997    /// - Pool is not initialized.
998    /// - Tick range is invalid.
999    /// - Position updates fail.
1000    pub fn process_burn(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1001        self.check_if_initialized(PoolEventKind::Burn)?;
1002
1003        if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
1004        {
1005            return Ok(());
1006        }
1007
1008        self.validate_ticks(update.tick_lower, update.tick_upper)?;
1009
1010        // Update the position with a negative liquidity delta for the burn
1011        let liquidity_delta = i128::try_from(update.position_liquidity).map_err(|_| {
1012            anyhow::anyhow!("Liquidity {} exceeds i128::MAX", update.position_liquidity)
1013        })?;
1014        let location = self.event_location(
1015            PoolEventKind::Burn,
1016            update.block,
1017            update.transaction_index,
1018            update.log_index,
1019        );
1020
1021        self.update_position(
1022            &update.owner,
1023            update.tick_lower,
1024            update.tick_upper,
1025            -liquidity_delta,
1026            update.amount0,
1027            update.amount1,
1028        )
1029        .map_err(|e| Self::wrap_liquidity_error(e, location))?;
1030
1031        self.analytics.total_burns += 1;
1032        self.last_processed_event = Some(BlockPosition::new(
1033            update.block,
1034            update.transaction_hash.clone(),
1035            update.transaction_index,
1036            update.log_index,
1037        ));
1038        self.last_processed_ts = Some(update.ts_event);
1039        self.update_reporter_if_enabled(update.block);
1040        self.update_liquidity_analytics();
1041
1042        Ok(())
1043    }
1044
1045    /// Executes a simulated burn (liquidity removal) operation.
1046    ///
1047    /// Calculates token amounts that would be withdrawn for the specified liquidity,
1048    /// updates pool state, and returns the resulting burn event.
1049    ///
1050    /// # Errors
1051    ///
1052    /// This function returns an error if:
1053    /// - Pool is not initialized.
1054    /// - Tick range is invalid.
1055    /// - Amount calculations fail.
1056    /// - Insufficient liquidity in position.
1057    pub fn execute_burn(
1058        &mut self,
1059        recipient: Address,
1060        block: BlockPosition,
1061        tick_lower: i32,
1062        tick_upper: i32,
1063        liquidity: u128,
1064    ) -> anyhow::Result<PoolLiquidityUpdate> {
1065        self.check_if_initialized(PoolEventKind::Burn)?;
1066
1067        self.validate_ticks(tick_lower, tick_upper)?;
1068        let (amount0, amount1) = get_amounts_for_liquidity(
1069            self.state.price_sqrt_ratio_x96,
1070            tick_lower,
1071            tick_upper,
1072            liquidity,
1073            false,
1074        );
1075
1076        // Update the position with a negative liquidity delta for the burn
1077        let liquidity_delta = i128::try_from(liquidity)
1078            .map_err(|_| anyhow::anyhow!("Liquidity {liquidity} exceeds i128::MAX"))?;
1079        self.update_position(
1080            &recipient,
1081            tick_lower,
1082            tick_upper,
1083            -liquidity_delta,
1084            amount0,
1085            amount1,
1086        )?;
1087
1088        self.analytics.total_burns += 1;
1089
1090        let event = PoolLiquidityUpdate::new(
1091            self.pool.chain.clone(),
1092            self.pool.dex.clone(),
1093            self.pool.instrument_id,
1094            self.pool.pool_identifier,
1095            PoolLiquidityUpdateType::Burn,
1096            block.number,
1097            block.transaction_hash,
1098            block.transaction_index,
1099            block.log_index,
1100            None,
1101            recipient,
1102            liquidity,
1103            amount0,
1104            amount1,
1105            tick_lower,
1106            tick_upper,
1107            self.pool.ts_init, // ts_event (simulated; pool init time)
1108            self.pool.ts_init, // ts_init
1109        );
1110
1111        Ok(event)
1112    }
1113
1114    /// Processes a fee collect event from historical data.
1115    ///
1116    /// Updates position state when accumulated fees are collected. Finds the
1117    /// position and delegates fee collection to the position object.
1118    ///
1119    /// Note: Tick validation is intentionally skipped to match Uniswap V3 behavior.
1120    /// Invalid positions have no fees to collect, so they're silently ignored.
1121    ///
1122    /// # Errors
1123    ///
1124    /// This function returns an error if:
1125    /// - Pool is not initialized.
1126    pub fn process_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1127        self.check_if_initialized(PoolEventKind::Collect)?;
1128
1129        if self.check_if_already_processed(
1130            collect.block,
1131            collect.transaction_index,
1132            collect.log_index,
1133        ) {
1134            return Ok(());
1135        }
1136        let position_key =
1137            PoolPosition::get_position_key(&collect.owner, collect.tick_lower, collect.tick_upper);
1138
1139        if let Some(position) = self.positions.get_mut(&position_key) {
1140            position.collect_fees(collect.amount0, collect.amount1);
1141        }
1142
1143        // Cleanup position if it became empty after collecting all fees
1144        self.cleanup_position_if_empty(&position_key);
1145
1146        self.analytics.total_amount0_collected += U256::from(collect.amount0);
1147        self.analytics.total_amount1_collected += U256::from(collect.amount1);
1148
1149        self.analytics.total_fee_collects += 1;
1150        self.last_processed_event = Some(BlockPosition::new(
1151            collect.block,
1152            collect.transaction_hash.clone(),
1153            collect.transaction_index,
1154            collect.log_index,
1155        ));
1156        self.last_processed_ts = Some(collect.ts_event);
1157        self.update_reporter_if_enabled(collect.block);
1158        self.update_liquidity_analytics();
1159
1160        Ok(())
1161    }
1162
1163    /// Applies a protocol-fee configuration change from a `SetFeeProtocol` event.
1164    ///
1165    /// Repacks the new per-token denominators into [`PoolState::fee_protocol`] so subsequent swap
1166    /// and flash fee splitting uses the correct setting. Not gated on pool initialization, since a
1167    /// protocol-fee change is independent of the pool's price/liquidity state.
1168    ///
1169    /// # Errors
1170    ///
1171    /// This function does not currently return an error; the `Result` keeps the signature uniform
1172    /// with the other `process_*` event handlers.
1173    pub fn process_fee_protocol_update(
1174        &mut self,
1175        update: &PoolFeeProtocolUpdate,
1176    ) -> anyhow::Result<()> {
1177        if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
1178        {
1179            return Ok(());
1180        }
1181
1182        self.state.fee_protocol = update.packed();
1183
1184        self.last_processed_event = Some(BlockPosition::new(
1185            update.block,
1186            update.transaction_hash.clone(),
1187            update.transaction_index,
1188            update.log_index,
1189        ));
1190        self.last_processed_ts = Some(update.ts_event);
1191        self.update_reporter_if_enabled(update.block);
1192
1193        Ok(())
1194    }
1195
1196    /// Applies a protocol-fee withdrawal from a `CollectProtocol` event.
1197    ///
1198    /// Decrements the accrued protocol-fee balances by the withdrawn amounts, leaving the on-chain
1199    /// remainder (Uniswap V3 keeps one wei in each slot to save gas). Saturating subtraction guards
1200    /// against replay accrual lagging behind the on-chain balance. Not gated on pool initialization,
1201    /// since the protocol-fee balances are independent of the pool's price/liquidity state.
1202    ///
1203    /// # Errors
1204    ///
1205    /// This function does not currently return an error; the `Result` keeps the signature uniform
1206    /// with the other `process_*` event handlers.
1207    pub fn process_fee_protocol_collect(
1208        &mut self,
1209        collect: &PoolFeeProtocolCollect,
1210    ) -> anyhow::Result<()> {
1211        if self.check_if_already_processed(
1212            collect.block,
1213            collect.transaction_index,
1214            collect.log_index,
1215        ) {
1216            return Ok(());
1217        }
1218
1219        self.state.protocol_fees_token0 = self
1220            .state
1221            .protocol_fees_token0
1222            .saturating_sub(U256::from(collect.amount0));
1223        self.state.protocol_fees_token1 = self
1224            .state
1225            .protocol_fees_token1
1226            .saturating_sub(U256::from(collect.amount1));
1227
1228        self.last_processed_event = Some(BlockPosition::new(
1229            collect.block,
1230            collect.transaction_hash.clone(),
1231            collect.transaction_index,
1232            collect.log_index,
1233        ));
1234        self.last_processed_ts = Some(collect.ts_event);
1235        self.update_reporter_if_enabled(collect.block);
1236
1237        Ok(())
1238    }
1239
1240    /// Processes a flash loan event from historical data.
1241    ///
1242    /// # Errors
1243    ///
1244    /// Returns an error if:
1245    /// - Pool has no active liquidity.
1246    /// - Fee growth arithmetic overflows.
1247    pub fn process_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1248        self.check_if_initialized(PoolEventKind::Flash)?;
1249
1250        if self.check_if_already_processed(flash.block, flash.transaction_index, flash.log_index) {
1251            return Ok(());
1252        }
1253
1254        self.update_flash_state(flash.paid0, flash.paid1)?;
1255
1256        self.analytics.total_flashes += 1;
1257        self.last_processed_event = Some(BlockPosition::new(
1258            flash.block,
1259            flash.transaction_hash.clone(),
1260            flash.transaction_index,
1261            flash.log_index,
1262        ));
1263        self.last_processed_ts = Some(flash.ts_event);
1264        self.update_reporter_if_enabled(flash.block);
1265        self.update_liquidity_analytics();
1266
1267        Ok(())
1268    }
1269
1270    /// Executes a simulated flash loan operation and returns the resulting event.
1271    ///
1272    /// # Errors
1273    ///
1274    /// Returns an error if:
1275    /// - Mathematical operations overflow when calculating fees.
1276    /// - Pool has no active liquidity.
1277    /// - Fee growth arithmetic overflows.
1278    ///
1279    /// # Panics
1280    ///
1281    /// Panics if the pool fee has not been set.
1282    pub fn execute_flash(
1283        &mut self,
1284        sender: Address,
1285        recipient: Address,
1286        block: BlockPosition,
1287        amount0: U256,
1288        amount1: U256,
1289    ) -> anyhow::Result<PoolFlash> {
1290        self.check_if_initialized(PoolEventKind::Flash)?;
1291
1292        let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
1293
1294        // Calculate fees or paid0/paid1
1295        let paid0 = if amount0 > U256::ZERO {
1296            FullMath::mul_div_rounding_up(amount0, U256::from(fee_tier), U256::from(1_000_000))?
1297        } else {
1298            U256::ZERO
1299        };
1300
1301        let paid1 = if amount1 > U256::ZERO {
1302            FullMath::mul_div_rounding_up(amount1, U256::from(fee_tier), U256::from(1_000_000))?
1303        } else {
1304            U256::ZERO
1305        };
1306
1307        self.update_flash_state(paid0, paid1)?;
1308        self.analytics.total_flashes += 1;
1309
1310        let flash_event = PoolFlash::new(
1311            self.pool.chain.clone(),
1312            self.pool.dex.clone(),
1313            self.pool.instrument_id,
1314            self.pool.pool_identifier,
1315            block.number,
1316            block.transaction_hash,
1317            block.transaction_index,
1318            block.log_index,
1319            self.pool.ts_init, // ts_event (simulated; pool init time)
1320            self.pool.ts_init, // ts_init
1321            sender,
1322            recipient,
1323            amount0,
1324            amount1,
1325            paid0,
1326            paid1,
1327        );
1328
1329        Ok(flash_event)
1330    }
1331
1332    /// Core flash loan state update logic.
1333    ///
1334    /// # Errors
1335    ///
1336    /// Returns error if:
1337    /// - No active liquidity in pool
1338    /// - Fee growth arithmetic overflows
1339    fn update_flash_state(&mut self, paid0: U256, paid1: U256) -> anyhow::Result<()> {
1340        let liquidity = self.tick_map.liquidity;
1341        if liquidity == 0 {
1342            anyhow::bail!("No liquidity")
1343        }
1344
1345        let fee_protocol_0 = self.state.fee_protocol % 16;
1346        let fee_protocol_1 = self.state.fee_protocol >> 4;
1347
1348        // Process token0 fees
1349        if paid0 > U256::ZERO {
1350            let protocol_fee_0 = if fee_protocol_0 > 0 {
1351                paid0 / U256::from(fee_protocol_0)
1352            } else {
1353                U256::ZERO
1354            };
1355
1356            if protocol_fee_0 > U256::ZERO {
1357                self.state.protocol_fees_token0 += protocol_fee_0;
1358            }
1359
1360            let lp_fee_0 = paid0 - protocol_fee_0;
1361            let delta = FullMath::mul_div(lp_fee_0, Q128, U256::from(liquidity))?;
1362            self.state.fee_growth_global_0 += delta;
1363        }
1364
1365        // Process token1 fees
1366        if paid1 > U256::ZERO {
1367            let protocol_fee_1 = if fee_protocol_1 > 0 {
1368                paid1 / U256::from(fee_protocol_1)
1369            } else {
1370                U256::ZERO
1371            };
1372
1373            if protocol_fee_1 > U256::ZERO {
1374                self.state.protocol_fees_token1 += protocol_fee_1;
1375            }
1376
1377            let lp_fee_1 = paid1 - protocol_fee_1;
1378            let delta = FullMath::mul_div(lp_fee_1, Q128, U256::from(liquidity))?;
1379            self.state.fee_growth_global_1 += delta;
1380        }
1381
1382        Ok(())
1383    }
1384
1385    /// Updates position state and tick maps when liquidity changes.
1386    ///
1387    /// Core internal method that handles position updates for both mints and burns.
1388    /// Updates tick maps, position tracking, fee growth, and active liquidity.
1389    fn update_position(
1390        &mut self,
1391        owner: &Address,
1392        tick_lower: i32,
1393        tick_upper: i32,
1394        liquidity_delta: i128,
1395        amount0: U256,
1396        amount1: U256,
1397    ) -> anyhow::Result<()> {
1398        let current_tick = self.state.current_tick;
1399        let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1400        let position = self
1401            .positions
1402            .entry(position_key)
1403            .or_insert(PoolPosition::new(*owner, tick_lower, tick_upper, 0));
1404
1405        // Only validate when burning (negative liquidity_delta)
1406        if liquidity_delta < 0 {
1407            let burn_amount = liquidity_delta.unsigned_abs();
1408            if position.liquidity < burn_amount {
1409                anyhow::bail!(
1410                    "Position liquidity {} is less than the requested burn amount of {}",
1411                    position.liquidity,
1412                    burn_amount
1413                );
1414            }
1415        }
1416
1417        // Pre-validate so an over/underflow error returns before mutating tick map
1418        // or position state.
1419        let new_active_liquidity = if tick_lower <= current_tick && current_tick < tick_upper {
1420            Some(try_liquidity_math_add(
1421                self.tick_map.liquidity,
1422                liquidity_delta,
1423            )?)
1424        } else {
1425            None
1426        };
1427
1428        // Update tickmaps.
1429        let flipped_lower = self.tick_map.update(
1430            tick_lower,
1431            current_tick,
1432            liquidity_delta,
1433            false,
1434            self.state.fee_growth_global_0,
1435            self.state.fee_growth_global_1,
1436        );
1437        let flipped_upper = self.tick_map.update(
1438            tick_upper,
1439            current_tick,
1440            liquidity_delta,
1441            true,
1442            self.state.fee_growth_global_0,
1443            self.state.fee_growth_global_1,
1444        );
1445
1446        let (fee_growth_inside_0, fee_growth_inside_1) = self.tick_map.get_fee_growth_inside(
1447            tick_lower,
1448            tick_upper,
1449            current_tick,
1450            self.state.fee_growth_global_0,
1451            self.state.fee_growth_global_1,
1452        );
1453        position.update_liquidity(liquidity_delta);
1454        position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
1455        position.update_amounts(liquidity_delta, amount0, amount1);
1456
1457        if let Some(active_liquidity) = new_active_liquidity {
1458            self.tick_map.liquidity = active_liquidity;
1459        }
1460
1461        // Clear the ticks if they are flipped and burned
1462        if liquidity_delta < 0 && flipped_lower {
1463            self.tick_map.clear(tick_lower);
1464        }
1465
1466        if liquidity_delta < 0 && flipped_upper {
1467            self.tick_map.clear(tick_upper);
1468        }
1469
1470        Ok(())
1471    }
1472
1473    /// Removes position from tracking if it's completely empty.
1474    ///
1475    /// This prevents accumulation of positions in the memory that are not used anymore.
1476    fn cleanup_position_if_empty(&mut self, position_key: &str) {
1477        if let Some(position) = self.positions.get(position_key)
1478            && position.is_empty()
1479        {
1480            log::debug!(
1481                "CLEANING UP EMPTY POSITION: owner={}, ticks=[{}, {}]",
1482                position.owner,
1483                position.tick_lower,
1484                position.tick_upper,
1485            );
1486            self.positions.remove(position_key);
1487        }
1488    }
1489
1490    /// Calculates the liquidity utilization rate for the pool.
1491    ///
1492    /// The utilization rate measures what percentage of total deployed liquidity
1493    /// is currently active (in-range and earning fees) at the current price tick.
1494    #[must_use]
1495    pub fn liquidity_utilization_rate(&self) -> f64 {
1496        const PRECISION: u32 = 1_000_000; // 6 decimal places
1497
1498        let total_liquidity = self.get_total_liquidity();
1499        let active_liquidity = self.get_active_liquidity();
1500
1501        if total_liquidity == U256::ZERO {
1502            return 0.0;
1503        }
1504        let ratio = FullMath::mul_div(
1505            U256::from(active_liquidity),
1506            U256::from(PRECISION),
1507            total_liquidity,
1508        )
1509        .unwrap_or(U256::ZERO);
1510
1511        // Safe to cast to u64: Since active_liquidity <= total_liquidity,
1512        // the ratio is guaranteed to be <= PRECISION (1_000_000), which fits in u64
1513        ratio.to::<u64>() as f64 / f64::from(PRECISION)
1514    }
1515
1516    /// Validates tick range for position operations.
1517    ///
1518    /// Ensures ticks are properly ordered, aligned to tick spacing, and within
1519    /// valid bounds. Used by all position-related operations.
1520    ///
1521    /// # Errors
1522    ///
1523    /// This function returns an error if:
1524    /// - `tick_lower >= tick_upper` (invalid range).
1525    /// - Ticks are not multiples of pool's tick spacing.
1526    /// - Ticks are outside `MIN_TICK/MAX_TICK` bounds.
1527    fn validate_ticks(&self, tick_lower: i32, tick_upper: i32) -> anyhow::Result<()> {
1528        if tick_lower >= tick_upper {
1529            anyhow::bail!("Invalid tick range: {tick_lower} >= {tick_upper}")
1530        }
1531
1532        if tick_lower % self.pool.tick_spacing.unwrap() as i32 != 0
1533            || tick_upper % self.pool.tick_spacing.unwrap() as i32 != 0
1534        {
1535            anyhow::bail!(
1536                "Ticks {tick_lower} and {tick_upper} must be multiples of the tick spacing"
1537            )
1538        }
1539
1540        if tick_lower < PoolTick::MIN_TICK || tick_upper > PoolTick::MAX_TICK {
1541            anyhow::bail!("Invalid tick bounds for {tick_lower} and {tick_upper}");
1542        }
1543        Ok(())
1544    }
1545
1546    /// Updates all liquidity analytics.
1547    fn update_liquidity_analytics(&mut self) {
1548        self.analytics.liquidity_utilization_rate = self.liquidity_utilization_rate();
1549    }
1550
1551    /// Returns the pool's active liquidity tracked by the tick map.
1552    ///
1553    /// This represents the effective liquidity available for trading at the current price.
1554    /// The tick map maintains this value efficiently by updating it during tick crossings
1555    /// as the price moves through different ranges.
1556    ///
1557    /// # Returns
1558    /// The active liquidity (u128) at the current tick from the tick map
1559    #[must_use]
1560    pub fn get_active_liquidity(&self) -> u128 {
1561        self.tick_map.liquidity
1562    }
1563
1564    /// Calculates total liquidity by summing all individual positions at the current tick.
1565    ///
1566    /// This computes liquidity by iterating through all positions and summing those that
1567    /// span the current tick. Unlike [`Self::get_active_liquidity`], which returns the maintained
1568    /// tick map value, this method performs a fresh calculation from position data.
1569    #[must_use]
1570    pub fn get_total_liquidity_from_active_positions(&self) -> u128 {
1571        self.positions
1572            .values()
1573            .filter(|position| {
1574                position.liquidity > 0
1575                    && position.tick_lower <= self.state.current_tick
1576                    && self.state.current_tick < position.tick_upper
1577            })
1578            .map(|position| position.liquidity)
1579            .sum()
1580    }
1581
1582    /// Calculates total liquidity across all positions, regardless of range status.
1583    #[must_use]
1584    pub fn get_total_liquidity(&self) -> U256 {
1585        self.positions
1586            .values()
1587            .map(|position| U256::from(position.liquidity))
1588            .fold(U256::ZERO, |acc, liq| acc + liq)
1589    }
1590
1591    /// Restores the profiler state from a saved snapshot.
1592    ///
1593    /// This method allows resuming profiling from a previously saved state,
1594    /// enabling incremental processing without reprocessing all historical events.
1595    ///
1596    /// # Errors
1597    ///
1598    /// Returns an error if:
1599    /// - Tick insertion into the tick map fails.
1600    ///
1601    /// # Panics
1602    ///
1603    /// Panics if the pool's tick spacing is not set.
1604    pub fn restore_from_snapshot(&mut self, snapshot: PoolSnapshot) -> anyhow::Result<()> {
1605        let liquidity = snapshot.state.liquidity;
1606
1607        // Restore state
1608        self.state = snapshot.state;
1609
1610        // Restore analytics (skip duration fields as they're debug-only)
1611        self.analytics.total_amount0_deposited = snapshot.analytics.total_amount0_deposited;
1612        self.analytics.total_amount1_deposited = snapshot.analytics.total_amount1_deposited;
1613        self.analytics.total_amount0_collected = snapshot.analytics.total_amount0_collected;
1614        self.analytics.total_amount1_collected = snapshot.analytics.total_amount1_collected;
1615        self.analytics.total_swaps = snapshot.analytics.total_swaps;
1616        self.analytics.total_mints = snapshot.analytics.total_mints;
1617        self.analytics.total_burns = snapshot.analytics.total_burns;
1618        self.analytics.total_fee_collects = snapshot.analytics.total_fee_collects;
1619        self.analytics.total_flashes = snapshot.analytics.total_flashes;
1620
1621        // Rebuild positions AHashMap
1622        self.positions.clear();
1623        for position in snapshot.positions {
1624            let key = PoolPosition::get_position_key(
1625                &position.owner,
1626                position.tick_lower,
1627                position.tick_upper,
1628            );
1629            self.positions.insert(key, position);
1630        }
1631
1632        // Rebuild tick_map
1633        self.tick_map = TickMap::new(
1634            self.pool
1635                .tick_spacing
1636                .expect("Pool tick spacing must be set"),
1637        );
1638
1639        for tick in snapshot.ticks {
1640            self.tick_map.restore_tick(tick);
1641        }
1642
1643        // Restore active liquidity
1644        self.tick_map.liquidity = liquidity;
1645
1646        // Set last processed event
1647        self.last_processed_event = Some(snapshot.block_position);
1648        self.last_processed_ts = Some(snapshot.ts_event);
1649
1650        // Mark as initialized
1651        self.is_initialized = true;
1652
1653        // Recalculate analytics
1654        self.update_liquidity_analytics();
1655
1656        Ok(())
1657    }
1658
1659    /// Gets a list of all initialized tick values.
1660    ///
1661    /// Returns tick values that have been initialized (have liquidity positions).
1662    /// Useful for understanding the liquidity distribution across price ranges.
1663    #[must_use]
1664    pub fn get_active_tick_values(&self) -> Vec<i32> {
1665        self.tick_map
1666            .get_all_ticks()
1667            .iter()
1668            .filter(|(_, tick)| self.tick_map.is_tick_initialized(tick.value))
1669            .map(|(tick_value, _)| *tick_value)
1670            .collect()
1671    }
1672
1673    /// Gets the number of active ticks.
1674    #[must_use]
1675    pub fn get_active_tick_count(&self) -> usize {
1676        self.tick_map.active_tick_count()
1677    }
1678
1679    /// Gets tick information for a specific tick value.
1680    ///
1681    /// Returns the tick data structure containing liquidity and fee information
1682    /// for the specified tick, if it exists.
1683    #[must_use]
1684    pub fn get_tick(&self, tick: i32) -> Option<&PoolTick> {
1685        self.tick_map.get_tick(tick)
1686    }
1687
1688    /// Gets the current tick position of the pool.
1689    ///
1690    /// Returns the tick that corresponds to the current pool price.
1691    /// The pool must be initialized before calling this method.
1692    #[must_use]
1693    pub fn get_current_tick(&self) -> i32 {
1694        self.state.current_tick
1695    }
1696
1697    /// Gets the total number of ticks tracked by the tick map.
1698    ///
1699    /// Returns count of all ticks that have ever been initialized,
1700    /// including those that may no longer have active liquidity.
1701    ///
1702    /// # Returns
1703    /// Total tick count in the tick map
1704    #[must_use]
1705    pub fn get_total_tick_count(&self) -> usize {
1706        self.tick_map.total_tick_count()
1707    }
1708
1709    /// Gets position information for a specific owner and tick range.
1710    ///
1711    /// Looks up a position by its unique key (owner + tick range) and returns
1712    /// the position data if it exists.
1713    #[must_use]
1714    pub fn get_position(
1715        &self,
1716        owner: &Address,
1717        tick_lower: i32,
1718        tick_upper: i32,
1719    ) -> Option<&PoolPosition> {
1720        let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1721        self.positions.get(&position_key)
1722    }
1723
1724    /// Returns a list of all currently active positions.
1725    ///
1726    /// Active positions are those with liquidity > 0 whose tick range includes
1727    /// the current pool tick, meaning they have tokens actively deployed in the pool
1728    /// and are earning fees from trades at the current price.
1729    ///
1730    /// # Returns
1731    ///
1732    /// A vector of references to active [`PoolPosition`] objects.
1733    #[must_use]
1734    pub fn get_active_positions(&self) -> Vec<&PoolPosition> {
1735        self.positions
1736            .values()
1737            .filter(|position| {
1738                let current_tick = self.get_current_tick();
1739                position.liquidity > 0
1740                    && position.tick_lower <= current_tick
1741                    && current_tick < position.tick_upper
1742            })
1743            .collect()
1744    }
1745
1746    /// Returns a list of all positions tracked by the profiler.
1747    ///
1748    /// This includes both active and inactive positions, regardless of their
1749    /// liquidity or tick range relative to the current pool tick.
1750    ///
1751    /// # Returns
1752    ///
1753    /// A vector of references to all [`PoolPosition`] objects.
1754    #[must_use]
1755    pub fn get_all_positions(&self) -> Vec<&PoolPosition> {
1756        self.positions.values().collect()
1757    }
1758
1759    /// Returns position keys for all tracked positions.
1760    #[must_use]
1761    pub fn get_all_position_keys(&self) -> Vec<(Address, i32, i32)> {
1762        self.get_all_positions()
1763            .iter()
1764            .map(|position| (position.owner, position.tick_lower, position.tick_upper))
1765            .collect()
1766    }
1767
1768    /// Extracts a complete snapshot of the current pool state.
1769    ///
1770    /// Extracts and bundles the complete pool state including global variables,
1771    /// all liquidity positions, and the full tick distribution into a portable
1772    /// [`PoolSnapshot`] structure. This snapshot can be serialized, persisted
1773    /// to database, or used to restore pool state later.
1774    ///
1775    /// # Errors
1776    ///
1777    /// Returns an error if no events have been processed yet, since there is no event watermark to
1778    /// anchor the snapshot to.
1779    pub fn extract_snapshot(&self) -> anyhow::Result<PoolSnapshot> {
1780        let positions: Vec<_> = self.positions.values().cloned().collect();
1781        let ticks: Vec<_> = self.tick_map.get_all_ticks().values().copied().collect();
1782
1783        let mut state = self.state.clone();
1784        state.liquidity = self.tick_map.liquidity;
1785
1786        let last_processed_event = self
1787            .last_processed_event
1788            .clone()
1789            .ok_or_else(|| anyhow::anyhow!("Cannot extract snapshot: no events processed yet"))?;
1790
1791        Ok(PoolSnapshot::new(
1792            self.pool.instrument_id,
1793            state,
1794            positions,
1795            ticks,
1796            self.analytics.clone(),
1797            last_processed_event,
1798            self.last_processed_ts.unwrap_or(self.pool.ts_init), // ts_event (last processed event)
1799            self.last_processed_ts.unwrap_or(self.pool.ts_init), // ts_init
1800        ))
1801    }
1802
1803    /// Gets the count of positions that are currently active.
1804    ///
1805    /// Active positions are those with liquidity > 0 and whose tick range
1806    /// includes the current pool tick (meaning they have tokens in the pool).
1807    #[must_use]
1808    pub fn get_total_active_positions(&self) -> usize {
1809        self.positions
1810            .iter()
1811            .filter(|(_, position)| {
1812                let current_tick = self.get_current_tick();
1813                position.liquidity > 0
1814                    && position.tick_lower <= current_tick
1815                    && current_tick < position.tick_upper
1816            })
1817            .count()
1818    }
1819
1820    /// Gets the count of positions that are currently inactive.
1821    ///
1822    /// Inactive positions are those that exist but don't span the current tick,
1823    /// meaning their liquidity is entirely in one token or the other.
1824    #[must_use]
1825    pub fn get_total_inactive_positions(&self) -> usize {
1826        self.positions.len() - self.get_total_active_positions()
1827    }
1828
1829    /// Estimates the total amount of token0 in the pool.
1830    ///
1831    /// Calculates token0 balance by summing:
1832    /// - Token0 amounts from all active liquidity positions
1833    /// - Accumulated trading fees (approximated from fee growth)
1834    /// - Protocol fees collected
1835    #[must_use]
1836    pub fn estimate_balance_of_token0(&self) -> U256 {
1837        let mut total_amount0 = U256::ZERO;
1838        let current_sqrt_price = self.state.price_sqrt_ratio_x96;
1839        let current_tick = self.state.current_tick;
1840        let mut total_fees_0_collected: u128 = 0;
1841
1842        // 1. Calculate token0 from active liquidity positions
1843        for position in self.positions.values() {
1844            if position.liquidity > 0 {
1845                if position.tick_upper <= current_tick {
1846                    // Position is below current price - no token0
1847                    continue;
1848                } else if position.tick_lower > current_tick {
1849                    // Position is above current price - all token0
1850                    let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
1851                    let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
1852                    let amount0 =
1853                        get_amount0_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
1854                    total_amount0 += amount0;
1855                } else {
1856                    // Position is active - token0 from current price to upper tick
1857                    let sqrt_ratio_upper = get_sqrt_ratio_at_tick(position.tick_upper);
1858                    let amount0 = get_amount0_delta(
1859                        current_sqrt_price,
1860                        sqrt_ratio_upper,
1861                        position.liquidity,
1862                        true,
1863                    );
1864                    total_amount0 += amount0;
1865                }
1866            }
1867
1868            total_fees_0_collected += position.total_amount0_collected;
1869        }
1870
1871        // 2. Add accumulated swap fees (fee_growth_global represents total fees accumulated)
1872        // Note: In a real pool, fees are distributed as liquidity, but for balance estimation
1873        // we can use a simplified approach by converting fee growth to token amounts
1874        let fee_growth_0 = self.state.fee_growth_global_0;
1875        if fee_growth_0 > U256::ZERO {
1876            // Convert fee growth to actual token amount using FullMath for precision
1877            // Fee growth is in Q128.128 format, so we need to scale it properly
1878            let active_liquidity = self.get_active_liquidity();
1879            if active_liquidity > 0 {
1880                // fee_growth_global is fees per unit of liquidity in Q128.128
1881                // To get total fees: mul_div(fee_growth, liquidity, 2^128)
1882                if let Ok(total_fees_0) =
1883                    FullMath::mul_div(fee_growth_0, U256::from(active_liquidity), Q128)
1884                {
1885                    total_amount0 += total_fees_0;
1886                }
1887            }
1888        }
1889
1890        let total_fees_0_left = fee_growth_0 - U256::from(total_fees_0_collected);
1891
1892        // 4. Add protocol fees
1893        total_amount0 += self.state.protocol_fees_token0;
1894
1895        total_amount0 + total_fees_0_left
1896    }
1897
1898    /// Estimates the total amount of token1 in the pool.
1899    ///
1900    /// Calculates token1 balance by summing:
1901    /// - Token1 amounts from all active liquidity positions
1902    /// - Accumulated trading fees (approximated from fee growth)
1903    /// - Protocol fees collected
1904    #[must_use]
1905    pub fn estimate_balance_of_token1(&self) -> U256 {
1906        let mut total_amount1 = U256::ZERO;
1907        let current_sqrt_price = self.state.price_sqrt_ratio_x96;
1908        let current_tick = self.state.current_tick;
1909        let mut total_fees_1_collected: u128 = 0;
1910
1911        // 1. Calculate token1 from active liquidity positions
1912        for position in self.positions.values() {
1913            if position.liquidity > 0 {
1914                if position.tick_lower > current_tick {
1915                    // Position is above current price - no token1
1916                    continue;
1917                } else if position.tick_upper <= current_tick {
1918                    // Position is below current price - all token1
1919                    let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
1920                    let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
1921                    let amount1 =
1922                        get_amount1_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
1923                    total_amount1 += amount1;
1924                } else {
1925                    // Position is active - token1 from lower tick to current price
1926                    let sqrt_ratio_lower = get_sqrt_ratio_at_tick(position.tick_lower);
1927                    let amount1 = get_amount1_delta(
1928                        sqrt_ratio_lower,
1929                        current_sqrt_price,
1930                        position.liquidity,
1931                        true,
1932                    );
1933                    total_amount1 += amount1;
1934                }
1935            }
1936
1937            // Sum collected fees
1938            total_fees_1_collected += position.total_amount1_collected;
1939        }
1940
1941        // 2. Add accumulated swap fees for token1
1942        let fee_growth_1 = self.state.fee_growth_global_1;
1943        if fee_growth_1 > U256::ZERO {
1944            let active_liquidity = self.get_active_liquidity();
1945            if active_liquidity > 0 {
1946                // Convert fee growth to actual token amount using FullMath
1947                if let Ok(total_fees_1) =
1948                    FullMath::mul_div(fee_growth_1, U256::from(active_liquidity), Q128)
1949                {
1950                    total_amount1 += total_fees_1;
1951                }
1952            }
1953        }
1954
1955        let total_fees_1_left = fee_growth_1 - U256::from(total_fees_1_collected);
1956
1957        // 4. Add protocol fees
1958        total_amount1 += self.state.protocol_fees_token1;
1959
1960        total_amount1 + total_fees_1_left
1961    }
1962
1963    /// Sets the global fee growth for both tokens.
1964    ///
1965    /// This is primarily used for testing to simulate specific fee growth scenarios.
1966    /// In production, fee growth is updated through swap operations.
1967    ///
1968    /// # Arguments
1969    /// * `fee_growth_global_0` - New global fee growth for token0
1970    /// * `fee_growth_global_1` - New global fee growth for token1
1971    pub fn set_fee_growth_global(&mut self, fee_growth_global_0: U256, fee_growth_global_1: U256) {
1972        self.state.fee_growth_global_0 = fee_growth_global_0;
1973        self.state.fee_growth_global_1 = fee_growth_global_1;
1974    }
1975
1976    /// Returns the total number of events processed.
1977    #[must_use]
1978    pub fn get_total_events(&self) -> u64 {
1979        self.analytics.total_swaps
1980            + self.analytics.total_mints
1981            + self.analytics.total_burns
1982            + self.analytics.total_fee_collects
1983            + self.analytics.total_flashes
1984    }
1985
1986    /// Enables progress reporting for pool profiler event processing.
1987    ///
1988    /// When enabled, the profiler will automatically track and log progress
1989    /// as events are processed through the `process()` method.
1990    pub fn enable_reporting(&mut self, from_block: u64, total_blocks: u64, update_interval: u64) {
1991        self.reporter = Some(BlockchainSyncReporter::new(
1992            BlockchainSyncReportItems::PoolProfiling,
1993            from_block,
1994            total_blocks,
1995            update_interval,
1996        ));
1997        self.last_reported_block = from_block;
1998    }
1999
2000    /// Finalizes reporting and logs final statistics.
2001    ///
2002    /// Should be called after all events have been processed to output
2003    /// the final summary of the profiler bootstrap operation.
2004    pub fn finalize_reporting(&mut self) {
2005        if let Some(reporter) = &self.reporter {
2006            reporter.log_final_stats();
2007        }
2008        self.reporter = None;
2009    }
2010}