1use std::{cmp::max, collections::BTreeMap, sync::Arc};
17
18use ahash::AHashMap;
19use anyhow::Context;
20use futures_util::StreamExt;
21use nautilus_common::{live::sender::EventSender, messages::DataEvent};
22use nautilus_core::{
23 UnixNanos, hex,
24 string::{formatting::Separable, secret::REDACTED},
25};
26use nautilus_live::SocketControl;
27use nautilus_model::defi::{
28 Block, Blockchain, DexType, Pool, PoolIdentifier, PoolLiquidityUpdate, PoolProfiler, PoolSwap,
29 SharedChain, SharedDex, SharedPool,
30 data::{
31 DefiData, DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate,
32 PoolFlash,
33 block::{BLOCK_SCOPED_SNAPSHOT_INDEX, BlockPosition},
34 },
35 pool_analysis::{compare::compare_pool_profiler_detailed, snapshot::PoolSnapshot},
36 reporting::{BlockchainSyncReportItems, BlockchainSyncReporter},
37 tick_map::tick::PoolTick,
38};
39use nautilus_network::websocket::TransportBackend;
40
41use crate::{
42 cache::{BlockchainCache, PoolEventSyncState},
43 config::BlockchainDataClientConfig,
44 contracts::{
45 erc20::Erc20Contract,
46 uniswap_v3_pool::{FeeProtocolEncoding, UniswapV3PoolContract},
47 },
48 data::subscription::DefiDataSubscriptionManager,
49 events::{
50 burn::BurnEvent, collect::CollectEvent, fee_protocol_collect::FeeProtocolCollectEvent,
51 fee_protocol_update::FeeProtocolUpdateEvent, flash::FlashEvent, mint::MintEvent,
52 swap::SwapEvent,
53 },
54 exchanges::{extended::DexExtended, get_dex_extended},
55 hypersync::{
56 client::{HyperSyncClient, PoolEventStreamItem},
57 log::{extract_block_number, extract_event_signature_bytes},
58 },
59 rpc::{
60 BlockchainRpcClient, BlockchainRpcClientAny,
61 chains::{
62 arbitrum::ArbitrumRpcClient, base::BaseRpcClient, bsc::BscRpcClient,
63 ethereum::EthereumRpcClient, polygon::PolygonRpcClient,
64 },
65 http::BlockchainHttpRpcClient,
66 types::BlockchainMessage,
67 },
68 services::PoolDiscoveryService,
69};
70
71const BLOCKS_PROCESS_IN_SYNC_REPORT: u64 = 50_000;
72const POOL_EVENT_BLOCK_BATCH_SIZE: usize = 20_000;
73const POOL_EVENT_SYNC_VERSION_LEGACY: u32 = 0;
74const POOL_EVENT_SYNC_VERSION_PROTOCOL_FEE: u32 = 1;
75const POOL_EVENT_SYNC_VERSION: u32 = POOL_EVENT_SYNC_VERSION_PROTOCOL_FEE;
77#[derive(Debug, Clone, PartialEq, Eq)]
80struct PoolEventFamily {
81 name: &'static str,
82 signature: String,
83 introduced_version: u32,
84}
85
86#[derive(Debug, Clone, PartialEq, Eq)]
87struct PoolEventSyncRange {
88 from_block: u64,
89 families: Vec<PoolEventFamily>,
90}
91
92#[derive(Debug)]
97pub struct BlockchainDataClientCore {
98 pub chain: SharedChain,
100 pub config: BlockchainDataClientConfig,
102 pub cache: BlockchainCache,
104 tokens: Erc20Contract,
106 univ3_pool: UniswapV3PoolContract,
108 pub hypersync_client: HyperSyncClient,
110 pub rpc_client: Option<BlockchainRpcClientAny>,
112 pub subscription_manager: DefiDataSubscriptionManager,
114 data_tx: Option<EventSender<DataEvent>>,
116 cancellation_token: tokio_util::sync::CancellationToken,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum SnapshotValidation {
123 OnChain,
125 Replay,
128 Invalid,
130}
131
132impl SnapshotValidation {
133 #[must_use]
135 pub const fn is_usable(self) -> bool {
136 !matches!(self, Self::Invalid)
137 }
138
139 #[must_use]
141 pub const fn as_str(self) -> &'static str {
142 match self {
143 Self::OnChain => "on_chain",
144 Self::Replay => "replay",
145 Self::Invalid => "invalid",
146 }
147 }
148
149 #[must_use]
153 pub fn from_db_token(token: &str) -> Option<Self> {
154 match token {
155 "on_chain" => Some(Self::OnChain),
156 "replay" => Some(Self::Replay),
157 "invalid" => Some(Self::Invalid),
158 _ => None,
159 }
160 }
161}
162
163impl BlockchainDataClientCore {
164 pub fn new(
170 config: BlockchainDataClientConfig,
171 hypersync_tx: Option<tokio::sync::mpsc::UnboundedSender<BlockchainMessage>>,
172 data_tx: Option<EventSender<DataEvent>>,
173 cancellation_token: tokio_util::sync::CancellationToken,
174 ) -> anyhow::Result<Self> {
175 config.validate()?;
176
177 let chain = Arc::clone(&config.chain);
178 let cache = BlockchainCache::new(Arc::clone(&chain));
179
180 log::debug!(
182 "Initializing blockchain data client for '{}' with HTTP RPC: {}",
183 chain.name,
184 REDACTED
185 );
186
187 let rpc_client = if let (false, Some(wss_rpc_url)) = (
188 config.use_hypersync_for_live_data,
189 config.wss_rpc_url.as_ref(),
190 ) {
191 log::debug!("WebSocket RPC URL: {REDACTED}");
192 Some(Self::initialize_rpc_client(
193 chain.name,
194 wss_rpc_url.clone().into_inner(),
195 config.transport_backend,
196 config.proxy_url.clone().map(|value| value.into_inner()),
197 ))
198 } else {
199 log::debug!("Using HyperSync for live data (no WebSocket RPC)");
200 None
201 };
202 let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
203 config.http_rpc_url.clone().into_inner(),
204 config.rpc_requests_per_second,
205 config.proxy_url.clone().map(|value| value.into_inner()),
206 )?);
207 let multicall_calls_per_rpc_request = config.multicall_calls_per_rpc_request;
208 let erc20_contract = Erc20Contract::new(
209 Arc::clone(&http_rpc_client),
210 config.pool_filters.remove_pools_with_empty_erc20fields,
211 );
212
213 let hypersync_client =
214 HyperSyncClient::new(Arc::clone(&chain), hypersync_tx, cancellation_token.clone())?;
215 Ok(Self {
216 chain,
217 config,
218 rpc_client,
219 tokens: erc20_contract,
220 univ3_pool: UniswapV3PoolContract::new(
221 http_rpc_client,
222 multicall_calls_per_rpc_request,
223 ),
224 cache,
225 hypersync_client,
226 subscription_manager: DefiDataSubscriptionManager::new(),
227 data_tx,
228 cancellation_token,
229 })
230 }
231
232 pub fn set_socket_control(&mut self, control: SocketControl) {
234 if let Some(rpc_client) = &mut self.rpc_client {
235 rpc_client.set_socket_control(control);
236 }
237 }
238
239 pub async fn initialize_cache_database(&mut self) {
241 if let Some(pg_connect_options) = &self.config.postgres_cache_database_config {
242 log::debug!(
243 "Initializing blockchain cache on database '{}'",
244 pg_connect_options.database
245 );
246 self.cache
247 .initialize_database(pg_connect_options.clone().into())
248 .await;
249 }
250 }
251
252 fn initialize_rpc_client(
254 blockchain: Blockchain,
255 wss_rpc_url: String,
256 transport_backend: TransportBackend,
257 proxy_url: Option<String>,
258 ) -> BlockchainRpcClientAny {
259 let mut client = match blockchain {
260 Blockchain::Ethereum => {
261 BlockchainRpcClientAny::Ethereum(EthereumRpcClient::new(wss_rpc_url, proxy_url))
262 }
263 Blockchain::Polygon => {
264 BlockchainRpcClientAny::Polygon(PolygonRpcClient::new(wss_rpc_url, proxy_url))
265 }
266 Blockchain::Base => {
267 BlockchainRpcClientAny::Base(BaseRpcClient::new(wss_rpc_url, proxy_url))
268 }
269 Blockchain::Arbitrum => {
270 BlockchainRpcClientAny::Arbitrum(ArbitrumRpcClient::new(wss_rpc_url, proxy_url))
271 }
272 Blockchain::Bsc => {
273 BlockchainRpcClientAny::Bsc(BscRpcClient::new(wss_rpc_url, proxy_url))
274 }
275 _ => panic!("Unsupported blockchain {blockchain} for RPC connection"),
276 };
277 client.set_transport_backend(transport_backend);
278 client
279 }
280
281 pub async fn connect(&mut self) -> anyhow::Result<()> {
288 self.config.validate()?;
289
290 log::debug!(
291 "Connecting blockchain data client for '{}'",
292 self.chain.name
293 );
294 self.initialize_cache_database().await;
295
296 if let Some(ref mut rpc_client) = self.rpc_client {
297 rpc_client.connect().await?;
298 }
299
300 let from_block = self.determine_from_block();
301
302 log::debug!(
303 "Connecting to blockchain data source for '{}' from block {}",
304 self.chain.name,
305 from_block.separate_with_commas()
306 );
307
308 self.cache.initialize_chain().await;
310 self.cache.connect(from_block).await?;
312 for dex in self.config.dex_ids.clone() {
316 self.register_dex_exchange(dex).await?;
317 self.sync_exchange_pools(&dex, from_block, None, false)
318 .await?;
319 }
320
321 Ok(())
322 }
323
324 pub async fn sync_blocks_checked(
330 &mut self,
331 from_block: u64,
332 to_block: Option<u64>,
333 ) -> anyhow::Result<()> {
334 if let Some(blocks_status) = self.cache.get_cache_block_consistency_status().await {
335 if blocks_status.is_consistent() {
337 log::debug!(
338 "Cache is consistent: no gaps detected (last continuous block: {})",
339 blocks_status.last_continuous_block
340 );
341 let target_block = max(blocks_status.max_block + 1, from_block);
342 log::debug!(
343 "Starting fast sync with COPY from block {}",
344 target_block.separate_with_commas()
345 );
346 self.sync_blocks(target_block, to_block, true).await?;
347 } else {
348 let gap_size = blocks_status.max_block - blocks_status.last_continuous_block;
349 log::debug!(
350 "Cache inconsistency detected: {} blocks missing between {} and {}",
351 gap_size,
352 blocks_status.last_continuous_block + 1,
353 blocks_status.max_block
354 );
355
356 log::debug!(
357 "Block syncing Phase 1: Filling gaps with INSERT (blocks {} to {})",
358 blocks_status.last_continuous_block + 1,
359 blocks_status.max_block
360 );
361 self.sync_blocks(
362 blocks_status.last_continuous_block + 1,
363 Some(blocks_status.max_block),
364 false,
365 )
366 .await?;
367
368 log::debug!(
369 "Block syncing Phase 2: Continuing with fast COPY from block {}",
370 (blocks_status.max_block + 1).separate_with_commas()
371 );
372 self.sync_blocks(blocks_status.max_block + 1, to_block, true)
373 .await?;
374 }
375 } else {
376 self.sync_blocks(from_block, to_block, true).await?;
377 }
378
379 Ok(())
380 }
381
382 pub async fn sync_blocks(
388 &mut self,
389 from_block: u64,
390 to_block: Option<u64>,
391 use_copy_command: bool,
392 ) -> anyhow::Result<()> {
393 const BATCH_SIZE: usize = 1000;
394
395 let to_block = if let Some(block) = to_block {
396 block
397 } else {
398 self.hypersync_client.current_block().await
399 };
400 let total_blocks = to_block.saturating_sub(from_block) + 1;
401 log::debug!(
402 "Syncing blocks from {} to {} (total: {} blocks)",
403 from_block.separate_with_commas(),
404 to_block.separate_with_commas(),
405 total_blocks.separate_with_commas()
406 );
407
408 if let Err(e) = self.cache.toggle_performance_settings(true).await {
410 log::warn!("Failed to enable performance settings: {e}");
411 }
412
413 let blocks_stream = self
414 .hypersync_client
415 .request_blocks_stream(from_block, Some(to_block))
416 .await;
417
418 tokio::pin!(blocks_stream);
419
420 let mut metrics = BlockchainSyncReporter::new(
421 BlockchainSyncReportItems::Blocks,
422 from_block,
423 total_blocks,
424 BLOCKS_PROCESS_IN_SYNC_REPORT,
425 );
426
427 let mut batch: Vec<Block> = Vec::with_capacity(BATCH_SIZE);
428
429 let cancellation_token = self.cancellation_token.clone();
430 let sync_result = tokio::select! {
431 () = cancellation_token.cancelled() => {
432 log::debug!("Block sync cancelled");
433 Err(anyhow::anyhow!("Sync cancelled"))
434 }
435 result = async {
436 while let Some(block) = blocks_stream.next().await {
437 let block_number = block.number;
438 if self.cache.get_block_timestamp(block_number).is_some() {
439 continue;
440 }
441 batch.push(block);
442
443 if batch.len() >= BATCH_SIZE || block_number >= to_block {
445 let batch_size = batch.len();
446
447 self.cache.add_blocks_batch(batch, use_copy_command).await?;
448 metrics.update(batch_size);
449
450 batch = Vec::with_capacity(BATCH_SIZE);
452 }
453
454 if metrics.should_log_progress(block_number, to_block) {
456 metrics.log_progress(block_number);
457 }
458 }
459
460 if !batch.is_empty() {
462 let batch_size = batch.len();
463 self.cache.add_blocks_batch(batch, use_copy_command).await?;
464 metrics.update(batch_size);
465 }
466
467 metrics.log_final_stats();
468 Ok(())
469 } => result
470 };
471
472 sync_result?;
473
474 if let Err(e) = self.cache.toggle_performance_settings(false).await {
476 log::warn!("Failed to restore default settings: {e}");
477 }
478
479 Ok(())
480 }
481
482 pub async fn sync_pool_events(
488 &mut self,
489 dex: &DexType,
490 pool_identifier: PoolIdentifier,
491 from_block: Option<u64>,
492 to_block: Option<u64>,
493 reset: bool,
494 ) -> anyhow::Result<()> {
495 const EVENT_BATCH_SIZE: usize = 20000;
496
497 let pool: SharedPool = Arc::clone(self.get_pool(&pool_identifier)?);
498 let pool_display = pool.to_full_spec_string();
499 let from_block = from_block.unwrap_or(pool.creation_block);
500 let pool_address = &pool.address;
502
503 let dex_extended = self.get_dex_extended(dex)?.clone();
504 let event_families = Self::pool_event_families(&dex_extended);
505 let sync_state = if reset {
506 PoolEventSyncState::default()
507 } else {
508 self.cache
509 .get_pool_event_sync_state(dex, &pool_identifier)
510 .await?
511 };
512
513 let to_block = match to_block {
514 Some(block) => block,
515 None => self.hypersync_client.current_block().await,
516 };
517
518 if from_block > to_block {
519 log::debug!(
520 "D {} Pool '{}' requested event range {} to {} is empty, skipping sync",
521 dex,
522 pool_display,
523 from_block.separate_with_commas(),
524 to_block.separate_with_commas(),
525 );
526 return Ok(());
527 }
528
529 let sync_ranges =
530 Self::pool_event_sync_ranges(&event_families, &sync_state, from_block, to_block);
531 let inherited_event_family_names = event_families
532 .iter()
533 .filter(|family| {
534 sync_state.version == POOL_EVENT_SYNC_VERSION_LEGACY
535 && family.introduced_version == POOL_EVENT_SYNC_VERSION_LEGACY
536 && !sync_state
537 .family_blocks
538 .iter()
539 .any(|(name, _)| name == family.name)
540 })
541 .map(|family| family.name)
542 .collect::<Vec<_>>();
543
544 let Some(sync_range) = sync_ranges.first().cloned() else {
545 if sync_state.version < POOL_EVENT_SYNC_VERSION {
546 if let Some(last_full_sync_block) = sync_state.last_full_sync_block {
547 self.cache
548 .update_pool_event_sync(
549 dex,
550 &pool_identifier,
551 &inherited_event_family_names,
552 last_full_sync_block,
553 None,
554 )
555 .await?;
556 }
557 self.cache
558 .update_pool_event_sync(
559 dex,
560 &pool_identifier,
561 &[],
562 to_block,
563 Some(POOL_EVENT_SYNC_VERSION),
564 )
565 .await?;
566 }
567
568 log::debug!(
569 "D {} Pool '{}' event families already synced to block {}, skipping sync",
570 dex,
571 pool_display,
572 to_block.separate_with_commas()
573 );
574 return Ok(());
575 };
576 let has_more_ranges = sync_ranges.len() > 1;
577
578 let last_block_across_pool_events_table = self
580 .cache
581 .get_pool_event_tables_last_block(&pool_identifier)
582 .await?;
583
584 let effective_from_block = sync_range.from_block;
585 let event_family_names = sync_range
586 .families
587 .iter()
588 .map(|family| family.name)
589 .collect::<Vec<_>>();
590 let total_blocks = to_block.saturating_sub(effective_from_block) + 1;
591 log::debug!(
592 "Syncing Pool: '{}' event families {:?} from {} to {} (total: {} blocks)",
593 pool_display,
594 event_family_names,
595 effective_from_block.separate_with_commas(),
596 to_block.separate_with_commas(),
597 total_blocks.separate_with_commas(),
598 );
599
600 let mut metrics = BlockchainSyncReporter::new(
601 BlockchainSyncReportItems::PoolEvents,
602 effective_from_block,
603 total_blocks,
604 BLOCKS_PROCESS_IN_SYNC_REPORT,
605 );
606 let swap_event_signature = dex_extended.swap_created_event.as_ref();
607 let mint_event_signature = dex_extended.mint_created_event.as_ref();
608 let burn_event_signature = dex_extended.burn_created_event.as_ref();
609 let collect_event_signature = dex_extended.collect_created_event.as_ref();
610 let flash_event_signature = dex_extended.flash_created_event.as_ref();
611 let protocol_update_event_signature = dex_extended.fee_protocol_update_event.as_ref();
612 let protocol_collect_event_signature = dex_extended.fee_protocol_collect_event.as_ref();
613 let initialize_event_signature: Option<&str> =
614 dex_extended.initialize_event.as_ref().map(|s| s.as_ref());
615
616 let swap_sig_bytes = hex::decode(
618 swap_event_signature
619 .strip_prefix("0x")
620 .unwrap_or(swap_event_signature),
621 )?;
622 let mint_sig_bytes = hex::decode(
623 mint_event_signature
624 .strip_prefix("0x")
625 .unwrap_or(mint_event_signature),
626 )?;
627 let burn_sig_bytes = hex::decode(
628 burn_event_signature
629 .strip_prefix("0x")
630 .unwrap_or(burn_event_signature),
631 )?;
632 let collect_sig_bytes = hex::decode(
633 collect_event_signature
634 .strip_prefix("0x")
635 .unwrap_or(collect_event_signature),
636 )?;
637 let flash_sig_bytes = flash_event_signature
638 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
639 let protocol_update_sig_bytes = protocol_update_event_signature
640 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
641 let protocol_collect_sig_bytes = protocol_collect_event_signature
642 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
643 let initialize_sig_bytes = initialize_event_signature
644 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
645
646 let event_signatures = sync_range
647 .families
648 .iter()
649 .map(|family| family.signature.as_str())
650 .collect();
651
652 let pool_events_stream = self
653 .hypersync_client
654 .request_contract_events_stream(
655 effective_from_block,
656 Some(to_block),
657 pool_address,
658 event_signatures,
659 )
660 .await;
661 tokio::pin!(pool_events_stream);
662
663 let mut last_block_saved = effective_from_block;
664 let mut blocks_processed = 0;
665
666 let mut block_batch: Vec<Block> = Vec::with_capacity(POOL_EVENT_BLOCK_BATCH_SIZE);
667 let mut swap_batch: Vec<PoolSwap> = Vec::with_capacity(EVENT_BATCH_SIZE);
668 let mut liquidity_batch: Vec<PoolLiquidityUpdate> = Vec::with_capacity(EVENT_BATCH_SIZE);
669 let mut collect_batch: Vec<PoolFeeCollect> = Vec::with_capacity(EVENT_BATCH_SIZE);
670 let mut protocol_update_batch: Vec<PoolFeeProtocolUpdate> =
671 Vec::with_capacity(EVENT_BATCH_SIZE);
672 let mut protocol_collect_batch: Vec<PoolFeeProtocolCollect> =
673 Vec::with_capacity(EVENT_BATCH_SIZE);
674 let mut flash_batch: Vec<PoolFlash> = Vec::with_capacity(EVENT_BATCH_SIZE);
675
676 let mut beyond_stale_data = last_block_across_pool_events_table
678 .is_none_or(|tables_max| effective_from_block > tables_max);
679
680 let cancellation_token = self.cancellation_token.clone();
681 let sync_result = tokio::select! {
682 () = cancellation_token.cancelled() => {
683 log::debug!("Pool event sync cancelled");
684 Err(anyhow::anyhow!("Sync cancelled"))
685 }
686 result = async {
687 while let Some(item) = pool_events_stream.next().await {
688 let log = match item {
689 PoolEventStreamItem::Block(block) => {
690 self.record_pool_event_block(block, &mut block_batch).await?;
691 continue;
692 }
693 PoolEventStreamItem::Log(log) => log,
694 };
695 let block_number = extract_block_number(&log)?;
696 blocks_processed += block_number - last_block_saved;
697 last_block_saved = block_number;
698
699 let event_sig_bytes = extract_event_signature_bytes(&log)?;
700 if event_sig_bytes == swap_sig_bytes.as_slice() {
701 let swap_event = dex_extended.parse_swap_event_hypersync(&log)?;
702 let swap = self
703 .process_pool_swap_event(&swap_event, &pool)
704 .with_context(|| {
705 format!("failed to process swap event at block {}", swap_event.block_number)
706 })?;
707 swap_batch.push(swap);
708 } else if event_sig_bytes == mint_sig_bytes.as_slice() {
709 let mint_event = dex_extended.parse_mint_event_hypersync(&log)?;
710 let liquidity_update = self
711 .process_pool_mint_event(&mint_event, &pool, &dex_extended)
712 .with_context(|| {
713 format!("failed to process mint event at block {}", mint_event.block_number)
714 })?;
715 liquidity_batch.push(liquidity_update);
716 } else if event_sig_bytes == burn_sig_bytes.as_slice() {
717 let burn_event = dex_extended.parse_burn_event_hypersync(&log)?;
718 let liquidity_update = self
719 .process_pool_burn_event(&burn_event, &pool, &dex_extended)
720 .with_context(|| {
721 format!("failed to process burn event at block {}", burn_event.block_number)
722 })?;
723 liquidity_batch.push(liquidity_update);
724 } else if event_sig_bytes == collect_sig_bytes.as_slice() {
725 let collect_event = dex_extended.parse_collect_event_hypersync(&log)?;
726 let fee_collect = self
727 .process_pool_collect_event(&collect_event, &pool, &dex_extended)
728 .with_context(|| {
729 format!(
730 "failed to process collect event at block {}",
731 collect_event.block_number
732 )
733 })?;
734 collect_batch.push(fee_collect);
735 } else if initialize_sig_bytes.as_ref().is_some_and(|sig| sig.as_slice() == event_sig_bytes) {
736 let initialize_event = dex_extended.parse_initialize_event_hypersync(&log)?;
737 self.cache
738 .update_pool_initialize_price_tick(&initialize_event)
739 .await?;
740 } else if protocol_update_sig_bytes.as_ref().is_some_and(|sig| sig.as_slice() == event_sig_bytes) {
741 let fee_protocol_update_event = dex_extended.parse_fee_protocol_update_event_hypersync(&log)?;
742 let update = self
743 .process_pool_fee_protocol_update_event(&fee_protocol_update_event, &pool)
744 .with_context(|| {
745 format!(
746 "failed to process SetFeeProtocol event at block {}",
747 fee_protocol_update_event.block_number
748 )
749 })?;
750 protocol_update_batch.push(update);
751 } else if protocol_collect_sig_bytes.as_ref().is_some_and(|sig| sig.as_slice() == event_sig_bytes) {
752 let fee_protocol_collect_event = dex_extended.parse_fee_protocol_collect_event_hypersync(&log)?;
753 let collect = self
754 .process_pool_fee_protocol_collect_event(&fee_protocol_collect_event, &pool)
755 .with_context(|| {
756 format!(
757 "failed to process CollectProtocol event at block {}",
758 fee_protocol_collect_event.block_number
759 )
760 })?;
761 protocol_collect_batch.push(collect);
762 } else if flash_sig_bytes.as_ref().is_some_and(|sig| sig.as_slice() == event_sig_bytes) {
763 let parse_fn = dex_extended
764 .parse_flash_event_hypersync_fn
765 .context("missing flash event parser")?;
766 let flash_event = parse_fn(Arc::clone(&dex_extended.dex), &log)
767 .context("failed to parse flash event")?;
768 let flash = self
769 .process_pool_flash_event(&flash_event, &pool)
770 .with_context(|| {
771 format!("failed to process flash event at block {}", flash_event.block_number)
772 })?;
773 flash_batch.push(flash);
774 } else {
775 let event_signature = hex::encode(event_sig_bytes);
776 anyhow::bail!("unexpected event signature {event_signature} for log {log:?}");
777 }
778
779 if !beyond_stale_data
781 && last_block_across_pool_events_table
782 .is_some_and(|table_max| block_number > table_max)
783 {
784 log::debug!(
785 "Crossed beyond stale data at block {block_number} - flushing current batches with ON CONFLICT, then switching to COPY"
786 );
787
788 self.flush_event_batches(
790 EVENT_BATCH_SIZE,
791 &mut block_batch,
792 &mut swap_batch,
793 &mut liquidity_batch,
794 &mut collect_batch,
795 &mut protocol_update_batch,
796 &mut protocol_collect_batch,
797 &mut flash_batch,
798 false,
799 true,
800 )
801 .await?;
802
803 beyond_stale_data = true;
804 log::debug!("Switched to COPY mode - future batches will use COPY command");
805 } else {
806 self.flush_event_batches(
808 EVENT_BATCH_SIZE,
809 &mut block_batch,
810 &mut swap_batch,
811 &mut liquidity_batch,
812 &mut collect_batch,
813 &mut protocol_update_batch,
814 &mut protocol_collect_batch,
815 &mut flash_batch,
816 false, false,
818 )
819 .await?;
820 }
821
822 metrics.update(blocks_processed as usize);
823 blocks_processed = 0;
824
825 if metrics.should_log_progress(block_number, to_block) {
827 metrics.log_progress(block_number);
828 self.flush_event_batches(
829 EVENT_BATCH_SIZE,
830 &mut block_batch,
831 &mut swap_batch,
832 &mut liquidity_batch,
833 &mut collect_batch,
834 &mut protocol_update_batch,
835 &mut protocol_collect_batch,
836 &mut flash_batch,
837 false,
838 true,
839 )
840 .await?;
841
842 if let Some(checkpoint_block) =
843 Self::completed_pool_event_checkpoint(block_number, effective_from_block)
844 {
845 self.cache
846 .update_pool_event_sync(
847 dex,
848 &pool_identifier,
849 &event_family_names,
850 checkpoint_block,
851 None,
852 )
853 .await?;
854 }
855 }
856 }
857
858 self.flush_event_batches(
859 EVENT_BATCH_SIZE,
860 &mut block_batch,
861 &mut swap_batch,
862 &mut liquidity_batch,
863 &mut collect_batch,
864 &mut protocol_update_batch,
865 &mut protocol_collect_batch,
866 &mut flash_batch,
867 false,
868 true,
869 )
870 .await?;
871
872 metrics.log_final_stats();
873 self.cache
874 .update_pool_event_sync(
875 dex,
876 &pool_identifier,
877 &event_family_names,
878 to_block,
879 None,
880 )
881 .await?;
882
883 log::debug!(
884 "Successfully synced Dex '{}' Pool '{}' event families {:?} up to block {}",
885 dex,
886 pool_display,
887 event_family_names,
888 to_block.separate_with_commas()
889 );
890 Ok(())
891 } => result
892 };
893
894 sync_result?;
895
896 if has_more_ranges {
897 return Box::pin(self.sync_pool_events(
898 dex,
899 pool_identifier,
900 Some(from_block),
901 Some(to_block),
902 false,
903 ))
904 .await;
905 }
906
907 if let Some(last_full_sync_block) = sync_state.last_full_sync_block {
908 self.cache
909 .update_pool_event_sync(
910 dex,
911 &pool_identifier,
912 &inherited_event_family_names,
913 last_full_sync_block,
914 None,
915 )
916 .await?;
917 }
918 self.cache
919 .update_pool_event_sync(
920 dex,
921 &pool_identifier,
922 &[],
923 to_block,
924 Some(POOL_EVENT_SYNC_VERSION),
925 )
926 .await?;
927
928 Ok(())
929 }
930
931 fn pool_event_families(dex: &DexExtended) -> Vec<PoolEventFamily> {
932 let mut families = vec![
933 PoolEventFamily {
934 name: "swap",
935 signature: dex.swap_created_event.to_string(),
936 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
937 },
938 PoolEventFamily {
939 name: "mint",
940 signature: dex.mint_created_event.to_string(),
941 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
942 },
943 PoolEventFamily {
944 name: "burn",
945 signature: dex.burn_created_event.to_string(),
946 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
947 },
948 PoolEventFamily {
949 name: "collect",
950 signature: dex.collect_created_event.to_string(),
951 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
952 },
953 ];
954
955 if let Some(signature) = &dex.initialize_event {
956 families.push(PoolEventFamily {
957 name: "initialize",
958 signature: signature.to_string(),
959 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
960 });
961 }
962
963 if let Some(signature) = &dex.flash_created_event {
964 families.push(PoolEventFamily {
965 name: "flash",
966 signature: signature.to_string(),
967 introduced_version: POOL_EVENT_SYNC_VERSION_LEGACY,
968 });
969 }
970
971 if let Some(signature) = &dex.fee_protocol_update_event {
972 families.push(PoolEventFamily {
973 name: "fee_protocol_update",
974 signature: signature.to_string(),
975 introduced_version: POOL_EVENT_SYNC_VERSION_PROTOCOL_FEE,
976 });
977 }
978
979 if let Some(signature) = &dex.fee_protocol_collect_event {
980 families.push(PoolEventFamily {
981 name: "fee_protocol_collect",
982 signature: signature.to_string(),
983 introduced_version: POOL_EVENT_SYNC_VERSION_PROTOCOL_FEE,
984 });
985 }
986
987 families
988 }
989
990 fn pool_event_sync_ranges(
991 families: &[PoolEventFamily],
992 state: &PoolEventSyncState,
993 from_block: u64,
994 to_block: u64,
995 ) -> Vec<PoolEventSyncRange> {
996 let mut grouped = BTreeMap::<u64, Vec<PoolEventFamily>>::new();
997
998 for family in families {
999 let explicit_checkpoint = state
1000 .family_blocks
1001 .iter()
1002 .find_map(|(name, block)| (name == family.name).then_some(*block));
1003 let legacy_checkpoint = (state.version == POOL_EVENT_SYNC_VERSION_LEGACY
1004 && family.introduced_version == POOL_EVENT_SYNC_VERSION_LEGACY)
1005 .then_some(state.last_full_sync_block)
1006 .flatten();
1007 let family_from_block = explicit_checkpoint
1008 .or(legacy_checkpoint)
1009 .map_or(from_block, |block| max(from_block, block.saturating_add(1)));
1010
1011 if family_from_block <= to_block {
1012 grouped
1013 .entry(family_from_block)
1014 .or_default()
1015 .push(family.clone());
1016 }
1017 }
1018
1019 grouped
1020 .into_iter()
1021 .map(|(from_block, families)| PoolEventSyncRange {
1022 from_block,
1023 families,
1024 })
1025 .collect()
1026 }
1027
1028 #[expect(clippy::too_many_arguments)]
1029 async fn flush_event_batches(
1030 &mut self,
1031 event_batch_size: usize,
1032 block_batch: &mut Vec<Block>,
1033 swap_batch: &mut Vec<PoolSwap>,
1034 liquidity_batch: &mut Vec<PoolLiquidityUpdate>,
1035 collect_batch: &mut Vec<PoolFeeCollect>,
1036 protocol_update_batch: &mut Vec<PoolFeeProtocolUpdate>,
1037 protocol_collect_batch: &mut Vec<PoolFeeProtocolCollect>,
1038 flash_batch: &mut Vec<PoolFlash>,
1039 use_copy_command: bool,
1040 force_flush_all: bool,
1041 ) -> anyhow::Result<()> {
1042 let should_flush_swaps =
1043 (force_flush_all || swap_batch.len() >= event_batch_size) && !swap_batch.is_empty();
1044 let should_flush_liquidity = (force_flush_all || liquidity_batch.len() >= event_batch_size)
1045 && !liquidity_batch.is_empty();
1046 let should_flush_collects = (force_flush_all || collect_batch.len() >= event_batch_size)
1047 && !collect_batch.is_empty();
1048 let should_flush_protocol_update = (force_flush_all
1049 || protocol_update_batch.len() >= event_batch_size)
1050 && !protocol_update_batch.is_empty();
1051 let should_flush_protocol_collect = (force_flush_all
1052 || protocol_collect_batch.len() >= event_batch_size)
1053 && !protocol_collect_batch.is_empty();
1054 let should_flush_flash =
1055 (force_flush_all || flash_batch.len() >= event_batch_size) && !flash_batch.is_empty();
1056
1057 if force_flush_all
1058 || should_flush_swaps
1059 || should_flush_liquidity
1060 || should_flush_collects
1061 || should_flush_protocol_update
1062 || should_flush_protocol_collect
1063 || should_flush_flash
1064 {
1065 self.flush_pool_event_blocks(block_batch).await?;
1066 }
1067
1068 if should_flush_swaps {
1069 self.cache
1070 .add_pool_swaps_batch(swap_batch, use_copy_command)
1071 .await?;
1072 swap_batch.clear();
1073 }
1074
1075 if should_flush_liquidity {
1076 self.cache
1077 .add_pool_liquidity_updates_batch(liquidity_batch, use_copy_command)
1078 .await?;
1079 liquidity_batch.clear();
1080 }
1081
1082 if should_flush_collects {
1083 self.cache
1084 .add_pool_fee_collects_batch(collect_batch, use_copy_command)
1085 .await?;
1086 collect_batch.clear();
1087 }
1088
1089 if should_flush_protocol_update {
1090 self.cache
1091 .add_pool_fee_protocol_updates_batch(protocol_update_batch)
1092 .await?;
1093 protocol_update_batch.clear();
1094 }
1095
1096 if should_flush_protocol_collect {
1097 self.cache
1098 .add_pool_fee_protocol_collect_batch(protocol_collect_batch)
1099 .await?;
1100 protocol_collect_batch.clear();
1101 }
1102
1103 if should_flush_flash {
1104 self.cache.add_pool_flash_batch(flash_batch).await?;
1105 flash_batch.clear();
1106 }
1107 Ok(())
1108 }
1109
1110 async fn record_pool_event_block(
1111 &mut self,
1112 block: Block,
1113 block_batch: &mut Vec<Block>,
1114 ) -> anyhow::Result<()> {
1115 self.cache.cache_block_metadata(&block);
1116 block_batch.push(block);
1117 if block_batch.len() >= POOL_EVENT_BLOCK_BATCH_SIZE {
1118 self.flush_pool_event_blocks(block_batch).await?;
1119 }
1120 Ok(())
1121 }
1122
1123 async fn flush_pool_event_blocks(
1124 &mut self,
1125 block_batch: &mut Vec<Block>,
1126 ) -> anyhow::Result<()> {
1127 if block_batch.is_empty() {
1128 return Ok(());
1129 }
1130
1131 self.cache
1132 .add_pool_event_blocks_batch(std::mem::take(block_batch))
1133 .await
1134 }
1135
1136 fn completed_pool_event_checkpoint(
1137 block_number: u64,
1138 effective_from_block: u64,
1139 ) -> Option<u64> {
1140 let checkpoint_block = block_number.checked_sub(1)?;
1141 (checkpoint_block >= effective_from_block).then_some(checkpoint_block)
1142 }
1143
1144 pub fn process_pool_swap_event(
1154 &self,
1155 swap_event: &SwapEvent,
1156 pool: &SharedPool,
1157 ) -> anyhow::Result<PoolSwap> {
1158 let timestamp = self
1159 .cache
1160 .get_block_timestamp(swap_event.block_number)
1161 .copied()
1162 .context("missing block timestamp for swap event")?;
1163 let mut swap = swap_event.to_pool_swap(
1164 Arc::clone(&self.chain),
1165 pool.instrument_id,
1166 pool.pool_identifier,
1167 timestamp,
1168 );
1169 swap.block_hash = Some(self.observed_block_hash(swap_event.block_number, "swap")?);
1170 if let Err(e) = swap.calculate_trade_info(&pool.token0, &pool.token1, None) {
1172 log::debug!(
1173 "Skipping trade info for swap at block {} on pool {}: {e}",
1174 swap_event.block_number,
1175 pool.instrument_id,
1176 );
1177 }
1178
1179 Ok(swap)
1180 }
1181
1182 pub fn process_pool_mint_event(
1188 &self,
1189 mint_event: &MintEvent,
1190 pool: &SharedPool,
1191 dex_extended: &DexExtended,
1192 ) -> anyhow::Result<PoolLiquidityUpdate> {
1193 let timestamp = self
1194 .cache
1195 .get_block_timestamp(mint_event.block_number)
1196 .copied()
1197 .context("missing block timestamp for mint event")?;
1198
1199 let mut liquidity_update = mint_event.to_pool_liquidity_update(
1200 Arc::clone(&self.chain),
1201 Arc::clone(&dex_extended.dex),
1202 pool.instrument_id,
1203 timestamp,
1204 );
1205 liquidity_update.block_hash =
1206 Some(self.observed_block_hash(mint_event.block_number, "mint")?);
1207
1208 Ok(liquidity_update)
1211 }
1212
1213 pub fn process_pool_burn_event(
1220 &self,
1221 burn_event: &BurnEvent,
1222 pool: &SharedPool,
1223 dex_extended: &DexExtended,
1224 ) -> anyhow::Result<PoolLiquidityUpdate> {
1225 let timestamp = self
1226 .cache
1227 .get_block_timestamp(burn_event.block_number)
1228 .copied()
1229 .context("missing block timestamp for burn event")?;
1230
1231 let mut liquidity_update = burn_event.to_pool_liquidity_update(
1232 Arc::clone(&self.chain),
1233 Arc::clone(&dex_extended.dex),
1234 pool.instrument_id,
1235 pool.pool_identifier,
1236 timestamp,
1237 );
1238 liquidity_update.block_hash =
1239 Some(self.observed_block_hash(burn_event.block_number, "burn")?);
1240
1241 Ok(liquidity_update)
1244 }
1245
1246 pub fn process_pool_collect_event(
1252 &self,
1253 collect_event: &CollectEvent,
1254 pool: &SharedPool,
1255 dex_extended: &DexExtended,
1256 ) -> anyhow::Result<PoolFeeCollect> {
1257 let timestamp = self
1258 .cache
1259 .get_block_timestamp(collect_event.block_number)
1260 .copied()
1261 .context("missing block timestamp for collect event")?;
1262
1263 let mut fee_collect = collect_event.to_pool_fee_collect(
1264 Arc::clone(&self.chain),
1265 Arc::clone(&dex_extended.dex),
1266 pool.instrument_id,
1267 timestamp,
1268 );
1269 fee_collect.block_hash =
1270 Some(self.observed_block_hash(collect_event.block_number, "collect")?);
1271
1272 Ok(fee_collect)
1273 }
1274
1275 pub fn process_pool_flash_event(
1281 &self,
1282 flash_event: &FlashEvent,
1283 pool: &SharedPool,
1284 ) -> anyhow::Result<PoolFlash> {
1285 let timestamp = self
1286 .cache
1287 .get_block_timestamp(flash_event.block_number)
1288 .copied()
1289 .context("missing block timestamp for flash event")?;
1290
1291 let mut flash =
1292 flash_event.to_pool_flash(Arc::clone(&self.chain), pool.instrument_id, timestamp);
1293 flash.block_hash = Some(self.observed_block_hash(flash_event.block_number, "flash")?);
1294
1295 Ok(flash)
1296 }
1297
1298 pub fn process_pool_fee_protocol_update_event(
1304 &self,
1305 fee_protocol_update_event: &FeeProtocolUpdateEvent,
1306 pool: &SharedPool,
1307 ) -> anyhow::Result<PoolFeeProtocolUpdate> {
1308 let timestamp = self
1309 .cache
1310 .get_block_timestamp(fee_protocol_update_event.block_number)
1311 .copied()
1312 .context("missing block timestamp for SetFeeProtocol event")?;
1313
1314 let mut update = fee_protocol_update_event.to_pool_fee_protocol_update(
1315 Arc::clone(&self.chain),
1316 pool.instrument_id,
1317 timestamp,
1318 );
1319 update.block_hash = Some(
1320 self.observed_block_hash(fee_protocol_update_event.block_number, "SetFeeProtocol")?,
1321 );
1322
1323 Ok(update)
1324 }
1325
1326 pub fn process_pool_fee_protocol_collect_event(
1332 &self,
1333 fee_protocol_collect_event: &FeeProtocolCollectEvent,
1334 pool: &SharedPool,
1335 ) -> anyhow::Result<PoolFeeProtocolCollect> {
1336 let timestamp = self
1337 .cache
1338 .get_block_timestamp(fee_protocol_collect_event.block_number)
1339 .copied()
1340 .context("missing block timestamp for CollectProtocol event")?;
1341
1342 let mut collect = fee_protocol_collect_event.to_pool_fee_protocol_collect(
1343 Arc::clone(&self.chain),
1344 pool.instrument_id,
1345 timestamp,
1346 );
1347 collect.block_hash = Some(
1348 self.observed_block_hash(fee_protocol_collect_event.block_number, "CollectProtocol")?,
1349 );
1350
1351 Ok(collect)
1352 }
1353
1354 fn observed_block_hash(&self, block_number: u64, event: &str) -> anyhow::Result<String> {
1355 self.cache
1356 .get_block_hash(block_number)
1357 .map(str::to_owned)
1358 .with_context(|| {
1359 format!("missing block hash for {event} event at block {block_number}")
1360 })
1361 }
1362
1363 pub async fn sync_exchange_pools(
1374 &mut self,
1375 dex: &DexType,
1376 from_block: u64,
1377 to_block: Option<u64>,
1378 reset: bool,
1379 ) -> anyhow::Result<()> {
1380 let dex_extended = self.get_dex_extended(dex)?.clone();
1381
1382 let mut service = PoolDiscoveryService::new(
1383 Arc::clone(&self.chain),
1384 &mut self.cache,
1385 &self.tokens,
1386 &self.hypersync_client,
1387 self.cancellation_token.clone(),
1388 self.config.clone(),
1389 );
1390
1391 service
1392 .sync_pools(&dex_extended, from_block, to_block, reset)
1393 .await?;
1394
1395 Ok(())
1396 }
1397
1398 pub async fn register_dex_exchange(&mut self, dex_id: DexType) -> anyhow::Result<()> {
1409 self.register_dex(dex_id).await?;
1410 let _ = self.cache.load_pools(&dex_id).await?;
1411 Ok(())
1412 }
1413
1414 pub async fn register_dex_exchange_for_pool(
1424 &mut self,
1425 dex_id: DexType,
1426 pool_identifier: &PoolIdentifier,
1427 ) -> anyhow::Result<()> {
1428 self.register_dex(dex_id).await?;
1429 let _ = self.cache.load_pool(&dex_id, pool_identifier).await?;
1430 Ok(())
1431 }
1432
1433 async fn register_dex(&mut self, dex_id: DexType) -> anyhow::Result<()> {
1435 let Some(dex_extended) = get_dex_extended(self.chain.name, &dex_id) else {
1436 anyhow::bail!("Unknown DEX {dex_id} on chain {}", self.chain.name);
1437 };
1438
1439 log::debug!("Registering DEX {dex_id} on chain {}", self.chain.name);
1440 self.cache.add_dex(Arc::clone(&dex_extended.dex)).await?;
1441 self.subscription_manager.register_dex_for_subscriptions(
1442 dex_id,
1443 dex_extended.swap_created_event.as_ref(),
1444 dex_extended.mint_created_event.as_ref(),
1445 dex_extended.burn_created_event.as_ref(),
1446 dex_extended.collect_created_event.as_ref(),
1447 dex_extended.flash_created_event.as_deref(),
1448 );
1449 self.subscription_manager.register_dex_fee_protocol_events(
1450 dex_id,
1451 dex_extended.fee_protocol_update_event.as_deref(),
1452 dex_extended.fee_protocol_collect_event.as_deref(),
1453 );
1454 Ok(())
1455 }
1456
1457 pub async fn bootstrap_latest_pool_profiler(
1478 &mut self,
1479 pool: &SharedPool,
1480 to_block: Option<u64>,
1481 ) -> anyhow::Result<(PoolProfiler, bool)> {
1482 log::debug!(
1483 "Bootstrapping latest pool profiler for pool {}",
1484 pool.address
1485 );
1486
1487 if self.cache.database.is_none() {
1488 anyhow::bail!(
1489 "Database is not initialized, so we cannot properly bootstrap the latest pool profiler"
1490 );
1491 }
1492
1493 let to_block = match to_block {
1494 Some(block) => block,
1495 None => self.hypersync_client.current_block().await,
1496 };
1497 let (mut profiler, from_position) = self
1498 .seed_pool_profiler_from_latest_snapshot(pool, to_block)
1499 .await?;
1500
1501 if self
1505 .cache
1506 .database
1507 .as_ref()
1508 .unwrap()
1509 .get_pool_last_synced_block(self.chain.chain_id, &pool.dex.name, &pool.pool_identifier)
1510 .await?
1511 .is_none()
1512 {
1513 return self
1514 .construct_pool_profiler_from_hypersync_rpc(profiler, from_position, to_block)
1515 .await;
1516 }
1517
1518 self.sync_pool_events(
1520 &pool.dex.name,
1521 pool.pool_identifier,
1522 None,
1523 Some(to_block),
1524 false,
1525 )
1526 .await
1527 .context("failed to sync pool events for snapshot request")?;
1528
1529 if !profiler.is_initialized {
1530 if let Some(initial_sqrt_price_x96) = pool.initial_sqrt_price_x96 {
1531 profiler.initialize(initial_sqrt_price_x96)?;
1532 } else {
1533 anyhow::bail!(
1534 "Pool is not initialized and it doesn't contain initial price, cannot bootstrap profiler"
1535 );
1536 }
1537 }
1538
1539 let from_block = from_position
1540 .as_ref()
1541 .map_or(profiler.pool.creation_block, |block_position| {
1542 block_position.number
1543 });
1544 let total_blocks = to_block.saturating_sub(from_block) + 1;
1545
1546 profiler.enable_reporting(from_block, total_blocks, BLOCKS_PROCESS_IN_SYNC_REPORT);
1548
1549 let mut stream = self.cache.database.as_ref().unwrap().stream_pool_events(
1550 Arc::clone(&pool.chain),
1551 Arc::clone(&pool.dex),
1552 pool.instrument_id,
1553 pool.pool_identifier,
1554 from_position.clone(),
1555 Some(to_block),
1556 );
1557
1558 while let Some(result) = stream.next().await {
1559 match result {
1560 Ok(event) => {
1561 profiler.process(&event)?;
1562 }
1563 Err(e) => return Err(e).context("failed to stream pool event from database"),
1564 }
1565 }
1566
1567 profiler.finalize_reporting();
1568
1569 Ok((profiler, false))
1570 }
1571
1572 pub async fn bootstrap_pool_profiler_from_rpc_snapshot(
1581 &mut self,
1582 pool: &SharedPool,
1583 to_block: u64,
1584 ) -> anyhow::Result<(PoolProfiler, bool)> {
1585 if self.cache.database.is_none() {
1586 anyhow::bail!(
1587 "Database is not initialized, so we cannot bootstrap the pool profiler from an RPC snapshot"
1588 );
1589 }
1590
1591 self.construct_pool_profiler_from_hypersync_rpc(
1592 PoolProfiler::new(Arc::clone(pool)),
1593 None,
1594 to_block,
1595 )
1596 .await
1597 }
1598
1599 pub async fn advance_pool_profiler_from_rpc_snapshot(
1610 &mut self,
1611 profiler: PoolProfiler,
1612 to_block: u64,
1613 ) -> anyhow::Result<(PoolProfiler, bool)> {
1614 let from_position = profiler.last_processed_event.clone().ok_or_else(|| {
1615 anyhow::anyhow!("cannot advance an RPC profiler without a snapshot watermark")
1616 })?;
1617
1618 if to_block < from_position.number {
1619 anyhow::bail!(
1620 "cannot advance RPC profiler from block {} to earlier block {to_block}",
1621 from_position.number
1622 );
1623 }
1624
1625 self.construct_pool_profiler_from_hypersync_rpc(profiler, Some(from_position), to_block)
1626 .await
1627 }
1628
1629 async fn seed_pool_profiler_from_latest_snapshot(
1630 &self,
1631 pool: &SharedPool,
1632 to_block: u64,
1633 ) -> anyhow::Result<(PoolProfiler, Option<BlockPosition>)> {
1634 let mut profiler = PoolProfiler::new(Arc::clone(pool));
1635
1636 let from_position = match self
1637 .cache
1638 .database
1639 .as_ref()
1640 .expect("database presence is checked by caller")
1641 .load_latest_pool_snapshot(
1642 pool.chain.chain_id,
1643 &pool.pool_identifier,
1644 Some(to_block),
1645 true,
1646 )
1647 .await
1648 {
1649 Ok(Some(snapshot)) => {
1650 if snapshot.positions.is_empty()
1656 && snapshot.ticks.is_empty()
1657 && snapshot.block_position.number == pool.creation_block
1658 {
1659 log::warn!(
1660 "Ignoring empty stub snapshot at pool creation block {} for {}; rebuilding from events",
1661 snapshot.block_position.number.separate_with_commas(),
1662 pool.instrument_id,
1663 );
1664 None
1665 } else {
1666 log::debug!(
1667 "Loaded valid snapshot from block {} which contains {} positions and {} ticks",
1668 snapshot.block_position.number.separate_with_commas(),
1669 snapshot.positions.len(),
1670 snapshot.ticks.len()
1671 );
1672 let block_position = snapshot.block_position.clone();
1673 profiler.restore_from_snapshot(snapshot)?;
1674 log::debug!("Restored profiler from snapshot");
1675 Some(block_position)
1676 }
1677 }
1678 _ => {
1679 log::debug!("No valid snapshot found, processing from beginning");
1680 None
1681 }
1682 };
1683
1684 Ok((profiler, from_position))
1685 }
1686
1687 async fn construct_pool_profiler_from_hypersync_rpc(
1708 &mut self,
1709 mut profiler: PoolProfiler,
1710 from_position: Option<BlockPosition>,
1711 to_block: u64,
1712 ) -> anyhow::Result<(PoolProfiler, bool)> {
1713 log::debug!("Constructing pool profiler from hypersync stream and RPC target block state");
1714 let dex_extended = self.get_dex_extended(&profiler.pool.dex.name)?.clone();
1715 let mint_event_signature = dex_extended.mint_created_event.as_ref();
1716 let burn_event_signature = dex_extended.burn_created_event.as_ref();
1717 let initialize_event_signature =
1718 if let Some(initialize_event) = &dex_extended.initialize_event {
1719 initialize_event.as_ref()
1720 } else {
1721 anyhow::bail!(
1722 "DEX {} does not have initialize event set.",
1723 profiler.pool.dex.name
1724 );
1725 };
1726 let mint_sig_bytes = hex::decode(
1727 mint_event_signature
1728 .strip_prefix("0x")
1729 .unwrap_or(mint_event_signature),
1730 )?;
1731 let burn_sig_bytes = hex::decode(
1732 burn_event_signature
1733 .strip_prefix("0x")
1734 .unwrap_or(burn_event_signature),
1735 )?;
1736 let initialize_sig_bytes = hex::decode(
1737 initialize_event_signature
1738 .strip_prefix("0x")
1739 .unwrap_or(initialize_event_signature),
1740 )?;
1741 let protocol_update_event_signature = dex_extended.fee_protocol_update_event.as_deref();
1742 let protocol_update_sig_bytes = protocol_update_event_signature
1743 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
1744 let protocol_collect_event_signature = dex_extended.fee_protocol_collect_event.as_deref();
1745 let protocol_collect_sig_bytes = protocol_collect_event_signature
1746 .map(|s| hex::decode(s.strip_prefix("0x").unwrap_or(s)).unwrap_or_default());
1747
1748 let from_block = from_position.map_or(profiler.pool.creation_block, |block_position| {
1749 block_position.number
1750 });
1751 let total_blocks = to_block.saturating_sub(from_block) + 1;
1752
1753 log::debug!(
1754 "Bootstrapping pool profiler for pool {} from block {} to {} (total: {} blocks)",
1755 profiler.pool.address,
1756 from_block.separate_with_commas(),
1757 to_block.separate_with_commas(),
1758 total_blocks.separate_with_commas()
1759 );
1760
1761 profiler.enable_reporting(from_block, total_blocks, BLOCKS_PROCESS_IN_SYNC_REPORT);
1763
1764 let mut event_signatures = vec![
1765 mint_event_signature,
1766 burn_event_signature,
1767 initialize_event_signature,
1768 ];
1769
1770 if let Some(event) = protocol_update_event_signature {
1771 event_signatures.push(event);
1772 }
1773
1774 if let Some(event) = protocol_collect_event_signature {
1775 event_signatures.push(event);
1776 }
1777
1778 let pool_events_stream = self
1779 .hypersync_client
1780 .request_contract_events_stream(
1781 from_block,
1782 Some(to_block),
1783 &profiler.pool.address,
1784 event_signatures,
1785 )
1786 .await;
1787 tokio::pin!(pool_events_stream);
1788 let mut block_batch: Vec<Block> = Vec::with_capacity(POOL_EVENT_BLOCK_BATCH_SIZE);
1789
1790 while let Some(item) = pool_events_stream.next().await {
1791 let log = match item {
1792 PoolEventStreamItem::Block(block) => {
1793 self.record_pool_event_block(block, &mut block_batch)
1794 .await?;
1795 continue;
1796 }
1797 PoolEventStreamItem::Log(log) => log,
1798 };
1799 let event_sig_bytes = extract_event_signature_bytes(&log)?;
1800
1801 if event_sig_bytes == initialize_sig_bytes {
1802 if profiler.is_initialized {
1803 log::debug!(
1807 "Profiler already initialized; skipping Initialize event at block {}",
1808 extract_block_number(&log)?.separate_with_commas(),
1809 );
1810 } else {
1811 let initialize_event = dex_extended.parse_initialize_event_hypersync(&log)?;
1812 profiler.initialize(initialize_event.sqrt_price_x96)?;
1813 self.cache
1814 .database
1815 .as_ref()
1816 .unwrap()
1817 .update_pool_initial_price_tick(self.chain.chain_id, &initialize_event)
1818 .await?;
1819 }
1820 } else if event_sig_bytes == mint_sig_bytes {
1821 let mint_event = dex_extended.parse_mint_event_hypersync(&log)?;
1822 let liquidity_update = self
1823 .process_pool_mint_event(&mint_event, &profiler.pool, &dex_extended)
1824 .with_context(|| {
1825 format!(
1826 "failed to process mint event at block {}",
1827 mint_event.block_number
1828 )
1829 })?;
1830 profiler.process(&DexPoolData::LiquidityUpdate(liquidity_update))?;
1831 } else if event_sig_bytes == burn_sig_bytes {
1832 let burn_event = dex_extended.parse_burn_event_hypersync(&log)?;
1833 let liquidity_update = self
1834 .process_pool_burn_event(&burn_event, &profiler.pool, &dex_extended)
1835 .with_context(|| {
1836 format!(
1837 "failed to process burn event at block {}",
1838 burn_event.block_number
1839 )
1840 })?;
1841 profiler.process(&DexPoolData::LiquidityUpdate(liquidity_update))?;
1842 } else if protocol_update_sig_bytes
1843 .as_ref()
1844 .is_some_and(|sig| sig.as_slice() == event_sig_bytes)
1845 {
1846 let fee_protocol_update_event =
1847 dex_extended.parse_fee_protocol_update_event_hypersync(&log)?;
1848 let update = self
1849 .process_pool_fee_protocol_update_event(
1850 &fee_protocol_update_event,
1851 &profiler.pool,
1852 )
1853 .with_context(|| {
1854 format!(
1855 "failed to process SetFeeProtocol event at block {}",
1856 fee_protocol_update_event.block_number
1857 )
1858 })?;
1859 profiler.process(&DexPoolData::FeeProtocolUpdate(update))?;
1860 } else if protocol_collect_sig_bytes
1861 .as_ref()
1862 .is_some_and(|sig| sig.as_slice() == event_sig_bytes)
1863 {
1864 let fee_protocol_collect_event =
1865 dex_extended.parse_fee_protocol_collect_event_hypersync(&log)?;
1866 let collect = self
1867 .process_pool_fee_protocol_collect_event(
1868 &fee_protocol_collect_event,
1869 &profiler.pool,
1870 )
1871 .with_context(|| {
1872 format!(
1873 "failed to process CollectProtocol event at block {}",
1874 fee_protocol_collect_event.block_number
1875 )
1876 })?;
1877 profiler.process(&DexPoolData::FeeProtocolCollect(collect))?;
1878 } else {
1879 let event_signature = hex::encode(event_sig_bytes);
1880 anyhow::bail!(
1881 "unexpected event signature in bootstrap_latest_pool_profiler: {event_signature} for log {log:?}"
1882 );
1883 }
1884 }
1885
1886 self.flush_pool_event_blocks(&mut block_batch).await?;
1887 profiler.finalize_reporting();
1888
1889 let snapshot_block_position = self.block_scoped_snapshot_position(to_block).await?;
1890 let on_chain_snapshot = self
1891 .get_on_chain_snapshot_at_position(&profiler, snapshot_block_position)
1892 .await
1893 .with_context(|| {
1894 format!(
1895 "failed to restore pool {} from RPC snapshot at target block {} with {} ticks and {} positions",
1896 profiler.pool.address,
1897 to_block.separate_with_commas(),
1898 profiler.get_active_tick_values().len().separate_with_commas(),
1899 profiler.get_all_position_keys().len().separate_with_commas()
1900 )
1901 })?;
1902 Self::validate_rpc_snapshot_topology(&profiler, &on_chain_snapshot).with_context(|| {
1903 format!(
1904 "RPC snapshot topology validation failed for pool {} at block {}",
1905 profiler.pool.address,
1906 to_block.separate_with_commas()
1907 )
1908 })?;
1909 profiler.restore_from_snapshot(on_chain_snapshot)?;
1910
1911 Ok((profiler, true))
1912 }
1913
1914 pub async fn check_snapshot_validity(
1934 &self,
1935 profiler: &PoolProfiler,
1936 already_validated: bool,
1937 ) -> anyhow::Result<SnapshotValidation> {
1938 let (validation, block_position) = if already_validated {
1939 log::debug!("Snapshot already validated from RPC, skipping on-chain comparison");
1941 let last_event = profiler
1942 .last_processed_event
1943 .clone()
1944 .expect("Profiler should have last_processed_event");
1945 (SnapshotValidation::OnChain, Some(last_event))
1946 } else {
1947 match self.get_on_chain_snapshot(profiler).await {
1949 Ok(on_chain_snapshot) => {
1950 log::debug!("Comparing profiler state with on-chain state...");
1951 let comparison = compare_pool_profiler_detailed(profiler, &on_chain_snapshot);
1952 let validation = if comparison.is_valid_for_snapshot() {
1953 if !comparison.is_exact_match() {
1954 log::warn!(
1955 "Pool profiler snapshot has a non-structural mismatch (sqrt ratio, fee protocol, or protocol fees); accepting snapshot"
1956 );
1957 }
1958 SnapshotValidation::OnChain
1959 } else {
1960 log::error!(
1961 "Pool profiler state does NOT match on-chain smart contract state"
1962 );
1963 SnapshotValidation::Invalid
1964 };
1965 (validation, Some(on_chain_snapshot.block_position))
1966 }
1967 Err(e) => {
1968 log::warn!(
1969 "Could not validate snapshot against on-chain state, keeping replay-derived snapshot: {e}"
1970 );
1971 let reported = self
1975 .stored_snapshot_validation(profiler)
1976 .await?
1977 .unwrap_or(SnapshotValidation::Replay);
1978 (reported, None)
1979 }
1980 }
1981 };
1982
1983 if let (Some(block_position), Some(cache_database)) = (block_position, &self.cache.database)
1984 {
1985 cache_database
1986 .set_pool_snapshot_validation_state(
1987 profiler.pool.chain.chain_id,
1988 &profiler.pool.pool_identifier,
1989 block_position.number,
1990 block_position.transaction_index,
1991 block_position.log_index,
1992 validation.as_str(),
1993 )
1994 .await?;
1995 log::debug!(
1996 "Set pool snapshot validation state to {}",
1997 validation.as_str()
1998 );
1999 }
2000
2001 Ok(validation)
2002 }
2003
2004 async fn stored_snapshot_validation(
2009 &self,
2010 profiler: &PoolProfiler,
2011 ) -> anyhow::Result<Option<SnapshotValidation>> {
2012 let (Some(block_position), Some(cache_database)) =
2013 (profiler.last_processed_event.as_ref(), &self.cache.database)
2014 else {
2015 return Ok(None);
2016 };
2017
2018 let stored = cache_database
2019 .get_pool_snapshot_validation_state(
2020 profiler.pool.chain.chain_id,
2021 &profiler.pool.pool_identifier,
2022 block_position.number,
2023 block_position.transaction_index,
2024 block_position.log_index,
2025 )
2026 .await?;
2027
2028 Ok(stored.and_then(|token| SnapshotValidation::from_db_token(&token)))
2029 }
2030
2031 async fn get_on_chain_snapshot(&self, profiler: &PoolProfiler) -> anyhow::Result<PoolSnapshot> {
2037 let last_processed_event = Self::last_processed_event_for_on_chain_snapshot(profiler)?;
2038 self.get_on_chain_snapshot_at_position(profiler, last_processed_event)
2039 .await
2040 }
2041
2042 async fn get_on_chain_snapshot_at_position(
2043 &self,
2044 profiler: &PoolProfiler,
2045 block_position: BlockPosition,
2046 ) -> anyhow::Result<PoolSnapshot> {
2047 if matches!(
2049 profiler.pool.dex.name,
2050 DexType::UniswapV3 | DexType::PancakeSwapV3
2051 ) {
2052 let fee_protocol_encoding = match profiler.pool.dex.name {
2053 DexType::PancakeSwapV3 => FeeProtocolEncoding::PancakeSwapV3BasisPoints,
2054 _ => FeeProtocolEncoding::UniswapV3Packed,
2055 };
2056 let timestamp = Self::timestamp_for_on_chain_snapshot(
2057 profiler,
2058 self.cache
2059 .get_block_timestamp(block_position.number)
2060 .copied(),
2061 )?;
2062 let on_chain_snapshot = self
2063 .univ3_pool
2064 .fetch_snapshot(
2065 &profiler.pool.address,
2066 profiler.pool.instrument_id,
2067 profiler.get_active_tick_values().as_slice(),
2068 &profiler.get_all_position_keys(),
2069 block_position,
2070 timestamp, timestamp, fee_protocol_encoding,
2073 )
2074 .await?;
2075
2076 Ok(on_chain_snapshot)
2077 } else {
2078 anyhow::bail!(
2079 "Fetching on-chain snapshot for Dex protocol {} is not supported yet.",
2080 profiler.pool.dex.name
2081 )
2082 }
2083 }
2084
2085 fn validate_rpc_snapshot_topology(
2086 profiler: &PoolProfiler,
2087 snapshot: &PoolSnapshot,
2088 ) -> anyhow::Result<()> {
2089 let tick_spacing = i32::try_from(
2090 profiler
2091 .pool
2092 .tick_spacing
2093 .context("pool tick spacing is not set")?,
2094 )?;
2095 let expected_positions: AHashMap<_, _> = profiler
2096 .get_all_positions()
2097 .into_iter()
2098 .map(|position| {
2099 (
2100 (position.owner, position.tick_lower, position.tick_upper),
2101 position.liquidity,
2102 )
2103 })
2104 .collect();
2105 let actual_positions: AHashMap<_, _> = snapshot
2106 .positions
2107 .iter()
2108 .map(|position| {
2109 (
2110 (position.owner, position.tick_lower, position.tick_upper),
2111 position.liquidity,
2112 )
2113 })
2114 .collect();
2115
2116 if actual_positions.len() != snapshot.positions.len()
2117 || actual_positions != expected_positions
2118 {
2119 anyhow::bail!(
2120 "RPC positions do not match the complete HyperSync topology: expected {} positions, received {}",
2121 expected_positions.len(),
2122 actual_positions.len()
2123 );
2124 }
2125
2126 let actual_ticks: AHashMap<_, _> = snapshot
2127 .ticks
2128 .iter()
2129 .map(|tick| (tick.value, tick))
2130 .collect();
2131
2132 if actual_ticks.len() != snapshot.ticks.len() {
2133 anyhow::bail!("RPC snapshot contains duplicate ticks");
2134 }
2135
2136 let expected_tick_values = profiler.get_active_tick_values();
2137 if actual_ticks.len() != expected_tick_values.len() {
2138 anyhow::bail!(
2139 "RPC ticks do not match the complete HyperSync topology: expected {} ticks, received {}",
2140 expected_tick_values.len(),
2141 actual_ticks.len()
2142 );
2143 }
2144
2145 for tick_value in expected_tick_values {
2146 let expected_tick = profiler
2147 .get_tick(tick_value)
2148 .with_context(|| format!("missing replay tick {tick_value}"))?;
2149 let actual_tick = actual_ticks
2150 .get(&tick_value)
2151 .with_context(|| format!("RPC snapshot omitted tick {tick_value}"))?;
2152
2153 if !actual_tick.initialized
2154 || actual_tick.liquidity_gross == 0
2155 || actual_tick.liquidity_gross != expected_tick.liquidity_gross
2156 || actual_tick.liquidity_net != expected_tick.liquidity_net
2157 {
2158 anyhow::bail!(
2159 "RPC tick {tick_value} topology mismatch: expected gross={} net={}, received gross={} net={} initialized={}",
2160 expected_tick.liquidity_gross,
2161 expected_tick.liquidity_net,
2162 actual_tick.liquidity_gross,
2163 actual_tick.liquidity_net,
2164 actual_tick.initialized
2165 );
2166 }
2167 }
2168
2169 let mut derived_ticks: AHashMap<i32, (u128, i128)> = AHashMap::new();
2170 let mut active_liquidity = 0_u128;
2171
2172 for position in &snapshot.positions {
2173 if position.tick_lower >= position.tick_upper
2174 || position.tick_lower < PoolTick::MIN_TICK
2175 || position.tick_upper > PoolTick::MAX_TICK
2176 || position.tick_lower % tick_spacing != 0
2177 || position.tick_upper % tick_spacing != 0
2178 {
2179 anyhow::bail!(
2180 "RPC position {} has invalid tick range [{}, {}) for spacing {tick_spacing}",
2181 position.owner,
2182 position.tick_lower,
2183 position.tick_upper
2184 );
2185 }
2186
2187 if position.liquidity == 0 {
2188 continue;
2189 }
2190
2191 let liquidity_net = i128::try_from(position.liquidity).with_context(|| {
2192 format!(
2193 "RPC position {} liquidity exceeds i128::MAX",
2194 position.owner
2195 )
2196 })?;
2197 let lower = derived_ticks.entry(position.tick_lower).or_default();
2198 lower.0 = lower
2199 .0
2200 .checked_add(position.liquidity)
2201 .context("lower tick liquidity gross overflow")?;
2202 lower.1 = lower
2203 .1
2204 .checked_add(liquidity_net)
2205 .context("lower tick liquidity net overflow")?;
2206 let upper = derived_ticks.entry(position.tick_upper).or_default();
2207 upper.0 = upper
2208 .0
2209 .checked_add(position.liquidity)
2210 .context("upper tick liquidity gross overflow")?;
2211 upper.1 = upper
2212 .1
2213 .checked_sub(liquidity_net)
2214 .context("upper tick liquidity net underflow")?;
2215
2216 if position.tick_lower <= snapshot.state.current_tick
2217 && snapshot.state.current_tick < position.tick_upper
2218 {
2219 active_liquidity = active_liquidity
2220 .checked_add(position.liquidity)
2221 .context("active position liquidity overflow")?;
2222 }
2223 }
2224
2225 if active_liquidity != snapshot.state.liquidity {
2226 anyhow::bail!(
2227 "RPC active liquidity mismatch: positions sum to {active_liquidity}, global state reports {}",
2228 snapshot.state.liquidity
2229 );
2230 }
2231
2232 if derived_ticks.len() != actual_ticks.len() {
2233 anyhow::bail!(
2234 "RPC tick topology does not match positions: derived {} ticks, received {}",
2235 derived_ticks.len(),
2236 actual_ticks.len()
2237 );
2238 }
2239
2240 for (tick_value, (liquidity_gross, liquidity_net)) in derived_ticks {
2241 let actual_tick = actual_ticks
2242 .get(&tick_value)
2243 .with_context(|| format!("RPC snapshot omitted position boundary {tick_value}"))?;
2244
2245 if actual_tick.liquidity_gross != liquidity_gross
2246 || actual_tick.liquidity_net != liquidity_net
2247 {
2248 anyhow::bail!(
2249 "RPC tick {tick_value} does not match positions: derived gross={liquidity_gross} net={liquidity_net}, received gross={} net={}",
2250 actual_tick.liquidity_gross,
2251 actual_tick.liquidity_net
2252 );
2253 }
2254 }
2255
2256 Ok(())
2257 }
2258
2259 fn timestamp_for_on_chain_snapshot(
2260 profiler: &PoolProfiler,
2261 cached_timestamp: Option<UnixNanos>,
2262 ) -> anyhow::Result<UnixNanos> {
2263 cached_timestamp
2264 .or(profiler.last_processed_ts)
2265 .context("missing block timestamp for on-chain snapshot")
2266 }
2267
2268 fn last_processed_event_for_on_chain_snapshot(
2269 profiler: &PoolProfiler,
2270 ) -> anyhow::Result<BlockPosition> {
2271 profiler.last_processed_event.clone().with_context(|| {
2272 format!(
2273 "cannot fetch on-chain snapshot for pool {} without a processed event",
2274 profiler.pool.address
2275 )
2276 })
2277 }
2278
2279 async fn block_scoped_snapshot_position(
2280 &mut self,
2281 block_number: u64,
2282 ) -> anyhow::Result<BlockPosition> {
2283 let blocks_stream = self
2284 .hypersync_client
2285 .request_blocks_stream(block_number, Some(block_number))
2286 .await;
2287 tokio::pin!(blocks_stream);
2288 let block = blocks_stream
2289 .next()
2290 .await
2291 .with_context(|| format!("failed to fetch block {block_number} for RPC snapshot"))?;
2292
2293 let block_position =
2294 Self::block_scoped_snapshot_position_from_block(&mut self.cache, &block, block_number)?;
2295 self.cache.add_pool_event_blocks_batch(vec![block]).await?;
2296
2297 Ok(block_position)
2298 }
2299
2300 fn block_scoped_snapshot_position_from_block(
2301 cache: &mut BlockchainCache,
2302 block: &Block,
2303 block_number: u64,
2304 ) -> anyhow::Result<BlockPosition> {
2305 if block.number != block_number {
2306 anyhow::bail!(
2307 "Fetched block {} while requesting RPC snapshot block {}",
2308 block.number,
2309 block_number
2310 );
2311 }
2312
2313 cache.cache_block_metadata(block);
2314
2315 Ok(BlockPosition::new(
2316 block.number,
2317 block.hash.clone(),
2318 BLOCK_SCOPED_SNAPSHOT_INDEX,
2319 BLOCK_SCOPED_SNAPSHOT_INDEX,
2320 )
2321 .with_block_hash(Some(block.hash.clone())))
2322 }
2323
2324 pub async fn replay_pool_events(&self, pool: &Pool, dex: &SharedDex) -> anyhow::Result<()> {
2333 if let Some(database) = &self.cache.database {
2334 log::debug!(
2335 "Replaying historical events for pool {} to hydrate profiler",
2336 pool.instrument_id
2337 );
2338
2339 let mut event_stream = database.stream_pool_events(
2340 Arc::clone(&self.chain),
2341 Arc::clone(dex),
2342 pool.instrument_id,
2343 pool.pool_identifier,
2344 None,
2345 None,
2346 );
2347 let mut event_count = 0;
2348
2349 while let Some(event_result) = event_stream.next().await {
2350 match event_result {
2351 Ok(event) => {
2352 let data_event = match event {
2353 DexPoolData::Swap(swap) => DataEvent::DeFi(DefiData::PoolSwap(swap)),
2354 DexPoolData::LiquidityUpdate(update) => {
2355 DataEvent::DeFi(DefiData::PoolLiquidityUpdate(update))
2356 }
2357 DexPoolData::FeeCollect(collect) => {
2358 DataEvent::DeFi(DefiData::PoolFeeCollect(collect))
2359 }
2360 DexPoolData::FeeProtocolUpdate(update) => {
2361 DataEvent::DeFi(DefiData::PoolFeeProtocolUpdate(update))
2362 }
2363 DexPoolData::FeeProtocolCollect(collect) => {
2364 DataEvent::DeFi(DefiData::PoolFeeProtocolCollect(collect))
2365 }
2366 DexPoolData::Flash(flash) => {
2367 DataEvent::DeFi(DefiData::PoolFlash(flash))
2368 }
2369 };
2370 self.send_data(data_event);
2371 event_count += 1;
2372 }
2373 Err(e) => {
2374 log::error!("Error streaming event for pool {}: {e}", pool.instrument_id);
2375 }
2376 }
2377 }
2378
2379 log::debug!(
2380 "Replayed {event_count} historical events for pool {}",
2381 pool.instrument_id
2382 );
2383 } else {
2384 log::debug!(
2385 "No database available, skipping event replay for pool {}",
2386 pool.instrument_id
2387 );
2388 }
2389
2390 Ok(())
2391 }
2392
2393 fn determine_from_block(&self) -> u64 {
2395 self.config
2396 .from_block
2397 .unwrap_or_else(|| self.cache.min_dex_creation_block().unwrap_or(0))
2398 }
2399
2400 fn get_dex_extended(&self, dex_id: &DexType) -> anyhow::Result<&DexExtended> {
2402 if !self.cache.get_registered_dexes().contains(dex_id) {
2403 anyhow::bail!("DEX {dex_id} is not registered in the data client");
2404 }
2405
2406 match get_dex_extended(self.chain.name, dex_id) {
2407 Some(dex) => Ok(dex),
2408 None => anyhow::bail!("Dex {dex_id} doesn't exist for chain {}", self.chain.name),
2409 }
2410 }
2411
2412 pub fn get_pool(&self, pool_identifier: &PoolIdentifier) -> anyhow::Result<&SharedPool> {
2418 match self.cache.get_pool(pool_identifier) {
2419 Some(pool) => Ok(pool),
2420 None => anyhow::bail!("Pool {pool_identifier} is not registered"),
2421 }
2422 }
2423
2424 pub fn send_data(&self, data: DataEvent) {
2426 if let Some(data_tx) = &self.data_tx {
2427 log::debug!("Sending {data}");
2428
2429 if let Err(e) = data_tx.send(data) {
2430 log::error!("Failed to send data: {e}");
2431 }
2432 } else {
2433 log::error!("No data event channel for sending data");
2434 }
2435 }
2436
2437 pub async fn disconnect(&mut self) {
2442 self.subscription_manager.clear_block_demand();
2443 self.hypersync_client.disconnect().await;
2444 }
2445}
2446
2447#[cfg(test)]
2448mod tests {
2449 use alloy::primitives::{Address, U160, U256, address};
2450 use nautilus_core::UnixNanos;
2451 use nautilus_model::defi::{
2452 Chain, Token,
2453 pool_analysis::{
2454 position::PoolPosition,
2455 snapshot::{PoolAnalytics, PoolState},
2456 },
2457 };
2458 use rstest::rstest;
2459 use ustr::Ustr;
2460
2461 use super::*;
2462
2463 const WETH_USDT_CREATION_BLOCK: u64 = 12_375_326;
2464
2465 #[rstest]
2466 #[case(SnapshotValidation::OnChain, "on_chain", true)]
2467 #[case(SnapshotValidation::Replay, "replay", true)]
2468 #[case(SnapshotValidation::Invalid, "invalid", false)]
2469 fn snapshot_validation_db_token_and_usability(
2470 #[case] validation: SnapshotValidation,
2471 #[case] expected_str: &str,
2472 #[case] expected_usable: bool,
2473 ) {
2474 assert_eq!(validation.as_str(), expected_str);
2477 assert_eq!(validation.is_usable(), expected_usable);
2478 assert_eq!(
2481 SnapshotValidation::from_db_token(expected_str),
2482 Some(validation)
2483 );
2484 }
2485
2486 #[rstest]
2487 fn snapshot_validation_from_db_token_rejects_unknown() {
2488 assert_eq!(SnapshotValidation::from_db_token("bogus"), None);
2489 }
2490
2491 #[rstest]
2492 fn last_processed_event_for_on_chain_snapshot_rejects_unprocessed_profiler() {
2493 let mut profiler = PoolProfiler::new(weth_usdt_pool());
2494 profiler
2495 .initialize(U160::from_str_radix("3cb0adde486484998be0b", 16).unwrap())
2496 .expect("Known WETH/USDT initial sqrt price should initialize");
2497
2498 let error = BlockchainDataClientCore::last_processed_event_for_on_chain_snapshot(&profiler)
2499 .expect_err("unprocessed profiler should not fetch on-chain state");
2500
2501 assert_eq!(
2502 error.to_string(),
2503 format!(
2504 "cannot fetch on-chain snapshot for pool {} without a processed event",
2505 profiler.pool.address
2506 )
2507 );
2508 }
2509
2510 #[rstest]
2511 fn validate_rpc_snapshot_topology_accepts_consistent_snapshot() {
2512 let (profiler, snapshot) = rpc_topology_fixture();
2513
2514 let result = BlockchainDataClientCore::validate_rpc_snapshot_topology(&profiler, &snapshot);
2515
2516 assert!(result.is_ok());
2517 }
2518
2519 #[rstest]
2520 fn validate_rpc_snapshot_topology_rejects_missing_tick() {
2521 let (profiler, mut snapshot) = rpc_topology_fixture();
2522 snapshot.ticks.pop();
2523
2524 let error = BlockchainDataClientCore::validate_rpc_snapshot_topology(&profiler, &snapshot)
2525 .unwrap_err();
2526
2527 assert!(error.to_string().contains("expected 2 ticks, received 1"));
2528 }
2529
2530 #[rstest]
2531 fn validate_rpc_snapshot_topology_rejects_zeroed_position() {
2532 let (profiler, mut snapshot) = rpc_topology_fixture();
2533 snapshot.positions[0].liquidity = 0;
2534
2535 let error = BlockchainDataClientCore::validate_rpc_snapshot_topology(&profiler, &snapshot)
2536 .unwrap_err();
2537
2538 assert!(
2539 error
2540 .to_string()
2541 .contains("RPC positions do not match the complete HyperSync topology")
2542 );
2543 }
2544
2545 #[rstest]
2546 fn validate_rpc_snapshot_topology_rejects_active_liquidity_mismatch() {
2547 let (profiler, mut snapshot) = rpc_topology_fixture();
2548 snapshot.state.liquidity -= 1;
2549
2550 let error = BlockchainDataClientCore::validate_rpc_snapshot_topology(&profiler, &snapshot)
2551 .unwrap_err();
2552
2553 assert!(error.to_string().contains("RPC active liquidity mismatch"));
2554 }
2555
2556 #[rstest]
2557 fn validate_rpc_snapshot_topology_rejects_tick_liquidity_mismatch() {
2558 let (profiler, mut snapshot) = rpc_topology_fixture();
2559 snapshot.ticks[0].liquidity_gross -= 1;
2560
2561 let error = BlockchainDataClientCore::validate_rpc_snapshot_topology(&profiler, &snapshot)
2562 .unwrap_err();
2563
2564 assert!(error.to_string().contains("RPC tick -60 topology mismatch"));
2565 }
2566
2567 #[rstest]
2568 fn timestamp_for_on_chain_snapshot_prefers_cached_block_timestamp() {
2569 let pool = weth_usdt_pool();
2570 let mut profiler = PoolProfiler::new(pool);
2571 let cached_ts = UnixNanos::from(1_700_000_001_000_000_000);
2572 let profiler_ts = UnixNanos::from(1_700_000_000_000_000_000);
2573 profiler.last_processed_ts = Some(profiler_ts);
2574
2575 let timestamp =
2576 BlockchainDataClientCore::timestamp_for_on_chain_snapshot(&profiler, Some(cached_ts))
2577 .unwrap();
2578
2579 assert_eq!(timestamp, cached_ts);
2580 }
2581
2582 #[rstest]
2583 fn timestamp_for_on_chain_snapshot_falls_back_to_profiler_timestamp() {
2584 let pool = weth_usdt_pool();
2585 let mut profiler = PoolProfiler::new(pool);
2586 let profiler_ts = UnixNanos::from(1_700_000_000_000_000_000);
2587 profiler.last_processed_ts = Some(profiler_ts);
2588
2589 let timestamp =
2590 BlockchainDataClientCore::timestamp_for_on_chain_snapshot(&profiler, None).unwrap();
2591
2592 assert_eq!(timestamp, profiler_ts);
2593 }
2594
2595 #[rstest]
2596 fn timestamp_for_on_chain_snapshot_rejects_missing_timestamp() {
2597 let pool = weth_usdt_pool();
2598 let profiler = PoolProfiler::new(pool);
2599
2600 let error = BlockchainDataClientCore::timestamp_for_on_chain_snapshot(&profiler, None)
2601 .expect_err("missing timestamps should fail");
2602
2603 assert_eq!(
2604 error.to_string(),
2605 "missing block timestamp for on-chain snapshot"
2606 );
2607 }
2608
2609 #[rstest]
2610 fn block_scoped_snapshot_position_from_block_caches_timestamp_and_uses_sentinel_indexes() {
2611 let chain = Arc::new(
2612 Chain::from_chain_id(42161)
2613 .expect("Arbitrum chain should exist")
2614 .clone(),
2615 );
2616 let mut cache = BlockchainCache::new(chain);
2617 let timestamp = UnixNanos::from(1_700_000_002_000_000_000);
2618 let block = test_block(123, timestamp);
2619
2620 let position = BlockchainDataClientCore::block_scoped_snapshot_position_from_block(
2621 &mut cache, &block, 123,
2622 )
2623 .unwrap();
2624
2625 assert_eq!(position.number, 123);
2626 assert_eq!(position.transaction_hash, block.hash);
2627 assert_eq!(position.transaction_index, BLOCK_SCOPED_SNAPSHOT_INDEX);
2628 assert_eq!(position.log_index, BLOCK_SCOPED_SNAPSHOT_INDEX);
2629 assert_eq!(cache.get_block_timestamp(123), Some(×tamp));
2630 }
2631
2632 #[rstest]
2633 fn block_scoped_snapshot_position_from_block_rejects_mismatched_block() {
2634 let chain = Arc::new(
2635 Chain::from_chain_id(42161)
2636 .expect("Arbitrum chain should exist")
2637 .clone(),
2638 );
2639 let mut cache = BlockchainCache::new(chain);
2640 let block = test_block(122, UnixNanos::from(1_700_000_002_000_000_000));
2641
2642 let error = BlockchainDataClientCore::block_scoped_snapshot_position_from_block(
2643 &mut cache, &block, 123,
2644 )
2645 .expect_err("mismatched block should fail");
2646
2647 assert_eq!(
2648 error.to_string(),
2649 "Fetched block 122 while requesting RPC snapshot block 123"
2650 );
2651 assert_eq!(cache.get_block_timestamp(122), None);
2652 }
2653
2654 #[rstest]
2655 #[case(100, 50, Some(99))]
2656 #[case(50, 50, None)]
2657 #[case(0, 0, None)]
2658 fn completed_pool_event_checkpoint_excludes_in_flight_block(
2659 #[case] block_number: u64,
2660 #[case] effective_from_block: u64,
2661 #[case] expected: Option<u64>,
2662 ) {
2663 let checkpoint = BlockchainDataClientCore::completed_pool_event_checkpoint(
2664 block_number,
2665 effective_from_block,
2666 );
2667
2668 assert_eq!(checkpoint, expected);
2669 }
2670
2671 #[rstest]
2672 fn pool_event_sync_ranges_preserve_legacy_history_during_new_family_backfill() {
2673 let families = vec![
2674 PoolEventFamily {
2675 name: "swap",
2676 signature: "swap".to_string(),
2677 introduced_version: 0,
2678 },
2679 PoolEventFamily {
2680 name: "fee_protocol_update",
2681 signature: "fee_protocol_update".to_string(),
2682 introduced_version: 1,
2683 },
2684 PoolEventFamily {
2685 name: "fee_protocol_collect",
2686 signature: "fee_protocol_collect".to_string(),
2687 introduced_version: 1,
2688 },
2689 ];
2690 let state = PoolEventSyncState {
2691 version: 0,
2692 last_full_sync_block: Some(100),
2693 family_blocks: Vec::new(),
2694 };
2695
2696 let ranges = BlockchainDataClientCore::pool_event_sync_ranges(&families, &state, 10, 110);
2697
2698 assert_eq!(
2699 ranges,
2700 vec![
2701 PoolEventSyncRange {
2702 from_block: 10,
2703 families: vec![families[1].clone(), families[2].clone()],
2704 },
2705 PoolEventSyncRange {
2706 from_block: 101,
2707 families: vec![families[0].clone()],
2708 },
2709 ]
2710 );
2711 }
2712
2713 #[rstest]
2714 fn pool_event_sync_ranges_group_families_by_checkpoint() {
2715 let families = vec![
2716 PoolEventFamily {
2717 name: "swap",
2718 signature: "swap".to_string(),
2719 introduced_version: 0,
2720 },
2721 PoolEventFamily {
2722 name: "fee_protocol_update",
2723 signature: "fee_protocol_update".to_string(),
2724 introduced_version: 1,
2725 },
2726 PoolEventFamily {
2727 name: "fee_protocol_collect",
2728 signature: "fee_protocol_collect".to_string(),
2729 introduced_version: 1,
2730 },
2731 ];
2732 let state = PoolEventSyncState {
2733 version: 1,
2734 last_full_sync_block: Some(100),
2735 family_blocks: vec![
2736 ("fee_protocol_collect".to_string(), 75),
2737 ("fee_protocol_update".to_string(), 50),
2738 ("swap".to_string(), 100),
2739 ],
2740 };
2741
2742 let ranges = BlockchainDataClientCore::pool_event_sync_ranges(&families, &state, 10, 100);
2743
2744 assert_eq!(
2745 ranges,
2746 vec![
2747 PoolEventSyncRange {
2748 from_block: 51,
2749 families: vec![families[1].clone()],
2750 },
2751 PoolEventSyncRange {
2752 from_block: 76,
2753 families: vec![families[2].clone()],
2754 },
2755 ]
2756 );
2757 }
2758
2759 #[rstest]
2760 fn pool_event_sync_ranges_backfill_future_family_without_schema_migration() {
2761 let families = vec![
2762 PoolEventFamily {
2763 name: "swap",
2764 signature: "swap".to_string(),
2765 introduced_version: 0,
2766 },
2767 PoolEventFamily {
2768 name: "future",
2769 signature: "future".to_string(),
2770 introduced_version: 2,
2771 },
2772 ];
2773 let state = PoolEventSyncState {
2774 version: 1,
2775 last_full_sync_block: Some(100),
2776 family_blocks: vec![("swap".to_string(), 100)],
2777 };
2778
2779 let ranges = BlockchainDataClientCore::pool_event_sync_ranges(&families, &state, 10, 100);
2780
2781 assert_eq!(
2782 ranges,
2783 vec![PoolEventSyncRange {
2784 from_block: 10,
2785 families: vec![families[1].clone()],
2786 }]
2787 );
2788 }
2789
2790 fn weth_usdt_pool() -> SharedPool {
2791 let chain = Arc::new(
2792 Chain::from_chain_id(1)
2793 .expect("Ethereum chain should exist")
2794 .clone(),
2795 );
2796 let dex = Arc::clone(
2797 &get_dex_extended(chain.name, &DexType::UniswapV3)
2798 .expect("Ethereum UniswapV3 should be registered")
2799 .dex,
2800 );
2801 let pool_address = address!("4e68ccd3e89f51c3074ca5072bbac773960dfa36");
2802 let token0 = Token::new(
2803 Arc::clone(&chain),
2804 address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
2805 "Wrapped Ether".to_string(),
2806 "WETH".to_string(),
2807 18,
2808 );
2809 let token1 = Token::new(
2810 Arc::clone(&chain),
2811 address!("dAC17F958D2ee523a2206206994597C13D831ec7"),
2812 "Tether USD".to_string(),
2813 "USDT".to_string(),
2814 6,
2815 );
2816
2817 Arc::new(Pool::new(
2818 chain,
2819 dex,
2820 pool_address,
2821 PoolIdentifier::from_address(pool_address),
2822 WETH_USDT_CREATION_BLOCK,
2823 token0,
2824 token1,
2825 Some(3_000),
2826 Some(60),
2827 UnixNanos::default(),
2828 ))
2829 }
2830
2831 fn rpc_topology_fixture() -> (PoolProfiler, PoolSnapshot) {
2832 let pool = weth_usdt_pool();
2833 let owner = Address::ZERO;
2834 let liquidity = 1_000_u128;
2835 let state = PoolState {
2836 current_tick: 0,
2837 liquidity,
2838 ..Default::default()
2839 };
2840 let position = PoolPosition::new(owner, -60, 60, liquidity as i128);
2841 let ticks = vec![
2842 PoolTick::new(
2843 -60,
2844 liquidity,
2845 liquidity as i128,
2846 U256::ZERO,
2847 U256::ZERO,
2848 true,
2849 0,
2850 ),
2851 PoolTick::new(
2852 60,
2853 liquidity,
2854 -(liquidity as i128),
2855 U256::ZERO,
2856 U256::ZERO,
2857 true,
2858 0,
2859 ),
2860 ];
2861 let timestamp = UnixNanos::from(1_700_000_000_000_000_000);
2862 let snapshot = PoolSnapshot::new(
2863 pool.instrument_id,
2864 state,
2865 vec![position],
2866 ticks,
2867 PoolAnalytics::default(),
2868 BlockPosition::new(100, "0xabc".to_string(), 0, 0),
2869 timestamp,
2870 timestamp,
2871 );
2872 let mut profiler = PoolProfiler::new(pool);
2873 profiler.restore_from_snapshot(snapshot.clone()).unwrap();
2874
2875 (profiler, snapshot)
2876 }
2877
2878 fn test_block(number: u64, timestamp: UnixNanos) -> Block {
2879 Block::new(
2880 format!("0x{number:064x}"),
2881 String::from("0x0"),
2882 number,
2883 Ustr::from("0x0000000000000000000000000000000000000000"),
2884 30_000_000,
2885 21_000,
2886 timestamp,
2887 Some(Blockchain::Arbitrum),
2888 )
2889 }
2890}