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 =
2222 BlockchainDataClientCore::new(config, None, None, CancellationToken::new()).unwrap();
2223 core.cache.database = Some(database);
2224 core.cache.add_dex(dex).await?;
2225 core.cache.add_token(token0).await?;
2226 core.cache.add_token(token1).await?;
2227 core.cache.add_pool(pool.clone()).await?;
2228
2229 let ts = UnixNanos::from(1_700_000_000_000_000_000);
2231 let block_position = BlockPosition::new(200, "0xabc".to_string(), 0, 0);
2232 let snapshot = PoolSnapshot::new(
2233 instrument_id,
2234 PoolState::default(),
2235 Vec::new(),
2236 Vec::new(),
2237 PoolAnalytics::default(),
2238 block_position.clone(),
2239 ts,
2240 ts,
2241 );
2242 core.cache
2243 .add_pool_snapshot(&DexType::UniswapV3, &pool_identifier, &snapshot)
2244 .await?;
2245 core.cache
2246 .database
2247 .as_ref()
2248 .expect("cache database must be set")
2249 .set_pool_snapshot_validation_state(
2250 chain.chain_id,
2251 &pool_identifier,
2252 200,
2253 0,
2254 0,
2255 "invalid",
2256 )
2257 .await?;
2258 core.cache
2259 .add_pool_snapshot(&DexType::UniswapV3, &pool_identifier, &snapshot)
2260 .await?;
2261
2262 let mut profiler = PoolProfiler::new(Arc::new(pool));
2263 profiler
2264 .initialize(U160::from_str_radix("3cb0adde486484998be0b", 16).unwrap())
2265 .expect("profiler should initialize from a known sqrt price");
2266 profiler.last_processed_event = Some(block_position);
2267 profiler.last_processed_ts = Some(ts);
2268
2269 let reported = core.check_snapshot_validity(&profiler, false).await;
2270 let stored_after = core
2271 .cache
2272 .database
2273 .as_ref()
2274 .expect("cache database must be set")
2275 .get_pool_snapshot_validation_state(chain.chain_id, &pool_identifier, 200, 0, 0)
2276 .await;
2277
2278 core.cache.database = None;
2279 schema.cleanup().await?;
2280
2281 let reported = reported?;
2285 let stored_after = stored_after?;
2286 if reported != SnapshotValidation::Invalid || stored_after.as_deref() != Some("invalid") {
2287 anyhow::bail!(
2288 "unexpected validity result: reported={reported:?}, stored_after={stored_after:?}"
2289 );
2290 }
2291
2292 Ok(())
2293 }
2294
2295 struct PoolEventSyncFixture {
2296 cache: BlockchainCache,
2297 schema: TestSchema,
2298 chain: SharedChain,
2299 dex: SharedDex,
2300 pool: Pool,
2301 }
2302
2303 impl PoolEventSyncFixture {
2304 fn database(&self) -> &BlockchainCacheDatabase {
2305 self.cache
2306 .database
2307 .as_ref()
2308 .expect("cache database must be set")
2309 }
2310
2311 async fn events(&self, to_block: u64) -> anyhow::Result<Vec<DexPoolData>> {
2312 self.database()
2313 .stream_pool_events(
2314 Arc::clone(&self.chain),
2315 Arc::clone(&self.dex),
2316 self.pool.instrument_id,
2317 self.pool.pool_identifier,
2318 None,
2319 Some(to_block),
2320 )
2321 .try_collect()
2322 .await
2323 }
2324 }
2325
2326 async fn pool_event_sync_fixture() -> anyhow::Result<Option<PoolEventSyncFixture>> {
2327 let Some((database, schema)) = connect_cache_test_database().await? else {
2328 return Ok(None);
2329 };
2330 let chain = arbitrum();
2331 let dex = uniswap_v3(&chain);
2332 let token0 = weth(&chain);
2333 let token1 = usdc(&chain);
2334 let pool_address = address!("0xd13040d4fe917EE704158CfCB3338dCd2838B245");
2335 let pool = Pool::new(
2336 Arc::clone(&chain),
2337 Arc::clone(&dex),
2338 pool_address,
2339 PoolIdentifier::from_address(pool_address),
2340 10,
2341 token0.clone(),
2342 token1.clone(),
2343 Some(500),
2344 Some(10),
2345 UnixNanos::default(),
2346 );
2347 let mut cache = BlockchainCache::new(Arc::clone(&chain));
2348 cache.database = Some(database);
2349 cache.add_dex(Arc::clone(&dex)).await?;
2350 cache.add_token(token0).await?;
2351 cache.add_token(token1).await?;
2352 cache.add_pool(pool.clone()).await?;
2353
2354 Ok(Some(PoolEventSyncFixture {
2355 cache,
2356 schema,
2357 chain,
2358 dex,
2359 pool,
2360 }))
2361 }
2362
2363 fn protocol_fee_events(
2364 fixture: &PoolEventSyncFixture,
2365 ) -> (PoolFeeProtocolUpdate, PoolFeeProtocolCollect) {
2366 let update = PoolFeeProtocolUpdate::new(
2367 Arc::clone(&fixture.chain),
2368 Arc::clone(&fixture.dex),
2369 fixture.pool.instrument_id,
2370 fixture.pool.pool_identifier,
2371 12,
2372 "0x00000000000000000000000000000000000000000000000000000000000000ab".to_string(),
2373 1,
2374 1,
2375 4,
2376 6,
2377 UnixNanos::from(1_700_000_012_000_000_000),
2378 UnixNanos::from(1_700_000_012_000_000_000),
2379 );
2380 let collect = PoolFeeProtocolCollect::new(
2381 Arc::clone(&fixture.chain),
2382 Arc::clone(&fixture.dex),
2383 fixture.pool.instrument_id,
2384 fixture.pool.pool_identifier,
2385 13,
2386 "0x00000000000000000000000000000000000000000000000000000000000000cd".to_string(),
2387 2,
2388 2,
2389 address!("0xc36442b4a4522e871399cd717abdd847ab11fe88"),
2390 address!("0xa61da382c18d9d5beb905ea192bae25e4c15d512"),
2391 111,
2392 222,
2393 UnixNanos::from(1_700_000_013_000_000_000),
2394 UnixNanos::from(1_700_000_013_000_000_000),
2395 );
2396 (update, collect)
2397 }
2398
2399 fn expected_swap(fixture: &PoolEventSyncFixture) -> PoolSwap {
2400 PoolSwap::new(
2401 Arc::clone(&fixture.chain),
2402 Arc::clone(&fixture.dex),
2403 fixture.pool.instrument_id,
2404 fixture.pool.pool_identifier,
2405 11,
2406 "0x000000000000000000000000000000000000000000000000000000000000000c".to_string(),
2407 0,
2408 0,
2409 UnixNanos::from(1_700_000_011_000_000_000),
2410 UnixNanos::from(1_700_000_011_000_000_000),
2411 address!("0x1111111111111111111111111111111111111111"),
2412 address!("0x2222222222222222222222222222222222222222"),
2413 I256::try_from(-1_000_000_000_000_000_000_i128).unwrap(),
2414 I256::try_from(2_000_000_i128).unwrap(),
2415 U160::from(79_228_162_514_264_337_593_543_950_336_u128),
2416 1_000_000,
2417 0,
2418 )
2419 }
2420
2421 async fn connect_cache_test_database()
2422 -> anyhow::Result<Option<(BlockchainCacheDatabase, TestSchema)>> {
2423 let config = get_postgres_connect_options(None, None, None, None, None);
2424 let mut connect_options: PgConnectOptions = config.clone().into();
2425 let Some(mut admin_pool) =
2426 connect_cache_test_pool(connect_options.clone(), &config.username).await
2427 else {
2428 return Ok(None);
2429 };
2430 let schema_name = cache_test_schema_name();
2431
2432 if let Err(e) = create_cache_test_schema(&admin_pool, &schema_name).await {
2433 if !is_database_create_permission_denied(&e) || config.username == "postgres" {
2434 return Err(e);
2435 }
2436
2437 eprintln!(
2438 "Postgres role {} cannot create isolated blockchain cache test schema; retrying with postgres role: {e}",
2439 config.username
2440 );
2441 admin_pool.close().await;
2442 connect_options = postgres_test_connect_options(&config);
2443 let Some(postgres_pool) =
2444 connect_cache_test_pool(connect_options.clone(), "postgres").await
2445 else {
2446 return Err(e);
2447 };
2448 admin_pool = postgres_pool;
2449 create_cache_test_schema(&admin_pool, &schema_name).await?;
2450 }
2451
2452 let database = crate::cache::database::tests::connect_test_database(
2453 connect_options.options([("search_path", format!("{schema_name},public"))]),
2454 )
2455 .await?;
2456
2457 Ok(Some((
2458 database,
2459 TestSchema {
2460 admin_pool,
2461 name: schema_name,
2462 },
2463 )))
2464 }
2465
2466 async fn connect_cache_test_pool(
2467 connect_options: PgConnectOptions,
2468 username: &str,
2469 ) -> Option<PgPool> {
2470 match PgPoolOptions::new()
2471 .max_connections(1)
2472 .connect_with(connect_options)
2473 .await
2474 {
2475 Ok(pool) => Some(pool),
2476 Err(e) => {
2477 eprintln!(
2478 "Postgres connection as {username} failed; skipping blockchain cache DB test: {e}"
2479 );
2480 None
2481 }
2482 }
2483 }
2484
2485 fn postgres_test_connect_options(config: &PostgresConnectOptions) -> PgConnectOptions {
2486 PostgresConnectOptions::new(
2487 config.host.clone(),
2488 config.port,
2489 String::from("postgres"),
2490 config.password.clone(),
2491 config.database.clone(),
2492 )
2493 .into()
2494 }
2495
2496 fn is_database_create_permission_denied(error: &anyhow::Error) -> bool {
2497 match error.downcast_ref::<SqlxError>() {
2498 Some(SqlxError::Database(database_error)) => {
2499 database_error
2500 .code()
2501 .is_some_and(|code| code.as_ref() == "42501")
2502 && database_error
2503 .message()
2504 .contains("permission denied for database")
2505 }
2506 _ => false,
2507 }
2508 }
2509
2510 struct TestSchema {
2511 admin_pool: PgPool,
2512 name: String,
2513 }
2514
2515 impl TestSchema {
2516 async fn cleanup(self) -> anyhow::Result<()> {
2517 drop_cache_test_schema(&self.admin_pool, &self.name).await?;
2518 self.admin_pool.close().await;
2519 Ok(())
2520 }
2521 }
2522
2523 #[expect(
2524 clippy::too_many_lines,
2525 reason = "test schema declares the narrow table set used by cache SQL"
2526 )]
2527 async fn create_cache_test_schema(pool: &PgPool, schema: &str) -> anyhow::Result<()> {
2528 execute_schema_statement(pool, format!("CREATE SCHEMA {schema}")).await?;
2529
2530 let statements = [
2531 format!("CREATE DOMAIN {schema}.U256 AS NUMERIC(78, 0)"),
2532 format!("CREATE DOMAIN {schema}.U160 AS NUMERIC(49, 0)"),
2533 format!("CREATE DOMAIN {schema}.U128 AS NUMERIC(39, 0)"),
2534 format!(
2535 r#"
2536 CREATE TABLE {schema}."chain" (
2537 chain_id INTEGER PRIMARY KEY,
2538 name TEXT NOT NULL
2539 )
2540 "#
2541 ),
2542 format!(
2543 r#"
2544 CREATE TABLE {schema}."block" (
2545 chain_id INTEGER NOT NULL,
2546 number BIGINT NOT NULL,
2547 hash TEXT,
2548 parent_hash TEXT,
2549 miner TEXT,
2550 gas_limit BIGINT,
2551 gas_used BIGINT,
2552 timestamp TEXT,
2553 base_fee_per_gas TEXT,
2554 blob_gas_used TEXT,
2555 excess_blob_gas TEXT,
2556 l1_gas_price TEXT,
2557 l1_gas_used BIGINT,
2558 l1_fee_scalar BIGINT,
2559 PRIMARY KEY (chain_id, number)
2560 )
2561 "#
2562 ),
2563 format!(
2564 r#"
2565 CREATE TABLE {schema}."pool_event_block" (
2566 chain_id INTEGER NOT NULL,
2567 number BIGINT NOT NULL,
2568 hash TEXT,
2569 timestamp TEXT NOT NULL,
2570 PRIMARY KEY (chain_id, number)
2571 )
2572 "#
2573 ),
2574 format!(
2575 r#"
2576 CREATE TABLE {schema}."token" (
2577 chain_id INTEGER NOT NULL,
2578 address TEXT NOT NULL,
2579 symbol TEXT,
2580 name TEXT,
2581 decimals INTEGER,
2582 error TEXT,
2583 PRIMARY KEY (chain_id, address)
2584 )
2585 "#
2586 ),
2587 format!(
2588 r#"
2589 CREATE TABLE {schema}."dex" (
2590 chain_id INTEGER NOT NULL,
2591 name TEXT NOT NULL,
2592 factory_address TEXT NOT NULL,
2593 creation_block BIGINT NOT NULL,
2594 last_full_sync_pools_block_number BIGINT,
2595 PRIMARY KEY (chain_id, name),
2596 UNIQUE (chain_id, factory_address)
2597 )
2598 "#
2599 ),
2600 format!(
2601 r#"
2602 CREATE TABLE {schema}."pool" (
2603 chain_id INTEGER NOT NULL,
2604 dex_name TEXT NOT NULL,
2605 address TEXT NOT NULL,
2606 pool_identifier TEXT NOT NULL,
2607 creation_block BIGINT NOT NULL,
2608 token0_chain INTEGER NOT NULL,
2609 token0_address TEXT NOT NULL,
2610 token1_chain INTEGER NOT NULL,
2611 token1_address TEXT NOT NULL,
2612 fee INTEGER,
2613 tick_spacing INTEGER,
2614 initial_tick INTEGER,
2615 initial_sqrt_price_x96 TEXT,
2616 hook_address TEXT,
2617 last_full_sync_block_number BIGINT,
2618 event_sync_version INTEGER NOT NULL DEFAULT 0,
2619 PRIMARY KEY (chain_id, dex_name, pool_identifier)
2620 )
2621 "#
2622 ),
2623 format!(
2624 r#"
2625 CREATE TABLE {schema}."pool_event_sync" (
2626 chain_id INTEGER NOT NULL,
2627 dex_name TEXT NOT NULL,
2628 pool_identifier TEXT NOT NULL,
2629 event_family TEXT NOT NULL,
2630 last_full_sync_block_number BIGINT NOT NULL,
2631 PRIMARY KEY (chain_id, dex_name, pool_identifier, event_family)
2632 )
2633 "#
2634 ),
2635 format!(
2636 r#"
2637 CREATE TABLE {schema}."pool_swap_event" (
2638 chain_id INTEGER NOT NULL,
2639 pool_identifier TEXT NOT NULL,
2640 dex_name TEXT NOT NULL,
2641 block BIGINT NOT NULL,
2642 transaction_hash TEXT NOT NULL,
2643 transaction_index INTEGER NOT NULL,
2644 log_index INTEGER NOT NULL,
2645 sender TEXT NOT NULL,
2646 recipient TEXT NOT NULL,
2647 sqrt_price_x96 TEXT NOT NULL,
2648 liquidity TEXT NOT NULL,
2649 tick INTEGER NOT NULL,
2650 amount0 TEXT NOT NULL,
2651 amount1 TEXT NOT NULL,
2652 order_side TEXT,
2653 base_quantity NUMERIC,
2654 quote_quantity NUMERIC,
2655 spot_price NUMERIC,
2656 execution_price NUMERIC,
2657 UNIQUE(chain_id, transaction_hash, log_index)
2658 )
2659 "#
2660 ),
2661 format!(
2662 r#"
2663 CREATE TABLE {schema}."pool_liquidity_event" (
2664 chain_id INTEGER NOT NULL,
2665 pool_identifier TEXT NOT NULL,
2666 dex_name TEXT NOT NULL,
2667 block BIGINT NOT NULL,
2668 transaction_hash TEXT NOT NULL,
2669 transaction_index INTEGER NOT NULL,
2670 log_index INTEGER NOT NULL,
2671 event_type TEXT NOT NULL,
2672 sender TEXT,
2673 owner TEXT NOT NULL,
2674 position_liquidity TEXT NOT NULL,
2675 amount0 TEXT NOT NULL,
2676 amount1 TEXT NOT NULL,
2677 tick_lower INTEGER NOT NULL,
2678 tick_upper INTEGER NOT NULL,
2679 UNIQUE(chain_id, transaction_hash, log_index)
2680 )
2681 "#
2682 ),
2683 format!(
2684 r#"
2685 CREATE TABLE {schema}."pool_collect_event" (
2686 chain_id INTEGER NOT NULL,
2687 pool_identifier TEXT NOT NULL,
2688 dex_name TEXT NOT NULL,
2689 block BIGINT NOT NULL,
2690 transaction_hash TEXT NOT NULL,
2691 transaction_index INTEGER NOT NULL,
2692 log_index INTEGER NOT NULL,
2693 owner TEXT NOT NULL,
2694 amount0 TEXT NOT NULL,
2695 amount1 TEXT NOT NULL,
2696 tick_lower INTEGER NOT NULL,
2697 tick_upper INTEGER NOT NULL,
2698 UNIQUE(chain_id, transaction_hash, log_index)
2699 )
2700 "#
2701 ),
2702 format!(
2703 r#"
2704 CREATE TABLE {schema}."pool_flash_event" (
2705 chain_id INTEGER NOT NULL,
2706 pool_identifier TEXT NOT NULL,
2707 dex_name TEXT NOT NULL,
2708 block BIGINT NOT NULL,
2709 transaction_hash TEXT NOT NULL,
2710 transaction_index INTEGER NOT NULL,
2711 log_index INTEGER NOT NULL,
2712 sender TEXT NOT NULL,
2713 recipient TEXT NOT NULL,
2714 amount0 TEXT NOT NULL,
2715 amount1 TEXT NOT NULL,
2716 paid0 TEXT NOT NULL,
2717 paid1 TEXT NOT NULL,
2718 UNIQUE(chain_id, transaction_hash, log_index)
2719 )
2720 "#
2721 ),
2722 format!(
2723 r#"
2724 CREATE TABLE {schema}."pool_fee_protocol_update_event" (
2725 chain_id INTEGER NOT NULL,
2726 pool_identifier TEXT NOT NULL,
2727 dex_name TEXT NOT NULL,
2728 block BIGINT NOT NULL,
2729 transaction_hash TEXT NOT NULL,
2730 transaction_index INTEGER NOT NULL,
2731 log_index INTEGER NOT NULL,
2732 fee_protocol0_new INTEGER NOT NULL,
2733 fee_protocol1_new INTEGER NOT NULL,
2734 UNIQUE(chain_id, transaction_hash, log_index)
2735 )
2736 "#
2737 ),
2738 format!(
2739 r#"
2740 CREATE TABLE {schema}."pool_fee_protocol_collect_event" (
2741 chain_id INTEGER NOT NULL,
2742 pool_identifier TEXT NOT NULL,
2743 dex_name TEXT NOT NULL,
2744 block BIGINT NOT NULL,
2745 transaction_hash TEXT NOT NULL,
2746 transaction_index INTEGER NOT NULL,
2747 log_index INTEGER NOT NULL,
2748 sender TEXT NOT NULL,
2749 recipient TEXT NOT NULL,
2750 amount0 TEXT NOT NULL,
2751 amount1 TEXT NOT NULL,
2752 UNIQUE(chain_id, transaction_hash, log_index)
2753 )
2754 "#
2755 ),
2756 format!(
2757 r#"
2758 CREATE TABLE {schema}."pool_snapshot" (
2759 chain_id INTEGER NOT NULL,
2760 pool_identifier TEXT NOT NULL,
2761 dex_name TEXT NOT NULL,
2762 block BIGINT NOT NULL,
2763 transaction_index INTEGER NOT NULL,
2764 log_index INTEGER NOT NULL,
2765 transaction_hash TEXT NOT NULL,
2766 current_tick INTEGER NOT NULL,
2767 price_sqrt_ratio_x96 NUMERIC NOT NULL,
2768 liquidity NUMERIC NOT NULL,
2769 protocol_fees_token0 NUMERIC NOT NULL,
2770 protocol_fees_token1 NUMERIC NOT NULL,
2771 fee_protocol SMALLINT NOT NULL,
2772 fee_protocol0_basis_points INTEGER,
2773 fee_protocol1_basis_points INTEGER,
2774 fee_growth_global_0 NUMERIC NOT NULL,
2775 fee_growth_global_1 NUMERIC NOT NULL,
2776 total_amount0_deposited NUMERIC NOT NULL,
2777 total_amount1_deposited NUMERIC NOT NULL,
2778 total_amount0_collected NUMERIC NOT NULL,
2779 total_amount1_collected NUMERIC NOT NULL,
2780 total_swaps INTEGER NOT NULL,
2781 total_mints INTEGER NOT NULL,
2782 total_burns INTEGER NOT NULL,
2783 total_fee_collects INTEGER NOT NULL,
2784 total_flashes INTEGER NOT NULL,
2785 liquidity_utilization_rate DOUBLE PRECISION,
2786 validation_state TEXT NOT NULL DEFAULT 'replay' CHECK (validation_state IN ('on_chain', 'replay', 'invalid')),
2787 PRIMARY KEY (chain_id, pool_identifier, block, transaction_index, log_index)
2788 )
2789 "#
2790 ),
2791 format!(
2792 r#"
2793 CREATE TABLE {schema}."pool_position" (
2794 chain_id INTEGER NOT NULL,
2795 pool_identifier TEXT NOT NULL,
2796 snapshot_block BIGINT NOT NULL,
2797 snapshot_transaction_index INTEGER NOT NULL,
2798 snapshot_log_index INTEGER NOT NULL,
2799 owner TEXT NOT NULL,
2800 tick_lower INTEGER NOT NULL,
2801 tick_upper INTEGER NOT NULL,
2802 liquidity NUMERIC NOT NULL,
2803 fee_growth_inside_0_last NUMERIC NOT NULL,
2804 fee_growth_inside_1_last NUMERIC NOT NULL,
2805 tokens_owed_0 NUMERIC NOT NULL,
2806 tokens_owed_1 NUMERIC NOT NULL,
2807 total_amount0_deposited NUMERIC,
2808 total_amount1_deposited NUMERIC,
2809 total_amount0_collected NUMERIC,
2810 total_amount1_collected NUMERIC,
2811 PRIMARY KEY (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, owner, tick_lower, tick_upper)
2812 )
2813 "#
2814 ),
2815 format!(
2816 r#"
2817 CREATE TABLE {schema}."pool_tick" (
2818 chain_id INTEGER NOT NULL,
2819 pool_identifier TEXT NOT NULL,
2820 snapshot_block BIGINT NOT NULL,
2821 snapshot_transaction_index INTEGER NOT NULL,
2822 snapshot_log_index INTEGER NOT NULL,
2823 tick_value INTEGER NOT NULL,
2824 liquidity_gross NUMERIC NOT NULL,
2825 liquidity_net NUMERIC NOT NULL,
2826 fee_growth_outside_0 NUMERIC NOT NULL,
2827 fee_growth_outside_1 NUMERIC NOT NULL,
2828 initialized BOOLEAN NOT NULL,
2829 last_updated_block BIGINT NOT NULL,
2830 PRIMARY KEY (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, tick_value)
2831 )
2832 "#
2833 ),
2834 ];
2835
2836 for statement in statements {
2837 execute_schema_statement(pool, statement).await?;
2838 }
2839
2840 Ok(())
2841 }
2842
2843 async fn insert_pool_swap_event(
2844 pool: &PgPool,
2845 schema: &str,
2846 chain_id: u32,
2847 pool_identifier: &PoolIdentifier,
2848 block: u64,
2849 ) -> anyhow::Result<()> {
2850 sqlx::query(AssertSqlSafe(format!(
2851 r#"
2852 INSERT INTO {schema}."pool_swap_event" (
2853 chain_id, pool_identifier, dex_name, block, transaction_hash, transaction_index,
2854 log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1
2855 )
2856 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
2857 "#
2858 )))
2859 .bind(chain_id as i32)
2860 .bind(pool_identifier.to_string())
2861 .bind(DexType::UniswapV3.to_string())
2862 .bind(block as i64)
2863 .bind("0x000000000000000000000000000000000000000000000000000000000000000c")
2864 .bind(0_i32)
2865 .bind(0_i32)
2866 .bind("0x1111111111111111111111111111111111111111")
2867 .bind("0x2222222222222222222222222222222222222222")
2868 .bind("79228162514264337593543950336")
2869 .bind("1000000")
2870 .bind(0_i32)
2871 .bind("-1000000000000000000")
2872 .bind("2000000")
2873 .execute(pool)
2874 .await?;
2875 Ok(())
2876 }
2877
2878 async fn insert_block_without_timestamp(
2879 pool: &PgPool,
2880 schema: &str,
2881 chain_id: u32,
2882 number: u64,
2883 ) -> anyhow::Result<()> {
2884 sqlx::query(AssertSqlSafe(format!(
2885 r#"
2886 INSERT INTO {schema}."block" (
2887 chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp
2888 )
2889 VALUES ($1, $2, $3, $4, $5, $6, $7, NULL)
2890 "#
2891 )))
2892 .bind(chain_id as i32)
2893 .bind(number as i64)
2894 .bind(format!("0x{number:064x}"))
2895 .bind("0x0")
2896 .bind("0x0000000000000000000000000000000000000000")
2897 .bind(30_000_000_i64)
2898 .bind(21_000_i64)
2899 .execute(pool)
2900 .await?;
2901 Ok(())
2902 }
2903
2904 async fn drop_cache_test_schema(pool: &PgPool, schema: &str) -> anyhow::Result<()> {
2905 execute_schema_statement(pool, format!("DROP SCHEMA IF EXISTS {schema} CASCADE")).await
2906 }
2907
2908 async fn execute_schema_statement(pool: &PgPool, statement: String) -> anyhow::Result<()> {
2909 sqlx::query(AssertSqlSafe(statement)).execute(pool).await?;
2910 Ok(())
2911 }
2912
2913 fn cache_test_schema_name() -> String {
2914 let nanos = SystemTime::now()
2915 .duration_since(UNIX_EPOCH)
2916 .expect("system clock must be after UNIX epoch")
2917 .as_nanos();
2918
2919 format!("nt_blockchain_cache_test_{}_{}", std::process::id(), nanos)
2920 }
2921
2922 fn arbitrum() -> SharedChain {
2923 let Some(chain) = Chain::from_chain_id(42161) else {
2924 panic!("Arbitrum chain must exist in model definitions");
2925 };
2926
2927 Arc::new(chain.clone())
2928 }
2929
2930 fn uniswap_v3(chain: &SharedChain) -> SharedDex {
2931 Arc::new(Dex::new(
2932 (**chain).clone(),
2933 DexType::UniswapV3,
2934 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
2935 0,
2936 AmmType::CLAMM,
2937 "PoolCreated",
2938 "Swap",
2939 "Mint",
2940 "Burn",
2941 "Collect",
2942 ))
2943 }
2944
2945 fn pancakeswap_v3(chain: &SharedChain) -> SharedDex {
2946 Arc::new(Dex::new(
2947 (**chain).clone(),
2948 DexType::PancakeSwapV3,
2949 "0x0BFbCF9fa4f9C56B0F40a671Ad40E0805A091865",
2950 0,
2951 AmmType::CLAMM,
2952 "PoolCreated",
2953 "Swap",
2954 "Mint",
2955 "Burn",
2956 "Collect",
2957 ))
2958 }
2959
2960 fn weth(chain: &SharedChain) -> Token {
2961 Token::new(
2962 Arc::clone(chain),
2963 address!("0x82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
2964 "Wrapped Ether".to_string(),
2965 "WETH".to_string(),
2966 18,
2967 )
2968 }
2969
2970 fn usdc(chain: &SharedChain) -> Token {
2971 Token::new(
2972 Arc::clone(chain),
2973 address!("0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8"),
2974 "USD Coin".to_string(),
2975 "USDC".to_string(),
2976 6,
2977 )
2978 }
2979
2980 fn test_block(number: u64, timestamp: UnixNanos) -> Block {
2981 Block::new(
2982 format!("0x{number:064x}"),
2983 String::from("0x0"),
2984 number,
2985 Ustr::from("0x0000000000000000000000000000000000000000"),
2986 30_000_000,
2987 21_000,
2988 timestamp,
2989 Some(Blockchain::Arbitrum),
2990 )
2991 }
2992}