1use std::sync::Arc;
19
20use ahash::AHashMap;
21use alloy_primitives::{Address, I256, U160, U256};
22use nautilus_core::UnixNanos;
23
24use crate::defi::{
25 DexType, PoolLiquidityUpdate, PoolSwap, SharedPool,
26 data::{
27 DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate,
28 PoolLiquidityUpdateType, block::BlockPosition, flash::PoolFlash,
29 },
30 pool_analysis::{
31 error::{
32 PoolEventKind, PoolEventLocation, PoolProfilerError, liquidity_error_with_location,
33 },
34 position::PoolPosition,
35 quote::SwapQuote,
36 size_estimator,
37 snapshot::{PROTOCOL_FEE_BASIS_POINTS_DENOMINATOR, PoolAnalytics, PoolSnapshot, PoolState},
38 swap_math::compute_swap_step,
39 },
40 reporting::{BlockchainSyncReportItems, BlockchainSyncReporter},
41 tick_map::{
42 TickMap,
43 full_math::{FullMath, Q128},
44 liquidity_math::{liquidity_math_add, try_liquidity_math_add},
45 sqrt_price_math::{get_amount0_delta, get_amount1_delta, get_amounts_for_liquidity},
46 tick::{CrossedTick, PoolTick},
47 tick_math::{
48 MAX_SQRT_RATIO, MIN_SQRT_RATIO, get_sqrt_ratio_at_tick, get_tick_at_sqrt_ratio,
49 },
50 },
51};
52
53#[derive(Debug, Clone)]
71#[cfg_attr(
72 feature = "python",
73 pyo3::pyclass(module = "nautilus_trader.model", from_py_object)
74)]
75#[cfg_attr(
76 feature = "python",
77 pyo3_stub_gen::derive::gen_stub_pyclass(module = "nautilus_trader.model")
78)]
79pub struct PoolProfiler {
80 pub pool: SharedPool,
82 positions: AHashMap<String, PoolPosition>,
84 pub tick_map: TickMap,
86 pub state: PoolState,
88 pub analytics: PoolAnalytics,
90 pub last_processed_event: Option<BlockPosition>,
92 pub last_processed_ts: Option<UnixNanos>,
94 pub is_initialized: bool,
96 reporter: Option<BlockchainSyncReporter>,
98 last_reported_block: u64,
100}
101
102impl PoolProfiler {
103 #[must_use]
109 pub fn new(pool: SharedPool) -> Self {
110 let tick_spacing = pool.tick_spacing.expect("Pool tick spacing must be set");
111 let mut state = PoolState::default();
112
113 if let Some((fee_protocol0, fee_protocol1)) =
114 initial_protocol_fee_basis_points(pool.dex.name, pool.fee)
115 {
116 state.set_protocol_fee_basis_points(fee_protocol0, fee_protocol1);
117 }
118
119 Self {
120 pool,
121 positions: AHashMap::new(),
122 tick_map: TickMap::new(tick_spacing),
123 state,
124 analytics: PoolAnalytics::default(),
125 last_processed_event: None,
126 last_processed_ts: None,
127 is_initialized: false,
128 reporter: None,
129 last_reported_block: 0,
130 }
131 }
132
133 pub fn initialize(&mut self, price_sqrt_ratio_x96: U160) -> Result<(), PoolProfilerError> {
140 if self.is_initialized {
141 return Err(PoolProfilerError::AlreadyInitialized {
142 instrument_id: self.pool.instrument_id,
143 pool_identifier: self.pool.pool_identifier,
144 });
145 }
146
147 let calculated_tick = get_tick_at_sqrt_ratio(price_sqrt_ratio_x96);
148
149 if let Some(initial_tick) = self.pool.initial_tick
150 && initial_tick != calculated_tick
151 {
152 return Err(PoolProfilerError::InitialTickMismatch {
153 instrument_id: self.pool.instrument_id,
154 pool_identifier: self.pool.pool_identifier,
155 initial_tick,
156 calculated_tick,
157 });
158 }
159
160 log::info!(
161 "Initializing pool profiler with tick {calculated_tick} and price sqrt ratio {price_sqrt_ratio_x96}"
162 );
163
164 self.state.current_tick = calculated_tick;
165 self.state.price_sqrt_ratio_x96 = price_sqrt_ratio_x96;
166 self.is_initialized = true;
167 Ok(())
168 }
169
170 pub fn check_if_initialized(&self, event_kind: PoolEventKind) -> Result<(), PoolProfilerError> {
177 if !self.is_initialized {
178 return Err(PoolProfilerError::NotInitialized {
179 instrument_id: self.pool.instrument_id,
180 pool_identifier: self.pool.pool_identifier,
181 event_kind,
182 });
183 }
184 Ok(())
185 }
186
187 fn event_location(
188 &self,
189 event_kind: PoolEventKind,
190 block: u64,
191 transaction_index: u32,
192 log_index: u32,
193 ) -> PoolEventLocation {
194 PoolEventLocation {
195 instrument_id: self.pool.instrument_id,
196 pool_identifier: self.pool.pool_identifier,
197 block,
198 transaction_index,
199 log_index,
200 event_kind,
201 }
202 }
203
204 pub fn process(&mut self, event: &DexPoolData) -> anyhow::Result<()> {
218 if self.check_if_already_processed(
219 event.block_number(),
220 event.transaction_index(),
221 event.log_index(),
222 ) {
223 return Ok(());
224 }
225
226 match event {
227 DexPoolData::Swap(swap) => self.process_swap(swap)?,
228 DexPoolData::LiquidityUpdate(update) => match update.kind {
229 PoolLiquidityUpdateType::Mint => self.process_mint(update)?,
230 PoolLiquidityUpdateType::Burn => self.process_burn(update)?,
231 },
232 DexPoolData::FeeCollect(collect) => self.process_collect(collect)?,
233 DexPoolData::FeeProtocolUpdate(update) => self.process_fee_protocol_update(update)?,
234 DexPoolData::FeeProtocolCollect(collect) => {
235 self.process_fee_protocol_collect(collect)?;
236 }
237 DexPoolData::Flash(flash) => self.process_flash(flash)?,
238 }
239
240 self.update_reporter_if_enabled(event.block_number());
241
242 Ok(())
243 }
244
245 fn check_if_already_processed(&self, block: u64, tx_idx: u32, log_idx: u32) -> bool {
247 if let Some(last_event) = &self.last_processed_event {
248 let should_skip = block < last_event.number
249 || (block == last_event.number && tx_idx < last_event.transaction_index)
250 || (block == last_event.number
251 && tx_idx == last_event.transaction_index
252 && log_idx <= last_event.log_index);
253
254 if should_skip {
255 log::debug!(
256 "Skipping already processed event at block {block} tx {tx_idx} log {log_idx}"
257 );
258 }
259 return should_skip;
260 }
261
262 false
263 }
264
265 fn update_reporter_if_enabled(&mut self, current_block: u64) {
267 if let Some(reporter) = &mut self.reporter {
269 let blocks_processed = current_block.saturating_sub(self.last_reported_block);
270
271 if blocks_processed > 0 {
272 reporter.update(blocks_processed as usize);
273 self.last_reported_block = current_block;
274
275 if reporter.should_log_progress(current_block, current_block) {
276 reporter.log_progress(current_block);
277 }
278 }
279 }
280 }
281
282 pub fn process_swap(&mut self, swap: &PoolSwap) -> anyhow::Result<()> {
303 self.check_if_initialized(PoolEventKind::Swap)?;
304
305 if self.check_if_already_processed(swap.block, swap.transaction_index, swap.log_index) {
306 return Ok(());
307 }
308
309 let zero_for_one = swap.amount0.is_positive();
310 let amount_specified = if zero_for_one {
311 swap.amount0
312 } else {
313 swap.amount1
314 };
315
316 let sqrt_price_limit_x96 = swap.sqrt_price_x96;
319 let location = self.event_location(
320 PoolEventKind::Swap,
321 swap.block,
322 swap.transaction_index,
323 swap.log_index,
324 );
325 let swap_quote = self
326 .simulate_swap_through_ticks(amount_specified, zero_for_one, sqrt_price_limit_x96, true)
327 .map_err(|e| Self::wrap_liquidity_error(e, location.clone()))?;
328
329 let tick_mismatch = swap.tick != swap_quote.tick_after;
330 let liquidity_mismatch = swap.liquidity != swap_quote.liquidity_after;
331 let sqrt_mismatch = swap.sqrt_price_x96 != swap_quote.sqrt_price_after_x96;
332 let structural_mismatch = tick_mismatch || liquidity_mismatch;
333 let anchoring_crossed_tick = if tick_mismatch {
334 self.tick_map
335 .get_all_ticks()
336 .keys()
337 .copied()
338 .filter(|&tick| {
339 self.tick_map.is_tick_initialized(tick)
340 && (swap_quote.tick_after >= tick) != (swap.tick >= tick)
341 })
342 .min()
343 } else {
344 None
345 };
346
347 if structural_mismatch
348 && (!swap_quote.crossed_ticks.is_empty() || anchoring_crossed_tick.is_some())
349 {
350 return Err(PoolProfilerError::SwapReplayMismatch {
351 location,
352 simulated_tick: swap_quote.tick_after,
353 event_tick: swap.tick,
354 simulated_liquidity: swap_quote.liquidity_after,
355 event_liquidity: swap.liquidity,
356 simulated_crossed_tick_count: swap_quote.crossed_ticks.len(),
357 anchoring_crossed_tick,
358 }
359 .into());
360 }
361 self.apply_swap_quote(&swap_quote);
362
363 if tick_mismatch {
365 log::warn!(
366 "Inconsistency in swap processing: Current tick mismatch: simulated {}, event {} on block {}",
367 swap_quote.tick_after,
368 swap.tick,
369 swap.block
370 );
371 }
372
373 if swap.tick != self.state.current_tick {
374 self.state.current_tick = swap.tick;
375 }
376
377 if liquidity_mismatch {
378 log::warn!(
379 "Inconsistency in swap processing: Active liquidity mismatch: simulated {}, event {} on block {}",
380 swap_quote.liquidity_after,
381 swap.liquidity,
382 swap.block
383 );
384 }
385
386 if swap.liquidity != self.tick_map.liquidity {
387 self.tick_map.liquidity = swap.liquidity;
388 }
389
390 if sqrt_mismatch {
391 log::warn!(
392 "Inconsistency in swap processing: Sqrt price mismatch: simulated {}, event {} on block {}",
393 swap_quote.sqrt_price_after_x96,
394 swap.sqrt_price_x96,
395 swap.block
396 );
397 }
398
399 if swap.sqrt_price_x96 != self.state.price_sqrt_ratio_x96 {
400 self.state.price_sqrt_ratio_x96 = swap.sqrt_price_x96;
401 }
402
403 self.last_processed_event = Some(
404 BlockPosition::new(
405 swap.block,
406 swap.transaction_hash.clone(),
407 swap.transaction_index,
408 swap.log_index,
409 )
410 .with_block_hash(swap.block_hash.clone()),
411 );
412 self.last_processed_ts = Some(swap.ts_event);
413 self.update_reporter_if_enabled(swap.block);
414 self.update_liquidity_analytics();
415
416 Ok(())
417 }
418
419 pub fn execute_swap(
434 &mut self,
435 sender: Address,
436 recipient: Address,
437 block: BlockPosition,
438 zero_for_one: bool,
439 amount_specified: I256,
440 sqrt_price_limit_x96: U160,
441 ) -> anyhow::Result<PoolSwap> {
442 self.check_if_initialized(PoolEventKind::Swap)?;
443
444 let swap_quote = self.simulate_swap_through_ticks(
445 amount_specified,
446 zero_for_one,
447 sqrt_price_limit_x96,
448 false,
449 )?;
450
451 self.apply_swap_quote(&swap_quote);
452
453 let swap_event = PoolSwap::new(
454 Arc::clone(&self.pool.chain),
455 Arc::clone(&self.pool.dex),
456 self.pool.instrument_id,
457 self.pool.pool_identifier,
458 block.number,
459 block.transaction_hash,
460 block.transaction_index,
461 block.log_index,
462 self.pool.ts_init, self.pool.ts_init, sender,
465 recipient,
466 swap_quote.amount0,
467 swap_quote.amount1,
468 self.state.price_sqrt_ratio_x96,
469 self.tick_map.liquidity,
470 self.state.current_tick,
471 );
472 Ok(swap_event)
473 }
474
475 pub fn simulate_swap_through_ticks(
512 &self,
513 amount_specified: I256,
514 zero_for_one: bool,
515 sqrt_price_limit_x96: U160,
516 traverse_empty_ranges: bool,
517 ) -> anyhow::Result<SwapQuote> {
518 let exact_input = amount_specified.is_positive();
519 let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
520
521 let mut current_sqrt_price = self.state.price_sqrt_ratio_x96;
522 let mut current_tick = self.state.current_tick;
523 let mut current_active_liquidity = self.tick_map.liquidity;
524 let mut amount_specified_remaining = amount_specified;
525 let mut amount_calculated = I256::ZERO;
526 let mut protocol_fee = U256::ZERO;
527 let mut lp_fee = U256::ZERO;
528 let mut crossed_ticks = Vec::new();
529
530 let fee_protocol = self.state.uniswap_v3_fee_protocol(zero_for_one);
532 let fee_protocol_basis_points = self.state.fee_protocol_basis_points(zero_for_one);
533
534 let mut current_fee_growth_global = if zero_for_one {
536 self.state.fee_growth_global_0
537 } else {
538 self.state.fee_growth_global_1
539 };
540
541 let replay_exact_input = traverse_empty_ranges && exact_input;
544
545 while (amount_specified_remaining != I256::ZERO
547 || (traverse_empty_ranges && current_active_liquidity == 0))
548 && (sqrt_price_limit_x96 != current_sqrt_price
549 || (replay_exact_input && amount_specified_remaining.is_positive()))
550 {
551 let sqrt_price_start_x96 = current_sqrt_price;
552
553 let (mut tick_next, initialized) = self
554 .tick_map
555 .next_initialized_tick(current_tick, zero_for_one);
556
557 tick_next = tick_next.clamp(PoolTick::MIN_TICK, PoolTick::MAX_TICK);
559
560 let sqrt_price_next = get_sqrt_ratio_at_tick(tick_next);
562
563 let sqrt_price_target = if (zero_for_one && sqrt_price_next < sqrt_price_limit_x96)
565 || (!zero_for_one && sqrt_price_next > sqrt_price_limit_x96)
566 {
567 sqrt_price_limit_x96
568 } else {
569 sqrt_price_next
570 };
571
572 let mut swap_step_result = compute_swap_step(
573 current_sqrt_price,
574 sqrt_price_target,
575 current_active_liquidity,
576 amount_specified_remaining,
577 fee_tier,
578 )?;
579
580 if replay_exact_input
583 && swap_step_result.sqrt_ratio_next_x96 == sqrt_price_limit_x96
584 && swap_step_result.sqrt_ratio_next_x96 != sqrt_price_next
585 {
586 swap_step_result.fee_amount =
587 amount_specified_remaining.into_raw() - swap_step_result.amount_in;
588 }
589
590 current_sqrt_price = swap_step_result.sqrt_ratio_next_x96;
592
593 if exact_input {
595 amount_specified_remaining -= FullMath::truncate_to_i256(
597 swap_step_result.amount_in + swap_step_result.fee_amount,
598 );
599 amount_calculated -= FullMath::truncate_to_i256(swap_step_result.amount_out);
600 } else {
601 amount_specified_remaining +=
603 FullMath::truncate_to_i256(swap_step_result.amount_out);
604 amount_calculated += FullMath::truncate_to_i256(
605 swap_step_result.amount_in + swap_step_result.fee_amount,
606 );
607 }
608
609 let mut step_fee_amount = swap_step_result.fee_amount;
611
612 if fee_protocol > 0 || fee_protocol_basis_points.is_some() {
613 let protocol_fee_delta = Self::protocol_fee_delta(
614 swap_step_result.fee_amount,
615 fee_protocol,
616 fee_protocol_basis_points,
617 )?;
618 step_fee_amount -= protocol_fee_delta;
619 protocol_fee += protocol_fee_delta;
620 }
621
622 lp_fee += step_fee_amount;
624
625 if current_active_liquidity > 0 {
627 let fee_growth_delta =
628 FullMath::mul_div(step_fee_amount, Q128, U256::from(current_active_liquidity))?;
629 current_fee_growth_global += fee_growth_delta;
630 }
631
632 if swap_step_result.sqrt_ratio_next_x96 == sqrt_price_next {
634 if initialized {
638 crossed_ticks.push(CrossedTick::new(
639 tick_next,
640 zero_for_one,
641 if zero_for_one {
642 current_fee_growth_global
643 } else {
644 self.state.fee_growth_global_0
645 },
646 if zero_for_one {
647 self.state.fee_growth_global_1
648 } else {
649 current_fee_growth_global
650 },
651 ));
652
653 if let Some(tick_data) = self.tick_map.get_tick(tick_next) {
655 let liquidity_net = tick_data.liquidity_net;
656 current_active_liquidity = if zero_for_one {
657 try_liquidity_math_add(current_active_liquidity, -liquidity_net)?
658 } else {
659 try_liquidity_math_add(current_active_liquidity, liquidity_net)?
660 };
661 }
662 }
663
664 current_tick = if zero_for_one {
665 tick_next - 1
666 } else {
667 tick_next
668 };
669 } else if swap_step_result.sqrt_ratio_next_x96 != sqrt_price_start_x96 {
670 current_tick = get_tick_at_sqrt_ratio(current_sqrt_price);
673 }
674 }
675
676 let (amount0, amount1) = if zero_for_one == exact_input {
678 (
679 amount_specified - amount_specified_remaining,
680 amount_calculated,
681 )
682 } else {
683 (
684 amount_calculated,
685 amount_specified - amount_specified_remaining,
686 )
687 };
688
689 let quote = SwapQuote::new(
690 self.pool.instrument_id,
691 amount0,
692 amount1,
693 self.state.price_sqrt_ratio_x96,
694 current_sqrt_price,
695 self.state.current_tick,
696 current_tick,
697 current_active_liquidity,
698 current_fee_growth_global,
699 lp_fee,
700 protocol_fee,
701 crossed_ticks,
702 );
703 Ok(quote)
704 }
705
706 pub fn apply_swap_quote(&mut self, swap_quote: &SwapQuote) {
714 self.state.current_tick = swap_quote.tick_after;
715 self.state.price_sqrt_ratio_x96 = swap_quote.sqrt_price_after_x96;
716
717 self.apply_swap_quote_fee_state(swap_quote);
718
719 for crossed in &swap_quote.crossed_ticks {
720 let liquidity_net =
721 self.tick_map
722 .cross_tick(crossed.tick, crossed.fee_growth_0, crossed.fee_growth_1);
723
724 self.tick_map.liquidity = if crossed.zero_for_one {
725 liquidity_math_add(self.tick_map.liquidity, -liquidity_net)
726 } else {
727 liquidity_math_add(self.tick_map.liquidity, liquidity_net)
728 };
729 }
730 self.analytics.total_swaps += 1;
731
732 debug_assert_eq!(
733 self.tick_map.liquidity, swap_quote.liquidity_after,
734 "Liquidity mismatch in apply_swap_quote: computed={}, quote={}",
735 self.tick_map.liquidity, swap_quote.liquidity_after
736 );
737 }
738
739 fn apply_swap_quote_fee_state(&mut self, swap_quote: &SwapQuote) {
740 if swap_quote.zero_for_one() {
741 self.state.fee_growth_global_0 = swap_quote.fee_growth_global_after;
742 self.state.protocol_fees_token0 += swap_quote.protocol_fee;
743 } else {
744 self.state.fee_growth_global_1 = swap_quote.fee_growth_global_after;
745 self.state.protocol_fees_token1 += swap_quote.protocol_fee;
746 }
747 }
748
749 #[must_use]
753 pub fn wrap_liquidity_error(err: anyhow::Error, location: PoolEventLocation) -> anyhow::Error {
754 match err.downcast::<super::error::LiquidityMathError>() {
755 Ok(math_err) => anyhow::Error::from(liquidity_error_with_location(math_err, location)),
756 Err(other) => other,
757 }
758 }
759
760 pub fn quote_swap(
775 &self,
776 amount_specified: I256,
777 zero_for_one: bool,
778 sqrt_price_limit_x96: Option<U160>,
779 ) -> anyhow::Result<SwapQuote> {
780 self.check_if_initialized(PoolEventKind::Swap)?;
781
782 if amount_specified.is_zero() {
783 anyhow::bail!("Cannot quote swap with zero amount");
784 }
785
786 if let Some(price_limit) = sqrt_price_limit_x96 {
787 self.validate_price_limit(price_limit, zero_for_one)?;
788 }
789
790 let limit = sqrt_price_limit_x96.unwrap_or_else(|| {
791 if zero_for_one {
792 MIN_SQRT_RATIO + U160::from(1)
793 } else {
794 MAX_SQRT_RATIO - U160::from(1)
795 }
796 });
797
798 self.simulate_swap_through_ticks(amount_specified, zero_for_one, limit, false)
799 }
800
801 pub fn swap_exact_in(
806 &self,
807 amount_in: U256,
808 zero_for_one: bool,
809 sqrt_price_limit_x96: Option<U160>,
810 ) -> anyhow::Result<SwapQuote> {
811 let amount_specified = I256::from(amount_in);
813 let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
814
815 Ok(quote)
816 }
817
818 pub fn swap_exact_out(
824 &self,
825 amount_out: U256,
826 zero_for_one: bool,
827 sqrt_price_limit_x96: Option<U160>,
828 ) -> anyhow::Result<SwapQuote> {
829 let amount_specified = -I256::from(amount_out);
831 let quote = self.quote_swap(amount_specified, zero_for_one, sqrt_price_limit_x96)?;
832 quote.validate_exact_output(amount_out)?;
833
834 Ok(quote)
835 }
836
837 pub fn swap_to_lower_sqrt_price(
842 &self,
843 sqrt_price_limit_x96: U160,
844 ) -> anyhow::Result<SwapQuote> {
845 self.quote_swap(I256::MAX, true, Some(sqrt_price_limit_x96))
846 }
847
848 pub fn swap_to_higher_sqrt_price(
853 &self,
854 sqrt_price_limit_x96: U160,
855 ) -> anyhow::Result<SwapQuote> {
856 self.quote_swap(I256::MAX, false, Some(sqrt_price_limit_x96))
857 }
858
859 pub fn size_for_impact_bps(&self, impact_bps: u32, zero_for_one: bool) -> anyhow::Result<U256> {
874 let config = size_estimator::EstimationConfig::default();
875 size_estimator::size_for_impact_bps(self, impact_bps, zero_for_one, &config)
876 }
877
878 pub fn size_for_impact_bps_detailed(
892 &self,
893 impact_bps: u32,
894 zero_for_one: bool,
895 ) -> anyhow::Result<size_estimator::SizeForImpactResult> {
896 let config = size_estimator::EstimationConfig::default();
897 size_estimator::size_for_impact_bps_detailed(self, impact_bps, zero_for_one, &config)
898 }
899
900 fn validate_price_limit(
906 &self,
907 limit_price_sqrt: U160,
908 zero_for_one: bool,
909 ) -> anyhow::Result<()> {
910 if zero_for_one {
911 if limit_price_sqrt >= self.state.price_sqrt_ratio_x96 {
913 anyhow::bail!("Price limit must be less than current price for zero_for_one swaps");
914 }
915
916 if limit_price_sqrt <= MIN_SQRT_RATIO {
917 anyhow::bail!(
918 "Price limit {limit_price_sqrt} must be greater than MIN_SQRT_RATIO {MIN_SQRT_RATIO}"
919 );
920 }
921 } else {
922 if limit_price_sqrt <= self.state.price_sqrt_ratio_x96 {
924 anyhow::bail!(
925 "Price limit must be greater than current price for one_for_zero swaps"
926 );
927 }
928
929 if limit_price_sqrt >= MAX_SQRT_RATIO {
930 anyhow::bail!(
931 "Price limit {limit_price_sqrt} must be less than MAX_SQRT_RATIO {MAX_SQRT_RATIO}"
932 );
933 }
934 }
935
936 Ok(())
937 }
938
939 pub fn process_mint(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
951 self.check_if_initialized(PoolEventKind::Mint)?;
952
953 if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
954 {
955 return Ok(());
956 }
957
958 self.validate_ticks(update.tick_lower, update.tick_upper)?;
959 let location = self.event_location(
960 PoolEventKind::Mint,
961 update.block,
962 update.transaction_index,
963 update.log_index,
964 );
965 self.add_liquidity(
966 &update.owner,
967 update.tick_lower,
968 update.tick_upper,
969 update.position_liquidity,
970 update.amount0,
971 update.amount1,
972 )
973 .map_err(|e| Self::wrap_liquidity_error(e, location))?;
974
975 self.analytics.total_mints += 1;
976 self.last_processed_event = Some(
977 BlockPosition::new(
978 update.block,
979 update.transaction_hash.clone(),
980 update.transaction_index,
981 update.log_index,
982 )
983 .with_block_hash(update.block_hash.clone()),
984 );
985 self.last_processed_ts = Some(update.ts_event);
986 self.update_reporter_if_enabled(update.block);
987 self.update_liquidity_analytics();
988
989 Ok(())
990 }
991
992 fn add_liquidity(
997 &mut self,
998 owner: &Address,
999 tick_lower: i32,
1000 tick_upper: i32,
1001 liquidity: u128,
1002 amount0: U256,
1003 amount1: U256,
1004 ) -> anyhow::Result<()> {
1005 let liquidity_delta = i128::try_from(liquidity)
1006 .map_err(|_| anyhow::anyhow!("Liquidity {liquidity} exceeds i128::MAX"))?;
1007 self.update_position(
1008 owner,
1009 tick_lower,
1010 tick_upper,
1011 liquidity_delta,
1012 amount0,
1013 amount1,
1014 )?;
1015
1016 self.analytics.total_amount0_deposited += amount0;
1018 self.analytics.total_amount1_deposited += amount1;
1019
1020 Ok(())
1021 }
1022
1023 pub fn execute_mint(
1038 &mut self,
1039 recipient: Address,
1040 block: BlockPosition,
1041 tick_lower: i32,
1042 tick_upper: i32,
1043 liquidity: u128,
1044 ) -> anyhow::Result<PoolLiquidityUpdate> {
1045 self.check_if_initialized(PoolEventKind::Mint)?;
1046
1047 self.validate_ticks(tick_lower, tick_upper)?;
1048 let (amount0, amount1) = get_amounts_for_liquidity(
1049 self.state.price_sqrt_ratio_x96,
1050 tick_lower,
1051 tick_upper,
1052 liquidity,
1053 true,
1054 );
1055 self.add_liquidity(
1056 &recipient, tick_lower, tick_upper, liquidity, amount0, amount1,
1057 )?;
1058
1059 self.analytics.total_mints += 1;
1060
1061 let event = PoolLiquidityUpdate::new(
1062 Arc::clone(&self.pool.chain),
1063 Arc::clone(&self.pool.dex),
1064 self.pool.instrument_id,
1065 self.pool.pool_identifier,
1066 PoolLiquidityUpdateType::Mint,
1067 block.number,
1068 block.transaction_hash,
1069 block.transaction_index,
1070 block.log_index,
1071 None,
1072 recipient,
1073 liquidity,
1074 amount0,
1075 amount1,
1076 tick_lower,
1077 tick_upper,
1078 self.pool.ts_init, self.pool.ts_init, );
1081
1082 Ok(event)
1083 }
1084
1085 pub fn process_burn(&mut self, update: &PoolLiquidityUpdate) -> anyhow::Result<()> {
1097 self.check_if_initialized(PoolEventKind::Burn)?;
1098
1099 if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
1100 {
1101 return Ok(());
1102 }
1103
1104 self.validate_ticks(update.tick_lower, update.tick_upper)?;
1105
1106 let liquidity_delta = i128::try_from(update.position_liquidity).map_err(|_| {
1108 anyhow::anyhow!("Liquidity {} exceeds i128::MAX", update.position_liquidity)
1109 })?;
1110 let location = self.event_location(
1111 PoolEventKind::Burn,
1112 update.block,
1113 update.transaction_index,
1114 update.log_index,
1115 );
1116
1117 self.update_position(
1118 &update.owner,
1119 update.tick_lower,
1120 update.tick_upper,
1121 -liquidity_delta,
1122 update.amount0,
1123 update.amount1,
1124 )
1125 .map_err(|e| Self::wrap_liquidity_error(e, location))?;
1126
1127 self.analytics.total_burns += 1;
1128 self.last_processed_event = Some(
1129 BlockPosition::new(
1130 update.block,
1131 update.transaction_hash.clone(),
1132 update.transaction_index,
1133 update.log_index,
1134 )
1135 .with_block_hash(update.block_hash.clone()),
1136 );
1137 self.last_processed_ts = Some(update.ts_event);
1138 self.update_reporter_if_enabled(update.block);
1139 self.update_liquidity_analytics();
1140
1141 Ok(())
1142 }
1143
1144 pub fn execute_burn(
1157 &mut self,
1158 recipient: Address,
1159 block: BlockPosition,
1160 tick_lower: i32,
1161 tick_upper: i32,
1162 liquidity: u128,
1163 ) -> anyhow::Result<PoolLiquidityUpdate> {
1164 self.check_if_initialized(PoolEventKind::Burn)?;
1165
1166 self.validate_ticks(tick_lower, tick_upper)?;
1167 let (amount0, amount1) = get_amounts_for_liquidity(
1168 self.state.price_sqrt_ratio_x96,
1169 tick_lower,
1170 tick_upper,
1171 liquidity,
1172 false,
1173 );
1174
1175 let liquidity_delta = i128::try_from(liquidity)
1177 .map_err(|_| anyhow::anyhow!("Liquidity {liquidity} exceeds i128::MAX"))?;
1178 self.update_position(
1179 &recipient,
1180 tick_lower,
1181 tick_upper,
1182 -liquidity_delta,
1183 amount0,
1184 amount1,
1185 )?;
1186
1187 self.analytics.total_burns += 1;
1188
1189 let event = PoolLiquidityUpdate::new(
1190 Arc::clone(&self.pool.chain),
1191 Arc::clone(&self.pool.dex),
1192 self.pool.instrument_id,
1193 self.pool.pool_identifier,
1194 PoolLiquidityUpdateType::Burn,
1195 block.number,
1196 block.transaction_hash,
1197 block.transaction_index,
1198 block.log_index,
1199 None,
1200 recipient,
1201 liquidity,
1202 amount0,
1203 amount1,
1204 tick_lower,
1205 tick_upper,
1206 self.pool.ts_init, self.pool.ts_init, );
1209
1210 Ok(event)
1211 }
1212
1213 pub fn process_collect(&mut self, collect: &PoolFeeCollect) -> anyhow::Result<()> {
1226 self.check_if_initialized(PoolEventKind::Collect)?;
1227
1228 if self.check_if_already_processed(
1229 collect.block,
1230 collect.transaction_index,
1231 collect.log_index,
1232 ) {
1233 return Ok(());
1234 }
1235 let position_key =
1236 PoolPosition::get_position_key(&collect.owner, collect.tick_lower, collect.tick_upper);
1237
1238 if let Some(position) = self.positions.get_mut(&position_key) {
1239 position.collect_fees(collect.amount0, collect.amount1);
1240 }
1241
1242 self.cleanup_position_if_empty(&position_key);
1244
1245 self.analytics.total_amount0_collected += U256::from(collect.amount0);
1246 self.analytics.total_amount1_collected += U256::from(collect.amount1);
1247
1248 self.analytics.total_fee_collects += 1;
1249 self.last_processed_event = Some(
1250 BlockPosition::new(
1251 collect.block,
1252 collect.transaction_hash.clone(),
1253 collect.transaction_index,
1254 collect.log_index,
1255 )
1256 .with_block_hash(collect.block_hash.clone()),
1257 );
1258 self.last_processed_ts = Some(collect.ts_event);
1259 self.update_reporter_if_enabled(collect.block);
1260 self.update_liquidity_analytics();
1261
1262 Ok(())
1263 }
1264
1265 pub fn process_fee_protocol_update(
1275 &mut self,
1276 update: &PoolFeeProtocolUpdate,
1277 ) -> anyhow::Result<()> {
1278 if self.check_if_already_processed(update.block, update.transaction_index, update.log_index)
1279 {
1280 return Ok(());
1281 }
1282
1283 if update.dex.name == DexType::PancakeSwapV3 {
1284 self.state
1285 .set_protocol_fee_basis_points(update.fee_protocol0_new, update.fee_protocol1_new);
1286 } else {
1287 let fee_protocol = update
1288 .uniswap_v3_packed()
1289 .ok_or_else(|| anyhow::anyhow!("invalid Uniswap V3 fee protocol update"))?;
1290 self.state.set_uniswap_v3_fee_protocol(fee_protocol);
1291 }
1292
1293 self.last_processed_event = Some(
1294 BlockPosition::new(
1295 update.block,
1296 update.transaction_hash.clone(),
1297 update.transaction_index,
1298 update.log_index,
1299 )
1300 .with_block_hash(update.block_hash.clone()),
1301 );
1302 self.last_processed_ts = Some(update.ts_event);
1303 self.update_reporter_if_enabled(update.block);
1304
1305 Ok(())
1306 }
1307
1308 #[allow(
1318 clippy::missing_errors_doc,
1319 reason = "protocol fee collection only updates in-memory state"
1320 )]
1321 pub fn process_fee_protocol_collect(
1322 &mut self,
1323 collect: &PoolFeeProtocolCollect,
1324 ) -> anyhow::Result<()> {
1325 if self.check_if_already_processed(
1326 collect.block,
1327 collect.transaction_index,
1328 collect.log_index,
1329 ) {
1330 return Ok(());
1331 }
1332
1333 self.state.protocol_fees_token0 = self
1334 .state
1335 .protocol_fees_token0
1336 .saturating_sub(U256::from(collect.amount0));
1337 self.state.protocol_fees_token1 = self
1338 .state
1339 .protocol_fees_token1
1340 .saturating_sub(U256::from(collect.amount1));
1341
1342 self.last_processed_event = Some(
1343 BlockPosition::new(
1344 collect.block,
1345 collect.transaction_hash.clone(),
1346 collect.transaction_index,
1347 collect.log_index,
1348 )
1349 .with_block_hash(collect.block_hash.clone()),
1350 );
1351 self.last_processed_ts = Some(collect.ts_event);
1352 self.update_reporter_if_enabled(collect.block);
1353
1354 Ok(())
1355 }
1356
1357 pub fn process_flash(&mut self, flash: &PoolFlash) -> anyhow::Result<()> {
1365 self.check_if_initialized(PoolEventKind::Flash)?;
1366
1367 if self.check_if_already_processed(flash.block, flash.transaction_index, flash.log_index) {
1368 return Ok(());
1369 }
1370
1371 self.update_flash_state(flash.paid0, flash.paid1)?;
1372
1373 self.analytics.total_flashes += 1;
1374 self.last_processed_event = Some(
1375 BlockPosition::new(
1376 flash.block,
1377 flash.transaction_hash.clone(),
1378 flash.transaction_index,
1379 flash.log_index,
1380 )
1381 .with_block_hash(flash.block_hash.clone()),
1382 );
1383 self.last_processed_ts = Some(flash.ts_event);
1384 self.update_reporter_if_enabled(flash.block);
1385 self.update_liquidity_analytics();
1386
1387 Ok(())
1388 }
1389
1390 pub fn execute_flash(
1403 &mut self,
1404 sender: Address,
1405 recipient: Address,
1406 block: BlockPosition,
1407 amount0: U256,
1408 amount1: U256,
1409 ) -> anyhow::Result<PoolFlash> {
1410 self.check_if_initialized(PoolEventKind::Flash)?;
1411
1412 let fee_tier = self.pool.fee.expect("Pool fee should be initialized");
1413
1414 let paid0 = if amount0 > U256::ZERO {
1416 FullMath::mul_div_rounding_up(amount0, U256::from(fee_tier), U256::from(1_000_000))?
1417 } else {
1418 U256::ZERO
1419 };
1420
1421 let paid1 = if amount1 > U256::ZERO {
1422 FullMath::mul_div_rounding_up(amount1, U256::from(fee_tier), U256::from(1_000_000))?
1423 } else {
1424 U256::ZERO
1425 };
1426
1427 self.update_flash_state(paid0, paid1)?;
1428 self.analytics.total_flashes += 1;
1429
1430 let flash_event = PoolFlash::new(
1431 Arc::clone(&self.pool.chain),
1432 Arc::clone(&self.pool.dex),
1433 self.pool.instrument_id,
1434 self.pool.pool_identifier,
1435 block.number,
1436 block.transaction_hash,
1437 block.transaction_index,
1438 block.log_index,
1439 self.pool.ts_init, self.pool.ts_init, sender,
1442 recipient,
1443 amount0,
1444 amount1,
1445 paid0,
1446 paid1,
1447 );
1448
1449 Ok(flash_event)
1450 }
1451
1452 fn update_flash_state(&mut self, paid0: U256, paid1: U256) -> anyhow::Result<()> {
1460 let liquidity = self.tick_map.liquidity;
1461 if liquidity == 0 {
1462 anyhow::bail!("No liquidity")
1463 }
1464
1465 let fee_protocol_0 = self.state.uniswap_v3_fee_protocol(true);
1466 let fee_protocol_1 = self.state.uniswap_v3_fee_protocol(false);
1467 let fee_protocol0_basis_points = self.state.fee_protocol_basis_points(true);
1468 let fee_protocol1_basis_points = self.state.fee_protocol_basis_points(false);
1469
1470 if paid0 > U256::ZERO {
1472 let protocol_fee_0 =
1473 Self::protocol_fee_delta(paid0, fee_protocol_0, fee_protocol0_basis_points)?;
1474
1475 if protocol_fee_0 > U256::ZERO {
1476 self.state.protocol_fees_token0 += protocol_fee_0;
1477 }
1478
1479 let lp_fee_0 = paid0 - protocol_fee_0;
1480 let delta = FullMath::mul_div(lp_fee_0, Q128, U256::from(liquidity))?;
1481 self.state.fee_growth_global_0 += delta;
1482 }
1483
1484 if paid1 > U256::ZERO {
1486 let protocol_fee_1 =
1487 Self::protocol_fee_delta(paid1, fee_protocol_1, fee_protocol1_basis_points)?;
1488
1489 if protocol_fee_1 > U256::ZERO {
1490 self.state.protocol_fees_token1 += protocol_fee_1;
1491 }
1492
1493 let lp_fee_1 = paid1 - protocol_fee_1;
1494 let delta = FullMath::mul_div(lp_fee_1, Q128, U256::from(liquidity))?;
1495 self.state.fee_growth_global_1 += delta;
1496 }
1497
1498 Ok(())
1499 }
1500
1501 fn protocol_fee_delta(
1502 fee_amount: U256,
1503 uniswap_v3_fee_protocol: u8,
1504 fee_protocol_basis_points: Option<u32>,
1505 ) -> anyhow::Result<U256> {
1506 if let Some(basis_points) = fee_protocol_basis_points {
1507 return FullMath::mul_div(
1508 fee_amount,
1509 U256::from(basis_points),
1510 U256::from(PROTOCOL_FEE_BASIS_POINTS_DENOMINATOR),
1511 );
1512 }
1513
1514 if uniswap_v3_fee_protocol > 0 {
1515 Ok(fee_amount / U256::from(uniswap_v3_fee_protocol))
1516 } else {
1517 Ok(U256::ZERO)
1518 }
1519 }
1520
1521 fn update_position(
1526 &mut self,
1527 owner: &Address,
1528 tick_lower: i32,
1529 tick_upper: i32,
1530 liquidity_delta: i128,
1531 amount0: U256,
1532 amount1: U256,
1533 ) -> anyhow::Result<()> {
1534 let current_tick = self.state.current_tick;
1535 let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1536
1537 if liquidity_delta < 0 {
1539 let position_liquidity = self
1540 .positions
1541 .get(&position_key)
1542 .map_or(0, |position| position.liquidity);
1543 let burn_amount = liquidity_delta.unsigned_abs();
1544 if position_liquidity < burn_amount {
1545 anyhow::bail!(
1546 "Position liquidity {position_liquidity} is less than the requested burn amount of {burn_amount}"
1547 );
1548 }
1549 }
1550
1551 let new_active_liquidity = if tick_lower <= current_tick && current_tick < tick_upper {
1554 Some(try_liquidity_math_add(
1555 self.tick_map.liquidity,
1556 liquidity_delta,
1557 )?)
1558 } else {
1559 None
1560 };
1561
1562 for tick_value in [tick_lower, tick_upper] {
1563 let liquidity_gross = self
1564 .tick_map
1565 .get_tick(tick_value)
1566 .map_or(0, |tick| tick.liquidity_gross);
1567 let liquidity_after = try_liquidity_math_add(liquidity_gross, liquidity_delta)?;
1568 if liquidity_after > self.tick_map.max_liquidity_per_tick {
1569 anyhow::bail!(
1570 "Liquidity {liquidity_after} exceeds maximum per tick {} at tick {tick_value}",
1571 self.tick_map.max_liquidity_per_tick,
1572 );
1573 }
1574 }
1575
1576 let position = self
1577 .positions
1578 .entry(position_key)
1579 .or_insert(PoolPosition::new(*owner, tick_lower, tick_upper, 0));
1580
1581 let flipped_lower = self.tick_map.update(
1583 tick_lower,
1584 current_tick,
1585 liquidity_delta,
1586 false,
1587 self.state.fee_growth_global_0,
1588 self.state.fee_growth_global_1,
1589 );
1590 let flipped_upper = self.tick_map.update(
1591 tick_upper,
1592 current_tick,
1593 liquidity_delta,
1594 true,
1595 self.state.fee_growth_global_0,
1596 self.state.fee_growth_global_1,
1597 );
1598
1599 let (fee_growth_inside_0, fee_growth_inside_1) = self.tick_map.get_fee_growth_inside(
1600 tick_lower,
1601 tick_upper,
1602 current_tick,
1603 self.state.fee_growth_global_0,
1604 self.state.fee_growth_global_1,
1605 );
1606 position.update_fees(fee_growth_inside_0, fee_growth_inside_1);
1607 position.update_liquidity(liquidity_delta);
1608 position.update_amounts(liquidity_delta, amount0, amount1);
1609
1610 if let Some(active_liquidity) = new_active_liquidity {
1611 self.tick_map.liquidity = active_liquidity;
1612 }
1613
1614 if liquidity_delta < 0 && flipped_lower {
1616 self.tick_map.clear(tick_lower);
1617 }
1618
1619 if liquidity_delta < 0 && flipped_upper {
1620 self.tick_map.clear(tick_upper);
1621 }
1622
1623 Ok(())
1624 }
1625
1626 fn cleanup_position_if_empty(&mut self, position_key: &str) {
1630 if let Some(position) = self.positions.get(position_key)
1631 && position.is_empty()
1632 {
1633 log::debug!(
1634 "CLEANING UP EMPTY POSITION: owner={}, ticks=[{}, {}]",
1635 position.owner,
1636 position.tick_lower,
1637 position.tick_upper,
1638 );
1639 self.positions.remove(position_key);
1640 }
1641 }
1642
1643 #[must_use]
1651 pub fn liquidity_utilization_rate(&self) -> f64 {
1652 const PRECISION: u32 = 1_000_000; let total_liquidity = self.get_total_liquidity();
1655 let active_liquidity = self.get_active_liquidity();
1656
1657 if total_liquidity == U256::ZERO {
1658 return 0.0;
1659 }
1660 let ratio = FullMath::mul_div(
1661 U256::from(active_liquidity),
1662 U256::from(PRECISION),
1663 total_liquidity,
1664 )
1665 .unwrap_or(U256::ZERO);
1666
1667 f64::from(ratio) / f64::from(PRECISION)
1668 }
1669
1670 fn validate_ticks(&self, tick_lower: i32, tick_upper: i32) -> anyhow::Result<()> {
1682 if tick_lower >= tick_upper {
1683 anyhow::bail!("Invalid tick range: {tick_lower} >= {tick_upper}")
1684 }
1685
1686 if tick_lower % self.pool.tick_spacing.unwrap() as i32 != 0
1687 || tick_upper % self.pool.tick_spacing.unwrap() as i32 != 0
1688 {
1689 anyhow::bail!(
1690 "Ticks {tick_lower} and {tick_upper} must be multiples of the tick spacing"
1691 )
1692 }
1693
1694 if tick_lower < PoolTick::MIN_TICK || tick_upper > PoolTick::MAX_TICK {
1695 anyhow::bail!("Invalid tick bounds for {tick_lower} and {tick_upper}");
1696 }
1697 Ok(())
1698 }
1699
1700 fn update_liquidity_analytics(&mut self) {
1702 self.analytics.liquidity_utilization_rate = self.liquidity_utilization_rate();
1703 }
1704
1705 #[must_use]
1714 pub fn get_active_liquidity(&self) -> u128 {
1715 self.tick_map.liquidity
1716 }
1717
1718 #[must_use]
1724 pub fn get_total_liquidity_from_active_positions(&self) -> u128 {
1725 self.positions
1726 .values()
1727 .filter(|position| {
1728 position.liquidity > 0
1729 && position.tick_lower <= self.state.current_tick
1730 && self.state.current_tick < position.tick_upper
1731 })
1732 .map(|position| position.liquidity)
1733 .sum()
1734 }
1735
1736 #[must_use]
1738 pub fn get_total_liquidity(&self) -> U256 {
1739 self.positions
1740 .values()
1741 .map(|position| U256::from(position.liquidity))
1742 .fold(U256::ZERO, |acc, liq| acc + liq)
1743 }
1744
1745 pub fn restore_from_snapshot(&mut self, snapshot: PoolSnapshot) -> anyhow::Result<()> {
1759 let liquidity = snapshot.state.liquidity;
1760
1761 self.state = snapshot.state;
1763
1764 self.analytics.total_amount0_deposited = snapshot.analytics.total_amount0_deposited;
1766 self.analytics.total_amount1_deposited = snapshot.analytics.total_amount1_deposited;
1767 self.analytics.total_amount0_collected = snapshot.analytics.total_amount0_collected;
1768 self.analytics.total_amount1_collected = snapshot.analytics.total_amount1_collected;
1769 self.analytics.total_swaps = snapshot.analytics.total_swaps;
1770 self.analytics.total_mints = snapshot.analytics.total_mints;
1771 self.analytics.total_burns = snapshot.analytics.total_burns;
1772 self.analytics.total_fee_collects = snapshot.analytics.total_fee_collects;
1773 self.analytics.total_flashes = snapshot.analytics.total_flashes;
1774
1775 self.positions.clear();
1777 for position in snapshot.positions {
1778 let key = PoolPosition::get_position_key(
1779 &position.owner,
1780 position.tick_lower,
1781 position.tick_upper,
1782 );
1783 self.positions.insert(key, position);
1784 }
1785
1786 self.tick_map = TickMap::new(
1788 self.pool
1789 .tick_spacing
1790 .expect("Pool tick spacing must be set"),
1791 );
1792
1793 for tick in snapshot.ticks {
1794 self.tick_map.restore_tick(tick);
1795 }
1796
1797 self.tick_map.liquidity = liquidity;
1799
1800 self.last_processed_event = Some(snapshot.block_position);
1802 self.last_processed_ts = Some(snapshot.ts_event);
1803
1804 self.is_initialized = true;
1806
1807 self.update_liquidity_analytics();
1809
1810 Ok(())
1811 }
1812
1813 #[must_use]
1818 pub fn get_active_tick_values(&self) -> Vec<i32> {
1819 self.tick_map
1820 .get_all_ticks()
1821 .iter()
1822 .filter(|(_, tick)| self.tick_map.is_tick_initialized(tick.value))
1823 .map(|(tick_value, _)| *tick_value)
1824 .collect()
1825 }
1826
1827 #[must_use]
1829 pub fn get_active_tick_count(&self) -> usize {
1830 self.tick_map.active_tick_count()
1831 }
1832
1833 #[must_use]
1838 pub fn get_tick(&self, tick: i32) -> Option<&PoolTick> {
1839 self.tick_map.get_tick(tick)
1840 }
1841
1842 #[must_use]
1847 pub fn get_current_tick(&self) -> i32 {
1848 self.state.current_tick
1849 }
1850
1851 #[must_use]
1859 pub fn get_total_tick_count(&self) -> usize {
1860 self.tick_map.total_tick_count()
1861 }
1862
1863 #[must_use]
1868 pub fn get_position(
1869 &self,
1870 owner: &Address,
1871 tick_lower: i32,
1872 tick_upper: i32,
1873 ) -> Option<&PoolPosition> {
1874 let position_key = PoolPosition::get_position_key(owner, tick_lower, tick_upper);
1875 self.positions.get(&position_key)
1876 }
1877
1878 #[must_use]
1888 pub fn get_active_positions(&self) -> Vec<&PoolPosition> {
1889 self.positions
1890 .values()
1891 .filter(|position| {
1892 let current_tick = self.get_current_tick();
1893 position.liquidity > 0
1894 && position.tick_lower <= current_tick
1895 && current_tick < position.tick_upper
1896 })
1897 .collect()
1898 }
1899
1900 #[must_use]
1909 pub fn get_all_positions(&self) -> Vec<&PoolPosition> {
1910 self.positions.values().collect()
1911 }
1912
1913 #[must_use]
1915 pub fn get_all_position_keys(&self) -> Vec<(Address, i32, i32)> {
1916 self.get_all_positions()
1917 .iter()
1918 .map(|position| (position.owner, position.tick_lower, position.tick_upper))
1919 .collect()
1920 }
1921
1922 pub fn extract_snapshot(&self) -> anyhow::Result<PoolSnapshot> {
1934 let positions: Vec<_> = self.positions.values().cloned().collect();
1935 let ticks: Vec<_> = self.tick_map.get_all_ticks().values().copied().collect();
1936
1937 let mut state = self.state.clone();
1938 state.liquidity = self.tick_map.liquidity;
1939
1940 let last_processed_event = self
1941 .last_processed_event
1942 .clone()
1943 .ok_or_else(|| anyhow::anyhow!("Cannot extract snapshot: no events processed yet"))?;
1944
1945 Ok(PoolSnapshot::new(
1946 self.pool.instrument_id,
1947 state,
1948 positions,
1949 ticks,
1950 self.analytics.clone(),
1951 last_processed_event,
1952 self.last_processed_ts.unwrap_or(self.pool.ts_init), self.last_processed_ts.unwrap_or(self.pool.ts_init), ))
1955 }
1956
1957 #[must_use]
1962 pub fn get_total_active_positions(&self) -> usize {
1963 self.positions
1964 .iter()
1965 .filter(|(_, position)| {
1966 let current_tick = self.get_current_tick();
1967 position.liquidity > 0
1968 && position.tick_lower <= current_tick
1969 && current_tick < position.tick_upper
1970 })
1971 .count()
1972 }
1973
1974 #[must_use]
1979 pub fn get_total_inactive_positions(&self) -> usize {
1980 self.positions.len() - self.get_total_active_positions()
1981 }
1982
1983 #[must_use]
1990 pub fn estimate_balance_of_token0(&self) -> U256 {
1991 let mut total_amount0 = U256::ZERO;
1992 let current_sqrt_price = self.state.price_sqrt_ratio_x96;
1993 let current_tick = self.state.current_tick;
1994
1995 for position in self.positions.values() {
1997 if position.liquidity == 0 || position.tick_upper <= current_tick {
1999 continue;
2000 }
2001
2002 if position.tick_lower > current_tick {
2003 let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
2005 let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
2006 let amount0 =
2007 get_amount0_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
2008 total_amount0 += amount0;
2009 } else {
2010 let sqrt_ratio_upper = get_sqrt_ratio_at_tick(position.tick_upper);
2012 let amount0 = get_amount0_delta(
2013 current_sqrt_price,
2014 sqrt_ratio_upper,
2015 position.liquidity,
2016 true,
2017 );
2018 total_amount0 += amount0;
2019 }
2020 }
2021
2022 let fee_growth_0 = self.state.fee_growth_global_0;
2026 if fee_growth_0 > U256::ZERO {
2027 let active_liquidity = self.get_active_liquidity();
2030 if active_liquidity > 0 {
2031 if let Ok(total_fees_0) =
2034 FullMath::mul_div(fee_growth_0, U256::from(active_liquidity), Q128)
2035 {
2036 total_amount0 += total_fees_0;
2037 }
2038 }
2039 }
2040
2041 total_amount0 += self.state.protocol_fees_token0;
2043
2044 total_amount0
2045 }
2046
2047 #[must_use]
2054 pub fn estimate_balance_of_token1(&self) -> U256 {
2055 let mut total_amount1 = U256::ZERO;
2056 let current_sqrt_price = self.state.price_sqrt_ratio_x96;
2057 let current_tick = self.state.current_tick;
2058
2059 for position in self.positions.values() {
2061 if position.liquidity == 0 || position.tick_lower > current_tick {
2063 continue;
2064 }
2065
2066 if position.tick_upper <= current_tick {
2067 let sqrt_ratio_a = get_sqrt_ratio_at_tick(position.tick_lower);
2069 let sqrt_ratio_b = get_sqrt_ratio_at_tick(position.tick_upper);
2070 let amount1 =
2071 get_amount1_delta(sqrt_ratio_a, sqrt_ratio_b, position.liquidity, true);
2072 total_amount1 += amount1;
2073 } else {
2074 let sqrt_ratio_lower = get_sqrt_ratio_at_tick(position.tick_lower);
2076 let amount1 = get_amount1_delta(
2077 sqrt_ratio_lower,
2078 current_sqrt_price,
2079 position.liquidity,
2080 true,
2081 );
2082 total_amount1 += amount1;
2083 }
2084 }
2085
2086 let fee_growth_1 = self.state.fee_growth_global_1;
2088 if fee_growth_1 > U256::ZERO {
2089 let active_liquidity = self.get_active_liquidity();
2090 if active_liquidity > 0 {
2091 if let Ok(total_fees_1) =
2093 FullMath::mul_div(fee_growth_1, U256::from(active_liquidity), Q128)
2094 {
2095 total_amount1 += total_fees_1;
2096 }
2097 }
2098 }
2099
2100 total_amount1 += self.state.protocol_fees_token1;
2102
2103 total_amount1
2104 }
2105
2106 pub fn set_fee_growth_global(&mut self, fee_growth_global_0: U256, fee_growth_global_1: U256) {
2115 self.state.fee_growth_global_0 = fee_growth_global_0;
2116 self.state.fee_growth_global_1 = fee_growth_global_1;
2117 }
2118
2119 #[must_use]
2121 pub fn get_total_events(&self) -> u64 {
2122 self.analytics.total_swaps
2123 + self.analytics.total_mints
2124 + self.analytics.total_burns
2125 + self.analytics.total_fee_collects
2126 + self.analytics.total_flashes
2127 }
2128
2129 pub fn enable_reporting(&mut self, from_block: u64, total_blocks: u64, update_interval: u64) {
2134 self.reporter = Some(BlockchainSyncReporter::new(
2135 BlockchainSyncReportItems::PoolProfiling,
2136 from_block,
2137 total_blocks,
2138 update_interval,
2139 ));
2140 self.last_reported_block = from_block;
2141 }
2142
2143 pub fn finalize_reporting(&mut self) {
2148 if let Some(reporter) = &self.reporter {
2149 reporter.log_final_stats();
2150 }
2151 self.reporter = None;
2152 }
2153}
2154
2155fn initial_protocol_fee_basis_points(
2156 dex_type: DexType,
2157 pool_fee: Option<u32>,
2158) -> Option<(u32, u32)> {
2159 if dex_type != DexType::PancakeSwapV3 {
2160 return None;
2161 }
2162
2163 let fee_protocol = match pool_fee {
2164 Some(100) => 3_300,
2165 Some(500) => 3_400,
2166 _ => 3_200,
2167 };
2168
2169 Some((fee_protocol, fee_protocol))
2170}