1use std::{
23 collections::{BTreeMap, HashMap, HashSet},
24 sync::Arc,
25};
26
27use alloy::primitives::Address;
28use nautilus_core::UnixNanos;
29use nautilus_model::defi::{
30 Block, DexType, Pool, PoolIdentifier, PoolLiquidityUpdate, PoolSwap, SharedChain, SharedDex,
31 SharedPool, Token,
32 data::{PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate, PoolFlash},
33 pool_analysis::{position::PoolPosition, snapshot::PoolSnapshot},
34 tick_map::tick::PoolTick,
35};
36use sqlx::postgres::PgConnectOptions;
37
38use crate::{
39 cache::{
40 consistency::CachedBlocksConsistencyStatus,
41 database::BlockchainCacheDatabase,
42 rows::{ExecutionTransactionRow, PoolRow},
43 },
44 events::initialize::InitializeEvent,
45};
46
47pub mod consistency;
48pub mod copy;
49pub mod database;
50pub mod rows;
51pub mod types;
52
53#[derive(Debug, Clone, Default, PartialEq, Eq)]
54pub(crate) struct PoolEventSyncState {
55 pub version: u32,
56 pub last_full_sync_block: Option<u64>,
57 pub family_blocks: Vec<(String, u64)>,
58}
59
60#[derive(Debug)]
62pub struct BlockchainCache {
63 chain: SharedChain,
65 block_timestamps: BTreeMap<u64, UnixNanos>,
67 block_hashes: BTreeMap<u64, String>,
69 dexes: HashMap<DexType, SharedDex>,
71 tokens: HashMap<Address, Token>,
73 invalid_tokens: HashSet<Address>,
75 pools: HashMap<PoolIdentifier, SharedPool>,
77 pub database: Option<BlockchainCacheDatabase>,
79}
80
81impl BlockchainCache {
82 #[must_use]
84 pub fn new(chain: SharedChain) -> Self {
85 Self {
86 chain,
87 dexes: HashMap::new(),
88 tokens: HashMap::new(),
89 invalid_tokens: HashSet::new(),
90 pools: HashMap::new(),
91 block_timestamps: BTreeMap::new(),
92 block_hashes: BTreeMap::new(),
93 database: None,
94 }
95 }
96
97 pub async fn get_cache_block_consistency_status(
99 &self,
100 ) -> Option<CachedBlocksConsistencyStatus> {
101 let database = self.database.as_ref()?;
102 database
103 .get_block_consistency_status(&self.chain)
104 .await
105 .map_err(|e| log::error!("Error getting block consistency status: {e}"))
106 .ok()
107 }
108
109 #[must_use]
111 pub fn min_dex_creation_block(&self) -> Option<u64> {
112 self.dexes
113 .values()
114 .map(|dex| dex.factory_creation_block)
115 .min()
116 }
117
118 #[must_use]
120 pub fn get_block_timestamp(&self, block_number: u64) -> Option<&UnixNanos> {
121 self.block_timestamps.get(&block_number)
122 }
123
124 #[must_use]
126 pub fn get_block_hash(&self, block_number: u64) -> Option<&str> {
127 self.block_hashes.get(&block_number).map(String::as_str)
128 }
129
130 pub fn cache_block_timestamp(&mut self, number: u64, timestamp: UnixNanos) {
135 self.block_timestamps.insert(number, timestamp);
136 }
137
138 pub fn cache_block_metadata(&mut self, block: &Block) {
140 self.block_timestamps.insert(block.number, block.timestamp);
141 self.block_hashes.insert(block.number, block.hash.clone());
142 }
143
144 pub async fn initialize_database(&mut self, pg_connect_options: PgConnectOptions) {
146 let database = BlockchainCacheDatabase::init(pg_connect_options).await;
147 self.database = Some(database);
148 }
149
150 #[must_use]
152 pub const fn has_database(&self) -> bool {
153 self.database.is_some()
154 }
155
156 #[expect(
162 clippy::too_many_arguments,
163 reason = "the parameters mirror the persisted execution transaction fields"
164 )]
165 pub async fn add_execution_transaction(
166 &self,
167 chain_id: u32,
168 wallet_address: &str,
169 nonce: u64,
170 transaction_hash: &str,
171 purpose: &str,
172 status: &str,
173 client_order_id: Option<&str>,
174 ) -> anyhow::Result<()> {
175 let database = self.database.as_ref().ok_or_else(|| {
176 anyhow::anyhow!(
177 "No durable store configured; refusing to persist execution transaction"
178 )
179 })?;
180
181 database
182 .add_execution_transaction(
183 chain_id,
184 wallet_address,
185 nonce,
186 transaction_hash,
187 purpose,
188 status,
189 client_order_id,
190 )
191 .await
192 }
193
194 pub async fn ensure_execution_transaction_schema(&self) -> anyhow::Result<()> {
201 let database = self.database.as_ref().ok_or_else(|| {
202 anyhow::anyhow!(
203 "No durable store configured; refusing to migrate execution transaction"
204 )
205 })?;
206
207 database.ensure_execution_transaction_schema().await
208 }
209
210 pub async fn update_execution_transaction_status(
217 &self,
218 chain_id: u32,
219 transaction_hash: &str,
220 status: &str,
221 ) -> anyhow::Result<()> {
222 let database = self.database.as_ref().ok_or_else(|| {
223 anyhow::anyhow!("No durable store configured; refusing to update execution transaction")
224 })?;
225
226 database
227 .update_execution_transaction_status(chain_id, transaction_hash, status)
228 .await
229 }
230
231 pub async fn get_execution_transaction(
238 &self,
239 chain_id: u32,
240 transaction_hash: &str,
241 ) -> anyhow::Result<Option<ExecutionTransactionRow>> {
242 let database = self.database.as_ref().ok_or_else(|| {
243 anyhow::anyhow!("No durable store configured; refusing to load execution transaction")
244 })?;
245
246 database
247 .get_execution_transaction(chain_id, transaction_hash)
248 .await
249 }
250
251 pub async fn toggle_performance_settings(&self, enable: bool) -> anyhow::Result<()> {
257 if let Some(database) = &self.database {
258 database.toggle_perf_sync_settings(enable).await
259 } else {
260 log::debug!("Database not initialized, skipping performance settings toggle");
261 Ok(())
262 }
263 }
264
265 pub async fn initialize_chain(&mut self) {
270 if let Some(database) = &self.database {
272 if let Err(e) = database.seed_chain(&self.chain).await {
273 log::error!(
274 "Error seeding chain in database: {e}. Continuing without database cache functionality"
275 );
276 return;
277 }
278 log::debug!("Chain seeded in the database");
279
280 if let Err(e) = database.ensure_pool_event_block_hash_schema().await {
281 log::error!("Error adding pool event block hash storage: {e}");
282 return;
283 }
284
285 match database.create_block_partition(&self.chain).await {
286 Ok(message) => log::debug!("Executing block partition creation: {message}"),
287 Err(e) => log::error!(
288 "Error creating block partition for chain {}: {e}. Continuing without partition creation...",
289 self.chain.chain_id
290 ),
291 }
292
293 match database.create_token_partition(&self.chain).await {
294 Ok(message) => log::debug!("Executing token partition creation: {message}"),
295 Err(e) => log::error!(
296 "Error creating token partition for chain {}: {e}. Continuing without partition creation...",
297 self.chain.chain_id
298 ),
299 }
300 }
301
302 if let Err(e) = self.load_tokens().await {
303 log::error!("Error loading tokens from the database: {e}");
304 }
305 }
306
307 pub async fn connect(&mut self, from_block: u64) -> anyhow::Result<()> {
313 log::debug!("Connecting and loading from_block {from_block}");
314
315 if let Err(e) = self.load_tokens().await {
316 log::error!("Error loading tokens from the database: {e}");
317 }
318
319 Ok(())
325 }
326
327 async fn load_tokens(&mut self) -> anyhow::Result<()> {
329 if let Some(database) = &self.database {
330 let (tokens, invalid_tokens) = tokio::try_join!(
331 database.load_tokens(Arc::clone(&self.chain)),
332 database.load_invalid_token_addresses(self.chain.chain_id)
333 )?;
334
335 log::debug!(
336 "Loading {} valid tokens and {} invalid tokens from cache database",
337 tokens.len(),
338 invalid_tokens.len()
339 );
340
341 self.tokens
342 .extend(tokens.into_iter().map(|token| (token.address, token)));
343 self.invalid_tokens.extend(invalid_tokens);
344 }
345 Ok(())
346 }
347
348 pub async fn load_pools(&mut self, dex_id: &DexType) -> anyhow::Result<Vec<Pool>> {
356 let mut loaded_pools = Vec::new();
357
358 if let Some(database) = &self.database {
359 let dex = self
360 .get_dex(dex_id)
361 .ok_or_else(|| anyhow::anyhow!("DEX {dex_id:?} has not been registered"))?;
362 let pool_rows = database
363 .load_pools(Arc::clone(&self.chain), &dex_id.to_string())
364 .await?;
365 log::debug!(
366 "Loading {} pools for DEX {} from cache database",
367 pool_rows.len(),
368 dex_id,
369 );
370
371 for pool_row in pool_rows {
372 if let Some(pool) = self.build_pool_from_row(&pool_row, &dex) {
373 loaded_pools.push(pool.clone());
374 self.pools.insert(pool.pool_identifier, Arc::new(pool));
375 }
376 }
377 }
378 Ok(loaded_pools)
379 }
380
381 pub async fn load_pool(
391 &mut self,
392 dex_id: &DexType,
393 pool_identifier: &PoolIdentifier,
394 ) -> anyhow::Result<Option<Pool>> {
395 let dex = self
396 .get_dex(dex_id)
397 .ok_or_else(|| anyhow::anyhow!("DEX {dex_id:?} has not been registered"))?;
398
399 let pool_row = {
400 let Some(database) = &self.database else {
401 return Ok(None);
402 };
403 database
404 .load_pool(
405 Arc::clone(&self.chain),
406 &dex_id.to_string(),
407 pool_identifier,
408 )
409 .await?
410 };
411
412 let Some(pool_row) = pool_row else {
413 return Ok(None);
414 };
415 let Some(pool) = self.build_pool_from_row(&pool_row, &dex) else {
416 return Ok(None);
417 };
418 self.pools
419 .insert(pool.pool_identifier, Arc::new(pool.clone()));
420 Ok(Some(pool))
421 }
422
423 fn build_pool_from_row(&self, pool_row: &PoolRow, dex: &SharedDex) -> Option<Pool> {
428 let Some(token0) = self.tokens.get(&pool_row.token0_address) else {
429 log::error!(
430 "Failed to load pool {} for DEX {}: Token0 with address {} not found in cache. \
431 This may indicate the token was not properly loaded from the database or the pool references an unknown token",
432 pool_row.address,
433 dex.name,
434 pool_row.token0_address
435 );
436 return None;
437 };
438
439 let Some(token1) = self.tokens.get(&pool_row.token1_address) else {
440 log::error!(
441 "Failed to load pool {} for DEX {}: Token1 with address {} not found in cache. \
442 This may indicate the token was not properly loaded from the database or the pool references an unknown token",
443 pool_row.address,
444 dex.name,
445 pool_row.token1_address
446 );
447 return None;
448 };
449
450 let Some(pool_identifier) = pool_row.pool_identifier.parse().ok() else {
451 log::error!(
452 "Invalid pool identifier '{}' in database for pool {}, skipping",
453 pool_row.pool_identifier,
454 pool_row.address
455 );
456 return None;
457 };
458
459 let ts_init = pool_row.creation_block_timestamp.unwrap_or_default();
460
461 let mut pool = Pool::new(
462 Arc::clone(&self.chain),
463 Arc::clone(dex),
464 pool_row.address,
465 pool_identifier,
466 pool_row.creation_block,
467 token0.clone(),
468 token1.clone(),
469 pool_row.fee,
470 pool_row.tick_spacing,
471 ts_init,
472 );
473
474 if let Some(ref hook_address_str) = pool_row.hook_address
475 && let Ok(hooks) = hook_address_str.parse()
476 {
477 pool.set_hooks(hooks);
478 }
479
480 if let Some(initial_sqrt_price_x96_str) = &pool_row.initial_sqrt_price_x96
481 && let Ok(initial_sqrt_price_x96) = initial_sqrt_price_x96_str.parse()
482 && let Some(initial_tick) = pool_row.initial_tick
483 {
484 pool.initialize(initial_sqrt_price_x96, initial_tick);
485 }
486
487 Some(pool)
488 }
489
490 #[allow(dead_code)]
493 async fn load_blocks(&mut self, from_block: u64) -> anyhow::Result<()> {
494 if let Some(database) = &self.database {
495 let block_timestamps = database
496 .load_block_timestamps(Arc::clone(&self.chain), from_block)
497 .await?;
498
499 if !block_timestamps.is_empty() {
501 let first = block_timestamps.first().unwrap().number;
502 let last = block_timestamps.last().unwrap().number;
503 let expected_len = (last - first + 1) as usize;
504 if block_timestamps.len() != expected_len {
505 anyhow::bail!(
506 "Block timestamps are not consistent and sequential. Expected {expected_len} blocks but got {}",
507 block_timestamps.len()
508 );
509 }
510 }
511
512 if block_timestamps.is_empty() {
513 log::debug!("No blocks found in database");
514 return Ok(());
515 }
516
517 log::debug!(
518 "Loading {} blocks timestamps from the cache database with last block number {}",
519 block_timestamps.len(),
520 block_timestamps.last().unwrap().number,
521 );
522
523 for block in block_timestamps {
524 self.block_timestamps.insert(block.number, block.timestamp);
525 }
526 }
527 Ok(())
528 }
529
530 pub async fn add_block(&mut self, block: Block) -> anyhow::Result<()> {
536 self.cache_block_metadata(&block);
538 if let Some(database) = &self.database {
539 database.add_block(self.chain.chain_id, &block).await?;
540 }
541 Ok(())
542 }
543
544 pub async fn add_blocks_batch(
550 &mut self,
551 blocks: Vec<Block>,
552 use_copy_command: bool,
553 ) -> anyhow::Result<()> {
554 if blocks.is_empty() {
555 return Ok(());
556 }
557
558 if let Some(database) = &self.database {
559 if use_copy_command {
560 database
561 .add_blocks_copy(self.chain.chain_id, &blocks)
562 .await?;
563 } else {
564 database
565 .add_blocks_batch(self.chain.chain_id, &blocks)
566 .await?;
567 }
568 }
569
570 for block in blocks {
572 self.block_timestamps.insert(block.number, block.timestamp);
573 self.block_hashes.insert(block.number, block.hash);
574 }
575
576 Ok(())
577 }
578
579 pub async fn add_pool_event_blocks_batch(&mut self, blocks: Vec<Block>) -> anyhow::Result<()> {
585 if blocks.is_empty() {
586 return Ok(());
587 }
588
589 if let Some(database) = &self.database {
590 database
591 .add_pool_event_blocks_batch(self.chain.chain_id, &blocks)
592 .await?;
593 }
594
595 for block in blocks {
596 self.block_timestamps.insert(block.number, block.timestamp);
597 self.block_hashes.insert(block.number, block.hash);
598 }
599
600 Ok(())
601 }
602
603 pub async fn add_dex(&mut self, dex: SharedDex) -> anyhow::Result<()> {
609 log::debug!("Adding dex {} to the cache", dex.name);
610
611 if let Some(database) = &self.database {
612 database.add_dex(Arc::clone(&dex)).await?;
613 }
614
615 self.dexes.insert(dex.name, dex);
616 Ok(())
617 }
618
619 pub async fn add_pool(&mut self, pool: Pool) -> anyhow::Result<()> {
625 if let Some(database) = &self.database {
626 database.add_pool(&pool).await?;
627 }
628
629 self.pools.insert(pool.pool_identifier, Arc::new(pool));
630 Ok(())
631 }
632
633 pub async fn add_pools_batch(&mut self, pools: Vec<Pool>) -> anyhow::Result<()> {
639 if pools.is_empty() {
640 return Ok(());
641 }
642
643 if let Some(database) = &self.database {
644 database.add_pools_copy(self.chain.chain_id, &pools).await?;
645 }
646 self.pools.extend(
647 pools
648 .into_iter()
649 .map(|pool| (pool.pool_identifier, Arc::new(pool))),
650 );
651
652 Ok(())
653 }
654
655 pub async fn add_token(&mut self, token: Token) -> anyhow::Result<()> {
661 if let Some(database) = &self.database {
662 database.add_token(&token).await?;
663 }
664 self.tokens.insert(token.address, token);
665 Ok(())
666 }
667
668 pub async fn add_tokens_batch(&mut self, tokens: Vec<Token>) -> anyhow::Result<()> {
674 if tokens.is_empty() {
675 return Ok(());
676 }
677
678 if let Some(database) = &self.database {
679 database
680 .add_tokens_copy(self.chain.chain_id, &tokens)
681 .await?;
682 }
683
684 self.tokens
685 .extend(tokens.into_iter().map(|token| (token.address, token)));
686
687 Ok(())
688 }
689
690 pub fn insert_token_in_memory(&mut self, token: Token) {
692 self.tokens.insert(token.address, token);
693 }
694
695 pub fn insert_invalid_token_in_memory(&mut self, address: Address) {
697 self.invalid_tokens.insert(address);
698 }
699
700 pub async fn add_invalid_token(
706 &mut self,
707 address: Address,
708 error_string: &str,
709 ) -> anyhow::Result<()> {
710 if let Some(database) = &self.database {
711 database
712 .add_invalid_token(self.chain.chain_id, &address, error_string)
713 .await?;
714 }
715 self.invalid_tokens.insert(address);
716 Ok(())
717 }
718
719 pub async fn add_pool_swap(&self, swap: &PoolSwap) -> anyhow::Result<()> {
725 if let Some(database) = &self.database {
726 database.add_swap(self.chain.chain_id, swap).await?;
727 }
728
729 Ok(())
730 }
731
732 pub async fn add_liquidity_update(
738 &self,
739 liquidity_update: &PoolLiquidityUpdate,
740 ) -> anyhow::Result<()> {
741 if let Some(database) = &self.database {
742 database
743 .add_pool_liquidity_update(self.chain.chain_id, liquidity_update)
744 .await?;
745 }
746
747 Ok(())
748 }
749
750 pub async fn add_pool_swaps_batch(
756 &self,
757 swaps: &[PoolSwap],
758 use_copy_command: bool,
759 ) -> anyhow::Result<()> {
760 if let Some(database) = &self.database {
761 if use_copy_command {
762 database
763 .add_pool_swaps_copy(self.chain.chain_id, swaps)
764 .await?;
765 } else {
766 database
767 .add_pool_swaps_batch(self.chain.chain_id, swaps)
768 .await?;
769 }
770 }
771
772 Ok(())
773 }
774
775 pub async fn add_pool_liquidity_updates_batch(
781 &self,
782 updates: &[PoolLiquidityUpdate],
783 use_copy_command: bool,
784 ) -> anyhow::Result<()> {
785 if let Some(database) = &self.database {
786 if use_copy_command {
787 database
788 .add_pool_liquidity_updates_copy(self.chain.chain_id, updates)
789 .await?;
790 } else {
791 database
792 .add_pool_liquidity_updates_batch(self.chain.chain_id, updates)
793 .await?;
794 }
795 }
796
797 Ok(())
798 }
799
800 pub async fn add_pool_fee_collects_batch(
806 &self,
807 collects: &[PoolFeeCollect],
808 use_copy_command: bool,
809 ) -> anyhow::Result<()> {
810 if let Some(database) = &self.database {
811 if use_copy_command {
812 database
813 .copy_pool_fee_collects_batch(self.chain.chain_id, collects)
814 .await?;
815 } else {
816 database
817 .add_pool_collects_batch(self.chain.chain_id, collects)
818 .await?;
819 }
820 }
821
822 Ok(())
823 }
824
825 pub async fn add_pool_flash_batch(&self, flash_events: &[PoolFlash]) -> anyhow::Result<()> {
831 if let Some(database) = &self.database {
832 database
833 .add_pool_flash_batch(self.chain.chain_id, flash_events)
834 .await?;
835 }
836
837 Ok(())
838 }
839
840 pub async fn add_pool_fee_protocol_updates_batch(
846 &self,
847 updates: &[PoolFeeProtocolUpdate],
848 ) -> anyhow::Result<()> {
849 if let Some(database) = &self.database {
850 database
851 .add_pool_fee_protocol_updates_batch(self.chain.chain_id, updates)
852 .await?;
853 }
854
855 Ok(())
856 }
857
858 pub async fn add_pool_fee_protocol_collect_batch(
864 &self,
865 collects: &[PoolFeeProtocolCollect],
866 ) -> anyhow::Result<()> {
867 if let Some(database) = &self.database {
868 database
869 .add_pool_fee_protocol_collect_batch(self.chain.chain_id, collects)
870 .await?;
871 }
872
873 Ok(())
874 }
875
876 pub async fn add_pool_snapshot(
887 &self,
888 dex: &DexType,
889 pool_identifier: &PoolIdentifier,
890 snapshot: &PoolSnapshot,
891 ) -> anyhow::Result<()> {
892 if snapshot.positions.is_empty()
898 && snapshot.ticks.is_empty()
899 && let Some(pool) = self.pools.get(pool_identifier)
900 && snapshot.block_position.number == pool.creation_block
901 {
902 log::warn!(
903 "Refusing to persist empty stub snapshot for {} at pool creation block {}",
904 snapshot.instrument_id,
905 snapshot.block_position.number,
906 );
907 return Ok(());
908 }
909
910 if let Some(database) = &self.database {
911 database
913 .add_pool_snapshot(self.chain.chain_id, dex, pool_identifier, snapshot)
914 .await?;
915
916 let positions: Vec<(PoolIdentifier, PoolPosition)> = snapshot
917 .positions
918 .iter()
919 .map(|pos| (*pool_identifier, pos.clone()))
920 .collect();
921
922 if !positions.is_empty() {
923 database
924 .add_pool_positions_batch(
925 self.chain.chain_id,
926 snapshot.block_position.number,
927 snapshot.block_position.transaction_index,
928 snapshot.block_position.log_index,
929 &positions,
930 )
931 .await?;
932 }
933
934 let ticks: Vec<(PoolIdentifier, &PoolTick)> = snapshot
935 .ticks
936 .iter()
937 .map(|tick| (*pool_identifier, tick))
938 .collect();
939
940 if !ticks.is_empty() {
941 database
942 .add_pool_ticks_batch(
943 self.chain.chain_id,
944 snapshot.block_position.number,
945 snapshot.block_position.transaction_index,
946 snapshot.block_position.log_index,
947 &ticks,
948 )
949 .await?;
950 }
951 }
952
953 Ok(())
954 }
955
956 pub async fn update_pool_initialize_price_tick(
962 &mut self,
963 initialize_event: &InitializeEvent,
964 ) -> anyhow::Result<()> {
965 if let Some(database) = &self.database {
966 database
967 .update_pool_initial_price_tick(self.chain.chain_id, initialize_event)
968 .await?;
969 }
970
971 let pool_identifier = initialize_event.pool_identifier;
973 if let Some(cached_pool) = self.pools.get(&pool_identifier) {
974 let mut updated_pool = (**cached_pool).clone();
975 updated_pool.initialize(initialize_event.sqrt_price_x96, initialize_event.tick);
976
977 self.pools.insert(pool_identifier, Arc::new(updated_pool));
978 }
979
980 Ok(())
981 }
982
983 #[must_use]
985 pub fn get_dex(&self, dex_id: &DexType) -> Option<SharedDex> {
986 self.dexes.get(dex_id).cloned()
987 }
988
989 #[must_use]
991 pub fn get_registered_dexes(&self) -> HashSet<DexType> {
992 self.dexes.keys().copied().collect()
993 }
994
995 #[must_use]
997 pub fn get_pool(&self, pool_identifier: &PoolIdentifier) -> Option<&SharedPool> {
998 self.pools.get(pool_identifier)
999 }
1000
1001 #[must_use]
1003 pub fn get_token(&self, address: &Address) -> Option<&Token> {
1004 self.tokens.get(address)
1005 }
1006
1007 #[must_use]
1012 pub fn is_invalid_token(&self, address: &Address) -> bool {
1013 self.invalid_tokens.contains(address)
1014 }
1015
1016 pub async fn update_dex_last_synced_block(
1022 &self,
1023 dex: &DexType,
1024 block_number: u64,
1025 ) -> anyhow::Result<()> {
1026 if let Some(database) = &self.database {
1027 database
1028 .update_dex_last_synced_block(self.chain.chain_id, dex, block_number)
1029 .await
1030 } else {
1031 Ok(())
1032 }
1033 }
1034
1035 pub async fn update_pool_last_synced_block(
1041 &self,
1042 dex: &DexType,
1043 pool_identifier: &PoolIdentifier,
1044 block_number: u64,
1045 ) -> anyhow::Result<()> {
1046 if let Some(database) = &self.database {
1047 database
1048 .update_pool_last_synced_block(
1049 self.chain.chain_id,
1050 dex,
1051 pool_identifier,
1052 block_number,
1053 )
1054 .await
1055 } else {
1056 Ok(())
1057 }
1058 }
1059
1060 pub async fn get_dex_last_synced_block(&self, dex: &DexType) -> anyhow::Result<Option<u64>> {
1066 if let Some(database) = &self.database {
1067 database
1068 .get_dex_last_synced_block(self.chain.chain_id, dex)
1069 .await
1070 } else {
1071 Ok(None)
1072 }
1073 }
1074
1075 pub async fn get_pool_last_synced_block(
1081 &self,
1082 dex: &DexType,
1083 pool_identifier: &PoolIdentifier,
1084 ) -> anyhow::Result<Option<u64>> {
1085 if let Some(database) = &self.database {
1086 database
1087 .get_pool_last_synced_block(self.chain.chain_id, dex, pool_identifier)
1088 .await
1089 } else {
1090 Ok(None)
1091 }
1092 }
1093
1094 pub(crate) async fn get_pool_event_sync_state(
1095 &self,
1096 dex: &DexType,
1097 pool_identifier: &PoolIdentifier,
1098 ) -> anyhow::Result<PoolEventSyncState> {
1099 if let Some(database) = &self.database {
1100 database
1101 .get_pool_event_sync_state(self.chain.chain_id, dex, pool_identifier)
1102 .await
1103 } else {
1104 Ok(PoolEventSyncState::default())
1105 }
1106 }
1107
1108 pub(crate) async fn update_pool_event_sync(
1109 &self,
1110 dex: &DexType,
1111 pool_identifier: &PoolIdentifier,
1112 event_families: &[&str],
1113 block_number: u64,
1114 version: Option<u32>,
1115 ) -> anyhow::Result<()> {
1116 if let Some(database) = &self.database {
1117 database
1118 .update_pool_event_sync(
1119 self.chain.chain_id,
1120 dex,
1121 pool_identifier,
1122 event_families,
1123 block_number,
1124 version,
1125 )
1126 .await
1127 } else {
1128 Ok(())
1129 }
1130 }
1131
1132 pub async fn get_pool_event_tables_last_block(
1138 &self,
1139 pool_identifier: &PoolIdentifier,
1140 ) -> anyhow::Result<Option<u64>> {
1141 if let Some(database) = &self.database {
1142 let (swaps_last_block, liquidity_last_block, collect_last_block, flash_last_block) = tokio::try_join!(
1143 database.get_table_last_block(
1144 self.chain.chain_id,
1145 "pool_swap_event",
1146 pool_identifier
1147 ),
1148 database.get_table_last_block(
1149 self.chain.chain_id,
1150 "pool_liquidity_event",
1151 pool_identifier
1152 ),
1153 database.get_table_last_block(
1154 self.chain.chain_id,
1155 "pool_collect_event",
1156 pool_identifier
1157 ),
1158 database.get_table_last_block(
1159 self.chain.chain_id,
1160 "pool_flash_event",
1161 pool_identifier
1162 ),
1163 )?;
1164
1165 let max_block = [
1166 swaps_last_block,
1167 liquidity_last_block,
1168 collect_last_block,
1169 flash_last_block,
1170 ]
1171 .into_iter()
1172 .flatten()
1173 .max();
1174 Ok(max_block)
1175 } else {
1176 Ok(None)
1177 }
1178 }
1179}
1180
1181#[cfg(test)]
1182mod tests {
1183 use std::{
1184 sync::Arc,
1185 time::{SystemTime, UNIX_EPOCH},
1186 };
1187
1188 use alloy::primitives::{I256, U160, U256, address};
1189 use futures_util::TryStreamExt;
1190 use nautilus_core::UnixNanos;
1191 use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
1192 use nautilus_model::defi::{
1193 AmmType, Block, Blockchain, Chain, Dex, PoolProfiler, SharedChain, SharedDex, Token,
1194 data::{DexPoolData, block::BlockPosition},
1195 pool_analysis::snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
1196 };
1197 use rstest::rstest;
1198 use sqlx::{
1199 AssertSqlSafe, Error as SqlxError, PgPool,
1200 postgres::{PgConnectOptions, PgPoolOptions},
1201 };
1202 use tokio_util::sync::CancellationToken;
1203 use ustr::Ustr;
1204
1205 use super::*;
1206 use crate::{
1207 config::BlockchainDataClientConfig,
1208 data::core::{BlockchainDataClientCore, SnapshotValidation},
1209 };
1210
1211 fn test_cache() -> BlockchainCache {
1212 BlockchainCache::new(Arc::new(Chain::new(Blockchain::Ethereum, 1)))
1213 }
1214
1215 #[rstest]
1216 fn cache_block_timestamp_records_in_memory() {
1217 let mut cache = test_cache();
1218 assert_eq!(cache.get_block_timestamp(100), None);
1219
1220 cache.cache_block_timestamp(100, UnixNanos::from(1_700_000_000_000_000_000));
1221
1222 assert_eq!(
1223 cache.get_block_timestamp(100),
1224 Some(&UnixNanos::from(1_700_000_000_000_000_000))
1225 );
1226 }
1227
1228 #[tokio::test]
1229 async fn add_block_populates_metadata_without_database() {
1230 let mut cache = test_cache();
1231 let block = Block::new(
1232 "0x1".to_string(),
1233 "0x0".to_string(),
1234 42,
1235 Ustr::from("miner"),
1236 30_000_000,
1237 21_000,
1238 UnixNanos::from(1_700_000_000_000_000_000),
1239 Some(Blockchain::Ethereum),
1240 );
1241
1242 cache.add_block(block).await.unwrap();
1243
1244 assert_eq!(
1245 cache.get_block_timestamp(42),
1246 Some(&UnixNanos::from(1_700_000_000_000_000_000))
1247 );
1248 assert_eq!(cache.get_block_hash(42), Some("0x1"));
1249 }
1250
1251 #[tokio::test]
1252 async fn pool_event_block_hash_schema_upgrades_legacy_table() -> anyhow::Result<()> {
1253 let Some((database, schema)) = connect_cache_test_database().await? else {
1254 return Ok(());
1255 };
1256 execute_schema_statement(
1257 &schema.admin_pool,
1258 format!(
1259 "ALTER TABLE {}.pool_event_block DROP COLUMN hash",
1260 schema.name
1261 ),
1262 )
1263 .await?;
1264
1265 database.ensure_pool_event_block_hash_schema().await?;
1266 database
1267 .add_pool_event_blocks_batch(
1268 1,
1269 &[test_block(42, UnixNanos::from(1_700_000_000_000_000_000))],
1270 )
1271 .await?;
1272
1273 let hash: String = sqlx::query_scalar(AssertSqlSafe(format!(
1274 "SELECT hash FROM {}.pool_event_block WHERE chain_id = 1 AND number = 42",
1275 schema.name
1276 )))
1277 .fetch_one(&schema.admin_pool)
1278 .await?;
1279 let expected_hash = format!("0x{:064x}", 42);
1280 anyhow::ensure!(
1281 hash == expected_hash,
1282 "Unexpected persisted pool event block hash: {hash}"
1283 );
1284
1285 schema.cleanup().await?;
1286 Ok(())
1287 }
1288
1289 #[tokio::test]
1290 async fn stream_pool_events_uses_pool_event_block_metadata_without_full_block()
1291 -> anyhow::Result<()> {
1292 let Some((database, schema)) = connect_cache_test_database().await? else {
1293 return Ok(());
1294 };
1295 let chain = arbitrum();
1296 let dex = uniswap_v3(&chain);
1297 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1298 let pool_identifier = PoolIdentifier::from_address(pool_address);
1299 let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
1300 let expected_ts = UnixNanos::from(1_700_000_000_123_456_789);
1301
1302 database
1303 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(12, expected_ts)])
1304 .await?;
1305 insert_pool_swap_event(
1306 &schema.admin_pool,
1307 &schema.name,
1308 chain.chain_id,
1309 &pool_identifier,
1310 12,
1311 )
1312 .await?;
1313 let events_result = database
1314 .stream_pool_events(chain, dex, instrument_id, pool_identifier, None, Some(12))
1315 .try_collect::<Vec<_>>()
1316 .await;
1317
1318 drop(database);
1319 schema.cleanup().await?;
1320
1321 let events = events_result?;
1322 let observed_metadata = match events.as_slice() {
1323 [DexPoolData::Swap(swap)] => {
1324 Some((swap.ts_event, swap.ts_init, swap.block_hash.clone()))
1325 }
1326 _ => None,
1327 };
1328
1329 let expected_metadata = Some((expected_ts, expected_ts, Some(format!("0x{:064x}", 12))));
1330 if observed_metadata != expected_metadata {
1331 anyhow::bail!(
1332 "unexpected stream metadata: expected {expected_metadata:?}, observed {observed_metadata:?}"
1333 );
1334 }
1335 Ok(())
1336 }
1337
1338 #[tokio::test]
1339 async fn stream_pool_events_round_trips_fee_protocol_update_in_order() -> anyhow::Result<()> {
1340 let Some((database, schema)) = connect_cache_test_database().await? else {
1341 return Ok(());
1342 };
1343 let chain = arbitrum();
1344 let dex = uniswap_v3(&chain);
1345 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1346 let pool_identifier = PoolIdentifier::from_address(pool_address);
1347 let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
1348 let ts = UnixNanos::from(1_700_000_000_000_000_000);
1349
1350 database
1351 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(12, ts), test_block(13, ts)])
1352 .await?;
1353 insert_pool_swap_event(
1355 &schema.admin_pool,
1356 &schema.name,
1357 chain.chain_id,
1358 &pool_identifier,
1359 12,
1360 )
1361 .await?;
1362 let update = PoolFeeProtocolUpdate::new(
1364 Arc::clone(&chain),
1365 Arc::clone(&dex),
1366 instrument_id,
1367 pool_identifier,
1368 13,
1369 "0x00000000000000000000000000000000000000000000000000000000000000ab".to_string(),
1370 0,
1371 0,
1372 4,
1373 6,
1374 ts,
1375 ts,
1376 );
1377 database
1378 .add_pool_fee_protocol_updates_batch(chain.chain_id, std::slice::from_ref(&update))
1379 .await?;
1380
1381 let events_result = database
1382 .stream_pool_events(chain, dex, instrument_id, pool_identifier, None, Some(13))
1383 .try_collect::<Vec<_>>()
1384 .await;
1385
1386 drop(database);
1387 schema.cleanup().await?;
1388
1389 let events = events_result?;
1390 match events.as_slice() {
1391 [DexPoolData::Swap(swap), DexPoolData::FeeProtocolUpdate(fp)] => {
1392 let observed = (
1395 swap.block,
1396 fp.block,
1397 fp.fee_protocol0_new,
1398 fp.fee_protocol1_new,
1399 fp.ts_event,
1400 );
1401
1402 if observed != (12, 13, 4, 6, ts) {
1403 anyhow::bail!("unexpected fee protocol round-trip: {observed:?}");
1404 }
1405 }
1406 other => anyhow::bail!("unexpected stream events: {other:?}"),
1407 }
1408 Ok(())
1409 }
1410
1411 #[tokio::test]
1412 async fn stream_pool_events_round_trips_fee_protocol_update_integer_width() -> anyhow::Result<()>
1413 {
1414 let Some((database, schema)) = connect_cache_test_database().await? else {
1415 return Ok(());
1416 };
1417 let chain = arbitrum();
1418 let dex = pancakeswap_v3(&chain);
1419 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1420 let pool_identifier = PoolIdentifier::from_address(pool_address);
1421 let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
1422 let ts = UnixNanos::from(1_700_000_000_000_000_000);
1423
1424 database
1425 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(13, ts)])
1426 .await?;
1427 let update = PoolFeeProtocolUpdate::new(
1428 Arc::clone(&chain),
1429 Arc::clone(&dex),
1430 instrument_id,
1431 pool_identifier,
1432 13,
1433 "0x00000000000000000000000000000000000000000000000000000000000000ac".to_string(),
1434 0,
1435 0,
1436 40_000,
1437 65_535,
1438 ts,
1439 ts,
1440 );
1441 database
1442 .add_pool_fee_protocol_updates_batch(chain.chain_id, std::slice::from_ref(&update))
1443 .await?;
1444
1445 let events_result = database
1446 .stream_pool_events(chain, dex, instrument_id, pool_identifier, None, Some(13))
1447 .try_collect::<Vec<_>>()
1448 .await;
1449
1450 drop(database);
1451 schema.cleanup().await?;
1452
1453 let events = events_result?;
1454 match events.as_slice() {
1455 [DexPoolData::FeeProtocolUpdate(fp)] => {
1456 let observed = (fp.fee_protocol0_new, fp.fee_protocol1_new, fp.ts_event);
1457
1458 if observed != (40_000, 65_535, ts) {
1459 anyhow::bail!("unexpected integer-width fee protocol round-trip: {observed:?}");
1460 }
1461 }
1462 other => anyhow::bail!("unexpected stream events: {other:?}"),
1463 }
1464 Ok(())
1465 }
1466
1467 #[tokio::test]
1468 async fn stream_pool_events_round_trips_fee_protocol_collect_in_order() -> anyhow::Result<()> {
1469 let Some((database, schema)) = connect_cache_test_database().await? else {
1470 return Ok(());
1471 };
1472 let chain = arbitrum();
1473 let dex = uniswap_v3(&chain);
1474 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1475 let pool_identifier = PoolIdentifier::from_address(pool_address);
1476 let instrument_id = Pool::create_instrument_id(chain.name, &dex, pool_identifier.as_str());
1477 let ts = UnixNanos::from(1_700_000_000_000_000_000);
1478
1479 database
1480 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(12, ts), test_block(13, ts)])
1481 .await?;
1482 insert_pool_swap_event(
1484 &schema.admin_pool,
1485 &schema.name,
1486 chain.chain_id,
1487 &pool_identifier,
1488 12,
1489 )
1490 .await?;
1491 let collect = PoolFeeProtocolCollect::new(
1493 Arc::clone(&chain),
1494 Arc::clone(&dex),
1495 instrument_id,
1496 pool_identifier,
1497 13,
1498 "0x00000000000000000000000000000000000000000000000000000000000000cd".to_string(),
1499 0,
1500 0,
1501 address!("0xc36442b4a4522e871399cd717abdd847ab11fe88"),
1502 address!("0xa61da382c18d9d5beb905ea192bae25e4c15d512"),
1503 111,
1504 222,
1505 ts,
1506 ts,
1507 );
1508 database
1509 .add_pool_fee_protocol_collect_batch(chain.chain_id, std::slice::from_ref(&collect))
1510 .await?;
1511
1512 let events_result = database
1513 .stream_pool_events(chain, dex, instrument_id, pool_identifier, None, Some(13))
1514 .try_collect::<Vec<_>>()
1515 .await;
1516
1517 drop(database);
1518 schema.cleanup().await?;
1519
1520 let events = events_result?;
1521 match events.as_slice() {
1522 [DexPoolData::Swap(swap), DexPoolData::FeeProtocolCollect(cp)] => {
1523 let observed = (swap.block, cp.block, cp.amount0, cp.amount1, cp.ts_event);
1526 if observed != (12, 13, 111, 222, ts) {
1527 anyhow::bail!("unexpected fee protocol collect round-trip: {observed:?}");
1528 }
1529
1530 if cp.sender != address!("0xc36442b4a4522e871399cd717abdd847ab11fe88")
1531 || cp.recipient != address!("0xa61da382c18d9d5beb905ea192bae25e4c15d512")
1532 {
1533 anyhow::bail!(
1534 "unexpected fee protocol collect addresses: sender={}, recipient={}",
1535 cp.sender,
1536 cp.recipient
1537 );
1538 }
1539 }
1540 other => anyhow::bail!("unexpected stream events: {other:?}"),
1541 }
1542 Ok(())
1543 }
1544
1545 #[tokio::test]
1546 #[expect(clippy::panic_in_result_fn)]
1547 async fn pre_upgrade_pool_event_cache_backfills_protocol_families_idempotently()
1548 -> anyhow::Result<()> {
1549 let Some(mut clean) = pool_event_sync_fixture().await? else {
1550 return Ok(());
1551 };
1552 let Some(mut migrated) = pool_event_sync_fixture().await? else {
1553 clean.cache.database = None;
1554 clean.schema.cleanup().await?;
1555 return Ok(());
1556 };
1557 let target_block = 13;
1558 let event_families = [
1559 "swap",
1560 "mint",
1561 "burn",
1562 "collect",
1563 "initialize",
1564 "flash",
1565 "fee_protocol_update",
1566 "fee_protocol_collect",
1567 ];
1568 let blocks = [
1569 test_block(11, UnixNanos::from(1_700_000_011_000_000_000)),
1570 test_block(12, UnixNanos::from(1_700_000_012_000_000_000)),
1571 test_block(13, UnixNanos::from(1_700_000_013_000_000_000)),
1572 ];
1573
1574 for fixture in [&mut clean, &mut migrated] {
1575 fixture
1576 .database()
1577 .add_pool_event_blocks_batch(fixture.chain.chain_id, &blocks)
1578 .await?;
1579 insert_pool_swap_event(
1580 &fixture.schema.admin_pool,
1581 &fixture.schema.name,
1582 fixture.chain.chain_id,
1583 &fixture.pool.pool_identifier,
1584 11,
1585 )
1586 .await?;
1587 }
1588
1589 let (mut update, mut collect) = protocol_fee_events(&clean);
1590 clean
1591 .database()
1592 .add_pool_fee_protocol_updates_batch(
1593 clean.chain.chain_id,
1594 std::slice::from_ref(&update),
1595 )
1596 .await?;
1597 clean
1598 .database()
1599 .add_pool_fee_protocol_collect_batch(
1600 clean.chain.chain_id,
1601 std::slice::from_ref(&collect),
1602 )
1603 .await?;
1604 clean
1605 .cache
1606 .update_pool_event_sync(
1607 &DexType::UniswapV3,
1608 &clean.pool.pool_identifier,
1609 &event_families,
1610 target_block,
1611 Some(1),
1612 )
1613 .await?;
1614
1615 migrated
1616 .cache
1617 .update_pool_last_synced_block(
1618 &DexType::UniswapV3,
1619 &migrated.pool.pool_identifier,
1620 target_block,
1621 )
1622 .await?;
1623 let pre_upgrade_state = migrated
1624 .cache
1625 .get_pool_event_sync_state(&DexType::UniswapV3, &migrated.pool.pool_identifier)
1626 .await?;
1627
1628 migrated
1629 .database()
1630 .add_pool_fee_protocol_updates_batch(
1631 migrated.chain.chain_id,
1632 std::slice::from_ref(&update),
1633 )
1634 .await?;
1635 migrated
1636 .cache
1637 .update_pool_event_sync(
1638 &DexType::UniswapV3,
1639 &migrated.pool.pool_identifier,
1640 &["fee_protocol_update"],
1641 12,
1642 None,
1643 )
1644 .await?;
1645 let interrupted_state = migrated
1646 .cache
1647 .get_pool_event_sync_state(&DexType::UniswapV3, &migrated.pool.pool_identifier)
1648 .await?;
1649
1650 migrated
1651 .database()
1652 .add_pool_fee_protocol_updates_batch(
1653 migrated.chain.chain_id,
1654 std::slice::from_ref(&update),
1655 )
1656 .await?;
1657 migrated
1658 .database()
1659 .add_pool_fee_protocol_collect_batch(
1660 migrated.chain.chain_id,
1661 std::slice::from_ref(&collect),
1662 )
1663 .await?;
1664 migrated
1665 .cache
1666 .update_pool_event_sync(
1667 &DexType::UniswapV3,
1668 &migrated.pool.pool_identifier,
1669 &["fee_protocol_update", "fee_protocol_collect"],
1670 target_block,
1671 None,
1672 )
1673 .await?;
1674 migrated
1675 .cache
1676 .update_pool_event_sync(
1677 &DexType::UniswapV3,
1678 &migrated.pool.pool_identifier,
1679 &event_families,
1680 target_block,
1681 Some(1),
1682 )
1683 .await?;
1684
1685 let final_state = migrated
1686 .cache
1687 .get_pool_event_sync_state(&DexType::UniswapV3, &migrated.pool.pool_identifier)
1688 .await?;
1689 let clean_events = clean.events(target_block).await?;
1690 let migrated_events = migrated.events(target_block).await?;
1691 let mut swap = expected_swap(&clean);
1692 swap.block_hash = Some(blocks[0].hash.clone());
1693 update.block_hash = Some(blocks[1].hash.clone());
1694 collect.block_hash = Some(blocks[2].hash.clone());
1695 let expected_events = vec![
1696 DexPoolData::Swap(swap),
1697 DexPoolData::FeeProtocolUpdate(update),
1698 DexPoolData::FeeProtocolCollect(collect),
1699 ];
1700
1701 clean.cache.database = None;
1702 migrated.cache.database = None;
1703 clean.schema.cleanup().await?;
1704 migrated.schema.cleanup().await?;
1705
1706 assert_eq!(
1707 pre_upgrade_state,
1708 PoolEventSyncState {
1709 version: 0,
1710 last_full_sync_block: Some(target_block),
1711 family_blocks: Vec::new(),
1712 }
1713 );
1714 assert_eq!(
1715 interrupted_state,
1716 PoolEventSyncState {
1717 version: 0,
1718 last_full_sync_block: Some(target_block),
1719 family_blocks: vec![("fee_protocol_update".to_string(), 12)],
1720 }
1721 );
1722 assert_eq!(
1723 final_state,
1724 PoolEventSyncState {
1725 version: 1,
1726 last_full_sync_block: Some(target_block),
1727 family_blocks: vec![
1728 ("burn".to_string(), target_block),
1729 ("collect".to_string(), target_block),
1730 ("fee_protocol_collect".to_string(), target_block),
1731 ("fee_protocol_update".to_string(), target_block),
1732 ("flash".to_string(), target_block),
1733 ("initialize".to_string(), target_block),
1734 ("mint".to_string(), target_block),
1735 ("swap".to_string(), target_block),
1736 ],
1737 }
1738 );
1739 assert_eq!(clean_events, expected_events);
1740 assert_eq!(migrated_events, clean_events);
1741 Ok(())
1742 }
1743
1744 #[tokio::test]
1745 async fn load_block_timestamps_prefers_full_block_over_pool_event_block() -> anyhow::Result<()>
1746 {
1747 let Some((database, schema)) = connect_cache_test_database().await? else {
1748 return Ok(());
1749 };
1750 let chain = arbitrum();
1751 let fallback_ts = UnixNanos::from(1_700_000_000_000_000_000);
1752 let pool_event_ts = UnixNanos::from(1_700_000_002_000_000_000);
1753 let full_block_ts = UnixNanos::from(1_700_000_001_000_000_000);
1754
1755 database
1756 .add_pool_event_blocks_batch(
1757 chain.chain_id,
1758 &[test_block(20, fallback_ts), test_block(21, pool_event_ts)],
1759 )
1760 .await?;
1761 database
1762 .add_block(chain.chain_id, &test_block(21, full_block_ts))
1763 .await?;
1764
1765 let rows_result = database.load_block_timestamps(chain, 20).await;
1766
1767 drop(database);
1768 schema.cleanup().await?;
1769
1770 let rows = rows_result?;
1771 let observed = rows
1772 .into_iter()
1773 .map(|row| (row.number, row.timestamp))
1774 .collect::<Vec<_>>();
1775
1776 let expected = vec![(20, fallback_ts), (21, full_block_ts)];
1777 if observed != expected {
1778 anyhow::bail!(
1779 "unexpected block timestamps: expected {expected:?}, observed {observed:?}"
1780 );
1781 }
1782 Ok(())
1783 }
1784
1785 #[tokio::test]
1786 async fn load_block_timestamps_uses_pool_event_block_when_full_block_timestamp_is_null()
1787 -> anyhow::Result<()> {
1788 let Some((database, schema)) = connect_cache_test_database().await? else {
1789 return Ok(());
1790 };
1791 let chain = arbitrum();
1792 let fallback_ts = UnixNanos::from(1_700_000_004_000_000_000);
1793
1794 database
1795 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(22, fallback_ts)])
1796 .await?;
1797 insert_block_without_timestamp(&schema.admin_pool, &schema.name, chain.chain_id, 22)
1798 .await?;
1799
1800 let rows_result = database.load_block_timestamps(chain, 22).await;
1801
1802 drop(database);
1803 schema.cleanup().await?;
1804
1805 let rows = rows_result?;
1806 let observed = rows
1807 .into_iter()
1808 .map(|row| (row.number, row.timestamp))
1809 .collect::<Vec<_>>();
1810
1811 let expected = vec![(22, fallback_ts)];
1812 if observed != expected {
1813 anyhow::bail!(
1814 "unexpected block timestamps: expected {expected:?}, observed {observed:?}"
1815 );
1816 }
1817 Ok(())
1818 }
1819
1820 #[tokio::test]
1821 async fn load_pools_sets_pool_timestamps_from_pool_event_block() -> anyhow::Result<()> {
1822 let Some((database, schema)) = connect_cache_test_database().await? else {
1823 return Ok(());
1824 };
1825 let chain = arbitrum();
1826 let dex = uniswap_v3(&chain);
1827 let token0 = weth(&chain);
1828 let token1 = usdc(&chain);
1829 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1830 let pool_identifier = PoolIdentifier::from_address(pool_address);
1831 let creation_block = 30;
1832 let creation_ts = UnixNanos::from(1_700_000_003_000_000_000);
1833
1834 let pool = Pool::new(
1835 Arc::clone(&chain),
1836 Arc::clone(&dex),
1837 pool_address,
1838 pool_identifier,
1839 creation_block,
1840 token0.clone(),
1841 token1.clone(),
1842 Some(500),
1843 Some(10),
1844 UnixNanos::default(),
1845 );
1846
1847 let mut cache = BlockchainCache::new(Arc::clone(&chain));
1848 cache.database = Some(database);
1849
1850 cache.add_dex(dex).await?;
1851 cache.add_token(token0).await?;
1852 cache.add_token(token1).await?;
1853 cache.add_pool(pool).await?;
1854 let Some(database) = cache.database.as_ref() else {
1855 anyhow::bail!("cache database must be set");
1856 };
1857 database
1858 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(creation_block, creation_ts)])
1859 .await?;
1860
1861 let pools_result = cache.load_pools(&DexType::UniswapV3).await;
1862
1863 cache.database = None;
1864 schema.cleanup().await?;
1865
1866 let pools = pools_result?;
1867 let observed_timestamps = pools
1868 .first()
1869 .map(|pool| (pool.ts_event, pool.ts_init, pools.len()));
1870
1871 let expected_timestamps = Some((creation_ts, creation_ts, 1));
1872 if observed_timestamps != expected_timestamps {
1873 anyhow::bail!(
1874 "unexpected pool timestamps: expected {expected_timestamps:?}, observed {observed_timestamps:?}"
1875 );
1876 }
1877 Ok(())
1878 }
1879
1880 #[tokio::test]
1881 async fn load_latest_pool_snapshot_filters_by_validation_state() -> anyhow::Result<()> {
1882 let Some((database, schema)) = connect_cache_test_database().await? else {
1883 return Ok(());
1884 };
1885 let chain = arbitrum();
1886 let dex = uniswap_v3(&chain);
1887 let token0 = weth(&chain);
1888 let token1 = usdc(&chain);
1889 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
1890 let pool_identifier = PoolIdentifier::from_address(pool_address);
1891
1892 let pool = Pool::new(
1893 Arc::clone(&chain),
1894 Arc::clone(&dex),
1895 pool_address,
1896 pool_identifier,
1897 10, token0.clone(),
1899 token1.clone(),
1900 Some(500),
1901 Some(10),
1902 UnixNanos::default(),
1903 );
1904 let instrument_id = pool.instrument_id;
1905 let mut cache = BlockchainCache::new(Arc::clone(&chain));
1906 cache.database = Some(database);
1907 cache.add_dex(dex).await?;
1908 cache.add_token(token0).await?;
1909 cache.add_token(token1).await?;
1910 cache.add_pool(pool).await?;
1911
1912 let ts = UnixNanos::from(1_700_000_000_000_000_000);
1913 let database = cache.database.as_ref().expect("cache database must be set");
1914 database
1915 .add_pool_event_blocks_batch(
1916 chain.chain_id,
1917 &[
1918 test_block(100, ts),
1919 test_block(150, ts),
1920 test_block(200, ts),
1921 ],
1922 )
1923 .await?;
1924
1925 for (block, state) in [
1927 (100u64, "replay"),
1928 (150u64, "on_chain"),
1929 (200u64, "invalid"),
1930 ] {
1931 let snapshot = PoolSnapshot::new(
1932 instrument_id,
1933 PoolState::default(),
1934 Vec::new(),
1935 Vec::new(),
1936 PoolAnalytics::default(),
1937 BlockPosition::new(block, "0xabc".to_string(), 0, 0),
1938 ts,
1939 ts,
1940 );
1941 cache
1942 .add_pool_snapshot(&DexType::UniswapV3, &pool_identifier, &snapshot)
1943 .await?;
1944
1945 if state != "replay" {
1946 database
1947 .set_pool_snapshot_validation_state(
1948 chain.chain_id,
1949 &pool_identifier,
1950 block,
1951 0,
1952 0,
1953 state,
1954 )
1955 .await?;
1956 }
1957 }
1958
1959 let latest_valid = database
1960 .load_latest_pool_snapshot(chain.chain_id, &pool_identifier, None, true)
1961 .await;
1962 let latest_any = database
1963 .load_latest_pool_snapshot(chain.chain_id, &pool_identifier, None, false)
1964 .await;
1965 let stored_invalid = database
1966 .get_pool_snapshot_validation_state(chain.chain_id, &pool_identifier, 200, 0, 0)
1967 .await;
1968 let stored_on_chain = database
1969 .get_pool_snapshot_validation_state(chain.chain_id, &pool_identifier, 150, 0, 0)
1970 .await;
1971
1972 cache.database = None;
1973 schema.cleanup().await?;
1974
1975 let latest_valid_metadata = latest_valid?.map(|snapshot| {
1976 (
1977 snapshot.block_position.number,
1978 snapshot.block_position.block_hash,
1979 )
1980 });
1981 let latest_any_block = latest_any?.map(|s| s.block_position.number);
1982 let stored_invalid = stored_invalid?;
1983 let stored_on_chain = stored_on_chain?;
1984
1985 if latest_valid_metadata != Some((150, Some(format!("0x{:064x}", 150))))
1989 || latest_any_block != Some(200)
1990 || stored_invalid.as_deref() != Some("invalid")
1991 || stored_on_chain.as_deref() != Some("on_chain")
1992 {
1993 anyhow::bail!(
1994 "unexpected load filter result: latest_valid_metadata={latest_valid_metadata:?}, latest_any_block={latest_any_block:?}, stored_invalid={stored_invalid:?}, stored_on_chain={stored_on_chain:?}"
1995 );
1996 }
1997
1998 Ok(())
1999 }
2000
2001 #[tokio::test]
2002 async fn add_pool_snapshot_upserts_existing_snapshot() -> anyhow::Result<()> {
2003 let Some((database, schema)) = connect_cache_test_database().await? else {
2004 return Ok(());
2005 };
2006 let chain = arbitrum();
2007 let dex = pancakeswap_v3(&chain);
2008 let token0 = weth(&chain);
2009 let token1 = usdc(&chain);
2010 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
2011 let pool_identifier = PoolIdentifier::from_address(pool_address);
2012
2013 let pool = Pool::new(
2014 Arc::clone(&chain),
2015 Arc::clone(&dex),
2016 pool_address,
2017 pool_identifier,
2018 10,
2019 token0.clone(),
2020 token1.clone(),
2021 Some(500),
2022 Some(10),
2023 UnixNanos::default(),
2024 );
2025 let instrument_id = pool.instrument_id;
2026 let mut cache = BlockchainCache::new(Arc::clone(&chain));
2027 cache.database = Some(database);
2028 cache.add_dex(dex).await?;
2029 cache.add_token(token0).await?;
2030 cache.add_token(token1).await?;
2031 cache.add_pool(pool).await?;
2032
2033 let ts = UnixNanos::from(1_700_000_000_000_000_000);
2034 let database = cache.database.as_ref().expect("cache database must be set");
2035 database
2036 .add_pool_event_blocks_batch(chain.chain_id, &[test_block(100, ts)])
2037 .await?;
2038
2039 let mut state = PoolState::default();
2040 state.set_protocol_fee_basis_points(3_200, 4_000);
2041 let owner = address!("0000000000000000000000000000000000000001");
2042 let mut snapshot = PoolSnapshot::new(
2043 instrument_id,
2044 state,
2045 vec![PoolPosition::new(owner, -10, 10, 100)],
2046 vec![PoolTick::new(
2047 -10,
2048 100,
2049 100,
2050 U256::ZERO,
2051 U256::ZERO,
2052 true,
2053 100,
2054 )],
2055 PoolAnalytics::default(),
2056 BlockPosition::new(100, "0xabc".to_string(), 0, 0),
2057 ts,
2058 ts,
2059 );
2060 cache
2061 .add_pool_snapshot(&DexType::PancakeSwapV3, &pool_identifier, &snapshot)
2062 .await?;
2063 database
2064 .set_pool_snapshot_validation_state(
2065 chain.chain_id,
2066 &pool_identifier,
2067 100,
2068 0,
2069 0,
2070 "on_chain",
2071 )
2072 .await?;
2073
2074 snapshot.state.set_protocol_fee_basis_points(3_300, 4_100);
2075 snapshot.positions[0].liquidity = 200;
2076 snapshot.ticks[0].liquidity_gross = 200;
2077 snapshot.ticks[0].liquidity_net = 200;
2078 cache
2079 .add_pool_snapshot(&DexType::PancakeSwapV3, &pool_identifier, &snapshot)
2080 .await?;
2081
2082 let loaded = database
2083 .load_latest_pool_snapshot(chain.chain_id, &pool_identifier, None, false)
2084 .await?;
2085 let validation = database
2086 .get_pool_snapshot_validation_state(chain.chain_id, &pool_identifier, 100, 0, 0)
2087 .await?;
2088
2089 cache.database = None;
2090 schema.cleanup().await?;
2091
2092 let Some(loaded) = loaded else {
2093 anyhow::bail!("expected snapshot to load");
2094 };
2095 let observed = (
2096 loaded.state.fee_protocol,
2097 loaded.state.fee_protocol0_basis_points,
2098 loaded.state.fee_protocol1_basis_points,
2099 loaded.positions[0].liquidity,
2100 loaded.ticks[0].liquidity_gross,
2101 loaded.ticks[0].liquidity_net,
2102 validation,
2103 );
2104
2105 if observed
2106 != (
2107 0,
2108 Some(3_300),
2109 Some(4_100),
2110 200,
2111 200,
2112 200,
2113 Some("on_chain".to_string()),
2114 )
2115 {
2116 anyhow::bail!("unexpected upserted snapshot state: {observed:?}");
2117 }
2118 Ok(())
2119 }
2120
2121 #[tokio::test]
2122 async fn load_pool_loads_only_the_requested_pool() -> anyhow::Result<()> {
2123 let Some((database, schema)) = connect_cache_test_database().await? else {
2124 return Ok(());
2125 };
2126 let chain = arbitrum();
2127 let dex = uniswap_v3(&chain);
2128 let token0 = weth(&chain);
2129 let token1 = usdc(&chain);
2130 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
2131 let pool_identifier = PoolIdentifier::from_address(pool_address);
2132 let absent_identifier =
2133 PoolIdentifier::from_address(address!("0x1111111111111111111111111111111111111111"));
2134
2135 let pool = Pool::new(
2136 Arc::clone(&chain),
2137 Arc::clone(&dex),
2138 pool_address,
2139 pool_identifier,
2140 30,
2141 token0.clone(),
2142 token1.clone(),
2143 Some(500),
2144 Some(10),
2145 UnixNanos::default(),
2146 );
2147 let mut cache = BlockchainCache::new(Arc::clone(&chain));
2148 cache.database = Some(database);
2149 cache.add_dex(dex).await?;
2150 cache.add_token(token0).await?;
2151 cache.add_token(token1).await?;
2152 cache.add_pool(pool).await?;
2153
2154 cache.pools.clear();
2157 let loaded = cache.load_pool(&DexType::UniswapV3, &pool_identifier).await;
2158 let cached_after_load = cache.get_pool(&pool_identifier).is_some();
2159 let absent = cache
2160 .load_pool(&DexType::UniswapV3, &absent_identifier)
2161 .await;
2162
2163 cache.database = None;
2164 schema.cleanup().await?;
2165
2166 let loaded_id = loaded?.map(|pool| pool.pool_identifier);
2167 let absent_is_some = absent?.is_some();
2168
2169 if loaded_id != Some(pool_identifier) || !cached_after_load || absent_is_some {
2170 anyhow::bail!(
2171 "unexpected load_pool result: loaded_id={loaded_id:?}, cached_after_load={cached_after_load}, absent_is_some={absent_is_some}"
2172 );
2173 }
2174
2175 Ok(())
2176 }
2177
2178 #[tokio::test]
2182 async fn check_snapshot_validity_reports_stored_verdict_when_rpc_unreachable()
2183 -> anyhow::Result<()> {
2184 if std::env::var("ENVIO_API_TOKEN").is_err() {
2188 return Ok(());
2189 }
2190 let Some((database, schema)) = connect_cache_test_database().await? else {
2191 return Ok(());
2192 };
2193 let chain = arbitrum();
2194 let dex = uniswap_v3(&chain);
2195 let token0 = weth(&chain);
2196 let token1 = usdc(&chain);
2197 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
2198 let pool_identifier = PoolIdentifier::from_address(pool_address);
2199 let pool = Pool::new(
2200 Arc::clone(&chain),
2201 Arc::clone(&dex),
2202 pool_address,
2203 pool_identifier,
2204 10,
2205 token0.clone(),
2206 token1.clone(),
2207 Some(500),
2208 Some(10),
2209 UnixNanos::default(),
2210 );
2211 let instrument_id = pool.instrument_id;
2212
2213 let config = BlockchainDataClientConfig::builder()
2216 .chain(Arc::clone(&chain))
2217 .dex_ids(vec![DexType::UniswapV3])
2218 .http_rpc_url("http://127.0.0.1:9".into())
2219 .use_hypersync_for_live_data(true)
2220 .build();
2221 let mut core = BlockchainDataClientCore::new(config, None, None, CancellationToken::new());
2222 core.cache.database = Some(database);
2223 core.cache.add_dex(dex).await?;
2224 core.cache.add_token(token0).await?;
2225 core.cache.add_token(token1).await?;
2226 core.cache.add_pool(pool.clone()).await?;
2227
2228 let ts = UnixNanos::from(1_700_000_000_000_000_000);
2230 let block_position = BlockPosition::new(200, "0xabc".to_string(), 0, 0);
2231 let snapshot = PoolSnapshot::new(
2232 instrument_id,
2233 PoolState::default(),
2234 Vec::new(),
2235 Vec::new(),
2236 PoolAnalytics::default(),
2237 block_position.clone(),
2238 ts,
2239 ts,
2240 );
2241 core.cache
2242 .add_pool_snapshot(&DexType::UniswapV3, &pool_identifier, &snapshot)
2243 .await?;
2244 core.cache
2245 .database
2246 .as_ref()
2247 .expect("cache database must be set")
2248 .set_pool_snapshot_validation_state(
2249 chain.chain_id,
2250 &pool_identifier,
2251 200,
2252 0,
2253 0,
2254 "invalid",
2255 )
2256 .await?;
2257 core.cache
2258 .add_pool_snapshot(&DexType::UniswapV3, &pool_identifier, &snapshot)
2259 .await?;
2260
2261 let mut profiler = PoolProfiler::new(Arc::new(pool));
2262 profiler
2263 .initialize(U160::from_str_radix("3cb0adde486484998be0b", 16).unwrap())
2264 .expect("profiler should initialize from a known sqrt price");
2265 profiler.last_processed_event = Some(block_position);
2266 profiler.last_processed_ts = Some(ts);
2267
2268 let reported = core.check_snapshot_validity(&profiler, false).await;
2269 let stored_after = core
2270 .cache
2271 .database
2272 .as_ref()
2273 .expect("cache database must be set")
2274 .get_pool_snapshot_validation_state(chain.chain_id, &pool_identifier, 200, 0, 0)
2275 .await;
2276
2277 core.cache.database = None;
2278 schema.cleanup().await?;
2279
2280 let reported = reported?;
2284 let stored_after = stored_after?;
2285 if reported != SnapshotValidation::Invalid || stored_after.as_deref() != Some("invalid") {
2286 anyhow::bail!(
2287 "unexpected validity result: reported={reported:?}, stored_after={stored_after:?}"
2288 );
2289 }
2290
2291 Ok(())
2292 }
2293
2294 struct PoolEventSyncFixture {
2295 cache: BlockchainCache,
2296 schema: TestSchema,
2297 chain: SharedChain,
2298 dex: SharedDex,
2299 pool: Pool,
2300 }
2301
2302 impl PoolEventSyncFixture {
2303 fn database(&self) -> &BlockchainCacheDatabase {
2304 self.cache
2305 .database
2306 .as_ref()
2307 .expect("cache database must be set")
2308 }
2309
2310 async fn events(&self, to_block: u64) -> anyhow::Result<Vec<DexPoolData>> {
2311 self.database()
2312 .stream_pool_events(
2313 Arc::clone(&self.chain),
2314 Arc::clone(&self.dex),
2315 self.pool.instrument_id,
2316 self.pool.pool_identifier,
2317 None,
2318 Some(to_block),
2319 )
2320 .try_collect()
2321 .await
2322 }
2323 }
2324
2325 async fn pool_event_sync_fixture() -> anyhow::Result<Option<PoolEventSyncFixture>> {
2326 let Some((database, schema)) = connect_cache_test_database().await? else {
2327 return Ok(None);
2328 };
2329 let chain = arbitrum();
2330 let dex = uniswap_v3(&chain);
2331 let token0 = weth(&chain);
2332 let token1 = usdc(&chain);
2333 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
2334 let pool = Pool::new(
2335 Arc::clone(&chain),
2336 Arc::clone(&dex),
2337 pool_address,
2338 PoolIdentifier::from_address(pool_address),
2339 10,
2340 token0.clone(),
2341 token1.clone(),
2342 Some(500),
2343 Some(10),
2344 UnixNanos::default(),
2345 );
2346 let mut cache = BlockchainCache::new(Arc::clone(&chain));
2347 cache.database = Some(database);
2348 cache.add_dex(Arc::clone(&dex)).await?;
2349 cache.add_token(token0).await?;
2350 cache.add_token(token1).await?;
2351 cache.add_pool(pool.clone()).await?;
2352
2353 Ok(Some(PoolEventSyncFixture {
2354 cache,
2355 schema,
2356 chain,
2357 dex,
2358 pool,
2359 }))
2360 }
2361
2362 fn protocol_fee_events(
2363 fixture: &PoolEventSyncFixture,
2364 ) -> (PoolFeeProtocolUpdate, PoolFeeProtocolCollect) {
2365 let update = PoolFeeProtocolUpdate::new(
2366 Arc::clone(&fixture.chain),
2367 Arc::clone(&fixture.dex),
2368 fixture.pool.instrument_id,
2369 fixture.pool.pool_identifier,
2370 12,
2371 "0x00000000000000000000000000000000000000000000000000000000000000ab".to_string(),
2372 1,
2373 1,
2374 4,
2375 6,
2376 UnixNanos::from(1_700_000_012_000_000_000),
2377 UnixNanos::from(1_700_000_012_000_000_000),
2378 );
2379 let collect = PoolFeeProtocolCollect::new(
2380 Arc::clone(&fixture.chain),
2381 Arc::clone(&fixture.dex),
2382 fixture.pool.instrument_id,
2383 fixture.pool.pool_identifier,
2384 13,
2385 "0x00000000000000000000000000000000000000000000000000000000000000cd".to_string(),
2386 2,
2387 2,
2388 address!("0xc36442b4a4522e871399cd717abdd847ab11fe88"),
2389 address!("0xa61da382c18d9d5beb905ea192bae25e4c15d512"),
2390 111,
2391 222,
2392 UnixNanos::from(1_700_000_013_000_000_000),
2393 UnixNanos::from(1_700_000_013_000_000_000),
2394 );
2395 (update, collect)
2396 }
2397
2398 fn expected_swap(fixture: &PoolEventSyncFixture) -> PoolSwap {
2399 PoolSwap::new(
2400 Arc::clone(&fixture.chain),
2401 Arc::clone(&fixture.dex),
2402 fixture.pool.instrument_id,
2403 fixture.pool.pool_identifier,
2404 11,
2405 "0x000000000000000000000000000000000000000000000000000000000000000c".to_string(),
2406 0,
2407 0,
2408 UnixNanos::from(1_700_000_011_000_000_000),
2409 UnixNanos::from(1_700_000_011_000_000_000),
2410 address!("0x1111111111111111111111111111111111111111"),
2411 address!("0x2222222222222222222222222222222222222222"),
2412 I256::try_from(-1_000_000_000_000_000_000_i128).unwrap(),
2413 I256::try_from(2_000_000_i128).unwrap(),
2414 U160::from(79_228_162_514_264_337_593_543_950_336_u128),
2415 1_000_000,
2416 0,
2417 )
2418 }
2419
2420 async fn connect_cache_test_database()
2421 -> anyhow::Result<Option<(BlockchainCacheDatabase, TestSchema)>> {
2422 let config = get_postgres_connect_options(None, None, None, None, None);
2423 let mut connect_options: PgConnectOptions = config.clone().into();
2424 let Some(mut admin_pool) =
2425 connect_cache_test_pool(connect_options.clone(), &config.username).await
2426 else {
2427 return Ok(None);
2428 };
2429 let schema_name = cache_test_schema_name();
2430
2431 if let Err(e) = create_cache_test_schema(&admin_pool, &schema_name).await {
2432 if !is_database_create_permission_denied(&e) || config.username == "postgres" {
2433 return Err(e);
2434 }
2435
2436 eprintln!(
2437 "Postgres role {} cannot create isolated blockchain cache test schema; retrying with postgres role: {e}",
2438 config.username
2439 );
2440 admin_pool.close().await;
2441 connect_options = postgres_test_connect_options(&config);
2442 let Some(postgres_pool) =
2443 connect_cache_test_pool(connect_options.clone(), "postgres").await
2444 else {
2445 return Err(e);
2446 };
2447 admin_pool = postgres_pool;
2448 create_cache_test_schema(&admin_pool, &schema_name).await?;
2449 }
2450
2451 let database = crate::cache::database::tests::connect_test_database(
2452 connect_options.options([("search_path", format!("{schema_name},public"))]),
2453 )
2454 .await?;
2455
2456 Ok(Some((
2457 database,
2458 TestSchema {
2459 admin_pool,
2460 name: schema_name,
2461 },
2462 )))
2463 }
2464
2465 async fn connect_cache_test_pool(
2466 connect_options: PgConnectOptions,
2467 username: &str,
2468 ) -> Option<PgPool> {
2469 match PgPoolOptions::new()
2470 .max_connections(1)
2471 .connect_with(connect_options)
2472 .await
2473 {
2474 Ok(pool) => Some(pool),
2475 Err(e) => {
2476 eprintln!(
2477 "Postgres connection as {username} failed; skipping blockchain cache DB test: {e}"
2478 );
2479 None
2480 }
2481 }
2482 }
2483
2484 fn postgres_test_connect_options(config: &PostgresConnectOptions) -> PgConnectOptions {
2485 PostgresConnectOptions::new(
2486 config.host.clone(),
2487 config.port,
2488 String::from("postgres"),
2489 config.password.clone(),
2490 config.database.clone(),
2491 )
2492 .into()
2493 }
2494
2495 fn is_database_create_permission_denied(error: &anyhow::Error) -> bool {
2496 match error.downcast_ref::<SqlxError>() {
2497 Some(SqlxError::Database(database_error)) => {
2498 database_error
2499 .code()
2500 .is_some_and(|code| code.as_ref() == "42501")
2501 && database_error
2502 .message()
2503 .contains("permission denied for database")
2504 }
2505 _ => false,
2506 }
2507 }
2508
2509 struct TestSchema {
2510 admin_pool: PgPool,
2511 name: String,
2512 }
2513
2514 impl TestSchema {
2515 async fn cleanup(self) -> anyhow::Result<()> {
2516 drop_cache_test_schema(&self.admin_pool, &self.name).await?;
2517 self.admin_pool.close().await;
2518 Ok(())
2519 }
2520 }
2521
2522 #[expect(
2523 clippy::too_many_lines,
2524 reason = "test schema declares the narrow table set used by cache SQL"
2525 )]
2526 async fn create_cache_test_schema(pool: &PgPool, schema: &str) -> anyhow::Result<()> {
2527 execute_schema_statement(pool, format!("CREATE SCHEMA {schema}")).await?;
2528
2529 let statements = [
2530 format!("CREATE DOMAIN {schema}.U256 AS NUMERIC(78, 0)"),
2531 format!("CREATE DOMAIN {schema}.U160 AS NUMERIC(49, 0)"),
2532 format!("CREATE DOMAIN {schema}.U128 AS NUMERIC(39, 0)"),
2533 format!(
2534 r#"
2535 CREATE TABLE {schema}."chain" (
2536 chain_id INTEGER PRIMARY KEY,
2537 name TEXT NOT NULL
2538 )
2539 "#
2540 ),
2541 format!(
2542 r#"
2543 CREATE TABLE {schema}."block" (
2544 chain_id INTEGER NOT NULL,
2545 number BIGINT NOT NULL,
2546 hash TEXT,
2547 parent_hash TEXT,
2548 miner TEXT,
2549 gas_limit BIGINT,
2550 gas_used BIGINT,
2551 timestamp TEXT,
2552 base_fee_per_gas TEXT,
2553 blob_gas_used TEXT,
2554 excess_blob_gas TEXT,
2555 l1_gas_price TEXT,
2556 l1_gas_used BIGINT,
2557 l1_fee_scalar BIGINT,
2558 PRIMARY KEY (chain_id, number)
2559 )
2560 "#
2561 ),
2562 format!(
2563 r#"
2564 CREATE TABLE {schema}."pool_event_block" (
2565 chain_id INTEGER NOT NULL,
2566 number BIGINT NOT NULL,
2567 hash TEXT,
2568 timestamp TEXT NOT NULL,
2569 PRIMARY KEY (chain_id, number)
2570 )
2571 "#
2572 ),
2573 format!(
2574 r#"
2575 CREATE TABLE {schema}."token" (
2576 chain_id INTEGER NOT NULL,
2577 address TEXT NOT NULL,
2578 symbol TEXT,
2579 name TEXT,
2580 decimals INTEGER,
2581 error TEXT,
2582 PRIMARY KEY (chain_id, address)
2583 )
2584 "#
2585 ),
2586 format!(
2587 r#"
2588 CREATE TABLE {schema}."dex" (
2589 chain_id INTEGER NOT NULL,
2590 name TEXT NOT NULL,
2591 factory_address TEXT NOT NULL,
2592 creation_block BIGINT NOT NULL,
2593 last_full_sync_pools_block_number BIGINT,
2594 PRIMARY KEY (chain_id, name),
2595 UNIQUE (chain_id, factory_address)
2596 )
2597 "#
2598 ),
2599 format!(
2600 r#"
2601 CREATE TABLE {schema}."pool" (
2602 chain_id INTEGER NOT NULL,
2603 dex_name TEXT NOT NULL,
2604 address TEXT NOT NULL,
2605 pool_identifier TEXT NOT NULL,
2606 creation_block BIGINT NOT NULL,
2607 token0_chain INTEGER NOT NULL,
2608 token0_address TEXT NOT NULL,
2609 token1_chain INTEGER NOT NULL,
2610 token1_address TEXT NOT NULL,
2611 fee INTEGER,
2612 tick_spacing INTEGER,
2613 initial_tick INTEGER,
2614 initial_sqrt_price_x96 TEXT,
2615 hook_address TEXT,
2616 last_full_sync_block_number BIGINT,
2617 event_sync_version INTEGER NOT NULL DEFAULT 0,
2618 PRIMARY KEY (chain_id, dex_name, pool_identifier)
2619 )
2620 "#
2621 ),
2622 format!(
2623 r#"
2624 CREATE TABLE {schema}."pool_event_sync" (
2625 chain_id INTEGER NOT NULL,
2626 dex_name TEXT NOT NULL,
2627 pool_identifier TEXT NOT NULL,
2628 event_family TEXT NOT NULL,
2629 last_full_sync_block_number BIGINT NOT NULL,
2630 PRIMARY KEY (chain_id, dex_name, pool_identifier, event_family)
2631 )
2632 "#
2633 ),
2634 format!(
2635 r#"
2636 CREATE TABLE {schema}."pool_swap_event" (
2637 chain_id INTEGER NOT NULL,
2638 pool_identifier TEXT NOT NULL,
2639 dex_name TEXT NOT NULL,
2640 block BIGINT NOT NULL,
2641 transaction_hash TEXT NOT NULL,
2642 transaction_index INTEGER NOT NULL,
2643 log_index INTEGER NOT NULL,
2644 sender TEXT NOT NULL,
2645 recipient TEXT NOT NULL,
2646 sqrt_price_x96 TEXT NOT NULL,
2647 liquidity TEXT NOT NULL,
2648 tick INTEGER NOT NULL,
2649 amount0 TEXT NOT NULL,
2650 amount1 TEXT NOT NULL,
2651 order_side TEXT,
2652 base_quantity NUMERIC,
2653 quote_quantity NUMERIC,
2654 spot_price NUMERIC,
2655 execution_price NUMERIC,
2656 UNIQUE(chain_id, transaction_hash, log_index)
2657 )
2658 "#
2659 ),
2660 format!(
2661 r#"
2662 CREATE TABLE {schema}."pool_liquidity_event" (
2663 chain_id INTEGER NOT NULL,
2664 pool_identifier TEXT NOT NULL,
2665 dex_name TEXT NOT NULL,
2666 block BIGINT NOT NULL,
2667 transaction_hash TEXT NOT NULL,
2668 transaction_index INTEGER NOT NULL,
2669 log_index INTEGER NOT NULL,
2670 event_type TEXT NOT NULL,
2671 sender TEXT,
2672 owner TEXT NOT NULL,
2673 position_liquidity TEXT NOT NULL,
2674 amount0 TEXT NOT NULL,
2675 amount1 TEXT NOT NULL,
2676 tick_lower INTEGER NOT NULL,
2677 tick_upper INTEGER NOT NULL,
2678 UNIQUE(chain_id, transaction_hash, log_index)
2679 )
2680 "#
2681 ),
2682 format!(
2683 r#"
2684 CREATE TABLE {schema}."pool_collect_event" (
2685 chain_id INTEGER NOT NULL,
2686 pool_identifier TEXT NOT NULL,
2687 dex_name TEXT NOT NULL,
2688 block BIGINT NOT NULL,
2689 transaction_hash TEXT NOT NULL,
2690 transaction_index INTEGER NOT NULL,
2691 log_index INTEGER NOT NULL,
2692 owner TEXT NOT NULL,
2693 amount0 TEXT NOT NULL,
2694 amount1 TEXT NOT NULL,
2695 tick_lower INTEGER NOT NULL,
2696 tick_upper INTEGER NOT NULL,
2697 UNIQUE(chain_id, transaction_hash, log_index)
2698 )
2699 "#
2700 ),
2701 format!(
2702 r#"
2703 CREATE TABLE {schema}."pool_flash_event" (
2704 chain_id INTEGER NOT NULL,
2705 pool_identifier TEXT NOT NULL,
2706 dex_name TEXT NOT NULL,
2707 block BIGINT NOT NULL,
2708 transaction_hash TEXT NOT NULL,
2709 transaction_index INTEGER NOT NULL,
2710 log_index INTEGER NOT NULL,
2711 sender TEXT NOT NULL,
2712 recipient TEXT NOT NULL,
2713 amount0 TEXT NOT NULL,
2714 amount1 TEXT NOT NULL,
2715 paid0 TEXT NOT NULL,
2716 paid1 TEXT NOT NULL,
2717 UNIQUE(chain_id, transaction_hash, log_index)
2718 )
2719 "#
2720 ),
2721 format!(
2722 r#"
2723 CREATE TABLE {schema}."pool_fee_protocol_update_event" (
2724 chain_id INTEGER NOT NULL,
2725 pool_identifier TEXT NOT NULL,
2726 dex_name TEXT NOT NULL,
2727 block BIGINT NOT NULL,
2728 transaction_hash TEXT NOT NULL,
2729 transaction_index INTEGER NOT NULL,
2730 log_index INTEGER NOT NULL,
2731 fee_protocol0_new INTEGER NOT NULL,
2732 fee_protocol1_new INTEGER NOT NULL,
2733 UNIQUE(chain_id, transaction_hash, log_index)
2734 )
2735 "#
2736 ),
2737 format!(
2738 r#"
2739 CREATE TABLE {schema}."pool_fee_protocol_collect_event" (
2740 chain_id INTEGER NOT NULL,
2741 pool_identifier TEXT NOT NULL,
2742 dex_name TEXT NOT NULL,
2743 block BIGINT NOT NULL,
2744 transaction_hash TEXT NOT NULL,
2745 transaction_index INTEGER NOT NULL,
2746 log_index INTEGER NOT NULL,
2747 sender TEXT NOT NULL,
2748 recipient TEXT NOT NULL,
2749 amount0 TEXT NOT NULL,
2750 amount1 TEXT NOT NULL,
2751 UNIQUE(chain_id, transaction_hash, log_index)
2752 )
2753 "#
2754 ),
2755 format!(
2756 r#"
2757 CREATE TABLE {schema}."pool_snapshot" (
2758 chain_id INTEGER NOT NULL,
2759 pool_identifier TEXT NOT NULL,
2760 dex_name TEXT NOT NULL,
2761 block BIGINT NOT NULL,
2762 transaction_index INTEGER NOT NULL,
2763 log_index INTEGER NOT NULL,
2764 transaction_hash TEXT NOT NULL,
2765 current_tick INTEGER NOT NULL,
2766 price_sqrt_ratio_x96 NUMERIC NOT NULL,
2767 liquidity NUMERIC NOT NULL,
2768 protocol_fees_token0 NUMERIC NOT NULL,
2769 protocol_fees_token1 NUMERIC NOT NULL,
2770 fee_protocol SMALLINT NOT NULL,
2771 fee_protocol0_basis_points INTEGER,
2772 fee_protocol1_basis_points INTEGER,
2773 fee_growth_global_0 NUMERIC NOT NULL,
2774 fee_growth_global_1 NUMERIC NOT NULL,
2775 total_amount0_deposited NUMERIC NOT NULL,
2776 total_amount1_deposited NUMERIC NOT NULL,
2777 total_amount0_collected NUMERIC NOT NULL,
2778 total_amount1_collected NUMERIC NOT NULL,
2779 total_swaps INTEGER NOT NULL,
2780 total_mints INTEGER NOT NULL,
2781 total_burns INTEGER NOT NULL,
2782 total_fee_collects INTEGER NOT NULL,
2783 total_flashes INTEGER NOT NULL,
2784 liquidity_utilization_rate DOUBLE PRECISION,
2785 validation_state TEXT NOT NULL DEFAULT 'replay' CHECK (validation_state IN ('on_chain', 'replay', 'invalid')),
2786 PRIMARY KEY (chain_id, pool_identifier, block, transaction_index, log_index)
2787 )
2788 "#
2789 ),
2790 format!(
2791 r#"
2792 CREATE TABLE {schema}."pool_position" (
2793 chain_id INTEGER NOT NULL,
2794 pool_identifier TEXT NOT NULL,
2795 snapshot_block BIGINT NOT NULL,
2796 snapshot_transaction_index INTEGER NOT NULL,
2797 snapshot_log_index INTEGER NOT NULL,
2798 owner TEXT NOT NULL,
2799 tick_lower INTEGER NOT NULL,
2800 tick_upper INTEGER NOT NULL,
2801 liquidity NUMERIC NOT NULL,
2802 fee_growth_inside_0_last NUMERIC NOT NULL,
2803 fee_growth_inside_1_last NUMERIC NOT NULL,
2804 tokens_owed_0 NUMERIC NOT NULL,
2805 tokens_owed_1 NUMERIC NOT NULL,
2806 total_amount0_deposited NUMERIC,
2807 total_amount1_deposited NUMERIC,
2808 total_amount0_collected NUMERIC,
2809 total_amount1_collected NUMERIC,
2810 PRIMARY KEY (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, owner, tick_lower, tick_upper)
2811 )
2812 "#
2813 ),
2814 format!(
2815 r#"
2816 CREATE TABLE {schema}."pool_tick" (
2817 chain_id INTEGER NOT NULL,
2818 pool_identifier TEXT NOT NULL,
2819 snapshot_block BIGINT NOT NULL,
2820 snapshot_transaction_index INTEGER NOT NULL,
2821 snapshot_log_index INTEGER NOT NULL,
2822 tick_value INTEGER NOT NULL,
2823 liquidity_gross NUMERIC NOT NULL,
2824 liquidity_net NUMERIC NOT NULL,
2825 fee_growth_outside_0 NUMERIC NOT NULL,
2826 fee_growth_outside_1 NUMERIC NOT NULL,
2827 initialized BOOLEAN NOT NULL,
2828 last_updated_block BIGINT NOT NULL,
2829 PRIMARY KEY (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, tick_value)
2830 )
2831 "#
2832 ),
2833 ];
2834
2835 for statement in statements {
2836 execute_schema_statement(pool, statement).await?;
2837 }
2838
2839 Ok(())
2840 }
2841
2842 async fn insert_pool_swap_event(
2843 pool: &PgPool,
2844 schema: &str,
2845 chain_id: u32,
2846 pool_identifier: &PoolIdentifier,
2847 block: u64,
2848 ) -> anyhow::Result<()> {
2849 sqlx::query(AssertSqlSafe(format!(
2850 r#"
2851 INSERT INTO {schema}."pool_swap_event" (
2852 chain_id, pool_identifier, dex_name, block, transaction_hash, transaction_index,
2853 log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1
2854 )
2855 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
2856 "#
2857 )))
2858 .bind(chain_id as i32)
2859 .bind(pool_identifier.to_string())
2860 .bind(DexType::UniswapV3.to_string())
2861 .bind(block as i64)
2862 .bind("0x000000000000000000000000000000000000000000000000000000000000000c")
2863 .bind(0_i32)
2864 .bind(0_i32)
2865 .bind("0x1111111111111111111111111111111111111111")
2866 .bind("0x2222222222222222222222222222222222222222")
2867 .bind("79228162514264337593543950336")
2868 .bind("1000000")
2869 .bind(0_i32)
2870 .bind("-1000000000000000000")
2871 .bind("2000000")
2872 .execute(pool)
2873 .await?;
2874 Ok(())
2875 }
2876
2877 async fn insert_block_without_timestamp(
2878 pool: &PgPool,
2879 schema: &str,
2880 chain_id: u32,
2881 number: u64,
2882 ) -> anyhow::Result<()> {
2883 sqlx::query(AssertSqlSafe(format!(
2884 r#"
2885 INSERT INTO {schema}."block" (
2886 chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp
2887 )
2888 VALUES ($1, $2, $3, $4, $5, $6, $7, NULL)
2889 "#
2890 )))
2891 .bind(chain_id as i32)
2892 .bind(number as i64)
2893 .bind(format!("0x{number:064x}"))
2894 .bind("0x0")
2895 .bind("0x0000000000000000000000000000000000000000")
2896 .bind(30_000_000_i64)
2897 .bind(21_000_i64)
2898 .execute(pool)
2899 .await?;
2900 Ok(())
2901 }
2902
2903 async fn drop_cache_test_schema(pool: &PgPool, schema: &str) -> anyhow::Result<()> {
2904 execute_schema_statement(pool, format!("DROP SCHEMA IF EXISTS {schema} CASCADE")).await
2905 }
2906
2907 async fn execute_schema_statement(pool: &PgPool, statement: String) -> anyhow::Result<()> {
2908 sqlx::query(AssertSqlSafe(statement)).execute(pool).await?;
2909 Ok(())
2910 }
2911
2912 fn cache_test_schema_name() -> String {
2913 let nanos = SystemTime::now()
2914 .duration_since(UNIX_EPOCH)
2915 .expect("system clock must be after UNIX epoch")
2916 .as_nanos();
2917
2918 format!("nt_blockchain_cache_test_{}_{}", std::process::id(), nanos)
2919 }
2920
2921 fn arbitrum() -> SharedChain {
2922 let Some(chain) = Chain::from_chain_id(42161) else {
2923 panic!("Arbitrum chain must exist in model definitions");
2924 };
2925
2926 Arc::new(chain.clone())
2927 }
2928
2929 fn uniswap_v3(chain: &SharedChain) -> SharedDex {
2930 Arc::new(Dex::new(
2931 (**chain).clone(),
2932 DexType::UniswapV3,
2933 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
2934 0,
2935 AmmType::CLAMM,
2936 "PoolCreated",
2937 "Swap",
2938 "Mint",
2939 "Burn",
2940 "Collect",
2941 ))
2942 }
2943
2944 fn pancakeswap_v3(chain: &SharedChain) -> SharedDex {
2945 Arc::new(Dex::new(
2946 (**chain).clone(),
2947 DexType::PancakeSwapV3,
2948 "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865",
2949 0,
2950 AmmType::CLAMM,
2951 "PoolCreated",
2952 "Swap",
2953 "Mint",
2954 "Burn",
2955 "Collect",
2956 ))
2957 }
2958
2959 fn weth(chain: &SharedChain) -> Token {
2960 Token::new(
2961 Arc::clone(chain),
2962 address!("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
2963 "Wrapped Ether".to_string(),
2964 "WETH".to_string(),
2965 18,
2966 )
2967 }
2968
2969 fn usdc(chain: &SharedChain) -> Token {
2970 Token::new(
2971 Arc::clone(chain),
2972 address!("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"),
2973 "USD Coin".to_string(),
2974 "USDC".to_string(),
2975 6,
2976 )
2977 }
2978
2979 fn test_block(number: u64, timestamp: UnixNanos) -> Block {
2980 Block::new(
2981 format!("0x{number:064x}"),
2982 String::from("0x0"),
2983 number,
2984 Ustr::from("0x0000000000000000000000000000000000000000"),
2985 30_000_000,
2986 21_000,
2987 timestamp,
2988 Some(Blockchain::Arbitrum),
2989 )
2990 }
2991}