1use 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#[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 pub pool: SharedPool,
80 positions: AHashMap<String, PoolPosition>,
82 pub tick_map: TickMap,
84 pub state: PoolState,
86 pub analytics: PoolAnalytics,
88 pub last_processed_event: Option<BlockPosition>,
90 pub last_processed_ts: Option<UnixNanos>,
92 pub is_initialized: bool,
94 reporter: Option<BlockchainSyncReporter>,
96 last_reported_block: u64,
98}
99
100impl PoolProfiler {
101 #[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 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 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 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 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 fn update_reporter_if_enabled(&mut self, current_block: u64) {
257 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 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 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 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 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, self.pool.ts_init, 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 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 let fee_protocol = if zero_for_one {
474 self.state.fee_protocol % 16
476 } else {
477 self.state.fee_protocol >> 4
479 };
480
481 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 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 tick_next = tick_next.clamp(PoolTick::MIN_TICK, PoolTick::MAX_TICK);
501
502 let sqrt_price_next = get_sqrt_ratio_at_tick(tick_next);
504
505 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 current_sqrt_price = swap_step_result.sqrt_ratio_next_x96;
523
524 if exact_input {
526 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 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 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 lp_fee += step_fee_amount;
551
552 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 if swap_step_result.sqrt_ratio_next_x96 == sqrt_price_next {
561 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 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 current_tick = get_tick_at_sqrt_ratio(current_sqrt_price);
600 }
601 }
602
603 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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 self.analytics.total_amount0_deposited += amount0;
925 self.analytics.total_amount1_deposited += amount1;
926
927 Ok(())
928 }
929
930 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, self.pool.ts_init, );
985
986 Ok(event)
987 }
988
989 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 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 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 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, self.pool.ts_init, );
1110
1111 Ok(event)
1112 }
1113
1114 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 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 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 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 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 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 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, self.pool.ts_init, sender,
1322 recipient,
1323 amount0,
1324 amount1,
1325 paid0,
1326 paid1,
1327 );
1328
1329 Ok(flash_event)
1330 }
1331
1332 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 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 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 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 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 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 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 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 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 #[must_use]
1495 pub fn liquidity_utilization_rate(&self) -> f64 {
1496 const PRECISION: u32 = 1_000_000; 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 ratio.to::<u64>() as f64 / f64::from(PRECISION)
1514 }
1515
1516 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 fn update_liquidity_analytics(&mut self) {
1548 self.analytics.liquidity_utilization_rate = self.liquidity_utilization_rate();
1549 }
1550
1551 #[must_use]
1560 pub fn get_active_liquidity(&self) -> u128 {
1561 self.tick_map.liquidity
1562 }
1563
1564 #[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 #[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 pub fn restore_from_snapshot(&mut self, snapshot: PoolSnapshot) -> anyhow::Result<()> {
1605 let liquidity = snapshot.state.liquidity;
1606
1607 self.state = snapshot.state;
1609
1610 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 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 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 self.tick_map.liquidity = liquidity;
1645
1646 self.last_processed_event = Some(snapshot.block_position);
1648 self.last_processed_ts = Some(snapshot.ts_event);
1649
1650 self.is_initialized = true;
1652
1653 self.update_liquidity_analytics();
1655
1656 Ok(())
1657 }
1658
1659 #[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 #[must_use]
1675 pub fn get_active_tick_count(&self) -> usize {
1676 self.tick_map.active_tick_count()
1677 }
1678
1679 #[must_use]
1684 pub fn get_tick(&self, tick: i32) -> Option<&PoolTick> {
1685 self.tick_map.get_tick(tick)
1686 }
1687
1688 #[must_use]
1693 pub fn get_current_tick(&self) -> i32 {
1694 self.state.current_tick
1695 }
1696
1697 #[must_use]
1705 pub fn get_total_tick_count(&self) -> usize {
1706 self.tick_map.total_tick_count()
1707 }
1708
1709 #[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 #[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 #[must_use]
1755 pub fn get_all_positions(&self) -> Vec<&PoolPosition> {
1756 self.positions.values().collect()
1757 }
1758
1759 #[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 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), self.last_processed_ts.unwrap_or(self.pool.ts_init), ))
1801 }
1802
1803 #[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 #[must_use]
1825 pub fn get_total_inactive_positions(&self) -> usize {
1826 self.positions.len() - self.get_total_active_positions()
1827 }
1828
1829 #[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 for position in self.positions.values() {
1844 if position.liquidity > 0 {
1845 if position.tick_upper <= current_tick {
1846 continue;
1848 } else if position.tick_lower > current_tick {
1849 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 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 let fee_growth_0 = self.state.fee_growth_global_0;
1875 if fee_growth_0 > U256::ZERO {
1876 let active_liquidity = self.get_active_liquidity();
1879 if active_liquidity > 0 {
1880 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 total_amount0 += self.state.protocol_fees_token0;
1894
1895 total_amount0 + total_fees_0_left
1896 }
1897
1898 #[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 for position in self.positions.values() {
1913 if position.liquidity > 0 {
1914 if position.tick_lower > current_tick {
1915 continue;
1917 } else if position.tick_upper <= current_tick {
1918 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 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 total_fees_1_collected += position.total_amount1_collected;
1939 }
1940
1941 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 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 total_amount1 += self.state.protocol_fees_token1;
1959
1960 total_amount1 + total_fees_1_left
1961 }
1962
1963 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 #[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 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 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}