1use std::pin::Pin;
17
18use alloy::primitives::{Address, U256};
19use futures_util::{Stream, StreamExt};
20use nautilus_model::{
21 defi::{
22 Block, Chain, DexType, Pool, PoolIdentifier, PoolLiquidityUpdate, PoolSwap, SharedChain,
23 SharedDex, Token,
24 data::{
25 DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate, PoolFlash,
26 block::BlockPosition,
27 },
28 pool_analysis::{
29 position::PoolPosition,
30 snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
31 },
32 tick_map::tick::PoolTick,
33 validation::validate_address,
34 },
35 identifiers::InstrumentId,
36};
37use rust_decimal::Decimal;
38use sqlx::{AssertSqlSafe, PgPool, Row, postgres::PgConnectOptions};
39
40use crate::{
41 cache::{
42 consistency::CachedBlocksConsistencyStatus,
43 copy::PostgresCopyHandler,
44 rows::{
45 BlockTimestampRow, PoolRow, TokenRow, parse_cached_block_timestamp,
46 transform_row_to_dex_pool_data,
47 },
48 types::{U128Pg, U256Pg},
49 },
50 events::initialize::InitializeEvent,
51};
52
53#[derive(Debug)]
55pub struct BlockchainCacheDatabase {
56 pool: PgPool,
58}
59
60const POOL_ROW_COLUMNS: &str = "
62 address,
63 pool_identifier,
64 dex_name,
65 creation_block,
66 COALESCE(
67 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool.chain_id AND block.number = pool.creation_block),
68 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool.chain_id AND pool_event_block.number = pool.creation_block)
69 ) as creation_block_timestamp,
70 token0_chain,
71 token0_address,
72 token1_chain,
73 token1_address,
74 fee,
75 tick_spacing,
76 initial_tick,
77 initial_sqrt_price_x96,
78 hook_address
79";
80
81impl BlockchainCacheDatabase {
82 pub async fn init(pg_options: PgConnectOptions) -> Self {
88 Self::connect(pg_options)
89 .await
90 .expect("Error connecting to Postgres")
91 }
92
93 pub async fn connect(pg_options: PgConnectOptions) -> anyhow::Result<Self> {
99 let pool = sqlx::postgres::PgPoolOptions::new()
100 .max_connections(32) .min_connections(5) .acquire_timeout(std::time::Duration::from_secs(3))
103 .connect_with(pg_options)
104 .await?;
105 Ok(Self { pool })
106 }
107
108 pub async fn seed_chain(&self, chain: &Chain) -> anyhow::Result<()> {
114 sqlx::query(
115 "
116 INSERT INTO chain (
117 chain_id, name
118 ) VALUES ($1,$2)
119 ON CONFLICT (chain_id)
120 DO NOTHING
121 ",
122 )
123 .bind(chain.chain_id as i32)
124 .bind(chain.name.to_string())
125 .execute(&self.pool)
126 .await
127 .map(|_| ())
128 .map_err(|e| anyhow::anyhow!("Failed to seed chain table: {e}"))
129 }
130
131 pub async fn create_block_partition(&self, chain: &Chain) -> anyhow::Result<String> {
138 let result: (String,) = sqlx::query_as("SELECT create_block_partition($1)")
139 .bind(chain.chain_id as i32)
140 .fetch_one(&self.pool)
141 .await
142 .map_err(|e| {
143 anyhow::anyhow!(
144 "Failed to call create_block_partition for chain {}: {e}",
145 chain.chain_id
146 )
147 })?;
148
149 Ok(result.0)
150 }
151
152 pub async fn create_token_partition(&self, chain: &Chain) -> anyhow::Result<String> {
159 let result: (String,) = sqlx::query_as("SELECT create_token_partition($1)")
160 .bind(chain.chain_id as i32)
161 .fetch_one(&self.pool)
162 .await
163 .map_err(|e| {
164 anyhow::anyhow!(
165 "Failed to call create_token_partition for chain {}: {e}",
166 chain.chain_id
167 )
168 })?;
169
170 Ok(result.0)
171 }
172
173 pub async fn get_block_consistency_status(
179 &self,
180 chain: &Chain,
181 ) -> anyhow::Result<CachedBlocksConsistencyStatus> {
182 log::debug!("Fetching block consistency status");
183
184 let result: (i64, i64) = sqlx::query_as(
185 "
186 SELECT
187 COALESCE((SELECT number FROM block WHERE chain_id = $1 ORDER BY number DESC LIMIT 1), 0) as max_block,
188 get_last_continuous_block($1) as last_continuous_block
189 "
190 )
191 .bind(chain.chain_id as i32)
192 .fetch_one(&self.pool)
193 .await
194 .map_err(|e| {
195 anyhow::anyhow!(
196 "Failed to get block info for chain {}: {}",
197 chain.chain_id,
198 e
199 )
200 })?;
201
202 Ok(CachedBlocksConsistencyStatus::new(
203 result.0 as u64,
204 result.1 as u64,
205 ))
206 }
207
208 pub async fn add_block(&self, chain_id: u32, block: &Block) -> anyhow::Result<()> {
214 sqlx::query(
215 "
216 INSERT INTO block (
217 chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp,
218 base_fee_per_gas, blob_gas_used, excess_blob_gas,
219 l1_gas_price, l1_gas_used, l1_fee_scalar
220 ) VALUES (
221 $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
222 )
223 ON CONFLICT (chain_id, number)
224 DO UPDATE
225 SET
226 hash = $3,
227 parent_hash = $4,
228 miner = $5,
229 gas_limit = $6,
230 gas_used = $7,
231 timestamp = $8,
232 base_fee_per_gas = $9,
233 blob_gas_used = $10,
234 excess_blob_gas = $11,
235 l1_gas_price = $12,
236 l1_gas_used = $13,
237 l1_fee_scalar = $14
238 ",
239 )
240 .bind(chain_id as i32)
241 .bind(block.number as i64)
242 .bind(block.hash.as_str())
243 .bind(block.parent_hash.as_str())
244 .bind(block.miner.as_str())
245 .bind(block.gas_limit as i64)
246 .bind(block.gas_used as i64)
247 .bind(block.timestamp.to_string())
248 .bind(block.base_fee_per_gas.as_ref().map(U256::to_string))
249 .bind(block.blob_gas_used.as_ref().map(U256::to_string))
250 .bind(block.excess_blob_gas.as_ref().map(U256::to_string))
251 .bind(block.l1_gas_price.as_ref().map(U256::to_string))
252 .bind(block.l1_gas_used.map(|v| v as i64))
253 .bind(block.l1_fee_scalar.map(|v| v as i64))
254 .execute(&self.pool)
255 .await
256 .map(|_| ())
257 .map_err(|e| anyhow::anyhow!("Failed to insert into block table: {e}"))
258 }
259
260 pub async fn add_blocks_batch(&self, chain_id: u32, blocks: &[Block]) -> anyhow::Result<()> {
266 if blocks.is_empty() {
267 return Ok(());
268 }
269
270 let mut numbers: Vec<i64> = Vec::with_capacity(blocks.len());
272 let mut hashes: Vec<String> = Vec::with_capacity(blocks.len());
273 let mut parent_hashes: Vec<String> = Vec::with_capacity(blocks.len());
274 let mut miners: Vec<String> = Vec::with_capacity(blocks.len());
275 let mut gas_limits: Vec<i64> = Vec::with_capacity(blocks.len());
276 let mut gas_useds: Vec<i64> = Vec::with_capacity(blocks.len());
277 let mut timestamps: Vec<String> = Vec::with_capacity(blocks.len());
278 let mut base_fee_per_gases: Vec<Option<String>> = Vec::with_capacity(blocks.len());
279 let mut blob_gas_useds: Vec<Option<String>> = Vec::with_capacity(blocks.len());
280 let mut excess_blob_gases: Vec<Option<String>> = Vec::with_capacity(blocks.len());
281 let mut l1_gas_prices: Vec<Option<String>> = Vec::with_capacity(blocks.len());
282 let mut l1_gas_useds: Vec<Option<i64>> = Vec::with_capacity(blocks.len());
283 let mut l1_fee_scalars: Vec<Option<i64>> = Vec::with_capacity(blocks.len());
284
285 for block in blocks {
287 numbers.push(block.number as i64);
288 hashes.push(block.hash.clone());
289 parent_hashes.push(block.parent_hash.clone());
290 miners.push(block.miner.to_string());
291 gas_limits.push(block.gas_limit as i64);
292 gas_useds.push(block.gas_used as i64);
293 timestamps.push(block.timestamp.to_string());
294 base_fee_per_gases.push(block.base_fee_per_gas.as_ref().map(U256::to_string));
295 blob_gas_useds.push(block.blob_gas_used.as_ref().map(U256::to_string));
296 excess_blob_gases.push(block.excess_blob_gas.as_ref().map(U256::to_string));
297 l1_gas_prices.push(block.l1_gas_price.as_ref().map(U256::to_string));
298 l1_gas_useds.push(block.l1_gas_used.map(|v| v as i64));
299 l1_fee_scalars.push(block.l1_fee_scalar.map(|v| v as i64));
300 }
301
302 sqlx::query(
304 "
305 INSERT INTO block (
306 chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp,
307 base_fee_per_gas, blob_gas_used, excess_blob_gas,
308 l1_gas_price, l1_gas_used, l1_fee_scalar
309 )
310 SELECT
311 $1, *
312 FROM UNNEST(
313 $2::int8[], $3::text[], $4::text[], $5::text[],
314 $6::int8[], $7::int8[], $8::text[],
315 $9::text[], $10::text[], $11::text[],
316 $12::text[], $13::int8[], $14::int8[]
317 )
318 ON CONFLICT (chain_id, number) DO NOTHING
319 ",
320 )
321 .bind(chain_id as i32)
322 .bind(&numbers[..])
323 .bind(&hashes[..])
324 .bind(&parent_hashes[..])
325 .bind(&miners[..])
326 .bind(&gas_limits[..])
327 .bind(&gas_useds[..])
328 .bind(×tamps[..])
329 .bind(&base_fee_per_gases as &[Option<String>])
330 .bind(&blob_gas_useds as &[Option<String>])
331 .bind(&excess_blob_gases as &[Option<String>])
332 .bind(&l1_gas_prices as &[Option<String>])
333 .bind(&l1_gas_useds as &[Option<i64>])
334 .bind(&l1_fee_scalars as &[Option<i64>])
335 .execute(&self.pool)
336 .await
337 .map(|_| ())
338 .map_err(|e| anyhow::anyhow!("Failed to batch insert into block table: {e}"))
339 }
340
341 pub async fn add_pool_event_blocks_batch(
347 &self,
348 chain_id: u32,
349 blocks: &[Block],
350 ) -> anyhow::Result<()> {
351 if blocks.is_empty() {
352 return Ok(());
353 }
354
355 let mut numbers: Vec<i64> = Vec::with_capacity(blocks.len());
356 let mut timestamps: Vec<String> = Vec::with_capacity(blocks.len());
357
358 for block in blocks {
359 numbers.push(block.number as i64);
360 timestamps.push(block.timestamp.to_string());
361 }
362
363 sqlx::query(
364 "
365 INSERT INTO pool_event_block (
366 chain_id, number, timestamp
367 )
368 SELECT
369 $1, *
370 FROM UNNEST(
371 $2::int8[], $3::text[]
372 )
373 ON CONFLICT (chain_id, number)
374 DO UPDATE SET timestamp = EXCLUDED.timestamp
375 ",
376 )
377 .bind(chain_id as i32)
378 .bind(&numbers[..])
379 .bind(×tamps[..])
380 .execute(&self.pool)
381 .await
382 .map(|_| ())
383 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_event_block table: {e}"))
384 }
385
386 pub async fn add_blocks_copy(&self, chain_id: u32, blocks: &[Block]) -> anyhow::Result<()> {
395 let copy_handler = PostgresCopyHandler::new(&self.pool);
396 copy_handler.copy_blocks(chain_id, blocks).await
397 }
398
399 pub async fn add_tokens_copy(&self, chain_id: u32, tokens: &[Token]) -> anyhow::Result<()> {
405 let copy_handler = PostgresCopyHandler::new(&self.pool);
406 copy_handler.copy_tokens(chain_id, tokens).await
407 }
408
409 pub async fn add_pools_copy(&self, chain_id: u32, pools: &[Pool]) -> anyhow::Result<()> {
415 let copy_handler = PostgresCopyHandler::new(&self.pool);
416 copy_handler.copy_pools(chain_id, pools).await
417 }
418
419 pub async fn add_pool_swaps_copy(
428 &self,
429 chain_id: u32,
430 swaps: &[PoolSwap],
431 ) -> anyhow::Result<()> {
432 let copy_handler = PostgresCopyHandler::new(&self.pool);
433 copy_handler.copy_pool_swaps(chain_id, swaps).await
434 }
435
436 pub async fn add_pool_liquidity_updates_copy(
445 &self,
446 chain_id: u32,
447 updates: &[PoolLiquidityUpdate],
448 ) -> anyhow::Result<()> {
449 let copy_handler = PostgresCopyHandler::new(&self.pool);
450 copy_handler
451 .copy_pool_liquidity_updates(chain_id, updates)
452 .await
453 }
454
455 pub async fn copy_pool_fee_collects_batch(
464 &self,
465 chain_id: u32,
466 collects: &[PoolFeeCollect],
467 ) -> anyhow::Result<()> {
468 let copy_handler = PostgresCopyHandler::new(&self.pool);
469 copy_handler.copy_pool_collects(chain_id, collects).await
470 }
471
472 pub async fn load_block_timestamps(
478 &self,
479 chain: SharedChain,
480 from_block: u64,
481 ) -> anyhow::Result<Vec<BlockTimestampRow>> {
482 sqlx::query_as::<_, BlockTimestampRow>(
483 "
484 SELECT DISTINCT ON (number)
485 number,
486 timestamp
487 FROM (
488 SELECT number, timestamp, 0 AS source_order
489 FROM block
490 WHERE chain_id = $1 AND number >= $2 AND timestamp IS NOT NULL
491 UNION ALL
492 SELECT number, timestamp, 1 AS source_order
493 FROM pool_event_block
494 WHERE chain_id = $1 AND number >= $2 AND timestamp IS NOT NULL
495 ) AS block_timestamps
496 ORDER BY number ASC, source_order ASC
497 ",
498 )
499 .bind(chain.chain_id as i32)
500 .bind(from_block as i64)
501 .fetch_all(&self.pool)
502 .await
503 .map_err(|e| anyhow::anyhow!("Failed to load block timestamps: {e}"))
504 }
505
506 pub async fn add_dex(&self, dex: SharedDex) -> anyhow::Result<()> {
512 sqlx::query(
513 "
514 INSERT INTO dex (
515 chain_id, name, factory_address, creation_block
516 ) VALUES ($1, $2, $3, $4)
517 ON CONFLICT (chain_id, name)
518 DO UPDATE
519 SET
520 factory_address = $3,
521 creation_block = $4
522 ",
523 )
524 .bind(dex.chain.chain_id as i32)
525 .bind(dex.name.to_string())
526 .bind(dex.factory.to_string())
527 .bind(dex.factory_creation_block as i64)
528 .execute(&self.pool)
529 .await
530 .map(|_| ())
531 .map_err(|e| anyhow::anyhow!("Failed to insert into dex table: {e}"))
532 }
533
534 pub async fn add_pool(&self, pool: &Pool) -> anyhow::Result<()> {
540 sqlx::query(
541 "
542 INSERT INTO pool (
543 chain_id, address, pool_identifier, dex_name, creation_block,
544 token0_chain, token0_address,
545 token1_chain, token1_address,
546 fee, tick_spacing, initial_tick, initial_sqrt_price_x96, hook_address
547 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
548 ON CONFLICT (chain_id, dex_name, pool_identifier)
549 DO UPDATE
550 SET
551 address = $2,
552 creation_block = $5,
553 token0_chain = $6,
554 token0_address = $7,
555 token1_chain = $8,
556 token1_address = $9,
557 fee = $10,
558 tick_spacing = $11,
559 initial_tick = $12,
560 initial_sqrt_price_x96 = $13,
561 hook_address = $14
562 ",
563 )
564 .bind(pool.chain.chain_id as i32)
565 .bind(pool.address.to_string())
566 .bind(pool.pool_identifier.as_ref())
567 .bind(pool.dex.name.to_string())
568 .bind(pool.creation_block as i64)
569 .bind(pool.token0.chain.chain_id as i32)
570 .bind(pool.token0.address.to_string())
571 .bind(pool.token1.chain.chain_id as i32)
572 .bind(pool.token1.address.to_string())
573 .bind(pool.fee.map(|fee| fee as i32))
574 .bind(pool.tick_spacing.map(|tick_spacing| tick_spacing as i32))
575 .bind(pool.initial_tick)
576 .bind(pool.initial_sqrt_price_x96.as_ref().map(|p| p.to_string()))
577 .bind(pool.hooks.as_ref().map(|h| h.to_string()))
578 .execute(&self.pool)
579 .await
580 .map(|_| ())
581 .map_err(|e| anyhow::anyhow!("Failed to insert into pool table: {e}"))
582 }
583
584 pub async fn add_pools_batch(&self, pools: &[Pool]) -> anyhow::Result<()> {
590 if pools.is_empty() {
591 return Ok(());
592 }
593
594 let len = pools.len();
596 let mut addresses: Vec<String> = Vec::with_capacity(len);
597 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
598 let mut dex_names: Vec<String> = Vec::with_capacity(len);
599 let mut creation_blocks: Vec<i64> = Vec::with_capacity(len);
600 let mut token0_chains: Vec<i32> = Vec::with_capacity(len);
601 let mut token0_addresses: Vec<String> = Vec::with_capacity(len);
602 let mut token1_chains: Vec<i32> = Vec::with_capacity(len);
603 let mut token1_addresses: Vec<String> = Vec::with_capacity(len);
604 let mut fees: Vec<Option<i32>> = Vec::with_capacity(len);
605 let mut tick_spacings: Vec<Option<i32>> = Vec::with_capacity(len);
606 let mut initial_ticks: Vec<Option<i32>> = Vec::with_capacity(len);
607 let mut initial_sqrt_price_x96s: Vec<Option<String>> = Vec::with_capacity(len);
608 let mut hook_addresses: Vec<Option<String>> = Vec::with_capacity(len);
609 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
610
611 for pool in pools {
613 chain_ids.push(pool.chain.chain_id as i32);
614 addresses.push(pool.address.to_string());
615 pool_identifiers.push(pool.pool_identifier.to_string());
616 dex_names.push(pool.dex.name.to_string());
617 creation_blocks.push(pool.creation_block as i64);
618 token0_chains.push(pool.token0.chain.chain_id as i32);
619 token0_addresses.push(pool.token0.address.to_string());
620 token1_chains.push(pool.token1.chain.chain_id as i32);
621 token1_addresses.push(pool.token1.address.to_string());
622 fees.push(pool.fee.map(|fee| fee as i32));
623 tick_spacings.push(pool.tick_spacing.map(|tick_spacing| tick_spacing as i32));
624 initial_ticks.push(pool.initial_tick);
625 initial_sqrt_price_x96s
626 .push(pool.initial_sqrt_price_x96.as_ref().map(|p| p.to_string()));
627 hook_addresses.push(pool.hooks.as_ref().map(|h| h.to_string()));
628 }
629
630 sqlx::query(
632 "
633 INSERT INTO pool (
634 chain_id, address, pool_identifier, dex_name, creation_block,
635 token0_chain, token0_address,
636 token1_chain, token1_address,
637 fee, tick_spacing, initial_tick, initial_sqrt_price_x96, hook_address
638 )
639 SELECT *
640 FROM UNNEST(
641 $1::int4[], $2::text[], $3::text[], $4::text[], $5::int8[],
642 $6::int4[], $7::text[], $8::int4[], $9::text[],
643 $10::int4[], $11::int4[], $12::int4[], $13::text[], $14::text[]
644 )
645 ON CONFLICT (chain_id, dex_name, pool_identifier) DO NOTHING
646 ",
647 )
648 .bind(&chain_ids[..])
649 .bind(&addresses[..])
650 .bind(&pool_identifiers[..])
651 .bind(&dex_names[..])
652 .bind(&creation_blocks[..])
653 .bind(&token0_chains[..])
654 .bind(&token0_addresses[..])
655 .bind(&token1_chains[..])
656 .bind(&token1_addresses[..])
657 .bind(&fees[..])
658 .bind(&tick_spacings[..])
659 .bind(&initial_ticks[..])
660 .bind(&initial_sqrt_price_x96s[..])
661 .bind(&hook_addresses as &[Option<String>])
662 .execute(&self.pool)
663 .await
664 .map(|_| ())
665 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool table: {e}"))
666 }
667
668 pub async fn add_pool_swaps_batch(
674 &self,
675 chain_id: u32,
676 swaps: &[PoolSwap],
677 ) -> anyhow::Result<()> {
678 if swaps.is_empty() {
679 return Ok(());
680 }
681
682 let len = swaps.len();
684 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
685 let mut dex_names: Vec<String> = Vec::with_capacity(len);
686 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
687 let mut blocks: Vec<i64> = Vec::with_capacity(len);
688 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
689 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
690 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
691 let mut senders: Vec<String> = Vec::with_capacity(len);
692 let mut recipients: Vec<String> = Vec::with_capacity(len);
693 let mut sqrt_price_x96s: Vec<String> = Vec::with_capacity(len);
694 let mut liquidities: Vec<String> = Vec::with_capacity(len);
695 let mut ticks: Vec<i32> = Vec::with_capacity(len);
696 let mut amount0s: Vec<String> = Vec::with_capacity(len);
697 let mut amount1s: Vec<String> = Vec::with_capacity(len);
698 let mut order_sides: Vec<Option<String>> = Vec::with_capacity(len);
699 let mut base_quantities: Vec<Option<Decimal>> = Vec::with_capacity(len);
700 let mut quote_quantities: Vec<Option<Decimal>> = Vec::with_capacity(len);
701 let mut spot_prices: Vec<Option<Decimal>> = Vec::with_capacity(len);
702 let mut execution_prices: Vec<Option<Decimal>> = Vec::with_capacity(len);
703
704 for swap in swaps {
706 chain_ids.push(chain_id as i32);
707 dex_names.push(swap.dex.name.to_string());
708 pool_identifiers.push(swap.pool_identifier.to_string());
709 blocks.push(swap.block as i64);
710 transaction_hashes.push(swap.transaction_hash.clone());
711 transaction_indices.push(swap.transaction_index as i32);
712 log_indices.push(swap.log_index as i32);
713 senders.push(swap.sender.to_string());
714 recipients.push(swap.recipient.to_string());
715 sqrt_price_x96s.push(swap.sqrt_price_x96.to_string());
716 liquidities.push(swap.liquidity.to_string());
717 ticks.push(swap.tick);
718 amount0s.push(swap.amount0.to_string());
719 amount1s.push(swap.amount1.to_string());
720
721 if let Some(ref trade_info) = swap.trade_info {
723 order_sides.push(Some(trade_info.order_side.to_string()));
724 base_quantities.push(Some(trade_info.quantity_base.as_decimal()));
725 quote_quantities.push(Some(trade_info.quantity_quote.as_decimal()));
726 spot_prices.push(Some(trade_info.spot_price.as_decimal()));
727 execution_prices.push(Some(trade_info.execution_price.as_decimal()));
728 } else {
729 order_sides.push(None);
730 base_quantities.push(None);
731 quote_quantities.push(None);
732 spot_prices.push(None);
733 execution_prices.push(None);
734 }
735 }
736
737 sqlx::query(
739 "
740 INSERT INTO pool_swap_event (
741 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
742 log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
743 order_side, base_quantity, quote_quantity, spot_price, execution_price
744 )
745 SELECT
746 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index, log_index, sender, recipient,
747 sqrt_price_x96::U160, liquidity::U128, tick, amount0::I256, amount1::I256,
748 order_side, base_quantity, quote_quantity, spot_price, execution_price
749 FROM UNNEST(
750 $1::INT[], $2::TEXT[], $3::TEXT[], $4::BIGINT[], $5::TEXT[], $6::INT[], $7::INT[],
751 $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[], $12::INT[], $13::TEXT[], $14::TEXT[],
752 $15::TEXT[], $16, $17, $18, $19
753 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
754 log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
755 order_side, base_quantity, quote_quantity, spot_price, execution_price)
756 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
757 ",
758 )
759 .bind(&chain_ids[..])
760 .bind(&dex_names[..])
761 .bind(&pool_identifiers[..])
762 .bind(&blocks[..])
763 .bind(&transaction_hashes[..])
764 .bind(&transaction_indices[..])
765 .bind(&log_indices[..])
766 .bind(&senders[..])
767 .bind(&recipients[..])
768 .bind(&sqrt_price_x96s[..])
769 .bind(&liquidities[..])
770 .bind(&ticks[..])
771 .bind(&amount0s[..])
772 .bind(&amount1s[..])
773 .bind(&order_sides[..])
774 .bind(&base_quantities[..])
775 .bind("e_quantities[..])
776 .bind(&spot_prices[..])
777 .bind(&execution_prices[..])
778 .execute(&self.pool)
779 .await
780 .map(|_| ())
781 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_swap_event table: {e}"))
782 }
783
784 pub async fn add_pool_liquidity_updates_batch(
790 &self,
791 chain_id: u32,
792 updates: &[PoolLiquidityUpdate],
793 ) -> anyhow::Result<()> {
794 if updates.is_empty() {
795 return Ok(());
796 }
797
798 let len = updates.len();
800 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
801 let mut dex_names: Vec<String> = Vec::with_capacity(len);
802 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
803 let mut blocks: Vec<i64> = Vec::with_capacity(len);
804 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
805 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
806 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
807 let mut event_types: Vec<String> = Vec::with_capacity(len);
808 let mut senders: Vec<Option<String>> = Vec::with_capacity(len);
809 let mut owners: Vec<String> = Vec::with_capacity(len);
810 let mut position_liquidities: Vec<String> = Vec::with_capacity(len);
811 let mut amount0s: Vec<String> = Vec::with_capacity(len);
812 let mut amount1s: Vec<String> = Vec::with_capacity(len);
813 let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
814 let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
815
816 for update in updates {
818 chain_ids.push(chain_id as i32);
819 dex_names.push(update.dex.name.to_string());
820 pool_identifiers.push(update.pool_identifier.to_string());
821 blocks.push(update.block as i64);
822 transaction_hashes.push(update.transaction_hash.clone());
823 transaction_indices.push(update.transaction_index as i32);
824 log_indices.push(update.log_index as i32);
825 event_types.push(update.kind.to_string());
826 senders.push(update.sender.map(|s| s.to_string()));
827 owners.push(update.owner.to_string());
828 position_liquidities.push(update.position_liquidity.to_string());
829 amount0s.push(update.amount0.to_string());
830 amount1s.push(update.amount1.to_string());
831 tick_lowers.push(update.tick_lower);
832 tick_uppers.push(update.tick_upper);
833 }
834
835 sqlx::query(
837 "
838 INSERT INTO pool_liquidity_event (
839 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
840 log_index, event_type, sender, owner, position_liquidity,
841 amount0, amount1, tick_lower, tick_upper
842 )
843 SELECT
844 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
845 log_index, event_type, sender, owner, position_liquidity::u128,
846 amount0::U256, amount1::U256, tick_lower, tick_upper
847 FROM UNNEST(
848 $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
849 $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[],
850 $12::TEXT[], $13::TEXT[], $14::INT[], $15::INT[]
851 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
852 log_index, event_type, sender, owner, position_liquidity,
853 amount0, amount1, tick_lower, tick_upper)
854 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
855 ",
856 )
857 .bind(&chain_ids[..])
858 .bind(&dex_names[..])
859 .bind(&pool_identifiers[..])
860 .bind(&blocks[..])
861 .bind(&transaction_hashes[..])
862 .bind(&transaction_indices[..])
863 .bind(&log_indices[..])
864 .bind(&event_types[..])
865 .bind(&senders[..])
866 .bind(&owners[..])
867 .bind(&position_liquidities[..])
868 .bind(&amount0s[..])
869 .bind(&amount1s[..])
870 .bind(&tick_lowers[..])
871 .bind(&tick_uppers[..])
872 .execute(&self.pool)
873 .await
874 .map(|_| ())
875 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_liquidity_event table: {e}"))
876 }
877
878 pub async fn add_token(&self, token: &Token) -> anyhow::Result<()> {
884 sqlx::query(
885 "
886 INSERT INTO token (
887 chain_id, address, name, symbol, decimals
888 ) VALUES ($1, $2, $3, $4, $5)
889 ON CONFLICT (chain_id, address)
890 DO UPDATE
891 SET
892 name = $3,
893 symbol = $4,
894 decimals = $5
895 ",
896 )
897 .bind(token.chain.chain_id as i32)
898 .bind(token.address.to_string())
899 .bind(token.name.as_str())
900 .bind(token.symbol.as_str())
901 .bind(i32::from(token.decimals))
902 .execute(&self.pool)
903 .await
904 .map(|_| ())
905 .map_err(|e| anyhow::anyhow!("Failed to insert into token table: {e}"))
906 }
907
908 pub async fn add_invalid_token(
914 &self,
915 chain_id: u32,
916 address: &Address,
917 error_string: &str,
918 ) -> anyhow::Result<()> {
919 sqlx::query(
920 "
921 INSERT INTO token (
922 chain_id, address, error
923 ) VALUES ($1, $2, $3)
924 ON CONFLICT (chain_id, address)
925 DO NOTHING;
926 ",
927 )
928 .bind(chain_id as i32)
929 .bind(address.to_string())
930 .bind(error_string)
931 .execute(&self.pool)
932 .await
933 .map(|_| ())
934 .map_err(|e| anyhow::anyhow!("Failed to insert into token table: {e}"))
935 }
936
937 pub async fn add_swap(&self, chain_id: u32, swap: &PoolSwap) -> anyhow::Result<()> {
943 let (order_side, base_quantity, quote_quantity, spot_price, execution_price) =
945 if let Some(ref trade_info) = swap.trade_info {
946 (
947 Some(trade_info.order_side.to_string()),
948 Some(trade_info.quantity_base.as_decimal()),
949 Some(trade_info.quantity_quote.as_decimal()),
950 Some(trade_info.spot_price.as_decimal()),
951 Some(trade_info.execution_price.as_decimal()),
952 )
953 } else {
954 (None, None, None, None, None)
955 };
956
957 sqlx::query(
958 "
959 INSERT INTO pool_swap_event (
960 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
961 log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
962 order_side, base_quantity, quote_quantity, spot_price, execution_price
963 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::U160, $11::U128, $12, $13::I256, $14::I256, $15, $16, $17, $18, $19)
964 ON CONFLICT (chain_id, transaction_hash, log_index)
965 DO NOTHING
966 ",
967 )
968 .bind(chain_id as i32)
969 .bind(swap.dex.name.to_string())
970 .bind(swap.pool_identifier.as_str())
971 .bind(swap.block as i64)
972 .bind(swap.transaction_hash.as_str())
973 .bind(swap.transaction_index as i32)
974 .bind(swap.log_index as i32)
975 .bind(swap.sender.to_string())
976 .bind(swap.recipient.to_string())
977 .bind(swap.sqrt_price_x96.to_string())
978 .bind(swap.liquidity.to_string())
979 .bind(swap.tick)
980 .bind(swap.amount0.to_string())
981 .bind(swap.amount1.to_string())
982 .bind(order_side)
983 .bind(base_quantity)
984 .bind(quote_quantity)
985 .bind(spot_price)
986 .bind(execution_price)
987 .execute(&self.pool)
988 .await
989 .map(|_| ())
990 .map_err(|e| anyhow::anyhow!("Failed to insert into pool_swap table: {e}"))
991 }
992
993 pub async fn add_pool_liquidity_update(
999 &self,
1000 chain_id: u32,
1001 liquidity_update: &PoolLiquidityUpdate,
1002 ) -> anyhow::Result<()> {
1003 sqlx::query(
1004 "
1005 INSERT INTO pool_liquidity_event (
1006 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index, log_index,
1007 event_type, sender, owner, position_liquidity, amount0, amount1, tick_lower, tick_upper
1008 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
1009 ON CONFLICT (chain_id, transaction_hash, log_index)
1010 DO NOTHING
1011 ",
1012 )
1013 .bind(chain_id as i32)
1014 .bind(liquidity_update.dex.name.to_string())
1015 .bind(liquidity_update.pool_identifier.as_str())
1016 .bind(liquidity_update.block as i64)
1017 .bind(liquidity_update.transaction_hash.as_str())
1018 .bind(liquidity_update.transaction_index as i32)
1019 .bind(liquidity_update.log_index as i32)
1020 .bind(liquidity_update.kind.to_string())
1021 .bind(liquidity_update.sender.map(|sender| sender.to_string()))
1022 .bind(liquidity_update.owner.to_string())
1023 .bind(U128Pg(liquidity_update.position_liquidity))
1024 .bind(U256Pg(liquidity_update.amount0))
1025 .bind(U256Pg(liquidity_update.amount1))
1026 .bind(liquidity_update.tick_lower)
1027 .bind(liquidity_update.tick_upper)
1028 .execute(&self.pool)
1029 .await
1030 .map(|_| ())
1031 .map_err(|e| anyhow::anyhow!("Failed to insert into pool_liquidity table: {e}"))
1032 }
1033
1034 pub async fn load_tokens(&self, chain: SharedChain) -> anyhow::Result<Vec<Token>> {
1043 sqlx::query_as::<_, TokenRow>("SELECT * FROM token WHERE chain_id = $1 AND error IS NULL")
1044 .bind(chain.chain_id as i32)
1045 .fetch_all(&self.pool)
1046 .await
1047 .map(|rows| {
1048 rows.into_iter()
1049 .map(|token_row| {
1050 Token::new(
1051 chain.clone(),
1052 token_row.address,
1053 token_row.name,
1054 token_row.symbol,
1055 token_row.decimals as u8,
1056 )
1057 })
1058 .collect::<Vec<_>>()
1059 })
1060 .map_err(|e| anyhow::anyhow!("Failed to load tokens: {e}"))
1061 }
1062
1063 pub async fn load_invalid_token_addresses(
1069 &self,
1070 chain_id: u32,
1071 ) -> anyhow::Result<Vec<Address>> {
1072 sqlx::query_as::<_, (String,)>(
1073 "SELECT address FROM token WHERE chain_id = $1 AND error IS NOT NULL",
1074 )
1075 .bind(chain_id as i32)
1076 .fetch_all(&self.pool)
1077 .await?
1078 .into_iter()
1079 .map(|(address,)| validate_address(&address))
1080 .collect::<Result<Vec<_>, _>>()
1081 .map_err(|e| anyhow::anyhow!("Failed to load invalid token addresses: {e}"))
1082 }
1083
1084 pub async fn load_pools(
1090 &self,
1091 chain: SharedChain,
1092 dex_id: &str,
1093 ) -> anyhow::Result<Vec<PoolRow>> {
1094 sqlx::query_as::<_, PoolRow>(AssertSqlSafe(format!(
1095 "SELECT {POOL_ROW_COLUMNS} FROM pool WHERE chain_id = $1 AND dex_name = $2 ORDER BY creation_block ASC"
1096 )))
1097 .bind(chain.chain_id as i32)
1098 .bind(dex_id)
1099 .fetch_all(&self.pool)
1100 .await
1101 .map_err(|e| anyhow::anyhow!("Failed to load pools: {e}"))
1102 }
1103
1104 pub async fn load_pool(
1115 &self,
1116 chain: SharedChain,
1117 dex_id: &str,
1118 pool_identifier: &PoolIdentifier,
1119 ) -> anyhow::Result<Option<PoolRow>> {
1120 sqlx::query_as::<_, PoolRow>(AssertSqlSafe(format!(
1121 "SELECT {POOL_ROW_COLUMNS} FROM pool WHERE chain_id = $1 AND dex_name = $2 AND pool_identifier = $3"
1122 )))
1123 .bind(chain.chain_id as i32)
1124 .bind(dex_id)
1125 .bind(pool_identifier.as_ref())
1126 .fetch_optional(&self.pool)
1127 .await
1128 .map_err(|e| anyhow::anyhow!("Failed to load pool {pool_identifier}: {e}"))
1129 }
1130
1131 pub async fn toggle_perf_sync_settings(&self, enable: bool) -> anyhow::Result<()> {
1145 if enable {
1146 log::debug!("Enabling performance sync settings for bulk operations");
1147
1148 sqlx::query("SET synchronous_commit = OFF")
1150 .execute(&self.pool)
1151 .await
1152 .map_err(|e| anyhow::anyhow!("Failed to set synchronous_commit OFF: {e}"))?;
1153
1154 sqlx::query("SET work_mem = '256MB'")
1156 .execute(&self.pool)
1157 .await
1158 .map_err(|e| anyhow::anyhow!("Failed to set work_mem: {e}"))?;
1159
1160 log::debug!("Performance settings enabled: synchronous_commit=OFF, work_mem=256MB");
1161 } else {
1162 log::debug!("Restoring default safe database performance settings");
1163
1164 sqlx::query("SET synchronous_commit = ON")
1166 .execute(&self.pool)
1167 .await
1168 .map_err(|e| anyhow::anyhow!("Failed to set synchronous_commit ON: {e}"))?;
1169
1170 sqlx::query("RESET work_mem")
1172 .execute(&self.pool)
1173 .await
1174 .map_err(|e| anyhow::anyhow!("Failed to reset work_mem: {e}"))?;
1175 }
1176
1177 Ok(())
1178 }
1179
1180 pub async fn update_dex_last_synced_block(
1186 &self,
1187 chain_id: u32,
1188 dex: &DexType,
1189 block_number: u64,
1190 ) -> anyhow::Result<()> {
1191 sqlx::query(
1192 "
1193 UPDATE dex
1194 SET last_full_sync_pools_block_number = $3
1195 WHERE chain_id = $1 AND name = $2
1196 ",
1197 )
1198 .bind(chain_id as i32)
1199 .bind(dex.to_string())
1200 .bind(block_number as i64)
1201 .execute(&self.pool)
1202 .await
1203 .map(|_| ())
1204 .map_err(|e| anyhow::anyhow!("Failed to update dex last synced block: {e}"))
1205 }
1206
1207 pub async fn update_pool_last_synced_block(
1213 &self,
1214 chain_id: u32,
1215 dex: &DexType,
1216 pool_identifier: &PoolIdentifier,
1217 block_number: u64,
1218 ) -> anyhow::Result<()> {
1219 sqlx::query(
1220 "
1221 UPDATE pool
1222 SET last_full_sync_block_number = $4
1223 WHERE chain_id = $1
1224 AND dex_name = $2
1225 AND pool_identifier = $3
1226 ",
1227 )
1228 .bind(chain_id as i32)
1229 .bind(dex.to_string())
1230 .bind(pool_identifier.as_ref())
1231 .bind(block_number as i64)
1232 .execute(&self.pool)
1233 .await
1234 .map(|_| ())
1235 .map_err(|e| anyhow::anyhow!("Failed to update pool last synced block: {e}"))
1236 }
1237
1238 pub async fn get_dex_last_synced_block(
1244 &self,
1245 chain_id: u32,
1246 dex: &DexType,
1247 ) -> anyhow::Result<Option<u64>> {
1248 let result = sqlx::query_as::<_, (Option<i64>,)>(
1249 "
1250 SELECT
1251 last_full_sync_pools_block_number
1252 FROM dex
1253 WHERE chain_id = $1
1254 AND name = $2
1255 ",
1256 )
1257 .bind(chain_id as i32)
1258 .bind(dex.to_string())
1259 .fetch_optional(&self.pool)
1260 .await
1261 .map_err(|e| anyhow::anyhow!("Failed to get dex last synced block: {e}"))?;
1262
1263 Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1264 }
1265
1266 pub async fn get_pool_last_synced_block(
1272 &self,
1273 chain_id: u32,
1274 dex: &DexType,
1275 pool_identifier: &PoolIdentifier,
1276 ) -> anyhow::Result<Option<u64>> {
1277 let result = sqlx::query_as::<_, (Option<i64>,)>(
1278 "
1279 SELECT
1280 last_full_sync_block_number
1281 FROM pool
1282 WHERE chain_id = $1
1283 AND dex_name = $2
1284 AND pool_identifier = $3
1285 ",
1286 )
1287 .bind(chain_id as i32)
1288 .bind(dex.to_string())
1289 .bind(pool_identifier.as_ref())
1290 .fetch_optional(&self.pool)
1291 .await
1292 .map_err(|e| anyhow::anyhow!("Failed to get pool last synced block: {e}"))?;
1293
1294 Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1295 }
1296
1297 pub async fn get_table_last_block(
1304 &self,
1305 chain_id: u32,
1306 table_name: &str,
1307 pool_identifier: &PoolIdentifier,
1308 ) -> anyhow::Result<Option<u64>> {
1309 let query = format!(
1310 "SELECT MAX(block) FROM {table_name} WHERE chain_id = $1 AND pool_identifier = $2"
1311 );
1312 let result = sqlx::query_as::<_, (Option<i64>,)>(AssertSqlSafe(query))
1313 .bind(chain_id as i32)
1314 .bind(pool_identifier.as_ref())
1315 .fetch_optional(&self.pool)
1316 .await
1317 .map_err(|e| anyhow::anyhow!("Failed to get table last block for {table_name}: {e}"))?;
1318
1319 Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1320 }
1321
1322 pub async fn add_pool_collects_batch(
1328 &self,
1329 chain_id: u32,
1330 collects: &[PoolFeeCollect],
1331 ) -> anyhow::Result<()> {
1332 if collects.is_empty() {
1333 return Ok(());
1334 }
1335
1336 let len = collects.len();
1338 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1339 let mut dex_names: Vec<String> = Vec::with_capacity(len);
1340 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1341 let mut blocks: Vec<i64> = Vec::with_capacity(len);
1342 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1343 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1344 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1345 let mut owners: Vec<String> = Vec::with_capacity(len);
1346 let mut amount0s: Vec<String> = Vec::with_capacity(len);
1347 let mut amount1s: Vec<String> = Vec::with_capacity(len);
1348 let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
1349 let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
1350
1351 for collect in collects {
1353 chain_ids.push(chain_id as i32);
1354 dex_names.push(collect.dex.name.to_string());
1355 pool_identifiers.push(collect.pool_identifier.to_string());
1356 blocks.push(collect.block as i64);
1357 transaction_hashes.push(collect.transaction_hash.clone());
1358 transaction_indices.push(collect.transaction_index as i32);
1359 log_indices.push(collect.log_index as i32);
1360 owners.push(collect.owner.to_string());
1361 amount0s.push(collect.amount0.to_string());
1362 amount1s.push(collect.amount1.to_string());
1363 tick_lowers.push(collect.tick_lower);
1364 tick_uppers.push(collect.tick_upper);
1365 }
1366
1367 sqlx::query(
1369 "
1370 INSERT INTO pool_collect_event (
1371 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1372 log_index, owner, amount0, amount1, tick_lower, tick_upper
1373 )
1374 SELECT
1375 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1376 log_index, owner, amount0::U256, amount1::U256, tick_lower, tick_upper
1377 FROM UNNEST(
1378 $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1379 $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::INT[], $12::INT[]
1380 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1381 log_index, owner, amount0, amount1, tick_lower, tick_upper)
1382 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1383 ",
1384 )
1385 .bind(&chain_ids[..])
1386 .bind(&dex_names[..])
1387 .bind(&pool_identifiers[..])
1388 .bind(&blocks[..])
1389 .bind(&transaction_hashes[..])
1390 .bind(&transaction_indices[..])
1391 .bind(&log_indices[..])
1392 .bind(&owners[..])
1393 .bind(&amount0s[..])
1394 .bind(&amount1s[..])
1395 .bind(&tick_lowers[..])
1396 .bind(&tick_uppers[..])
1397 .execute(&self.pool)
1398 .await
1399 .map(|_| ())
1400 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_fee_collect table: {e}"))
1401 }
1402
1403 pub async fn add_pool_flash_batch(
1409 &self,
1410 chain_id: u32,
1411 flash_events: &[PoolFlash],
1412 ) -> anyhow::Result<()> {
1413 if flash_events.is_empty() {
1414 return Ok(());
1415 }
1416
1417 let len = flash_events.len();
1419 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1420 let mut dex_names: Vec<String> = Vec::with_capacity(len);
1421 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1422 let mut blocks: Vec<i64> = Vec::with_capacity(len);
1423 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1424 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1425 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1426 let mut senders: Vec<String> = Vec::with_capacity(len);
1427 let mut recipients: Vec<String> = Vec::with_capacity(len);
1428 let mut amount0s: Vec<String> = Vec::with_capacity(len);
1429 let mut amount1s: Vec<String> = Vec::with_capacity(len);
1430 let mut paid0s: Vec<String> = Vec::with_capacity(len);
1431 let mut paid1s: Vec<String> = Vec::with_capacity(len);
1432
1433 for flash in flash_events {
1435 chain_ids.push(chain_id as i32);
1436 dex_names.push(flash.dex.name.to_string());
1437 pool_identifiers.push(flash.pool_identifier.to_string());
1438 blocks.push(flash.block as i64);
1439 transaction_hashes.push(flash.transaction_hash.clone());
1440 transaction_indices.push(flash.transaction_index as i32);
1441 log_indices.push(flash.log_index as i32);
1442 senders.push(flash.sender.to_string());
1443 recipients.push(flash.recipient.to_string());
1444 amount0s.push(flash.amount0.to_string());
1445 amount1s.push(flash.amount1.to_string());
1446 paid0s.push(flash.paid0.to_string());
1447 paid1s.push(flash.paid1.to_string());
1448 }
1449
1450 sqlx::query(
1452 "
1453 INSERT INTO pool_flash_event (
1454 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1455 log_index, sender, recipient, amount0, amount1, paid0, paid1
1456 )
1457 SELECT
1458 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1459 log_index, sender, recipient, amount0::U256, amount1::U256, paid0::U256, paid1::U256
1460 FROM UNNEST(
1461 $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1462 $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[], $12::TEXT[], $13::TEXT[]
1463 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1464 log_index, sender, recipient, amount0, amount1, paid0, paid1)
1465 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1466 ",
1467 )
1468 .bind(&chain_ids[..])
1469 .bind(&dex_names[..])
1470 .bind(&pool_identifiers[..])
1471 .bind(&blocks[..])
1472 .bind(&transaction_hashes[..])
1473 .bind(&transaction_indices[..])
1474 .bind(&log_indices[..])
1475 .bind(&senders[..])
1476 .bind(&recipients[..])
1477 .bind(&amount0s[..])
1478 .bind(&amount1s[..])
1479 .bind(&paid0s[..])
1480 .bind(&paid1s[..])
1481 .execute(&self.pool)
1482 .await
1483 .map(|_| ())
1484 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_flash_event table: {e}"))
1485 }
1486
1487 pub async fn add_pool_fee_protocol_updates_batch(
1493 &self,
1494 chain_id: u32,
1495 updates: &[PoolFeeProtocolUpdate],
1496 ) -> anyhow::Result<()> {
1497 if updates.is_empty() {
1498 return Ok(());
1499 }
1500
1501 let len = updates.len();
1503 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1504 let mut dex_names: Vec<String> = Vec::with_capacity(len);
1505 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1506 let mut blocks: Vec<i64> = Vec::with_capacity(len);
1507 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1508 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1509 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1510 let mut fee_protocol0s: Vec<i16> = Vec::with_capacity(len);
1511 let mut fee_protocol1s: Vec<i16> = Vec::with_capacity(len);
1512
1513 for update in updates {
1515 chain_ids.push(chain_id as i32);
1516 dex_names.push(update.dex.name.to_string());
1517 pool_identifiers.push(update.pool_identifier.to_string());
1518 blocks.push(update.block as i64);
1519 transaction_hashes.push(update.transaction_hash.clone());
1520 transaction_indices.push(update.transaction_index as i32);
1521 log_indices.push(update.log_index as i32);
1522 fee_protocol0s.push(i16::from(update.fee_protocol0_new));
1523 fee_protocol1s.push(i16::from(update.fee_protocol1_new));
1524 }
1525
1526 sqlx::query(
1528 "
1529 INSERT INTO pool_fee_protocol_update_event (
1530 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1531 log_index, fee_protocol0_new, fee_protocol1_new
1532 )
1533 SELECT
1534 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1535 log_index, fee_protocol0_new, fee_protocol1_new
1536 FROM UNNEST(
1537 $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1538 $7::INT[], $8::SMALLINT[], $9::SMALLINT[]
1539 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1540 log_index, fee_protocol0_new, fee_protocol1_new)
1541 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1542 ",
1543 )
1544 .bind(&chain_ids[..])
1545 .bind(&dex_names[..])
1546 .bind(&pool_identifiers[..])
1547 .bind(&blocks[..])
1548 .bind(&transaction_hashes[..])
1549 .bind(&transaction_indices[..])
1550 .bind(&log_indices[..])
1551 .bind(&fee_protocol0s[..])
1552 .bind(&fee_protocol1s[..])
1553 .execute(&self.pool)
1554 .await
1555 .map(|_| ())
1556 .map_err(|e| {
1557 anyhow::anyhow!("Failed to batch insert into pool_fee_protocol_update_event table: {e}")
1558 })
1559 }
1560
1561 pub async fn add_pool_fee_protocol_collect_batch(
1567 &self,
1568 chain_id: u32,
1569 collects: &[PoolFeeProtocolCollect],
1570 ) -> anyhow::Result<()> {
1571 if collects.is_empty() {
1572 return Ok(());
1573 }
1574
1575 let len = collects.len();
1577 let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1578 let mut dex_names: Vec<String> = Vec::with_capacity(len);
1579 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1580 let mut blocks: Vec<i64> = Vec::with_capacity(len);
1581 let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1582 let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1583 let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1584 let mut senders: Vec<String> = Vec::with_capacity(len);
1585 let mut recipients: Vec<String> = Vec::with_capacity(len);
1586 let mut amount0s: Vec<String> = Vec::with_capacity(len);
1587 let mut amount1s: Vec<String> = Vec::with_capacity(len);
1588
1589 for collect in collects {
1591 chain_ids.push(chain_id as i32);
1592 dex_names.push(collect.dex.name.to_string());
1593 pool_identifiers.push(collect.pool_identifier.to_string());
1594 blocks.push(collect.block as i64);
1595 transaction_hashes.push(collect.transaction_hash.clone());
1596 transaction_indices.push(collect.transaction_index as i32);
1597 log_indices.push(collect.log_index as i32);
1598 senders.push(collect.sender.to_string());
1599 recipients.push(collect.recipient.to_string());
1600 amount0s.push(collect.amount0.to_string());
1601 amount1s.push(collect.amount1.to_string());
1602 }
1603
1604 sqlx::query(
1606 "
1607 INSERT INTO pool_fee_protocol_collect_event (
1608 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1609 log_index, sender, recipient, amount0, amount1
1610 )
1611 SELECT
1612 chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1613 log_index, sender, recipient, amount0::U256, amount1::U256
1614 FROM UNNEST(
1615 $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1616 $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[]
1617 ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1618 log_index, sender, recipient, amount0, amount1)
1619 ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1620 ",
1621 )
1622 .bind(&chain_ids[..])
1623 .bind(&dex_names[..])
1624 .bind(&pool_identifiers[..])
1625 .bind(&blocks[..])
1626 .bind(&transaction_hashes[..])
1627 .bind(&transaction_indices[..])
1628 .bind(&log_indices[..])
1629 .bind(&senders[..])
1630 .bind(&recipients[..])
1631 .bind(&amount0s[..])
1632 .bind(&amount1s[..])
1633 .execute(&self.pool)
1634 .await
1635 .map(|_| ())
1636 .map_err(|e| {
1637 anyhow::anyhow!(
1638 "Failed to batch insert into pool_fee_protocol_collect_event table: {e}"
1639 )
1640 })
1641 }
1642
1643 pub async fn add_pool_snapshot(
1649 &self,
1650 chain_id: u32,
1651 dex_name: &DexType,
1652 pool_identifier: &PoolIdentifier,
1653 snapshot: &PoolSnapshot,
1654 ) -> anyhow::Result<()> {
1655 sqlx::query(
1656 "
1657 INSERT INTO pool_snapshot (
1658 chain_id, dex_name, pool_identifier, block, transaction_index, log_index, transaction_hash,
1659 current_tick, price_sqrt_ratio_x96, liquidity,
1660 protocol_fees_token0, protocol_fees_token1, fee_protocol,
1661 fee_growth_global_0, fee_growth_global_1,
1662 total_amount0_deposited, total_amount1_deposited,
1663 total_amount0_collected, total_amount1_collected,
1664 total_swaps, total_mints, total_burns, total_fee_collects, total_flashes,
1665 liquidity_utilization_rate
1666 ) VALUES (
1667 $1, $2, $3, $4, $5, $6, $7,
1668 $8, $9::U160, $10::U128, $11::U256, $12::U256, $13,
1669 $14::U256, $15::U256, $16::U256, $17::U256, $18::U256, $19::U256,
1670 $20, $21, $22, $23, $24, $25
1671 )
1672 ON CONFLICT (chain_id, pool_identifier, block, transaction_index, log_index)
1673 DO NOTHING
1674 ",
1675 )
1676 .bind(chain_id as i32)
1677 .bind(dex_name.to_string())
1678 .bind(pool_identifier.as_ref())
1679 .bind(snapshot.block_position.number as i64)
1680 .bind(snapshot.block_position.transaction_index as i32)
1681 .bind(snapshot.block_position.log_index as i32)
1682 .bind(snapshot.block_position.transaction_hash.clone())
1683 .bind(snapshot.state.current_tick)
1684 .bind(snapshot.state.price_sqrt_ratio_x96.to_string())
1685 .bind(snapshot.state.liquidity.to_string())
1686 .bind(snapshot.state.protocol_fees_token0.to_string())
1687 .bind(snapshot.state.protocol_fees_token1.to_string())
1688 .bind(snapshot.state.fee_protocol as i16)
1689 .bind(snapshot.state.fee_growth_global_0.to_string())
1690 .bind(snapshot.state.fee_growth_global_1.to_string())
1691 .bind(snapshot.analytics.total_amount0_deposited.to_string())
1692 .bind(snapshot.analytics.total_amount1_deposited.to_string())
1693 .bind(snapshot.analytics.total_amount0_collected.to_string())
1694 .bind(snapshot.analytics.total_amount1_collected.to_string())
1695 .bind(snapshot.analytics.total_swaps as i32)
1696 .bind(snapshot.analytics.total_mints as i32)
1697 .bind(snapshot.analytics.total_burns as i32)
1698 .bind(snapshot.analytics.total_fee_collects as i32)
1699 .bind(snapshot.analytics.total_flashes as i32)
1700 .bind(snapshot.analytics.liquidity_utilization_rate)
1701 .execute(&self.pool)
1702 .await
1703 .map(|_| ())
1704 .map_err(|e| anyhow::anyhow!("Failed to insert into pool_snapshot table: {e}"))
1705 }
1706
1707 pub async fn add_pool_positions_batch(
1713 &self,
1714 chain_id: u32,
1715 snapshot_block: u64,
1716 snapshot_transaction_index: u32,
1717 snapshot_log_index: u32,
1718 positions: &[(PoolIdentifier, PoolPosition)],
1719 ) -> anyhow::Result<()> {
1720 if positions.is_empty() {
1721 return Ok(());
1722 }
1723
1724 let len = positions.len();
1726 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1727 let mut owners: Vec<String> = Vec::with_capacity(len);
1728 let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
1729 let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
1730 let mut liquidities: Vec<String> = Vec::with_capacity(len);
1731 let mut fee_growth_inside_0_lasts: Vec<String> = Vec::with_capacity(len);
1732 let mut fee_growth_inside_1_lasts: Vec<String> = Vec::with_capacity(len);
1733 let mut tokens_owed_0s: Vec<String> = Vec::with_capacity(len);
1734 let mut tokens_owed_1s: Vec<String> = Vec::with_capacity(len);
1735 let mut total_amount0_depositeds: Vec<Option<String>> = Vec::with_capacity(len);
1736 let mut total_amount1_depositeds: Vec<Option<String>> = Vec::with_capacity(len);
1737 let mut total_amount0_collecteds: Vec<Option<String>> = Vec::with_capacity(len);
1738 let mut total_amount1_collecteds: Vec<Option<String>> = Vec::with_capacity(len);
1739
1740 for (pool_address, position) in positions {
1742 pool_identifiers.push(pool_address.to_string());
1743 owners.push(position.owner.to_string());
1744 tick_lowers.push(position.tick_lower);
1745 tick_uppers.push(position.tick_upper);
1746 liquidities.push(position.liquidity.to_string());
1747 fee_growth_inside_0_lasts.push(position.fee_growth_inside_0_last.to_string());
1748 fee_growth_inside_1_lasts.push(position.fee_growth_inside_1_last.to_string());
1749 tokens_owed_0s.push(position.tokens_owed_0.to_string());
1750 tokens_owed_1s.push(position.tokens_owed_1.to_string());
1751 total_amount0_depositeds.push(Some(position.total_amount0_deposited.to_string()));
1752 total_amount1_depositeds.push(Some(position.total_amount1_deposited.to_string()));
1753 total_amount0_collecteds.push(Some(position.total_amount0_collected.to_string()));
1754 total_amount1_collecteds.push(Some(position.total_amount1_collected.to_string()));
1755 }
1756
1757 sqlx::query(
1759 "
1760 INSERT INTO pool_position (
1761 chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index,
1762 owner, tick_lower, tick_upper,
1763 liquidity, fee_growth_inside_0_last, fee_growth_inside_1_last,
1764 tokens_owed_0, tokens_owed_1,
1765 total_amount0_deposited, total_amount1_deposited,
1766 total_amount0_collected, total_amount1_collected
1767 )
1768 SELECT
1769 $1, pool_identifier, $2, $3, $4,
1770 owner, tick_lower, tick_upper,
1771 liquidity::U128, fee_growth_inside_0_last::U256, fee_growth_inside_1_last::U256,
1772 tokens_owed_0::U128, tokens_owed_1::U128,
1773 total_amount0_deposited::U256, total_amount1_deposited::U256,
1774 total_amount0_collected::U128, total_amount1_collected::U128
1775 FROM UNNEST(
1776 $5::TEXT[], $6::TEXT[], $7::INT[], $8::INT[], $9::TEXT[], $10::TEXT[],
1777 $11::TEXT[], $12::TEXT[], $13::TEXT[], $14::TEXT[], $15::TEXT[],
1778 $16::TEXT[], $17::TEXT[]
1779 ) AS t(pool_identifier, owner, tick_lower, tick_upper,
1780 liquidity, fee_growth_inside_0_last, fee_growth_inside_1_last,
1781 tokens_owed_0, tokens_owed_1,
1782 total_amount0_deposited, total_amount1_deposited,
1783 total_amount0_collected, total_amount1_collected)
1784 ON CONFLICT (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, owner, tick_lower, tick_upper)
1785 DO NOTHING
1786 ",
1787 )
1788 .bind(chain_id as i32)
1789 .bind(snapshot_block as i64)
1790 .bind(snapshot_transaction_index as i32)
1791 .bind(snapshot_log_index as i32)
1792 .bind(&pool_identifiers[..])
1793 .bind(&owners[..])
1794 .bind(&tick_lowers[..])
1795 .bind(&tick_uppers[..])
1796 .bind(&liquidities[..])
1797 .bind(&fee_growth_inside_0_lasts[..])
1798 .bind(&fee_growth_inside_1_lasts[..])
1799 .bind(&tokens_owed_0s[..])
1800 .bind(&tokens_owed_1s[..])
1801 .bind(&total_amount0_depositeds as &[Option<String>])
1802 .bind(&total_amount1_depositeds as &[Option<String>])
1803 .bind(&total_amount0_collecteds as &[Option<String>])
1804 .bind(&total_amount1_collecteds as &[Option<String>])
1805 .execute(&self.pool)
1806 .await
1807 .map(|_| ())
1808 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_position table: {e}"))
1809 }
1810
1811 pub async fn add_pool_ticks_batch(
1817 &self,
1818 chain_id: u32,
1819 snapshot_block: u64,
1820 snapshot_transaction_index: u32,
1821 snapshot_log_index: u32,
1822 ticks: &[(PoolIdentifier, &PoolTick)],
1823 ) -> anyhow::Result<()> {
1824 if ticks.is_empty() {
1825 return Ok(());
1826 }
1827
1828 let len = ticks.len();
1830 let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1831 let mut tick_values: Vec<i32> = Vec::with_capacity(len);
1832 let mut liquidity_grosses: Vec<String> = Vec::with_capacity(len);
1833 let mut liquidity_nets: Vec<String> = Vec::with_capacity(len);
1834 let mut fee_growth_outside_0s: Vec<String> = Vec::with_capacity(len);
1835 let mut fee_growth_outside_1s: Vec<String> = Vec::with_capacity(len);
1836 let mut initializeds: Vec<bool> = Vec::with_capacity(len);
1837 let mut last_updated_blocks: Vec<i64> = Vec::with_capacity(len);
1838
1839 for (pool_address, tick) in ticks {
1841 pool_identifiers.push(pool_address.to_string());
1842 tick_values.push(tick.value);
1843 liquidity_grosses.push(tick.liquidity_gross.to_string());
1844 liquidity_nets.push(tick.liquidity_net.to_string());
1845 fee_growth_outside_0s.push(tick.fee_growth_outside_0.to_string());
1846 fee_growth_outside_1s.push(tick.fee_growth_outside_1.to_string());
1847 initializeds.push(tick.initialized);
1848 last_updated_blocks.push(tick.last_updated_block as i64);
1849 }
1850
1851 sqlx::query(
1853 "
1854 INSERT INTO pool_tick (
1855 chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index,
1856 tick_value, liquidity_gross, liquidity_net,
1857 fee_growth_outside_0, fee_growth_outside_1, initialized, last_updated_block
1858 )
1859 SELECT
1860 $1, pool_identifier, $2, $3, $4,
1861 tick_value, liquidity_gross::U128, liquidity_net::I128,
1862 fee_growth_outside_0::U256, fee_growth_outside_1::U256, initialized, last_updated_block
1863 FROM UNNEST(
1864 $5::TEXT[], $6::INT[], $7::TEXT[], $8::TEXT[], $9::TEXT[],
1865 $10::TEXT[], $11::BOOLEAN[], $12::BIGINT[]
1866 ) AS t(pool_identifier, tick_value, liquidity_gross, liquidity_net,
1867 fee_growth_outside_0, fee_growth_outside_1, initialized, last_updated_block)
1868 ON CONFLICT (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, tick_value)
1869 DO NOTHING
1870 ",
1871 )
1872 .bind(chain_id as i32)
1873 .bind(snapshot_block as i64)
1874 .bind(snapshot_transaction_index as i32)
1875 .bind(snapshot_log_index as i32)
1876 .bind(&pool_identifiers[..])
1877 .bind(&tick_values[..])
1878 .bind(&liquidity_grosses[..])
1879 .bind(&liquidity_nets[..])
1880 .bind(&fee_growth_outside_0s[..])
1881 .bind(&fee_growth_outside_1s[..])
1882 .bind(&initializeds[..])
1883 .bind(&last_updated_blocks[..])
1884 .execute(&self.pool)
1885 .await
1886 .map(|_| ())
1887 .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_tick table: {e}"))
1888 }
1889
1890 pub async fn update_pool_initial_price_tick(
1896 &self,
1897 chain_id: u32,
1898 initialize_event: &InitializeEvent,
1899 ) -> anyhow::Result<()> {
1900 sqlx::query(
1901 "
1902 UPDATE pool
1903 SET
1904 initial_tick = $4,
1905 initial_sqrt_price_x96 = $5
1906 WHERE chain_id = $1
1907 AND dex_name = $2
1908 AND pool_identifier = $3
1909 ",
1910 )
1911 .bind(chain_id as i32)
1912 .bind(initialize_event.dex.name.to_string())
1913 .bind(initialize_event.pool_identifier.as_ref())
1914 .bind(initialize_event.tick)
1915 .bind(initialize_event.sqrt_price_x96.to_string())
1916 .execute(&self.pool)
1917 .await
1918 .map(|_| ())
1919 .map_err(|e| anyhow::anyhow!("Failed to update pool initial price and tick: {e}"))
1920 }
1921
1922 pub async fn load_latest_valid_pool_snapshot(
1931 &self,
1932 chain_id: u32,
1933 pool_identifier: &PoolIdentifier,
1934 ) -> anyhow::Result<Option<PoolSnapshot>> {
1935 self.load_latest_pool_snapshot(chain_id, pool_identifier, None, true)
1936 .await
1937 }
1938
1939 pub async fn load_latest_pool_snapshot(
1950 &self,
1951 chain_id: u32,
1952 pool_identifier: &PoolIdentifier,
1953 max_block: Option<u64>,
1954 require_valid: bool,
1955 ) -> anyhow::Result<Option<PoolSnapshot>> {
1956 let allow_invalid = !require_valid;
1957 let result = sqlx::query(
1958 "
1959 SELECT
1960 block, transaction_index, log_index, transaction_hash,
1961 current_tick, price_sqrt_ratio_x96::TEXT, liquidity::TEXT,
1962 protocol_fees_token0::TEXT, protocol_fees_token1::TEXT, fee_protocol,
1963 fee_growth_global_0::TEXT, fee_growth_global_1::TEXT,
1964 total_amount0_deposited::TEXT, total_amount1_deposited::TEXT,
1965 total_amount0_collected::TEXT, total_amount1_collected::TEXT,
1966 total_swaps, total_mints, total_burns, total_fee_collects, total_flashes,
1967 liquidity_utilization_rate,
1968 (SELECT dex_name FROM pool WHERE chain_id = $1 AND address = $2) as dex_name,
1969 COALESCE(
1970 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_snapshot.chain_id AND block.number = pool_snapshot.block),
1971 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_snapshot.chain_id AND pool_event_block.number = pool_snapshot.block)
1972 ) as block_timestamp
1973 FROM pool_snapshot
1974 WHERE chain_id = $1 AND pool_identifier = $2
1975 AND ($3::BIGINT IS NULL OR block <= $3)
1976 AND ($4 OR validation_state <> 'invalid')
1977 ORDER BY block DESC, transaction_index DESC, log_index DESC
1978 LIMIT 1
1979 ",
1980 )
1981 .bind(chain_id as i32)
1982 .bind(pool_identifier.as_ref())
1983 .bind(max_block.map(|b| b as i64))
1984 .bind(allow_invalid)
1985 .fetch_optional(&self.pool)
1986 .await
1987 .map_err(|e| anyhow::anyhow!("Failed to load latest valid pool snapshot: {e}"))?;
1988
1989 if let Some(row) = result {
1990 let block: i64 = row.get("block");
1992 let transaction_index: i32 = row.get("transaction_index");
1993 let log_index: i32 = row.get("log_index");
1994 let transaction_hash: String = row.get("transaction_hash");
1995 let block_timestamp = row
1996 .try_get::<Option<String>, _>("block_timestamp")?
1997 .ok_or_else(|| {
1998 anyhow::anyhow!(
1999 "Missing block timestamp for pool snapshot {} at block {}",
2000 pool_identifier,
2001 block
2002 )
2003 })?;
2004 let timestamp = parse_cached_block_timestamp(&block_timestamp)
2005 .map_err(|e| anyhow::anyhow!("Invalid block timestamp '{block_timestamp}': {e}"))?;
2006
2007 let block_position = BlockPosition::new(
2008 block as u64,
2009 transaction_hash,
2010 transaction_index as u32,
2011 log_index as u32,
2012 );
2013
2014 let state = PoolState {
2015 current_tick: row.get("current_tick"),
2016 price_sqrt_ratio_x96: row.get::<String, _>("price_sqrt_ratio_x96").parse()?,
2017 liquidity: row.get::<String, _>("liquidity").parse()?,
2018 protocol_fees_token0: row.get::<String, _>("protocol_fees_token0").parse()?,
2019 protocol_fees_token1: row.get::<String, _>("protocol_fees_token1").parse()?,
2020 fee_protocol: row.get::<i16, _>("fee_protocol") as u8,
2021 fee_growth_global_0: row.get::<String, _>("fee_growth_global_0").parse()?,
2022 fee_growth_global_1: row.get::<String, _>("fee_growth_global_1").parse()?,
2023 };
2024
2025 let analytics = PoolAnalytics {
2026 total_amount0_deposited: row.get::<String, _>("total_amount0_deposited").parse()?,
2027 total_amount1_deposited: row.get::<String, _>("total_amount1_deposited").parse()?,
2028 total_amount0_collected: row.get::<String, _>("total_amount0_collected").parse()?,
2029 total_amount1_collected: row.get::<String, _>("total_amount1_collected").parse()?,
2030 total_swaps: row.get::<i32, _>("total_swaps") as u64,
2031 total_mints: row.get::<i32, _>("total_mints") as u64,
2032 total_burns: row.get::<i32, _>("total_burns") as u64,
2033 total_fee_collects: row.get::<i32, _>("total_fee_collects") as u64,
2034 total_flashes: row.get::<i32, _>("total_flashes") as u64,
2035 liquidity_utilization_rate: row.get::<f64, _>("liquidity_utilization_rate"),
2036 };
2037
2038 let positions = self
2040 .load_pool_positions_for_snapshot(
2041 chain_id,
2042 pool_identifier,
2043 block as u64,
2044 transaction_index as u32,
2045 log_index as u32,
2046 )
2047 .await?;
2048
2049 let ticks = self
2050 .load_pool_ticks_for_snapshot(
2051 chain_id,
2052 pool_identifier,
2053 block as u64,
2054 transaction_index as u32,
2055 log_index as u32,
2056 )
2057 .await?;
2058
2059 let dex_name = row
2060 .try_get::<Option<String>, _>("dex_name")?
2061 .ok_or_else(|| {
2062 anyhow::anyhow!("Missing dex_name for pool snapshot {}", pool_identifier)
2063 })?;
2064 let chain = Chain::from_chain_id(chain_id)
2065 .ok_or_else(|| anyhow::anyhow!("Unknown chain_id: {chain_id}"))?;
2066
2067 let dex_type = DexType::from_dex_name(&dex_name)
2068 .ok_or_else(|| anyhow::anyhow!("Unknown dex_name: {dex_name}"))?;
2069
2070 let dex_extended = crate::exchanges::get_dex_extended(chain.name, &dex_type)
2071 .ok_or_else(|| {
2072 anyhow::anyhow!("No DEX extended found for {} on {}", dex_name, chain.name)
2073 })?;
2074
2075 let instrument_id =
2076 Pool::create_instrument_id(chain.name, &dex_extended.dex, pool_identifier.as_ref());
2077
2078 Ok(Some(PoolSnapshot::new(
2079 instrument_id,
2080 state,
2081 positions,
2082 ticks,
2083 analytics,
2084 block_position,
2085 timestamp, timestamp, )))
2088 } else {
2089 Ok(None)
2090 }
2091 }
2092
2093 pub async fn set_pool_snapshot_validation_state(
2102 &self,
2103 chain_id: u32,
2104 pool_identifier: &PoolIdentifier,
2105 block: u64,
2106 transaction_index: u32,
2107 log_index: u32,
2108 state: &str,
2109 ) -> anyhow::Result<()> {
2110 sqlx::query(
2111 "
2112 UPDATE pool_snapshot
2113 SET validation_state = $6
2114 WHERE chain_id = $1
2115 AND pool_identifier = $2
2116 AND block = $3
2117 AND transaction_index = $4
2118 AND log_index = $5
2119 ",
2120 )
2121 .bind(chain_id as i32)
2122 .bind(pool_identifier.as_ref())
2123 .bind(block as i64)
2124 .bind(transaction_index as i32)
2125 .bind(log_index as i32)
2126 .bind(state)
2127 .execute(&self.pool)
2128 .await
2129 .map(|_| ())
2130 .map_err(|e| anyhow::anyhow!("Failed to set pool snapshot validation state: {e}"))
2131 }
2132
2133 pub async fn get_pool_snapshot_validation_state(
2142 &self,
2143 chain_id: u32,
2144 pool_identifier: &PoolIdentifier,
2145 block: u64,
2146 transaction_index: u32,
2147 log_index: u32,
2148 ) -> anyhow::Result<Option<String>> {
2149 let row = sqlx::query(
2150 "
2151 SELECT validation_state
2152 FROM pool_snapshot
2153 WHERE chain_id = $1
2154 AND pool_identifier = $2
2155 AND block = $3
2156 AND transaction_index = $4
2157 AND log_index = $5
2158 ",
2159 )
2160 .bind(chain_id as i32)
2161 .bind(pool_identifier.as_ref())
2162 .bind(block as i64)
2163 .bind(transaction_index as i32)
2164 .bind(log_index as i32)
2165 .fetch_optional(&self.pool)
2166 .await
2167 .map_err(|e| anyhow::anyhow!("Failed to get pool snapshot validation state: {e}"))?;
2168
2169 Ok(row.map(|row| row.get::<String, _>("validation_state")))
2170 }
2171
2172 pub async fn load_pool_positions_for_snapshot(
2178 &self,
2179 chain_id: u32,
2180 pool_identifier: &PoolIdentifier,
2181 snapshot_block: u64,
2182 snapshot_transaction_index: u32,
2183 snapshot_log_index: u32,
2184 ) -> anyhow::Result<Vec<PoolPosition>> {
2185 let rows = sqlx::query(
2186 "
2187 SELECT
2188 owner, tick_lower, tick_upper,
2189 liquidity::TEXT, fee_growth_inside_0_last::TEXT, fee_growth_inside_1_last::TEXT,
2190 tokens_owed_0::TEXT, tokens_owed_1::TEXT,
2191 total_amount0_deposited::TEXT, total_amount1_deposited::TEXT,
2192 total_amount0_collected::TEXT, total_amount1_collected::TEXT
2193 FROM pool_position
2194 WHERE chain_id = $1
2195 AND pool_identifier = $2
2196 AND snapshot_block = $3
2197 AND snapshot_transaction_index = $4
2198 AND snapshot_log_index = $5
2199 ",
2200 )
2201 .bind(chain_id as i32)
2202 .bind(pool_identifier.as_ref())
2203 .bind(snapshot_block as i64)
2204 .bind(snapshot_transaction_index as i32)
2205 .bind(snapshot_log_index as i32)
2206 .fetch_all(&self.pool)
2207 .await
2208 .map_err(|e| anyhow::anyhow!("Failed to load pool positions: {e}"))?;
2209
2210 rows.iter()
2211 .map(|row| {
2212 let owner: String = row.get("owner");
2213 let position = PoolPosition {
2214 owner: validate_address(&owner)?,
2215 tick_lower: row.get("tick_lower"),
2216 tick_upper: row.get("tick_upper"),
2217 liquidity: row.get::<String, _>("liquidity").parse()?,
2218 fee_growth_inside_0_last: row
2219 .get::<String, _>("fee_growth_inside_0_last")
2220 .parse()?,
2221 fee_growth_inside_1_last: row
2222 .get::<String, _>("fee_growth_inside_1_last")
2223 .parse()?,
2224 tokens_owed_0: row.get::<String, _>("tokens_owed_0").parse()?,
2225 tokens_owed_1: row.get::<String, _>("tokens_owed_1").parse()?,
2226 total_amount0_deposited: row
2227 .get::<String, _>("total_amount0_deposited")
2228 .parse()?,
2229 total_amount1_deposited: row
2230 .get::<String, _>("total_amount1_deposited")
2231 .parse()?,
2232 total_amount0_collected: row
2233 .get::<String, _>("total_amount0_collected")
2234 .parse()?,
2235 total_amount1_collected: row
2236 .get::<String, _>("total_amount1_collected")
2237 .parse()?,
2238 };
2239 Ok(position)
2240 })
2241 .collect()
2242 }
2243
2244 pub async fn load_pool_ticks_for_snapshot(
2250 &self,
2251 chain_id: u32,
2252 pool_identifier: &PoolIdentifier,
2253 snapshot_block: u64,
2254 snapshot_transaction_index: u32,
2255 snapshot_log_index: u32,
2256 ) -> anyhow::Result<Vec<PoolTick>> {
2257 let rows = sqlx::query(
2258 "
2259 SELECT
2260 tick_value, liquidity_gross::TEXT, liquidity_net::TEXT,
2261 fee_growth_outside_0::TEXT, fee_growth_outside_1::TEXT, initialized,
2262 last_updated_block
2263 FROM pool_tick
2264 WHERE chain_id = $1
2265 AND pool_identifier = $2
2266 AND snapshot_block = $3
2267 AND snapshot_transaction_index = $4
2268 AND snapshot_log_index = $5
2269 ",
2270 )
2271 .bind(chain_id as i32)
2272 .bind(pool_identifier.as_ref())
2273 .bind(snapshot_block as i64)
2274 .bind(snapshot_transaction_index as i32)
2275 .bind(snapshot_log_index as i32)
2276 .fetch_all(&self.pool)
2277 .await
2278 .map_err(|e| anyhow::anyhow!("Failed to load pool ticks: {e}"))?;
2279
2280 rows.iter()
2281 .map(|row| {
2282 let tick = PoolTick::new(
2283 row.get("tick_value"),
2284 row.get::<String, _>("liquidity_gross").parse()?,
2285 row.get::<String, _>("liquidity_net").parse()?,
2286 row.get::<String, _>("fee_growth_outside_0").parse()?,
2287 row.get::<String, _>("fee_growth_outside_1").parse()?,
2288 row.get("initialized"),
2289 row.get::<i64, _>("last_updated_block") as u64,
2290 );
2291 Ok(tick)
2292 })
2293 .collect()
2294 }
2295
2296 pub fn stream_pool_events<'a>(
2310 &'a self,
2311 chain: SharedChain,
2312 dex: SharedDex,
2313 instrument_id: InstrumentId,
2314 pool_identifier: PoolIdentifier,
2315 from_position: Option<BlockPosition>,
2316 to_block: Option<u64>,
2317 ) -> Pin<Box<dyn Stream<Item = Result<DexPoolData, anyhow::Error>> + Send + 'a>> {
2318 const QUERY_ALL: &str = "
2319 (SELECT
2320 'swap' as event_type,
2321 chain_id,
2322 pool_identifier,
2323 block,
2324 transaction_hash,
2325 transaction_index,
2326 log_index,
2327 COALESCE(
2328 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_swap_event.chain_id AND block.number = pool_swap_event.block),
2329 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_swap_event.chain_id AND pool_event_block.number = pool_swap_event.block)
2330 ) as block_timestamp,
2331 sender,
2332 recipient,
2333 NULL::TEXT as owner,
2334 sqrt_price_x96::TEXT,
2335 liquidity::TEXT as swap_liquidity,
2336 tick as swap_tick,
2337 amount0::TEXT as swap_amount0,
2338 amount1::TEXT as swap_amount1,
2339 NULL::TEXT as position_liquidity,
2340 NULL::TEXT as amount0,
2341 NULL::TEXT as amount1,
2342 NULL::INT as tick_lower,
2343 NULL::INT as tick_upper,
2344 NULL::TEXT as liquidity_event_type,
2345 NULL::TEXT as flash_amount0,
2346 NULL::TEXT as flash_amount1,
2347 NULL::TEXT as flash_paid0,
2348 NULL::TEXT as flash_paid1,
2349 NULL::SMALLINT as fee_protocol0_new,
2350 NULL::SMALLINT as fee_protocol1_new
2351 FROM pool_swap_event
2352 WHERE chain_id = $1 AND pool_identifier = $2
2353 AND ($3::BIGINT IS NULL OR block <= $3))
2354 UNION ALL
2355 (SELECT
2356 'liquidity' as event_type,
2357 chain_id,
2358 pool_identifier,
2359 block,
2360 transaction_hash,
2361 transaction_index,
2362 log_index,
2363 COALESCE(
2364 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_liquidity_event.chain_id AND block.number = pool_liquidity_event.block),
2365 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_liquidity_event.chain_id AND pool_event_block.number = pool_liquidity_event.block)
2366 ) as block_timestamp,
2367 sender,
2368 NULL::TEXT as recipient,
2369 owner,
2370 NULL::text as sqrt_price_x96,
2371 NULL::TEXT as swap_liquidity,
2372 NULL::INT as swap_tick,
2373 amount0::TEXT as swap_amount0,
2374 amount1::TEXT as swap_amount1,
2375 position_liquidity::TEXT,
2376 amount0::TEXT,
2377 amount1::TEXT,
2378 tick_lower::INT,
2379 tick_upper::INT,
2380 event_type as liquidity_event_type,
2381 NULL::TEXT as flash_amount0,
2382 NULL::TEXT as flash_amount1,
2383 NULL::TEXT as flash_paid0,
2384 NULL::TEXT as flash_paid1,
2385 NULL::SMALLINT as fee_protocol0_new,
2386 NULL::SMALLINT as fee_protocol1_new
2387 FROM pool_liquidity_event
2388 WHERE chain_id = $1 AND pool_identifier = $2
2389 AND ($3::BIGINT IS NULL OR block <= $3))
2390 UNION ALL
2391 (SELECT
2392 'collect' as event_type,
2393 chain_id,
2394 pool_identifier,
2395 block,
2396 transaction_hash,
2397 transaction_index,
2398 log_index,
2399 COALESCE(
2400 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_collect_event.chain_id AND block.number = pool_collect_event.block),
2401 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_collect_event.chain_id AND pool_event_block.number = pool_collect_event.block)
2402 ) as block_timestamp,
2403 NULL::TEXT as sender,
2404 NULL::TEXT as recipient,
2405 owner,
2406 NULL::TEXT as sqrt_price_x96,
2407 NULL::TEXT as swap_liquidity,
2408 NULL::INT AS swap_tick,
2409 amount0::TEXT as swap_amount0,
2410 amount1::TEXT as swap_amount1,
2411 NULL::TEXT as position_liquidity,
2412 amount0::TEXT,
2413 amount1::TEXT,
2414 tick_lower::INT,
2415 tick_upper::INT,
2416 NULL::TEXT as liquidity_event_type,
2417 NULL::TEXT as flash_amount0,
2418 NULL::TEXT as flash_amount1,
2419 NULL::TEXT as flash_paid0,
2420 NULL::TEXT as flash_paid1,
2421 NULL::SMALLINT as fee_protocol0_new,
2422 NULL::SMALLINT as fee_protocol1_new
2423 FROM pool_collect_event
2424 WHERE chain_id = $1 AND pool_identifier = $2
2425 AND ($3::BIGINT IS NULL OR block <= $3))
2426 UNION ALL
2427 (SELECT
2428 'fee_protocol_update' as event_type,
2429 chain_id,
2430 pool_identifier,
2431 block,
2432 transaction_hash,
2433 transaction_index,
2434 log_index,
2435 COALESCE(
2436 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_update_event.chain_id AND block.number = pool_fee_protocol_update_event.block),
2437 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_update_event.chain_id AND pool_event_block.number = pool_fee_protocol_update_event.block)
2438 ) as block_timestamp,
2439 NULL::TEXT as sender,
2440 NULL::TEXT as recipient,
2441 NULL::TEXT as owner,
2442 NULL::TEXT as sqrt_price_x96,
2443 NULL::TEXT as swap_liquidity,
2444 NULL::INT AS swap_tick,
2445 NULL::TEXT as swap_amount0,
2446 NULL::TEXT as swap_amount1,
2447 NULL::TEXT as position_liquidity,
2448 NULL::TEXT as amount0,
2449 NULL::TEXT as amount1,
2450 NULL::INT as tick_lower,
2451 NULL::INT as tick_upper,
2452 NULL::TEXT as liquidity_event_type,
2453 NULL::TEXT as flash_amount0,
2454 NULL::TEXT as flash_amount1,
2455 NULL::TEXT as flash_paid0,
2456 NULL::TEXT as flash_paid1,
2457 fee_protocol0_new::SMALLINT,
2458 fee_protocol1_new::SMALLINT
2459 FROM pool_fee_protocol_update_event
2460 WHERE chain_id = $1 AND pool_identifier = $2
2461 AND ($3::BIGINT IS NULL OR block <= $3))
2462 UNION ALL
2463 (SELECT
2464 'fee_protocol_collect' as event_type,
2465 chain_id,
2466 pool_identifier,
2467 block,
2468 transaction_hash,
2469 transaction_index,
2470 log_index,
2471 COALESCE(
2472 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_collect_event.chain_id AND block.number = pool_fee_protocol_collect_event.block),
2473 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_collect_event.chain_id AND pool_event_block.number = pool_fee_protocol_collect_event.block)
2474 ) as block_timestamp,
2475 sender,
2476 recipient,
2477 NULL::TEXT as owner,
2478 NULL::TEXT as sqrt_price_x96,
2479 NULL::TEXT as swap_liquidity,
2480 NULL::INT AS swap_tick,
2481 NULL::TEXT as swap_amount0,
2482 NULL::TEXT as swap_amount1,
2483 NULL::TEXT as position_liquidity,
2484 amount0::TEXT,
2485 amount1::TEXT,
2486 NULL::INT as tick_lower,
2487 NULL::INT as tick_upper,
2488 NULL::TEXT as liquidity_event_type,
2489 NULL::TEXT as flash_amount0,
2490 NULL::TEXT as flash_amount1,
2491 NULL::TEXT as flash_paid0,
2492 NULL::TEXT as flash_paid1,
2493 NULL::SMALLINT as fee_protocol0_new,
2494 NULL::SMALLINT as fee_protocol1_new
2495 FROM pool_fee_protocol_collect_event
2496 WHERE chain_id = $1 AND pool_identifier = $2
2497 AND ($3::BIGINT IS NULL OR block <= $3))
2498 UNION ALL
2499 (SELECT
2500 'flash' as event_type,
2501 chain_id,
2502 pool_identifier,
2503 block,
2504 transaction_hash,
2505 transaction_index,
2506 log_index,
2507 COALESCE(
2508 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_flash_event.chain_id AND block.number = pool_flash_event.block),
2509 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_flash_event.chain_id AND pool_event_block.number = pool_flash_event.block)
2510 ) as block_timestamp,
2511 sender,
2512 recipient,
2513 NULL::TEXT as owner,
2514 NULL::TEXT as sqrt_price_x96,
2515 NULL::TEXT as swap_liquidity,
2516 NULL::INT AS swap_tick,
2517 NULL::TEXT as swap_amount0,
2518 NULL::TEXT as swap_amount1,
2519 NULL::TEXT as position_liquidity,
2520 NULL::TEXT as amount0,
2521 NULL::TEXT as amount1,
2522 NULL::INT as tick_lower,
2523 NULL::INT as tick_upper,
2524 NULL::TEXT as liquidity_event_type,
2525 amount0::TEXT as flash_amount0,
2526 amount1::TEXT as flash_amount1,
2527 paid0::TEXT as flash_paid0,
2528 paid1::TEXT as flash_paid1,
2529 NULL::SMALLINT as fee_protocol0_new,
2530 NULL::SMALLINT as fee_protocol1_new
2531 FROM pool_flash_event
2532 WHERE chain_id = $1 AND pool_identifier = $2
2533 AND ($3::BIGINT IS NULL OR block <= $3))
2534 ORDER BY block, transaction_index, log_index";
2535
2536 const QUERY_FROM_POSITION: &str = "
2537 (SELECT
2538 'swap' as event_type,
2539 chain_id,
2540 pool_identifier,
2541 block,
2542 transaction_hash,
2543 transaction_index,
2544 log_index,
2545 COALESCE(
2546 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_swap_event.chain_id AND block.number = pool_swap_event.block),
2547 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_swap_event.chain_id AND pool_event_block.number = pool_swap_event.block)
2548 ) as block_timestamp,
2549 sender,
2550 recipient,
2551 NULL::TEXT as owner,
2552 sqrt_price_x96::TEXT,
2553 liquidity::TEXT as swap_liquidity,
2554 tick as swap_tick,
2555 amount0::TEXT as swap_amount0,
2556 amount1::TEXT as swap_amount1,
2557 NULL::TEXT as position_liquidity,
2558 NULL::TEXT as amount0,
2559 NULL::TEXT as amount1,
2560 NULL::INT as tick_lower,
2561 NULL::INT as tick_upper,
2562 NULL::TEXT as liquidity_event_type,
2563 NULL::TEXT as flash_amount0,
2564 NULL::TEXT as flash_amount1,
2565 NULL::TEXT as flash_paid0,
2566 NULL::TEXT as flash_paid1,
2567 NULL::SMALLINT as fee_protocol0_new,
2568 NULL::SMALLINT as fee_protocol1_new
2569 FROM pool_swap_event
2570 WHERE chain_id = $1 AND pool_identifier = $2
2571 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2572 AND ($6::BIGINT IS NULL OR block <= $6))
2573 UNION ALL
2574 (SELECT
2575 'liquidity' as event_type,
2576 chain_id,
2577 pool_identifier,
2578 block,
2579 transaction_hash,
2580 transaction_index,
2581 log_index,
2582 COALESCE(
2583 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_liquidity_event.chain_id AND block.number = pool_liquidity_event.block),
2584 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_liquidity_event.chain_id AND pool_event_block.number = pool_liquidity_event.block)
2585 ) as block_timestamp,
2586 sender,
2587 NULL::TEXT as recipient,
2588 owner,
2589 NULL::text as sqrt_price_x96,
2590 NULL::TEXT as swap_liquidity,
2591 NULL::INT as swap_tick,
2592 amount0::TEXT as swap_amount0,
2593 amount1::TEXT as swap_amount1,
2594 position_liquidity::TEXT,
2595 amount0::TEXT,
2596 amount1::TEXT,
2597 tick_lower::INT,
2598 tick_upper::INT,
2599 event_type as liquidity_event_type,
2600 NULL::TEXT as flash_amount0,
2601 NULL::TEXT as flash_amount1,
2602 NULL::TEXT as flash_paid0,
2603 NULL::TEXT as flash_paid1,
2604 NULL::SMALLINT as fee_protocol0_new,
2605 NULL::SMALLINT as fee_protocol1_new
2606 FROM pool_liquidity_event
2607 WHERE chain_id = $1 AND pool_identifier = $2
2608 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2609 AND ($6::BIGINT IS NULL OR block <= $6))
2610 UNION ALL
2611 (SELECT
2612 'collect' as event_type,
2613 chain_id,
2614 pool_identifier,
2615 block,
2616 transaction_hash,
2617 transaction_index,
2618 log_index,
2619 COALESCE(
2620 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_collect_event.chain_id AND block.number = pool_collect_event.block),
2621 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_collect_event.chain_id AND pool_event_block.number = pool_collect_event.block)
2622 ) as block_timestamp,
2623 NULL::TEXT as sender,
2624 NULL::TEXT as recipient,
2625 owner,
2626 NULL::TEXT as sqrt_price_x96,
2627 NULL::TEXT as swap_liquidity,
2628 NULL::INT AS swap_tick,
2629 amount0::TEXT as swap_amount0,
2630 amount1::TEXT as swap_amount1,
2631 NULL::TEXT as position_liquidity,
2632 amount0::TEXT,
2633 amount1::TEXT,
2634 tick_lower::INT,
2635 tick_upper::INT,
2636 NULL::TEXT as liquidity_event_type,
2637 NULL::TEXT as flash_amount0,
2638 NULL::TEXT as flash_amount1,
2639 NULL::TEXT as flash_paid0,
2640 NULL::TEXT as flash_paid1,
2641 NULL::SMALLINT as fee_protocol0_new,
2642 NULL::SMALLINT as fee_protocol1_new
2643 FROM pool_collect_event
2644 WHERE chain_id = $1 AND pool_identifier = $2
2645 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2646 AND ($6::BIGINT IS NULL OR block <= $6))
2647 UNION ALL
2648 (SELECT
2649 'fee_protocol_update' as event_type,
2650 chain_id,
2651 pool_identifier,
2652 block,
2653 transaction_hash,
2654 transaction_index,
2655 log_index,
2656 COALESCE(
2657 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_update_event.chain_id AND block.number = pool_fee_protocol_update_event.block),
2658 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_update_event.chain_id AND pool_event_block.number = pool_fee_protocol_update_event.block)
2659 ) as block_timestamp,
2660 NULL::TEXT as sender,
2661 NULL::TEXT as recipient,
2662 NULL::TEXT as owner,
2663 NULL::TEXT as sqrt_price_x96,
2664 NULL::TEXT as swap_liquidity,
2665 NULL::INT AS swap_tick,
2666 NULL::TEXT as swap_amount0,
2667 NULL::TEXT as swap_amount1,
2668 NULL::TEXT as position_liquidity,
2669 NULL::TEXT as amount0,
2670 NULL::TEXT as amount1,
2671 NULL::INT as tick_lower,
2672 NULL::INT as tick_upper,
2673 NULL::TEXT as liquidity_event_type,
2674 NULL::TEXT as flash_amount0,
2675 NULL::TEXT as flash_amount1,
2676 NULL::TEXT as flash_paid0,
2677 NULL::TEXT as flash_paid1,
2678 fee_protocol0_new::SMALLINT,
2679 fee_protocol1_new::SMALLINT
2680 FROM pool_fee_protocol_update_event
2681 WHERE chain_id = $1 AND pool_identifier = $2
2682 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2683 AND ($6::BIGINT IS NULL OR block <= $6))
2684 UNION ALL
2685 (SELECT
2686 'fee_protocol_collect' as event_type,
2687 chain_id,
2688 pool_identifier,
2689 block,
2690 transaction_hash,
2691 transaction_index,
2692 log_index,
2693 COALESCE(
2694 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_collect_event.chain_id AND block.number = pool_fee_protocol_collect_event.block),
2695 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_collect_event.chain_id AND pool_event_block.number = pool_fee_protocol_collect_event.block)
2696 ) as block_timestamp,
2697 sender,
2698 recipient,
2699 NULL::TEXT as owner,
2700 NULL::TEXT as sqrt_price_x96,
2701 NULL::TEXT as swap_liquidity,
2702 NULL::INT AS swap_tick,
2703 NULL::TEXT as swap_amount0,
2704 NULL::TEXT as swap_amount1,
2705 NULL::TEXT as position_liquidity,
2706 amount0::TEXT,
2707 amount1::TEXT,
2708 NULL::INT as tick_lower,
2709 NULL::INT as tick_upper,
2710 NULL::TEXT as liquidity_event_type,
2711 NULL::TEXT as flash_amount0,
2712 NULL::TEXT as flash_amount1,
2713 NULL::TEXT as flash_paid0,
2714 NULL::TEXT as flash_paid1,
2715 NULL::SMALLINT as fee_protocol0_new,
2716 NULL::SMALLINT as fee_protocol1_new
2717 FROM pool_fee_protocol_collect_event
2718 WHERE chain_id = $1 AND pool_identifier = $2
2719 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2720 AND ($6::BIGINT IS NULL OR block <= $6))
2721 UNION ALL
2722 (SELECT
2723 'flash' as event_type,
2724 chain_id,
2725 pool_identifier,
2726 block,
2727 transaction_hash,
2728 transaction_index,
2729 log_index,
2730 COALESCE(
2731 (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_flash_event.chain_id AND block.number = pool_flash_event.block),
2732 (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_flash_event.chain_id AND pool_event_block.number = pool_flash_event.block)
2733 ) as block_timestamp,
2734 sender,
2735 recipient,
2736 NULL::TEXT as owner,
2737 NULL::TEXT as sqrt_price_x96,
2738 NULL::TEXT as swap_liquidity,
2739 NULL::INT AS swap_tick,
2740 NULL::TEXT as swap_amount0,
2741 NULL::TEXT as swap_amount1,
2742 NULL::TEXT as position_liquidity,
2743 NULL::TEXT as amount0,
2744 NULL::TEXT as amount1,
2745 NULL::INT as tick_lower,
2746 NULL::INT as tick_upper,
2747 NULL::TEXT as liquidity_event_type,
2748 amount0::TEXT as flash_amount0,
2749 amount1::TEXT as flash_amount1,
2750 paid0::TEXT as flash_paid0,
2751 paid1::TEXT as flash_paid1,
2752 NULL::SMALLINT as fee_protocol0_new,
2753 NULL::SMALLINT as fee_protocol1_new
2754 FROM pool_flash_event
2755 WHERE chain_id = $1 AND pool_identifier = $2
2756 AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
2757 AND ($6::BIGINT IS NULL OR block <= $6))
2758 ORDER BY block, transaction_index, log_index";
2759
2760 let query = if let Some(pos) = from_position {
2762 sqlx::query(QUERY_FROM_POSITION)
2763 .bind(chain.chain_id as i32)
2764 .bind(pool_identifier.to_string())
2765 .bind(pos.number as i64)
2766 .bind(pos.transaction_index as i32)
2767 .bind(pos.log_index as i32)
2768 .bind(to_block.map(|block| block as i64))
2769 .fetch(&self.pool)
2770 } else {
2771 sqlx::query(QUERY_ALL)
2772 .bind(chain.chain_id as i32)
2773 .bind(pool_identifier.to_string())
2774 .bind(to_block.map(|block| block as i64))
2775 .fetch(&self.pool)
2776 };
2777
2778 let stream = query.map(move |row_result| match row_result {
2780 Ok(row) => {
2781 transform_row_to_dex_pool_data(&row, chain.clone(), dex.clone(), instrument_id)
2782 .map_err(|e| anyhow::anyhow!("Steam pool event transform error: {e}"))
2783 }
2784 Err(e) => Err(anyhow::anyhow!("Stream pool events database error: {e}")),
2785 });
2786
2787 Box::pin(stream)
2788 }
2789}
2790
2791#[cfg(test)]
2792mod tests {
2793 use sqlx::postgres::PgConnectOptions;
2794
2795 use super::BlockchainCacheDatabase;
2796
2797 #[tokio::test]
2798 async fn connect_returns_err_for_unreachable_database() {
2799 let options = PgConnectOptions::new()
2802 .host("127.0.0.1")
2803 .port(1)
2804 .username("nautilus")
2805 .password("pass")
2806 .database("nautilus");
2807
2808 let result = BlockchainCacheDatabase::connect(options).await;
2809
2810 assert!(result.is_err());
2811 }
2812}