nautilus_blockchain/services/
pool_discovery.rs1use std::{cmp::max, collections::HashSet};
17
18use alloy::primitives::Address;
19use futures_util::StreamExt;
20use nautilus_core::string::formatting::Separable;
21use nautilus_model::defi::{
22 Block, SharedDex,
23 amm::Pool,
24 chain::SharedChain,
25 reporting::{BlockchainSyncReportItems, BlockchainSyncReporter},
26 token::Token,
27};
28use tokio_util::sync::CancellationToken;
29
30use crate::{
31 cache::BlockchainCache,
32 config::BlockchainDataClientConfig,
33 contracts::erc20::Erc20Contract,
34 events::pool_created::PoolCreatedEvent,
35 exchanges::extended::DexExtended,
36 hypersync::{
37 client::{HyperSyncClient, PoolEventStreamItem},
38 helpers::extract_block_number,
39 },
40};
41
42const BLOCKS_PROCESS_IN_SYNC_REPORT: u64 = 50_000;
43const POOL_DB_BATCH_SIZE: usize = 2000;
44const POOL_EVENT_BLOCK_DB_BATCH_SIZE: usize = 20_000;
45
46fn sanitize_string(s: &str) -> String {
52 s.chars()
53 .filter(|c| {
54 *c != '\0' && (*c >= ' ' || *c == '\t' || *c == '\n' || *c == '\r')
57 })
58 .collect()
59}
60
61#[derive(Debug)]
66pub struct PoolDiscoveryService<'a> {
67 chain: SharedChain,
69 cache: &'a mut BlockchainCache,
71 erc20_contract: &'a Erc20Contract,
73 hypersync_client: &'a HyperSyncClient,
75 cancellation_token: CancellationToken,
77 config: BlockchainDataClientConfig,
79}
80
81impl<'a> PoolDiscoveryService<'a> {
82 #[must_use]
84 pub const fn new(
85 chain: SharedChain,
86 cache: &'a mut BlockchainCache,
87 erc20_contract: &'a Erc20Contract,
88 hypersync_client: &'a HyperSyncClient,
89 cancellation_token: CancellationToken,
90 config: BlockchainDataClientConfig,
91 ) -> Self {
92 Self {
93 chain,
94 cache,
95 erc20_contract,
96 hypersync_client,
97 cancellation_token,
98 config,
99 }
100 }
101
102 pub async fn sync_pools(
112 &mut self,
113 dex: &DexExtended,
114 from_block: u64,
115 to_block: Option<u64>,
116 reset: bool,
117 ) -> anyhow::Result<()> {
118 let (last_synced_block, effective_from_block) = if reset {
120 (None, from_block)
121 } else {
122 let last_synced_block = self.cache.get_dex_last_synced_block(&dex.dex.name).await?;
123 let effective_from_block = last_synced_block
124 .map_or(from_block, |last_synced| max(from_block, last_synced + 1));
125 (last_synced_block, effective_from_block)
126 };
127
128 let to_block = match to_block {
129 Some(block) => block,
130 None => self.hypersync_client.current_block().await,
131 };
132
133 if effective_from_block > to_block {
135 log::debug!(
136 "DEX {} already synced to block {} (current: {}), skipping sync",
137 dex.dex.name,
138 last_synced_block.unwrap_or(0).separate_with_commas(),
139 to_block.separate_with_commas()
140 );
141 return Ok(());
142 }
143
144 let total_blocks = to_block.saturating_sub(effective_from_block) + 1;
145 log::debug!(
146 "Syncing DEX exchange pools from {} to {} (total: {} blocks){}",
147 effective_from_block.separate_with_commas(),
148 to_block.separate_with_commas(),
149 total_blocks.separate_with_commas(),
150 if let Some(last_synced) = last_synced_block {
151 format!(
152 " - resuming from last synced block {}",
153 last_synced.separate_with_commas()
154 )
155 } else {
156 String::new()
157 },
158 );
159 log::debug!(
160 "Syncing {} pool creation events from factory contract {} on chain {}",
161 dex.dex.name,
162 dex.factory,
163 self.chain.name
164 );
165
166 if let Err(e) = self.cache.toggle_performance_settings(true).await {
168 log::warn!("Failed to enable performance settings: {e}");
169 }
170
171 let mut metrics = BlockchainSyncReporter::new(
172 BlockchainSyncReportItems::PoolCreatedEvents,
173 effective_from_block,
174 total_blocks,
175 BLOCKS_PROCESS_IN_SYNC_REPORT,
176 );
177
178 let factory_address = &dex.factory;
179 let pair_created_event_signature = dex.pool_created_event.as_ref();
180 let pools_stream = self
181 .hypersync_client
182 .request_contract_events_stream(
183 effective_from_block,
184 Some(to_block),
185 factory_address,
186 vec![pair_created_event_signature],
187 )
188 .await;
189
190 tokio::pin!(pools_stream);
191
192 let token_rpc_batch_size = (self.config.multicall_calls_per_rpc_request / 3) as usize;
194 let mut token_rpc_buffer: HashSet<Address> = HashSet::new();
195
196 let mut token_db_buffer: Vec<Token> = Vec::new();
198 let mut pool_events_buffer: Vec<PoolCreatedEvent> = Vec::new();
199 let mut block_db_buffer: Vec<Block> = Vec::new();
200
201 let mut last_block_saved = effective_from_block;
202
203 let mut total_discovered = 0;
205 let mut total_skipped_exists = 0;
206 let mut total_skipped_invalid_tokens = 0;
207 let mut total_saved = 0;
208
209 let cancellation_token = self.cancellation_token.clone();
210 let sync_result = tokio::select! {
211 () = cancellation_token.cancelled() => {
212 log::debug!("Exchange pool sync cancelled");
213 Err(anyhow::anyhow!("Sync cancelled"))
214 }
215
216 result = async {
217 while let Some(item) = pools_stream.next().await {
218 let log = match item {
219 PoolEventStreamItem::Block(block) => {
220 self.cache.cache_block_timestamp(block.number, block.timestamp);
221 block_db_buffer.push(block);
222 if block_db_buffer.len() >= POOL_EVENT_BLOCK_DB_BATCH_SIZE {
223 self.flush_pool_event_blocks(&mut block_db_buffer).await?;
224 }
225 continue;
226 }
227 PoolEventStreamItem::Log(log) => log,
228 };
229 let block_number = extract_block_number(&log)?;
230 let blocks_progress = block_number - last_block_saved;
231 last_block_saved = block_number;
232
233 let pool = dex.parse_pool_created_event_hypersync(log)?;
234 total_discovered += 1;
235
236 if self.cache.get_pool(&pool.pool_identifier).is_some() {
237 total_skipped_exists += 1;
239 continue;
240 }
241
242 if self.cache.is_invalid_token(&pool.token0)
243 || self.cache.is_invalid_token(&pool.token1)
244 {
245 total_skipped_invalid_tokens += 1;
247 continue;
248 }
249
250 if self.cache.get_token(&pool.token0).is_none() {
252 token_rpc_buffer.insert(pool.token0);
253 }
254
255 if self.cache.get_token(&pool.token1).is_none() {
256 token_rpc_buffer.insert(pool.token1);
257 }
258
259 pool_events_buffer.push(pool);
261
262 if token_rpc_buffer.len() >= token_rpc_batch_size {
264 let fetched_tokens = self
265 .fetch_and_cache_tokens_in_memory(&mut token_rpc_buffer)
266 .await?;
267
268 token_db_buffer.extend(fetched_tokens);
270 }
271
272 if pool_events_buffer.len() >= POOL_DB_BATCH_SIZE {
275 if !token_rpc_buffer.is_empty() {
277 let fetched_tokens = self
278 .fetch_and_cache_tokens_in_memory(&mut token_rpc_buffer)
279 .await?;
280 token_db_buffer.extend(fetched_tokens);
281 }
282
283 if !token_db_buffer.is_empty() {
285 self.cache
286 .add_tokens_batch(std::mem::take(&mut token_db_buffer))
287 .await?;
288 }
289
290 let pools = self
292 .construct_pools_batch(&mut pool_events_buffer, &dex.dex)
293 .await?;
294 total_saved += pools.len();
295 self.cache.add_pools_batch(pools).await?;
296 }
297
298 metrics.update(blocks_progress as usize);
299 if metrics.should_log_progress(block_number, to_block) {
301 metrics.log_progress(block_number);
302 }
303 }
304
305 if !token_rpc_buffer.is_empty() {
308 let fetched_tokens = self
309 .fetch_and_cache_tokens_in_memory(&mut token_rpc_buffer)
310 .await?;
311 token_db_buffer.extend(fetched_tokens);
312 }
313
314 if !token_db_buffer.is_empty() {
316 self.cache
317 .add_tokens_batch(std::mem::take(&mut token_db_buffer))
318 .await?;
319 }
320
321 if !pool_events_buffer.is_empty() {
323 let pools = self
324 .construct_pools_batch(&mut pool_events_buffer, &dex.dex)
325 .await?;
326 total_saved += pools.len();
327 self.cache.add_pools_batch(pools).await?;
328 }
329
330 self.flush_pool_event_blocks(&mut block_db_buffer).await?;
331 metrics.log_final_stats();
332
333 self.cache
335 .update_dex_last_synced_block(&dex.dex.name, to_block)
336 .await?;
337
338 log::debug!(
339 "Successfully synced DEX {} pools up to block {} | Summary: discovered={}, saved={}, skipped_exists={}, skipped_invalid_tokens={}",
340 dex.dex.name,
341 to_block.separate_with_commas(),
342 total_discovered,
343 total_saved,
344 total_skipped_exists,
345 total_skipped_invalid_tokens
346 );
347
348 Ok(())
349 } => result
350 };
351
352 sync_result?;
353
354 if let Err(e) = self.cache.toggle_performance_settings(false).await {
356 log::warn!("Failed to restore default settings: {e}");
357 }
358
359 Ok(())
360 }
361
362 async fn flush_pool_event_blocks(&mut self, blocks: &mut Vec<Block>) -> anyhow::Result<()> {
363 if blocks.is_empty() {
364 return Ok(());
365 }
366
367 self.cache
368 .add_pool_event_blocks_batch(std::mem::take(blocks))
369 .await
370 }
371
372 async fn fetch_and_cache_tokens_in_memory(
381 &mut self,
382 token_buffer: &mut HashSet<Address>,
383 ) -> anyhow::Result<Vec<Token>> {
384 let batch_addresses: Vec<Address> = token_buffer.drain().collect();
385 let token_infos = self
386 .erc20_contract
387 .batch_fetch_token_info(&batch_addresses)
388 .await?;
389
390 let mut valid_tokens = Vec::new();
391
392 for (token_address, token_info) in token_infos {
393 match token_info {
394 Ok(token_info) => {
395 let sanitized_name = sanitize_string(&token_info.name);
397 let sanitized_symbol = sanitize_string(&token_info.symbol);
398
399 let token = Token::new(
400 self.chain.clone(),
401 token_address,
402 sanitized_name,
403 sanitized_symbol,
404 token_info.decimals,
405 );
406
407 self.cache.insert_token_in_memory(token.clone());
409
410 valid_tokens.push(token);
412 }
413 Err(token_info_error) => {
414 self.cache.insert_invalid_token_in_memory(token_address);
415 if let Some(database) = &self.cache.database {
416 let sanitized_error = sanitize_string(&token_info_error.to_string());
417 database
418 .add_invalid_token(
419 self.chain.chain_id,
420 &token_address,
421 &sanitized_error,
422 )
423 .await?;
424 }
425 }
426 }
427 }
428
429 Ok(valid_tokens)
430 }
431
432 async fn construct_pools_batch(
441 &self,
442 pool_events: &mut Vec<PoolCreatedEvent>,
443 dex: &SharedDex,
444 ) -> anyhow::Result<Vec<Pool>> {
445 let mut pools = Vec::with_capacity(pool_events.len());
446
447 for pool_event in pool_events.drain(..) {
448 let token0 = match self.cache.get_token(&pool_event.token0) {
450 Some(token) => token.clone(),
451 None => {
452 if !self.cache.is_invalid_token(&pool_event.token0) {
453 log::warn!(
454 "Skipping pool {}: Token0 {} not in cache and not marked as invalid",
455 pool_event.pool_address,
456 pool_event.token0
457 );
458 }
459 continue;
460 }
461 };
462
463 let token1 = match self.cache.get_token(&pool_event.token1) {
464 Some(token) => token.clone(),
465 None => {
466 if !self.cache.is_invalid_token(&pool_event.token1) {
467 log::warn!(
468 "Skipping pool {}: Token1 {} not in cache and not marked as invalid",
469 pool_event.pool_address,
470 pool_event.token1
471 );
472 }
473 continue;
474 }
475 };
476
477 let ts_init = self
478 .cache
479 .get_block_timestamp(pool_event.block_number)
480 .copied()
481 .unwrap_or_default();
482
483 let mut pool = Pool::new(
484 self.chain.clone(),
485 dex.clone(),
486 pool_event.pool_address,
487 pool_event.pool_identifier,
488 pool_event.block_number,
489 token0,
490 token1,
491 pool_event.fee,
492 pool_event.tick_spacing,
493 ts_init,
494 );
495
496 if let Some(hooks) = pool_event.hooks {
498 pool.set_hooks(hooks);
499 }
500
501 if let (Some(sqrt_price_x96), Some(tick)) = (pool_event.sqrt_price_x96, pool_event.tick)
503 {
504 pool.initialize(sqrt_price_x96, tick);
505 }
506
507 pools.push(pool);
508 }
509
510 Ok(pools)
511 }
512}