1use std::{collections::VecDeque, sync::Arc, time::Duration};
17
18use anyhow::Context;
19use nautilus_common::{
20 clients::DataClient,
21 defi::RequestPoolSnapshot,
22 messages::{
23 DataEvent,
24 defi::{
25 DefiDataCommand, DefiRequestCommand, DefiSubscribeCommand, DefiUnsubscribeCommand,
26 PoolSnapshotResponse, SubscribeBlocks, SubscribePool, SubscribePoolFeeCollects,
27 SubscribePoolFlashEvents, SubscribePoolLiquidityUpdates, SubscribePoolSwaps,
28 UnsubscribeBlocks, UnsubscribePool, UnsubscribePoolFeeCollects,
29 UnsubscribePoolFlashEvents, UnsubscribePoolLiquidityUpdates, UnsubscribePoolSwaps,
30 },
31 },
32};
33use nautilus_live::{
34 SocketControlFactory,
35 task::{TaskGroup, TaskGroupGuard},
36};
37use nautilus_model::{
38 defi::{DefiData, DexType, PoolIdentifier, SharedChain, validation::validate_address},
39 identifiers::{ClientId, Venue},
40};
41use ustr::Ustr;
42
43use crate::{
44 cache::BlockchainCache,
45 config::BlockchainDataClientConfig,
46 data::{
47 core::BlockchainDataClientCore,
48 subscription::{BlockFeedBackend, BlockFeedOwner},
49 },
50 exchanges::get_dex_extended,
51 rpc::{
52 BlockchainRpcClient,
53 types::{BlockchainMessage, RpcEventType},
54 },
55};
56
57const MAX_PENDING_POOL_MESSAGES: usize = 10_000;
58
59#[derive(Debug)]
69pub struct BlockchainDataClient {
70 pub client_id: ClientId,
72 pub chain: SharedChain,
74 pub config: BlockchainDataClientConfig,
76 pub core_client: Option<BlockchainDataClientCore>,
79 socket_factory: SocketControlFactory,
80 hypersync_rx: Option<tokio::sync::mpsc::UnboundedReceiver<BlockchainMessage>>,
81 hypersync_tx: Option<tokio::sync::mpsc::UnboundedSender<BlockchainMessage>>,
82 command_tx: tokio::sync::mpsc::UnboundedSender<DefiDataCommand>,
83 command_rx: Option<tokio::sync::mpsc::UnboundedReceiver<DefiDataCommand>>,
84 session_tasks: TaskGroup,
85 cancellation_token: tokio_util::sync::CancellationToken,
86}
87
88impl BlockchainDataClient {
89 #[must_use]
91 pub fn new(client_id: ClientId, config: BlockchainDataClientConfig) -> Self {
92 let chain = Arc::clone(&config.chain);
93 let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
94 let (hypersync_tx, hypersync_rx) = tokio::sync::mpsc::unbounded_channel();
95 let socket_factory = SocketControlFactory::new(client_id, None);
96 let session_tasks = TaskGroup::new();
97 Self {
98 client_id,
99 chain,
100 core_client: None,
101 socket_factory,
102 config,
103 hypersync_rx: Some(hypersync_rx),
104 hypersync_tx: Some(hypersync_tx),
105 command_tx,
106 command_rx: Some(command_rx),
107 cancellation_token: session_tasks.cancellation_token(),
108 session_tasks,
109 }
110 }
111
112 fn spawn_process_task(
120 &mut self,
121 ) -> anyhow::Result<tokio::sync::oneshot::Receiver<anyhow::Result<()>>> {
122 let Some(command_rx) = self.command_rx.take() else {
123 anyhow::bail!("Command receiver already taken");
124 };
125
126 let cancellation_token = self.cancellation_token.clone();
127
128 let data_tx = nautilus_common::live::runner::get_data_event_sender();
129
130 let mut hypersync_rx = self.hypersync_rx.take().unwrap();
131 let hypersync_tx = self.hypersync_tx.take();
132
133 let mut core_client = BlockchainDataClientCore::new(
134 self.config.clone(),
135 hypersync_tx,
136 Some(data_tx),
137 cancellation_token.clone(),
138 );
139 core_client.set_socket_control(self.socket_factory.control("blockchain-rpc"));
140
141 let (startup_tx, startup_rx) = tokio::sync::oneshot::channel();
142 let future = async move {
143 log::debug!("Started task 'process'");
144
145 if let Err(e) = core_client.connect().await {
146 let _ = startup_tx.send(Err(e));
147 return;
148 }
149 let _ = startup_tx.send(Ok(()));
150
151 let mut command_rx = command_rx;
152 let mut pending_pool_messages = VecDeque::new();
153
154 loop {
155 tokio::select! {
156 () = cancellation_token.cancelled() => {
157 log::debug!("Received cancellation signal in Blockchain data client process task");
158 core_client.disconnect().await;
159 break;
160 }
161 command = command_rx.recv() => {
162 if let Some(cmd) = command {
163 match cmd {
164 DefiDataCommand::Subscribe(cmd) => {
165 let chain = cmd.blockchain();
166 if chain != core_client.chain.name {
167 log::error!("Incorrect blockchain for subscribe command: {chain}");
168 continue;
169 }
170
171 if let Err(e) = Self::handle_subscribe_command(cmd, &mut core_client).await{
172 log::error!("Error processing subscribe command: {e}");
173 }
174 }
175 DefiDataCommand::Unsubscribe(cmd) => {
176 let chain = cmd.blockchain();
177 if chain != core_client.chain.name {
178 log::error!("Incorrect blockchain for subscribe command: {chain}");
179 continue;
180 }
181
182 if let Err(e) = Self::handle_unsubscribe_command(cmd, &mut core_client).await{
183 log::error!("Error processing subscribe command: {e}");
184 }
185 }
186 DefiDataCommand::Request(cmd) => {
187 if let Err(e) = Self::handle_request_command(cmd, &mut core_client).await {
188 log::error!("Error processing request command: {e}");
189 }
190 }
191 }
192 } else {
193 log::debug!("Command channel closed");
194 break;
195 }
196 }
197 data = hypersync_rx.recv() => {
198 if let Some(msg) = data {
199 Self::process_live_blockchain_message(
200 msg,
201 &mut core_client,
202 &mut pending_pool_messages,
203 )
204 .await;
205 } else {
206 log::debug!("HyperSync data channel closed");
207 break;
208 }
209 }
210 msg = async {
211 match core_client.rpc_client {
212 Some(ref mut rpc_client) => rpc_client.next_rpc_message().await,
213 None => std::future::pending().await, }
215 } => {
216 match msg {
218 Ok(msg) => {
219 Self::process_live_blockchain_message(
220 msg,
221 &mut core_client,
222 &mut pending_pool_messages,
223 )
224 .await;
225 }
226 Err(e) => {
227 log::error!("Error processing RPC message: {e}");
228 }
229 }
230 }
231 }
232 }
233
234 log::debug!("Stopped task 'process'");
235 };
236
237 self.session_tasks
238 .spawn(future)
239 .map_err(|e| anyhow::anyhow!("Failed to register blockchain process task: {e}"))?;
240 Ok(startup_rx)
241 }
242
243 async fn process_live_blockchain_message(
244 msg: BlockchainMessage,
245 core_client: &mut BlockchainDataClientCore,
246 pending_pool_messages: &mut VecDeque<BlockchainMessage>,
247 ) {
248 let is_block = matches!(&msg, BlockchainMessage::Block(_));
249 let Some(msg) =
250 Self::ready_live_blockchain_message(msg, &core_client.cache, pending_pool_messages)
251 else {
252 return;
253 };
254
255 if let Some(data) = Self::data_event_from_blockchain_message(msg, core_client).await {
256 core_client.send_data(data);
257 }
258
259 if is_block {
260 for data in Self::drain_pending_pool_messages(core_client, pending_pool_messages).await
261 {
262 core_client.send_data(data);
263 }
264 }
265 }
266
267 async fn drain_pending_pool_messages(
268 core_client: &mut BlockchainDataClientCore,
269 pending_pool_messages: &mut VecDeque<BlockchainMessage>,
270 ) -> Vec<DataEvent> {
271 let ready_messages = Self::drain_pending_pool_messages_with_cached_metadata(
272 pending_pool_messages,
273 &core_client.cache,
274 );
275 let mut data_events = Vec::with_capacity(ready_messages.len());
276
277 for msg in ready_messages {
278 if let Some(data) = Self::data_event_from_blockchain_message(msg, core_client).await {
279 data_events.push(data);
280 }
281 }
282
283 data_events
284 }
285
286 fn drain_pending_pool_messages_with_cached_metadata(
287 pending_pool_messages: &mut VecDeque<BlockchainMessage>,
288 cache: &BlockchainCache,
289 ) -> Vec<BlockchainMessage> {
290 let pending_count = pending_pool_messages.len();
291 let mut ready_messages = Vec::new();
292
293 for _ in 0..pending_count {
294 let Some(msg) = pending_pool_messages.pop_front() else {
295 break;
296 };
297
298 if Self::pool_event_missing_block_metadata(&msg, cache).is_some() {
299 pending_pool_messages.push_back(msg);
300 continue;
301 }
302
303 ready_messages.push(msg);
304 }
305
306 ready_messages
307 }
308
309 fn queue_pending_pool_message(
310 pending_pool_messages: &mut VecDeque<BlockchainMessage>,
311 msg: BlockchainMessage,
312 ) {
313 if pending_pool_messages.len() >= MAX_PENDING_POOL_MESSAGES
314 && let Some(dropped_msg) = pending_pool_messages.pop_front()
315 && let Some(block_number) = Self::pool_event_block_number(&dropped_msg)
316 {
317 log::warn!(
318 "Dropping oldest live pool event waiting for uncached block {block_number}; pending buffer reached {MAX_PENDING_POOL_MESSAGES} messages"
319 );
320 }
321
322 pending_pool_messages.push_back(msg);
323 }
324
325 fn ready_live_blockchain_message(
326 msg: BlockchainMessage,
327 cache: &BlockchainCache,
328 pending_pool_messages: &mut VecDeque<BlockchainMessage>,
329 ) -> Option<BlockchainMessage> {
330 if let Some(block_number) = Self::pool_event_missing_block_metadata(&msg, cache) {
331 log::debug!("Deferring live pool event until block {block_number} metadata is cached");
332 Self::queue_pending_pool_message(pending_pool_messages, msg);
333 None
334 } else {
335 Some(msg)
336 }
337 }
338
339 fn pool_event_missing_block_metadata(
340 msg: &BlockchainMessage,
341 cache: &BlockchainCache,
342 ) -> Option<u64> {
343 let block_number = Self::pool_event_block_number(msg)?;
344 (cache.get_block_timestamp(block_number).is_none()
345 || cache.get_block_hash(block_number).is_none())
346 .then_some(block_number)
347 }
348
349 fn pool_event_block_number(msg: &BlockchainMessage) -> Option<u64> {
350 match msg {
351 BlockchainMessage::SwapEvent(event) => Some(event.block_number),
352 BlockchainMessage::MintEvent(event) => Some(event.block_number),
353 BlockchainMessage::BurnEvent(event) => Some(event.block_number),
354 BlockchainMessage::CollectEvent(event) => Some(event.block_number),
355 BlockchainMessage::FlashEvent(event) => Some(event.block_number),
356 BlockchainMessage::FeeProtocolUpdateEvent(event) => Some(event.block_number),
357 BlockchainMessage::FeeProtocolCollectEvent(event) => Some(event.block_number),
358 BlockchainMessage::Block(_) => None,
359 }
360 }
361
362 async fn data_event_from_blockchain_message(
363 msg: BlockchainMessage,
364 core_client: &mut BlockchainDataClientCore,
365 ) -> Option<DataEvent> {
366 match msg {
367 BlockchainMessage::Block(block) => {
368 if let Err(e) = core_client.cache.add_block(block.clone()).await {
369 log::error!("Failed to cache block {}: {e}", block.number);
370 }
371
372 Some(DataEvent::DeFi(DefiData::Block(block)))
373 }
374 BlockchainMessage::SwapEvent(swap_event) => {
375 match core_client.get_pool(&swap_event.pool_identifier) {
376 Ok(pool) => match core_client.process_pool_swap_event(&swap_event, pool) {
377 Ok(swap) => Some(DataEvent::DeFi(DefiData::PoolSwap(swap))),
378 Err(e) => {
379 log::error!("Error processing pool swap event: {e}");
380 None
381 }
382 },
383 Err(e) => {
384 log::error!(
385 "Failed to get pool {} with error {:?}",
386 swap_event.pool_identifier,
387 e
388 );
389 None
390 }
391 }
392 }
393 BlockchainMessage::BurnEvent(burn_event) => {
394 match core_client.get_pool(&burn_event.pool_identifier) {
395 Ok(pool) => {
396 let dex_extended = get_dex_extended(core_client.chain.name, &pool.dex.name)
397 .expect("Failed to get dex extended");
398
399 match core_client.process_pool_burn_event(&burn_event, pool, dex_extended) {
400 Ok(update) => {
401 Some(DataEvent::DeFi(DefiData::PoolLiquidityUpdate(update)))
402 }
403 Err(e) => {
404 log::error!("Error processing pool burn event: {e}");
405 None
406 }
407 }
408 }
409 Err(e) => {
410 log::error!(
411 "Failed to get pool {} with error {:?}",
412 burn_event.pool_identifier,
413 e
414 );
415 None
416 }
417 }
418 }
419 BlockchainMessage::MintEvent(mint_event) => {
420 match core_client.get_pool(&mint_event.pool_identifier) {
421 Ok(pool) => {
422 let dex_extended = get_dex_extended(core_client.chain.name, &pool.dex.name)
423 .expect("Failed to get dex extended");
424
425 match core_client.process_pool_mint_event(&mint_event, pool, dex_extended) {
426 Ok(update) => {
427 Some(DataEvent::DeFi(DefiData::PoolLiquidityUpdate(update)))
428 }
429 Err(e) => {
430 log::error!("Error processing pool mint event: {e}");
431 None
432 }
433 }
434 }
435 Err(e) => {
436 log::error!(
437 "Failed to get pool {} with error {:?}",
438 mint_event.pool_identifier,
439 e
440 );
441 None
442 }
443 }
444 }
445 BlockchainMessage::CollectEvent(collect_event) => {
446 match core_client.get_pool(&collect_event.pool_identifier) {
447 Ok(pool) => {
448 let dex_extended = get_dex_extended(core_client.chain.name, &pool.dex.name)
449 .expect("Failed to get dex extended");
450
451 match core_client.process_pool_collect_event(
452 &collect_event,
453 pool,
454 dex_extended,
455 ) {
456 Ok(update) => Some(DataEvent::DeFi(DefiData::PoolFeeCollect(update))),
457 Err(e) => {
458 log::error!("Error processing pool collect event: {e}");
459 None
460 }
461 }
462 }
463 Err(e) => {
464 log::error!(
465 "Failed to get pool {} with error {:?}",
466 collect_event.pool_identifier,
467 e
468 );
469 None
470 }
471 }
472 }
473 BlockchainMessage::FlashEvent(flash_event) => {
474 match core_client.get_pool(&flash_event.pool_identifier) {
475 Ok(pool) => match core_client.process_pool_flash_event(&flash_event, pool) {
476 Ok(flash) => Some(DataEvent::DeFi(DefiData::PoolFlash(flash))),
477 Err(e) => {
478 log::error!("Error processing pool flash event: {e}");
479 None
480 }
481 },
482 Err(e) => {
483 log::error!(
484 "Failed to get pool {} with error {:?}",
485 flash_event.pool_identifier,
486 e
487 );
488 None
489 }
490 }
491 }
492 BlockchainMessage::FeeProtocolUpdateEvent(update_event) => {
493 match core_client.get_pool(&update_event.pool_identifier) {
494 Ok(pool) => match core_client
495 .process_pool_fee_protocol_update_event(&update_event, pool)
496 {
497 Ok(update) => {
498 Some(DataEvent::DeFi(DefiData::PoolFeeProtocolUpdate(update)))
499 }
500 Err(e) => {
501 log::error!("Error processing pool fee-protocol update event: {e}");
502 None
503 }
504 },
505 Err(e) => {
506 log::error!(
507 "Failed to get pool {} with error {:?}",
508 update_event.pool_identifier,
509 e
510 );
511 None
512 }
513 }
514 }
515 BlockchainMessage::FeeProtocolCollectEvent(collect_event) => {
516 match core_client.get_pool(&collect_event.pool_identifier) {
517 Ok(pool) => match core_client
518 .process_pool_fee_protocol_collect_event(&collect_event, pool)
519 {
520 Ok(collect) => {
521 Some(DataEvent::DeFi(DefiData::PoolFeeProtocolCollect(collect)))
522 }
523 Err(e) => {
524 log::error!("Error processing pool fee-protocol collect event: {e}");
525 None
526 }
527 },
528 Err(e) => {
529 log::error!(
530 "Failed to get pool {} with error {:?}",
531 collect_event.pool_identifier,
532 e
533 );
534 None
535 }
536 }
537 }
538 }
539 }
540
541 async fn handle_subscribe_command(
543 command: DefiSubscribeCommand,
544 core_client: &mut BlockchainDataClientCore,
545 ) -> anyhow::Result<()> {
546 match command {
547 DefiSubscribeCommand::Blocks(_cmd) => {
548 log::debug!("Processing subscribe blocks command");
549
550 Self::subscribe_block_feed(core_client, BlockFeedOwner::Explicit).await?;
551
552 Ok(())
553 }
554 DefiSubscribeCommand::Pool(cmd) => {
555 log::debug!(
556 "Processing subscribe pool command for {}",
557 cmd.instrument_id
558 );
559
560 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
561 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
562 .map_err(|e| {
563 anyhow::anyhow!(
564 "Invalid pool address '{}' failed with error: {:?}",
565 cmd.instrument_id,
566 e
567 )
568 })?;
569
570 core_client
571 .subscription_manager
572 .subscribe_pool(dex, pool_address);
573 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
574 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
575
576 log::debug!(
577 "Subscribed to all pool events for {} at address {}",
578 cmd.instrument_id,
579 pool_address
580 );
581 } else {
582 anyhow::bail!(
583 "Invalid venue {}, expected Blockchain DEX format",
584 cmd.instrument_id.venue
585 )
586 }
587
588 Ok(())
589 }
590 DefiSubscribeCommand::PoolSwaps(cmd) => {
591 log::debug!(
592 "Processing subscribe pool swaps command for {}",
593 cmd.instrument_id
594 );
595
596 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
597 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
598 .map_err(|e| {
599 anyhow::anyhow!(
600 "Invalid pool swap address '{}' failed with error: {:?}",
601 cmd.instrument_id,
602 e
603 )
604 })?;
605 core_client
606 .subscription_manager
607 .subscribe_swaps(dex, pool_address);
608 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
609 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
610 } else {
611 anyhow::bail!(
612 "Invalid venue {}, expected Blockchain DEX format",
613 cmd.instrument_id.venue
614 )
615 }
616
617 Ok(())
618 }
619 DefiSubscribeCommand::PoolLiquidityUpdates(cmd) => {
620 log::debug!(
621 "Processing subscribe pool liquidity updates command for address: {}",
622 cmd.instrument_id
623 );
624
625 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
626 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
627 .map_err(|_| {
628 anyhow::anyhow!("Invalid pool swap address: {}", cmd.instrument_id)
629 })?;
630 core_client
631 .subscription_manager
632 .subscribe_burns(dex, pool_address);
633 core_client
634 .subscription_manager
635 .subscribe_mints(dex, pool_address);
636 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
637 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
638 } else {
639 anyhow::bail!(
640 "Invalid venue {}, expected Blockchain DEX format",
641 cmd.instrument_id.venue
642 )
643 }
644
645 Ok(())
646 }
647 DefiSubscribeCommand::PoolFeeCollects(cmd) => {
648 log::debug!(
649 "Processing subscribe pool fee collects command for address: {}",
650 cmd.instrument_id
651 );
652
653 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
654 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
655 .map_err(|_| {
656 anyhow::anyhow!(
657 "Invalid pool fee collect address: {}",
658 cmd.instrument_id
659 )
660 })?;
661 core_client
662 .subscription_manager
663 .subscribe_collects(dex, pool_address);
664 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
665 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
666 } else {
667 anyhow::bail!(
668 "Invalid venue {}, expected Blockchain DEX format",
669 cmd.instrument_id.venue
670 )
671 }
672
673 Ok(())
674 }
675 DefiSubscribeCommand::PoolFlashEvents(cmd) => {
676 log::debug!(
677 "Processing subscribe pool flash command for address: {}",
678 cmd.instrument_id
679 );
680
681 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
682 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
683 .map_err(|_| {
684 anyhow::anyhow!(
685 "Invalid pool flash subscribe address: {}",
686 cmd.instrument_id
687 )
688 })?;
689 core_client
690 .subscription_manager
691 .subscribe_flashes(dex, pool_address);
692 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
693 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
694 } else {
695 anyhow::bail!(
696 "Invalid venue {}, expected Blockchain DEX format",
697 cmd.instrument_id.venue
698 )
699 }
700
701 Ok(())
702 }
703 }
704 }
705
706 async fn handle_unsubscribe_command(
708 command: DefiUnsubscribeCommand,
709 core_client: &mut BlockchainDataClientCore,
710 ) -> anyhow::Result<()> {
711 match command {
712 DefiUnsubscribeCommand::Blocks(_cmd) => {
713 log::debug!("Processing unsubscribe blocks command");
714
715 Self::unsubscribe_block_feed(core_client, BlockFeedOwner::Explicit).await?;
716
717 Ok(())
718 }
719 DefiUnsubscribeCommand::Pool(cmd) => {
720 log::debug!(
721 "Processing unsubscribe pool command for {}",
722 cmd.instrument_id
723 );
724
725 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
726 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
727 .map_err(|_| {
728 anyhow::anyhow!("Invalid pool address: {}", cmd.instrument_id)
729 })?;
730
731 core_client
732 .subscription_manager
733 .unsubscribe_pool(dex, pool_address);
734 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
735 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
736
737 log::debug!(
738 "Unsubscribed from all pool events for {} at address {}",
739 cmd.instrument_id,
740 pool_address
741 );
742 } else {
743 anyhow::bail!(
744 "Invalid venue {}, expected Blockchain DEX format",
745 cmd.instrument_id.venue
746 )
747 }
748
749 Ok(())
750 }
751 DefiUnsubscribeCommand::PoolSwaps(cmd) => {
752 log::debug!("Processing unsubscribe pool swaps command");
753
754 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
755 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
756 .map_err(|_| {
757 anyhow::anyhow!("Invalid pool swap address: {}", cmd.instrument_id)
758 })?;
759 core_client
760 .subscription_manager
761 .unsubscribe_swaps(dex, pool_address);
762 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
763 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
764 } else {
765 anyhow::bail!(
766 "Invalid venue {}, expected Blockchain DEX format",
767 cmd.instrument_id.venue
768 )
769 }
770
771 Ok(())
772 }
773 DefiUnsubscribeCommand::PoolLiquidityUpdates(cmd) => {
774 log::debug!(
775 "Processing unsubscribe pool liquidity updates command for {}",
776 cmd.instrument_id
777 );
778
779 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
780 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
781 .map_err(|_| {
782 anyhow::anyhow!("Invalid pool swap address: {}", cmd.instrument_id)
783 })?;
784 core_client
785 .subscription_manager
786 .unsubscribe_burns(dex, pool_address);
787 core_client
788 .subscription_manager
789 .unsubscribe_mints(dex, pool_address);
790 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
791 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
792 } else {
793 anyhow::bail!(
794 "Invalid venue {}, expected Blockchain DEX format",
795 cmd.instrument_id.venue
796 )
797 }
798
799 Ok(())
800 }
801 DefiUnsubscribeCommand::PoolFeeCollects(cmd) => {
802 log::debug!(
803 "Processing unsubscribe pool fee collects command for {}",
804 cmd.instrument_id
805 );
806
807 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
808 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
809 .map_err(|_| {
810 anyhow::anyhow!(
811 "Invalid pool fee collect address: {}",
812 cmd.instrument_id
813 )
814 })?;
815 core_client
816 .subscription_manager
817 .unsubscribe_collects(dex, pool_address);
818 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
819 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
820 } else {
821 anyhow::bail!(
822 "Invalid venue {}, expected Blockchain DEX format",
823 cmd.instrument_id.venue
824 )
825 }
826
827 Ok(())
828 }
829 DefiUnsubscribeCommand::PoolFlashEvents(cmd) => {
830 log::debug!(
831 "Processing unsubscribe pool flash command for {}",
832 cmd.instrument_id
833 );
834
835 if let Ok((_, dex)) = cmd.instrument_id.venue.parse_dex() {
836 let pool_address = validate_address(cmd.instrument_id.symbol.as_str())
837 .map_err(|_| {
838 anyhow::anyhow!("Invalid pool flash address: {}", cmd.instrument_id)
839 })?;
840 core_client
841 .subscription_manager
842 .unsubscribe_flashes(dex, pool_address);
843 Self::update_rpc_pool_event_subscriptions(core_client, dex).await?;
844 Self::update_hypersync_pool_event_stream(core_client, dex).await?;
845 } else {
846 anyhow::bail!(
847 "Invalid venue {}, expected Blockchain DEX format",
848 cmd.instrument_id.venue
849 )
850 }
851
852 Ok(())
853 }
854 }
855 }
856
857 async fn update_rpc_pool_event_subscriptions(
858 core_client: &mut BlockchainDataClientCore,
859 dex: DexType,
860 ) -> anyhow::Result<()> {
861 let updates = vec![
862 (
863 RpcEventType::PoolSwap(dex),
864 core_client
865 .subscription_manager
866 .get_subscribed_pool_swap_addresses(&dex),
867 core_client
868 .subscription_manager
869 .get_dex_pool_swap_event_signature(&dex),
870 ),
871 (
872 RpcEventType::PoolMint(dex),
873 core_client
874 .subscription_manager
875 .get_subscribed_pool_mint_addresses(&dex),
876 core_client
877 .subscription_manager
878 .get_dex_pool_mint_event_signature(&dex),
879 ),
880 (
881 RpcEventType::PoolBurn(dex),
882 core_client
883 .subscription_manager
884 .get_subscribed_pool_burn_addresses(&dex),
885 core_client
886 .subscription_manager
887 .get_dex_pool_burn_event_signature(&dex),
888 ),
889 (
890 RpcEventType::PoolCollect(dex),
891 core_client
892 .subscription_manager
893 .get_subscribed_pool_collect_addresses(&dex),
894 core_client
895 .subscription_manager
896 .get_dex_pool_collect_event_signature(&dex),
897 ),
898 (
899 RpcEventType::PoolFlash(dex),
900 core_client
901 .subscription_manager
902 .get_subscribed_pool_flash_addresses(&dex),
903 core_client
904 .subscription_manager
905 .get_dex_pool_flash_event_signature(&dex),
906 ),
907 (
908 RpcEventType::PoolFeeProtocolUpdate(dex),
909 core_client
910 .subscription_manager
911 .get_subscribed_pool_fee_protocol_update_addresses(&dex),
912 core_client
913 .subscription_manager
914 .get_dex_pool_fee_protocol_update_event_signature(&dex),
915 ),
916 (
917 RpcEventType::PoolFeeProtocolCollect(dex),
918 core_client
919 .subscription_manager
920 .get_subscribed_pool_fee_protocol_collect_addresses(&dex),
921 core_client
922 .subscription_manager
923 .get_dex_pool_fee_protocol_collect_event_signature(&dex),
924 ),
925 ];
926
927 let has_pool_event_subscriptions = Self::has_active_pool_event_subscriptions(core_client);
928
929 if core_client.rpc_client.is_none() {
930 return Ok(());
931 }
932
933 if has_pool_event_subscriptions {
934 Self::subscribe_block_feed(core_client, BlockFeedOwner::PoolEvents).await?;
935 }
936
937 if let Some(ref mut rpc) = core_client.rpc_client {
938 for (event_type, addresses, event_signature) in updates {
939 if let Some(event_signature) = event_signature {
940 rpc.subscribe_pool_events(event_type, &addresses, event_signature)
941 .await?;
942 }
943 }
944 }
945
946 if !has_pool_event_subscriptions {
947 Self::unsubscribe_block_feed(core_client, BlockFeedOwner::PoolEvents).await?;
948 }
949
950 Ok(())
951 }
952
953 async fn update_hypersync_pool_event_stream(
954 core_client: &mut BlockchainDataClientCore,
955 dex: DexType,
956 ) -> anyhow::Result<()> {
957 if core_client.rpc_client.is_some() {
958 return Ok(());
959 }
960
961 let addresses = core_client
962 .subscription_manager
963 .get_subscribed_dex_contract_addresses(&dex);
964 let event_signatures = core_client
965 .subscription_manager
966 .get_active_subscribed_dex_event_signatures(&dex);
967 let has_pool_event_subscriptions = Self::has_active_pool_event_subscriptions(core_client);
968
969 if has_pool_event_subscriptions {
970 Self::subscribe_block_feed(core_client, BlockFeedOwner::PoolEvents).await?;
971 }
972
973 core_client
974 .hypersync_client
975 .update_dex_event_stream(dex, addresses, event_signatures)
976 .await;
977
978 if !has_pool_event_subscriptions {
979 Self::unsubscribe_block_feed(core_client, BlockFeedOwner::PoolEvents).await?;
980 }
981
982 Ok(())
983 }
984
985 fn has_active_pool_event_subscriptions(core_client: &BlockchainDataClientCore) -> bool {
986 core_client
987 .subscription_manager
988 .has_pool_event_subscriptions()
989 }
990
991 async fn subscribe_block_feed(
992 core_client: &mut BlockchainDataClientCore,
993 owner: BlockFeedOwner,
994 ) -> anyhow::Result<()> {
995 let preferred_backend = if core_client.rpc_client.is_some() {
996 BlockFeedBackend::Rpc
997 } else {
998 BlockFeedBackend::HyperSync
999 };
1000 let Some(backend) = core_client
1001 .subscription_manager
1002 .add_block_demand(owner, preferred_backend)
1003 else {
1004 return Ok(());
1005 };
1006
1007 let started_backend = match backend {
1008 BlockFeedBackend::Rpc => {
1009 let Some(rpc) = core_client.rpc_client.as_mut() else {
1010 anyhow::bail!("RPC block feed selected without an RPC client")
1011 };
1012
1013 match rpc.subscribe_blocks().await {
1014 Ok(()) => {
1015 log::debug!("Successfully subscribed to blocks via RPC");
1016 BlockFeedBackend::Rpc
1017 }
1018 Err(e) if owner == BlockFeedOwner::Explicit => {
1019 log::warn!(
1020 "RPC blocks subscription failed: {e}, falling back to HyperSync"
1021 );
1022 core_client.hypersync_client.subscribe_blocks();
1023 tokio::task::yield_now().await;
1024 BlockFeedBackend::HyperSync
1025 }
1026 Err(e) => return Err(e.into()),
1027 }
1028 }
1029 BlockFeedBackend::HyperSync => {
1030 log::debug!("Subscribing to blocks via HyperSync");
1031 core_client.hypersync_client.subscribe_blocks();
1032 tokio::task::yield_now().await;
1033 BlockFeedBackend::HyperSync
1034 }
1035 };
1036
1037 core_client
1038 .subscription_manager
1039 .block_feed_started(started_backend);
1040 Ok(())
1041 }
1042
1043 async fn unsubscribe_block_feed(
1044 core_client: &mut BlockchainDataClientCore,
1045 owner: BlockFeedOwner,
1046 ) -> anyhow::Result<()> {
1047 let Some(backend) = core_client.subscription_manager.remove_block_demand(owner) else {
1048 log::debug!("Keeping block subscription active while another owner remains");
1049 return Ok(());
1050 };
1051
1052 match backend {
1053 BlockFeedBackend::Rpc => {
1054 let Some(rpc) = core_client.rpc_client.as_mut() else {
1055 anyhow::bail!("RPC block feed active without an RPC client")
1056 };
1057 rpc.unsubscribe_blocks().await?;
1058 log::debug!("Unsubscribed from blocks via RPC");
1059 }
1060 BlockFeedBackend::HyperSync => {
1061 core_client.hypersync_client.unsubscribe_blocks().await;
1062 log::debug!("Unsubscribed from blocks via HyperSync");
1063 }
1064 }
1065
1066 core_client.subscription_manager.block_feed_stopped(backend);
1067 Ok(())
1068 }
1069
1070 async fn handle_request_command(
1072 command: DefiRequestCommand,
1073 core_client: &mut BlockchainDataClientCore,
1074 ) -> anyhow::Result<()> {
1075 match command {
1076 DefiRequestCommand::PoolSnapshot(cmd) => {
1077 log::debug!("Processing pool snapshot request for {}", cmd.instrument_id);
1078
1079 let pool_address =
1080 validate_address(cmd.instrument_id.symbol.as_str()).map_err(|e| {
1081 anyhow::anyhow!(
1082 "Invalid pool address '{}' failed with error: {:?}",
1083 cmd.instrument_id,
1084 e
1085 )
1086 })?;
1087
1088 let pool_identifier =
1089 PoolIdentifier::Address(Ustr::from(&pool_address.to_string()));
1090
1091 match core_client.get_pool(&pool_identifier) {
1092 Ok(pool) => {
1093 let pool = Arc::clone(pool);
1094 log::debug!("Found pool for snapshot request: {}", cmd.instrument_id);
1095
1096 let pool_data = DataEvent::DeFi(DefiData::Pool(pool.as_ref().clone()));
1098 core_client.send_data(pool_data);
1099
1100 match core_client
1101 .bootstrap_latest_pool_profiler(&pool, None)
1102 .await
1103 {
1104 Ok((profiler, already_valid)) => match profiler.extract_snapshot() {
1105 Ok(snapshot) => {
1106 log::debug!(
1107 "Saving pool snapshot with {} positions and {} ticks to database...",
1108 snapshot.positions.len(),
1109 snapshot.ticks.len()
1110 );
1111 core_client
1112 .cache
1113 .add_pool_snapshot(
1114 &pool.dex.name,
1115 &pool.pool_identifier,
1116 &snapshot,
1117 )
1118 .await?;
1119
1120 if core_client
1122 .check_snapshot_validity(&profiler, already_valid)
1123 .await?
1124 .is_usable()
1125 {
1126 let response =
1127 PoolSnapshotResponse::new(cmd.request_id, snapshot);
1128 core_client
1129 .send_data(DataEvent::PoolSnapshotResponse(response));
1130 }
1131 }
1132 Err(e) => log::error!(
1133 "Failed to extract snapshot for {}: {e}",
1134 cmd.instrument_id
1135 ),
1136 },
1137 Err(e) => log::error!(
1138 "Failed to bootstrap pool profiler for {} and extract snapshot with error {e}",
1139 cmd.instrument_id
1140 ),
1141 }
1142 }
1143 Err(e) => {
1144 log::warn!("Pool {} not found in cache: {e}", cmd.instrument_id);
1145 }
1146 }
1147
1148 Ok(())
1149 }
1150 }
1151 }
1152
1153 pub async fn await_process_task_close(&self) -> anyhow::Result<()> {
1162 self.session_tasks
1163 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2))
1164 .await
1165 .map_err(|e| anyhow::anyhow!("Blockchain process task shutdown failed: {e}"))?;
1166 Ok(())
1167 }
1168
1169 async fn prepare_task_group(&mut self) -> anyhow::Result<()> {
1170 if !self.session_tasks.is_open() {
1171 self.session_tasks.begin_shutdown();
1172 self.await_process_task_close().await?;
1173 self.reset_channels();
1174 self.session_tasks
1175 .start_generation()
1176 .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
1177 self.cancellation_token = self.session_tasks.cancellation_token();
1178 }
1179 Ok(())
1180 }
1181
1182 fn reset_channels(&mut self) {
1183 let (hypersync_tx, hypersync_rx) = tokio::sync::mpsc::unbounded_channel();
1184 self.hypersync_tx = Some(hypersync_tx);
1185 self.hypersync_rx = Some(hypersync_rx);
1186 let (command_tx, command_rx) = tokio::sync::mpsc::unbounded_channel();
1187 self.command_tx = command_tx;
1188 self.command_rx = Some(command_rx);
1189 }
1190}
1191
1192#[async_trait::async_trait(?Send)]
1193impl DataClient for BlockchainDataClient {
1194 fn client_id(&self) -> ClientId {
1195 self.client_id
1196 }
1197
1198 fn venue(&self) -> Option<Venue> {
1199 None
1202 }
1203
1204 fn start(&mut self) -> anyhow::Result<()> {
1205 log::info!(
1206 "Starting blockchain data client: chain_name={}, dex_ids={:?}, use_hypersync_for_live_data={}, proxy_url={:?}",
1207 self.chain.name,
1208 self.config.dex_ids,
1209 self.config.use_hypersync_for_live_data,
1210 self.config.proxy_url
1211 );
1212 Ok(())
1213 }
1214
1215 fn stop(&mut self) -> anyhow::Result<()> {
1216 log::info!(
1217 "Stopping blockchain data client for '{chain_name}'",
1218 chain_name = self.chain.name
1219 );
1220 self.session_tasks.begin_shutdown();
1221 Ok(())
1222 }
1223
1224 fn reset(&mut self) -> anyhow::Result<()> {
1225 log::info!(
1226 "Resetting blockchain data client for '{chain_name}'",
1227 chain_name = self.chain.name
1228 );
1229 self.session_tasks.begin_shutdown();
1230 Ok(())
1231 }
1232
1233 fn dispose(&mut self) -> anyhow::Result<()> {
1234 log::info!(
1235 "Disposing blockchain data client for '{chain_name}'",
1236 chain_name = self.chain.name
1237 );
1238 self.session_tasks.begin_shutdown();
1239 Ok(())
1240 }
1241
1242 async fn connect(&mut self) -> anyhow::Result<()> {
1243 if self.session_tasks.is_open() && !self.session_tasks.is_empty() {
1244 return Ok(());
1245 }
1246
1247 log::info!(
1248 "Connecting blockchain data client for '{}'",
1249 self.chain.name
1250 );
1251
1252 self.prepare_task_group().await?;
1253 let setup_guard = TaskGroupGuard::new(&[&self.session_tasks], || {});
1254 let startup = self.spawn_process_task()?;
1255 if let Err(e) = startup
1256 .await
1257 .context("Blockchain process task stopped before startup")?
1258 {
1259 self.session_tasks.begin_shutdown();
1260 let teardown_result = self.await_process_task_close().await;
1261 self.reset_channels();
1262
1263 if let Err(teardown_error) = teardown_result {
1264 return Err(e.context(format!(
1265 "Blockchain data startup teardown failed: {teardown_error}"
1266 )));
1267 }
1268 return Err(e);
1269 }
1270
1271 setup_guard.disarm();
1272 Ok(())
1273 }
1274
1275 async fn disconnect(&mut self) -> anyhow::Result<()> {
1276 log::info!(
1277 "Disconnecting blockchain data client for '{}'",
1278 self.chain.name
1279 );
1280
1281 self.session_tasks.begin_shutdown();
1282 let tasks_result = self.await_process_task_close().await;
1283 self.reset_channels();
1284
1285 tasks_result
1286 }
1287
1288 fn is_connected(&self) -> bool {
1289 self.session_tasks.is_open()
1290 && !self.session_tasks.is_empty()
1291 && !self.session_tasks.all_finished()
1292 }
1293
1294 fn is_disconnected(&self) -> bool {
1295 !self.is_connected()
1296 }
1297
1298 fn subscribe_blocks(&mut self, cmd: SubscribeBlocks) -> anyhow::Result<()> {
1299 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::Blocks(cmd));
1300 self.command_tx.send(command)?;
1301 Ok(())
1302 }
1303
1304 fn subscribe_pool(&mut self, cmd: SubscribePool) -> anyhow::Result<()> {
1305 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::Pool(cmd));
1306 self.command_tx.send(command)?;
1307 Ok(())
1308 }
1309
1310 fn subscribe_pool_swaps(&mut self, cmd: SubscribePoolSwaps) -> anyhow::Result<()> {
1311 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::PoolSwaps(cmd));
1312 self.command_tx.send(command)?;
1313 Ok(())
1314 }
1315
1316 fn subscribe_pool_liquidity_updates(
1317 &mut self,
1318 cmd: SubscribePoolLiquidityUpdates,
1319 ) -> anyhow::Result<()> {
1320 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::PoolLiquidityUpdates(cmd));
1321 self.command_tx.send(command)?;
1322 Ok(())
1323 }
1324
1325 fn subscribe_pool_fee_collects(&mut self, cmd: SubscribePoolFeeCollects) -> anyhow::Result<()> {
1326 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::PoolFeeCollects(cmd));
1327 self.command_tx.send(command)?;
1328 Ok(())
1329 }
1330
1331 fn subscribe_pool_flash_events(&mut self, cmd: SubscribePoolFlashEvents) -> anyhow::Result<()> {
1332 let command = DefiDataCommand::Subscribe(DefiSubscribeCommand::PoolFlashEvents(cmd));
1333 self.command_tx.send(command)?;
1334 Ok(())
1335 }
1336
1337 fn unsubscribe_blocks(&mut self, cmd: &UnsubscribeBlocks) -> anyhow::Result<()> {
1338 let command = DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::Blocks(cmd.clone()));
1339 self.command_tx.send(command)?;
1340 Ok(())
1341 }
1342
1343 fn unsubscribe_pool(&mut self, cmd: &UnsubscribePool) -> anyhow::Result<()> {
1344 let command = DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::Pool(cmd.clone()));
1345 self.command_tx.send(command)?;
1346 Ok(())
1347 }
1348
1349 fn unsubscribe_pool_swaps(&mut self, cmd: &UnsubscribePoolSwaps) -> anyhow::Result<()> {
1350 let command = DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::PoolSwaps(cmd.clone()));
1351 self.command_tx.send(command)?;
1352 Ok(())
1353 }
1354
1355 fn unsubscribe_pool_liquidity_updates(
1356 &mut self,
1357 cmd: &UnsubscribePoolLiquidityUpdates,
1358 ) -> anyhow::Result<()> {
1359 let command =
1360 DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::PoolLiquidityUpdates(cmd.clone()));
1361 self.command_tx.send(command)?;
1362 Ok(())
1363 }
1364
1365 fn unsubscribe_pool_fee_collects(
1366 &mut self,
1367 cmd: &UnsubscribePoolFeeCollects,
1368 ) -> anyhow::Result<()> {
1369 let command =
1370 DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::PoolFeeCollects(cmd.clone()));
1371 self.command_tx.send(command)?;
1372 Ok(())
1373 }
1374
1375 fn unsubscribe_pool_flash_events(
1376 &mut self,
1377 cmd: &UnsubscribePoolFlashEvents,
1378 ) -> anyhow::Result<()> {
1379 let command =
1380 DefiDataCommand::Unsubscribe(DefiUnsubscribeCommand::PoolFlashEvents(cmd.clone()));
1381 self.command_tx.send(command)?;
1382 Ok(())
1383 }
1384
1385 fn request_pool_snapshot(&self, cmd: RequestPoolSnapshot) -> anyhow::Result<()> {
1386 let command = DefiDataCommand::Request(DefiRequestCommand::PoolSnapshot(cmd));
1387 self.command_tx.send(command)?;
1388 Ok(())
1389 }
1390}
1391
1392#[cfg(test)]
1393mod tests {
1394 use std::sync::Arc;
1395
1396 use alloy::primitives::{I256, U160, U256, address};
1397 use nautilus_core::UnixNanos;
1398 use nautilus_model::defi::{Block, Blockchain, Chain, DexType, Pool, PoolIdentifier, Token};
1399 use rstest::rstest;
1400
1401 use super::*;
1402 use crate::events::{flash::FlashEvent, swap::SwapEvent};
1403
1404 const WETH_USDT_CREATION_BLOCK: u64 = 12_375_326;
1405
1406 #[rstest]
1407 fn pool_event_missing_block_metadata_clears_after_block_cache_update() {
1408 let chain = Arc::new(
1409 Chain::from_chain_id(1)
1410 .expect("Ethereum chain should exist")
1411 .clone(),
1412 );
1413 let mut cache = BlockchainCache::new(chain);
1414 let msg = flash_message(42);
1415
1416 assert_eq!(
1417 BlockchainDataClient::pool_event_block_number(&msg),
1418 Some(42)
1419 );
1420 assert_eq!(
1421 BlockchainDataClient::pool_event_missing_block_metadata(&msg, &cache),
1422 Some(42)
1423 );
1424
1425 let block = test_block(42, "0x1", 1_700_000_000_000_000_000);
1426 cache.cache_block_metadata(&block);
1427
1428 assert_eq!(
1429 BlockchainDataClient::pool_event_missing_block_metadata(&msg, &cache),
1430 None
1431 );
1432 }
1433
1434 #[rstest]
1435 fn queue_pending_pool_message_drops_oldest_entry_at_cap() {
1436 let mut pending = VecDeque::new();
1437
1438 for block_number in 0..MAX_PENDING_POOL_MESSAGES {
1439 BlockchainDataClient::queue_pending_pool_message(
1440 &mut pending,
1441 swap_message(block_number as u64),
1442 );
1443 }
1444
1445 BlockchainDataClient::queue_pending_pool_message(
1446 &mut pending,
1447 swap_message(MAX_PENDING_POOL_MESSAGES as u64),
1448 );
1449
1450 assert_eq!(pending.len(), MAX_PENDING_POOL_MESSAGES);
1451 assert_eq!(
1452 BlockchainDataClient::pool_event_block_number(pending.front().unwrap()),
1453 Some(1)
1454 );
1455 assert_eq!(
1456 BlockchainDataClient::pool_event_block_number(pending.back().unwrap()),
1457 Some(MAX_PENDING_POOL_MESSAGES as u64)
1458 );
1459 }
1460
1461 #[rstest]
1462 fn drain_pending_pool_messages_releases_events_after_block_metadata_is_cached() {
1463 let chain = Arc::new(
1464 Chain::from_chain_id(1)
1465 .expect("Ethereum chain should exist")
1466 .clone(),
1467 );
1468 let mut cache = BlockchainCache::new(chain);
1469 let block_42 = test_block(42, "0x42", 1_700_000_000_000_000_000);
1470 cache.cache_block_metadata(&block_42);
1471 let mut pending = VecDeque::from([flash_message(41), flash_message(42)]);
1472
1473 let ready_messages = BlockchainDataClient::drain_pending_pool_messages_with_cached_metadata(
1474 &mut pending,
1475 &cache,
1476 );
1477
1478 assert_eq!(ready_messages.len(), 1);
1479 assert_eq!(
1480 BlockchainDataClient::pool_event_block_number(&ready_messages[0]),
1481 Some(42)
1482 );
1483 assert_eq!(pending.len(), 1);
1484 assert_eq!(
1485 BlockchainDataClient::pool_event_block_number(pending.front().unwrap()),
1486 Some(41)
1487 );
1488
1489 let block_41 = test_block(41, "0x41", 1_700_000_000_000_000_001);
1490 cache.cache_block_metadata(&block_41);
1491
1492 let ready_messages = BlockchainDataClient::drain_pending_pool_messages_with_cached_metadata(
1493 &mut pending,
1494 &cache,
1495 );
1496
1497 assert_eq!(ready_messages.len(), 1);
1498 assert_eq!(
1499 BlockchainDataClient::pool_event_block_number(&ready_messages[0]),
1500 Some(41)
1501 );
1502 assert!(pending.is_empty());
1503 }
1504
1505 #[rstest]
1506 fn ready_live_blockchain_message_queues_until_block_metadata_is_cached() {
1507 let chain = Arc::new(
1508 Chain::from_chain_id(1)
1509 .expect("Ethereum chain should exist")
1510 .clone(),
1511 );
1512 let mut cache = BlockchainCache::new(chain);
1513 let mut pending = VecDeque::new();
1514
1515 let message = BlockchainDataClient::ready_live_blockchain_message(
1516 flash_message(42),
1517 &cache,
1518 &mut pending,
1519 );
1520
1521 assert!(message.is_none());
1522 assert_eq!(pending.len(), 1);
1523 assert_eq!(
1524 BlockchainDataClient::pool_event_block_number(pending.front().unwrap()),
1525 Some(42)
1526 );
1527
1528 let block = test_block(42, "0x1", 1_700_000_000_000_000_000);
1529 cache.cache_block_metadata(&block);
1530
1531 let message = BlockchainDataClient::ready_live_blockchain_message(
1532 BlockchainMessage::Block(block),
1533 &cache,
1534 &mut pending,
1535 );
1536 let ready_messages = BlockchainDataClient::drain_pending_pool_messages_with_cached_metadata(
1537 &mut pending,
1538 &cache,
1539 );
1540
1541 assert!(matches!(message, Some(BlockchainMessage::Block(_))));
1542 assert_eq!(ready_messages.len(), 1);
1543 assert_eq!(
1544 BlockchainDataClient::pool_event_block_number(&ready_messages[0]),
1545 Some(42)
1546 );
1547 assert!(pending.is_empty());
1548 }
1549
1550 fn swap_message(block_number: u64) -> BlockchainMessage {
1551 let pool = weth_usdt_pool();
1552 let address = address!("1111111111111111111111111111111111111111");
1553
1554 BlockchainMessage::SwapEvent(SwapEvent::new(
1555 Arc::clone(&pool.dex),
1556 pool.pool_identifier,
1557 block_number,
1558 "0x1".to_string(),
1559 0,
1560 0,
1561 address,
1562 address,
1563 I256::ZERO,
1564 I256::ZERO,
1565 U160::ZERO,
1566 0,
1567 0,
1568 ))
1569 }
1570
1571 fn test_block(number: u64, hash: &str, timestamp: u64) -> Block {
1572 Block::new(
1573 hash.to_string(),
1574 "0x0".to_string(),
1575 number,
1576 Ustr::from("0x0000000000000000000000000000000000000000"),
1577 30_000_000,
1578 21_000,
1579 UnixNanos::from(timestamp),
1580 Some(Blockchain::Ethereum),
1581 )
1582 }
1583
1584 fn flash_message(block_number: u64) -> BlockchainMessage {
1585 let pool = weth_usdt_pool();
1586 let address = address!("1111111111111111111111111111111111111111");
1587
1588 BlockchainMessage::FlashEvent(FlashEvent::new(
1589 Arc::clone(&pool.dex),
1590 pool.pool_identifier,
1591 block_number,
1592 "0x1".to_string(),
1593 0,
1594 0,
1595 address,
1596 address,
1597 U256::ZERO,
1598 U256::ZERO,
1599 U256::ZERO,
1600 U256::ZERO,
1601 ))
1602 }
1603
1604 fn weth_usdt_pool() -> Arc<Pool> {
1605 let chain = Arc::new(
1606 Chain::from_chain_id(1)
1607 .expect("Ethereum chain should exist")
1608 .clone(),
1609 );
1610 let dex = Arc::clone(
1611 &get_dex_extended(chain.name, &DexType::UniswapV3)
1612 .expect("Ethereum UniswapV3 should be registered")
1613 .dex,
1614 );
1615 let pool_address = address!("4e68ccd3e89f51c3074ca5072bbac773960dfa36");
1616 let token0 = Token::new(
1617 Arc::clone(&chain),
1618 address!("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"),
1619 "Wrapped Ether".to_string(),
1620 "WETH".to_string(),
1621 18,
1622 );
1623 let token1 = Token::new(
1624 Arc::clone(&chain),
1625 address!("dAC17F958D2ee523a2206206994597C13D831ec7"),
1626 "Tether USD".to_string(),
1627 "USDT".to_string(),
1628 6,
1629 );
1630
1631 Arc::new(Pool::new(
1632 chain,
1633 dex,
1634 pool_address,
1635 PoolIdentifier::from_address(pool_address),
1636 WETH_USDT_CREATION_BLOCK,
1637 token0,
1638 token1,
1639 Some(3_000),
1640 Some(60),
1641 UnixNanos::default(),
1642 ))
1643 }
1644}