1use std::{
17 sync::{
18 Arc,
19 atomic::{AtomicU64, Ordering},
20 },
21 time::Duration,
22};
23
24use ahash::AHashMap;
25use alloy::primitives::Address;
26use futures_util::Stream;
27use hypersync_client::{
28 StreamConfig,
29 net_types::{BlockField, BlockSelection, FieldSelection, Query},
30 simple_types::Log,
31};
32use nautilus_core::{consts::NAUTILUS_USER_AGENT, hex};
33use nautilus_live::task::{TaskJoinOutcome, TaskSlot, finish_task};
34use nautilus_model::{
35 defi::{Block, Blockchain, DexType, SharedChain},
36 identifiers::InstrumentId,
37};
38
39use crate::{
40 exchanges::{extended::DexExtended, get_dex_extended},
41 hypersync::transform::transform_hypersync_block,
42 rpc::{http::validate_execution_endpoint, types::BlockchainMessage},
43};
44
45#[derive(Debug)]
50pub enum PoolEventStreamItem {
51 Block(Block),
53 Log(Log),
55}
56
57const BLOCK_POLLING_INTERVAL_MS: u64 = 50;
60
61const HYPERSYNC_REQUEST_TIMEOUT_SECS: u64 = 30;
63
64const DISCONNECT_TIMEOUT_SECS: u64 = 5;
67
68const DEX_EVENT_STREAM_RETRY_DELAY_MS: u64 = 1_000;
70
71#[derive(Debug)]
73pub struct HyperSyncClient {
74 chain: SharedChain,
76 client: Arc<hypersync_client::Client>,
78 blocks_task: TaskSlot<()>,
80 blocks_cancellation_token: Option<tokio_util::sync::CancellationToken>,
82 dex_event_tasks: AHashMap<DexType, DexEventStreamTask>,
84 tx: Option<tokio::sync::mpsc::UnboundedSender<BlockchainMessage>>,
86 pool_addresses: AHashMap<InstrumentId, Address>,
88 cancellation_token: tokio_util::sync::CancellationToken,
90}
91
92impl HyperSyncClient {
93 #[must_use]
102 pub fn new(
103 chain: SharedChain,
104 tx: Option<tokio::sync::mpsc::UnboundedSender<BlockchainMessage>>,
105 cancellation_token: tokio_util::sync::CancellationToken,
106 ) -> Self {
107 let mut config = hypersync_client::ClientConfig::default();
108 let hypersync_url = validate_execution_endpoint(chain.hypersync_url.as_str(), "HyperSync")
109 .expect("Invalid HyperSync URL");
110 config.url = hypersync_url.to_string();
111 config.api_token = std::env::var("ENVIO_API_TOKEN")
112 .expect("ENVIO_API_TOKEN environment variable must be set");
113
114 config
115 .validate()
116 .expect("Failed to create HyperSync client - check ENVIO_API_TOKEN is a valid UUID");
117 let client = hypersync_client::Client::new_with_agent(config, NAUTILUS_USER_AGENT)
118 .expect("Failed to create HyperSync client - check ENVIO_API_TOKEN is a valid UUID");
119
120 Self {
121 chain,
122 client: Arc::new(client),
123 blocks_task: TaskSlot::new(),
124 blocks_cancellation_token: None,
125 dex_event_tasks: AHashMap::new(),
126 tx,
127 pool_addresses: AHashMap::new(),
128 cancellation_token,
129 }
130 }
131
132 #[must_use]
133 pub fn get_pool_address(&self, instrument_id: InstrumentId) -> Option<&Address> {
134 self.pool_addresses.get(&instrument_id)
135 }
136
137 pub async fn update_dex_event_stream(
142 &mut self,
143 dex: DexType,
144 contract_addresses: Vec<Address>,
145 event_signatures: Vec<String>,
146 ) {
147 let filter = DexEventStreamFilter::new(contract_addresses, event_signatures);
148
149 if filter.is_empty() {
150 self.stop_dex_event_stream(dex).await;
151 return;
152 }
153
154 if self.dex_event_tasks.get(&dex).is_some_and(|task| {
155 task.filter == filter
156 && task
157 .task
158 .as_ref()
159 .is_some_and(|handle| !handle.is_finished())
160 }) {
161 return;
162 }
163
164 let next_from_block = self.stop_dex_event_stream(dex).await;
165 if self.dex_event_tasks.contains_key(&dex) {
166 log::error!("Previous HyperSync DEX event stream for {dex} is still stopping");
167 return;
168 }
169
170 let from_block = match next_from_block {
171 Some(block) => block,
172 None => match self.client.get_height().await {
173 Ok(block) => block,
174 Err(e) => {
175 log::error!("Failed to get HyperSync height for DEX event stream: {e}");
176 return;
177 }
178 },
179 };
180
181 let tx = if let Some(tx) = &self.tx {
182 tx.clone()
183 } else {
184 log::error!("Hypersync client channel should have been initialized");
185 return;
186 };
187
188 let client = self.client.clone();
189 let chain = self.chain.name;
190 let Some(dex_extended) = get_dex_extended(chain, &dex) else {
191 log::error!("Failed to get DEX registration for {dex} on {chain}");
192 return;
193 };
194 let stream_token = self.cancellation_token.child_token();
195 let task_token = stream_token.clone();
196 let next_from_block = Arc::new(AtomicU64::new(from_block));
197 let task_next_from_block = next_from_block.clone();
198 let task_filter = filter.clone();
199
200 let mut task = TaskSlot::new();
201 if let Err(e) = task.spawn(async move {
202 Self::run_dex_event_stream(
203 dex,
204 client,
205 tx,
206 task_filter,
207 dex_extended,
208 task_next_from_block,
209 task_token,
210 )
211 .await;
212 }) {
213 stream_token.cancel();
214 log::error!("Failed to start HyperSync DEX event stream for {dex}: {e}");
215 }
216
217 self.dex_event_tasks.insert(
218 dex,
219 DexEventStreamTask {
220 filter,
221 next_from_block,
222 cancellation_token: stream_token,
223 task,
224 },
225 );
226 }
227
228 pub async fn request_contract_events_stream(
234 &self,
235 from_block: u64,
236 to_block: Option<u64>,
237 contract_address: &Address,
238 topics: Vec<&str>,
239 ) -> impl Stream<Item = PoolEventStreamItem> + use<> {
240 let query = Self::construct_contract_events_query(
241 from_block,
242 to_block,
243 &[*contract_address],
244 &topics,
245 );
246
247 let chain = self.chain.name;
248 let mut rx = self
249 .client
250 .clone()
251 .stream(query, StreamConfig::default())
252 .await
253 .expect("Failed to create stream");
254
255 async_stream::stream! {
256 while let Some(response) = rx.recv().await {
257 let response = response.unwrap();
258 for item in pool_events_from_response(chain, response.data.blocks, response.data.logs) {
259 yield item;
260 }
261 }
262 }
263 }
264
265 pub async fn disconnect(&mut self) {
267 log::debug!("Disconnecting HyperSync client");
268 self.cancellation_token.cancel();
269
270 if let Some(outcome) = finish_task(
271 &mut self.blocks_task,
272 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
273 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
274 )
275 .await
276 {
277 match outcome {
278 TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => {}
279 TaskJoinOutcome::Failed(error) => {
280 log::error!("HyperSync blocks task failed: {error}");
281 }
282 TaskJoinOutcome::Incomplete => {
283 log::error!("HyperSync blocks task did not stop after abort");
284 }
285 }
286 }
287
288 let dexes = self.dex_event_tasks.keys().copied().collect::<Vec<_>>();
289 for dex in dexes {
290 self.stop_dex_event_stream(dex).await;
291 }
292
293 log::debug!("HyperSync client disconnected");
294 }
295
296 pub async fn current_block(&self) -> u64 {
302 self.client.get_height().await.unwrap()
303 }
304
305 pub async fn request_blocks_stream(
311 &self,
312 from_block: u64,
313 to_block: Option<u64>,
314 ) -> impl Stream<Item = Block> {
315 let query = Self::construct_block_query(from_block, to_block);
316 let mut rx = self
317 .client
318 .clone()
319 .stream(query, StreamConfig::default())
320 .await
321 .unwrap();
322
323 let chain = self.chain.name;
324
325 async_stream::stream! {
326 while let Some(response) = rx.recv().await {
327 let response = response.unwrap();
328 for received_block in response.data.blocks.into_iter().flatten() {
329 let block = transform_hypersync_block(chain, received_block).unwrap();
330 yield block
331 }
332 }
333 }
334 }
335
336 pub fn subscribe_blocks(&mut self) {
342 if self.blocks_task.is_some() {
343 return;
344 }
345
346 let chain = self.chain.name;
347 let client = self.client.clone();
348 let tx = if let Some(tx) = &self.tx {
349 tx.clone()
350 } else {
351 log::error!("Hypersync client channel should have been initialized");
352 return;
353 };
354
355 let blocks_token = self.cancellation_token.child_token();
357 let cancellation_token = blocks_token.clone();
358 self.blocks_cancellation_token = Some(blocks_token);
359
360 if let Err(e) = self.blocks_task.spawn(async move {
361 log::debug!("Starting task 'blocks_feed");
362
363 let current_block_height = client.get_height().await.unwrap();
364 let mut query = Self::construct_block_query(current_block_height, None);
365
366 loop {
367 tokio::select! {
368 () = cancellation_token.cancelled() => {
369 log::debug!("Blocks subscription task received cancellation signal");
370 break;
371 }
372 result = tokio::time::timeout(
373 Duration::from_secs(HYPERSYNC_REQUEST_TIMEOUT_SECS),
374 client.get(&query)
375 ) => {
376 let response = match result {
377 Ok(Ok(resp)) => resp,
378 Ok(Err(e)) => {
379 log::error!("Hypersync request failed: {e}");
380 break;
381 }
382 Err(_) => {
383 log::warn!("Hypersync request timed out after {HYPERSYNC_REQUEST_TIMEOUT_SECS}s, retrying...");
384 continue;
385 }
386 };
387
388 for received_block in response.data.blocks.into_iter().flatten() {
389 let block = transform_hypersync_block(chain, received_block).unwrap();
390 let msg = BlockchainMessage::Block(block);
391 if let Err(e) = tx.send(msg) {
392 log::error!("Error sending message: {e}");
393 }
394 }
395
396 if let Some(archive_block_height) = response.archive_height
397 && archive_block_height < response.next_block
398 {
399 while client.get_height().await.unwrap() < response.next_block {
400 tokio::select! {
401 () = cancellation_token.cancelled() => {
402 log::debug!("Blocks subscription task received cancellation signal during polling");
403 return;
404 }
405 () = tokio::time::sleep(Duration::from_millis(
406 BLOCK_POLLING_INTERVAL_MS,
407 )) => {}
408 }
409 }
410 }
411
412 query.from_block = response.next_block;
413 }
414 }
415 }
416 }) {
417 if let Some(token) = self.blocks_cancellation_token.take() {
418 token.cancel();
419 }
420 log::error!("Failed to start HyperSync blocks subscription task: {e}");
421 }
422 }
423
424 pub async fn unsubscribe_blocks(&mut self) {
426 if self.blocks_task.is_none() {
427 return;
428 }
429
430 if let Some(token) = self.blocks_cancellation_token.take() {
432 token.cancel();
433 }
434
435 if let Some(outcome) = finish_task(
436 &mut self.blocks_task,
437 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
438 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
439 )
440 .await
441 {
442 match outcome {
443 TaskJoinOutcome::Completed(()) | TaskJoinOutcome::Aborted => {}
444 TaskJoinOutcome::Failed(error) => {
445 log::error!("HyperSync blocks task failed during unsubscribe: {error}");
446 }
447 TaskJoinOutcome::Incomplete => {
448 log::error!("HyperSync blocks task did not stop after abort");
449 }
450 }
451 }
452 log::debug!("Unsubscribed from blocks");
453 }
454
455 fn construct_block_query(from_block: u64, to_block: Option<u64>) -> Query {
457 Query {
458 from_block,
459 to_block: Self::to_hypersync_exclusive_bound(to_block),
460 blocks: vec![BlockSelection::default()],
461 field_selection: FieldSelection {
462 block: BlockField::all(),
463 ..Default::default()
464 },
465 ..Default::default()
466 }
467 }
468
469 fn construct_contract_events_query(
470 from_block: u64,
471 to_block: Option<u64>,
472 contract_addresses: &[Address],
473 topics: &[&str],
474 ) -> Query {
475 let mut query_value = serde_json::json!({
476 "from_block": from_block,
477 "logs": [{
478 "topics": [topics],
479 "address": contract_addresses
480 }],
481 "field_selection": {
482 "log": [
483 "block_number",
484 "transaction_hash",
485 "transaction_index",
486 "log_index",
487 "address",
488 "data",
489 "topic0",
490 "topic1",
491 "topic2",
492 "topic3",
493 ],
494 "block": [
496 "number",
497 "hash",
498 "parent_hash",
499 "miner",
500 "gas_limit",
501 "gas_used",
502 "timestamp",
503 ]
504 }
505 });
506
507 if let Some(to_block) = Self::to_hypersync_exclusive_bound(to_block)
508 && let Some(obj) = query_value.as_object_mut()
509 {
510 obj.insert("to_block".to_string(), serde_json::json!(to_block));
511 }
512
513 serde_json::from_value(query_value).unwrap()
514 }
515
516 fn to_hypersync_exclusive_bound(to_block: Option<u64>) -> Option<u64> {
517 to_block.map(|block| block.saturating_add(1))
518 }
519
520 async fn run_dex_event_stream(
521 dex: DexType,
522 client: Arc<hypersync_client::Client>,
523 tx: tokio::sync::mpsc::UnboundedSender<BlockchainMessage>,
524 filter: DexEventStreamFilter,
525 dex_extended: &'static DexExtended,
526 next_from_block: Arc<AtomicU64>,
527 cancellation_token: tokio_util::sync::CancellationToken,
528 ) {
529 log::debug!("Starting task 'dex_event_stream' for {dex}");
530
531 loop {
532 let from_block = next_from_block.load(Ordering::Relaxed);
533 Self::wait_for_stream_start_block(&client, from_block, &cancellation_token).await;
534 if cancellation_token.is_cancelled() {
535 break;
536 }
537
538 let topics = filter
539 .event_signatures
540 .iter()
541 .map(String::as_str)
542 .collect::<Vec<_>>();
543 let query = Self::construct_contract_events_query(
544 from_block,
545 None,
546 &filter.contract_addresses,
547 &topics,
548 );
549 let mut rx = match client.stream(query, StreamConfig::default()).await {
550 Ok(rx) => rx,
551 Err(e) => {
552 log::error!("Failed to create DEX event stream for {dex}: {e}");
553
554 if !Self::sleep_or_cancel(
555 Duration::from_millis(DEX_EVENT_STREAM_RETRY_DELAY_MS),
556 &cancellation_token,
557 )
558 .await
559 {
560 break;
561 }
562 continue;
563 }
564 };
565
566 let mut received_response = false;
567
568 loop {
569 tokio::select! {
570 () = cancellation_token.cancelled() => {
571 log::debug!("DEX event stream task for {dex} received cancellation signal");
572 break;
573 }
574 response = rx.recv() => {
575 let Some(response) = response else {
576 break;
577 };
578
579 let response = match response {
580 Ok(resp) => resp,
581 Err(e) => {
582 if received_response {
583 log::debug!("DEX event stream drained for {dex}: {e}");
584 } else {
585 log::error!("Failed to receive DEX event stream response for {dex}: {e}");
586 }
587 break;
588 }
589 };
590
591 received_response = true;
592 next_from_block.fetch_max(response.next_block, Ordering::Relaxed);
593
594 for log in response.data.logs.into_iter().flatten() {
595 Self::send_dex_event_log(&tx, dex_extended, &log);
596 }
597 }
598 }
599 }
600
601 if !Self::sleep_or_cancel(
602 Duration::from_millis(DEX_EVENT_STREAM_RETRY_DELAY_MS),
603 &cancellation_token,
604 )
605 .await
606 {
607 break;
608 }
609 }
610
611 log::debug!("Stopped task 'dex_event_stream' for {dex}");
612 }
613
614 async fn wait_for_stream_start_block(
615 client: &hypersync_client::Client,
616 from_block: u64,
617 cancellation_token: &tokio_util::sync::CancellationToken,
618 ) {
619 loop {
620 match client.get_height().await {
621 Ok(height) if height >= from_block => return,
622 Ok(_) => {}
623 Err(e) => log::error!("Failed to get HyperSync height for DEX event stream: {e}"),
624 }
625
626 if !Self::sleep_or_cancel(
627 Duration::from_millis(BLOCK_POLLING_INTERVAL_MS),
628 cancellation_token,
629 )
630 .await
631 {
632 return;
633 }
634 }
635 }
636
637 async fn sleep_or_cancel(
638 duration: Duration,
639 cancellation_token: &tokio_util::sync::CancellationToken,
640 ) -> bool {
641 tokio::select! {
642 () = cancellation_token.cancelled() => false,
643 () = tokio::time::sleep(duration) => true,
644 }
645 }
646
647 fn send_dex_event_log(
648 tx: &tokio::sync::mpsc::UnboundedSender<BlockchainMessage>,
649 dex_extended: &DexExtended,
650 log: &Log,
651 ) {
652 let event_signature = match log.topics.first().and_then(|t| t.as_ref()) {
653 Some(log_argument) => hex::encode_prefixed(log_argument.as_ref()),
654 None => return,
655 };
656
657 let (event_name, event) = if event_signature == dex_extended.swap_created_event.as_ref() {
658 (
659 "swap",
660 dex_extended
661 .parse_swap_event_hypersync(log)
662 .map(BlockchainMessage::SwapEvent),
663 )
664 } else if event_signature == dex_extended.mint_created_event.as_ref() {
665 (
666 "mint",
667 dex_extended
668 .parse_mint_event_hypersync(log)
669 .map(BlockchainMessage::MintEvent),
670 )
671 } else if event_signature == dex_extended.burn_created_event.as_ref() {
672 (
673 "burn",
674 dex_extended
675 .parse_burn_event_hypersync(log)
676 .map(BlockchainMessage::BurnEvent),
677 )
678 } else if event_signature == dex_extended.collect_created_event.as_ref() {
679 (
680 "collect",
681 dex_extended
682 .parse_collect_event_hypersync(log)
683 .map(BlockchainMessage::CollectEvent),
684 )
685 } else if dex_extended.flash_created_event.as_deref() == Some(event_signature.as_str()) {
686 (
687 "flash",
688 dex_extended
689 .parse_flash_event_hypersync(log)
690 .map(BlockchainMessage::FlashEvent),
691 )
692 } else if dex_extended.fee_protocol_update_event.as_deref()
693 == Some(event_signature.as_str())
694 {
695 (
696 "fee-protocol update",
697 dex_extended
698 .parse_fee_protocol_update_event_hypersync(log)
699 .map(BlockchainMessage::FeeProtocolUpdateEvent),
700 )
701 } else if dex_extended.fee_protocol_collect_event.as_deref()
702 == Some(event_signature.as_str())
703 {
704 (
705 "fee-protocol collect",
706 dex_extended
707 .parse_fee_protocol_collect_event_hypersync(log)
708 .map(BlockchainMessage::FeeProtocolCollectEvent),
709 )
710 } else {
711 log::error!("Unknown event signature: {event_signature}");
712 return;
713 };
714
715 match event {
716 Ok(event) => {
717 if let Err(e) = tx.send(event) {
718 log::error!("Failed to send {event_name} event: {e}");
719 }
720 }
721 Err(e) => {
722 log::error!("Failed to parse {event_name} with error '{e:?}' for event: {log:?}",);
723 }
724 }
725 }
726
727 async fn stop_dex_event_stream(&mut self, dex: DexType) -> Option<u64> {
728 let task = self.dex_event_tasks.get_mut(&dex)?;
729 let terminated = Self::stop_dex_event_task(dex, task).await;
730 let next_from_block = task.next_from_block.load(Ordering::Relaxed);
731
732 if terminated {
733 self.dex_event_tasks.remove(&dex);
734 }
735
736 Some(next_from_block)
737 }
738
739 async fn stop_dex_event_task(dex: DexType, task: &mut DexEventStreamTask) -> bool {
740 task.cancellation_token.cancel();
741
742 let outcome = finish_task(
743 &mut task.task,
744 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
745 Duration::from_secs(DISCONNECT_TIMEOUT_SECS),
746 )
747 .await;
748
749 match outcome {
750 None | Some(TaskJoinOutcome::Completed(())) | Some(TaskJoinOutcome::Aborted) => true,
751 Some(TaskJoinOutcome::Failed(error)) => {
752 log::error!("HyperSync DEX event stream task for {dex} failed: {error}");
753 true
754 }
755 Some(TaskJoinOutcome::Incomplete) => {
756 log::error!("HyperSync DEX event stream task for {dex} did not stop after abort");
757 false
758 }
759 }
760 }
761}
762
763impl Drop for HyperSyncClient {
764 fn drop(&mut self) {
765 self.cancellation_token.cancel();
766 if let Some(token) = self.blocks_cancellation_token.as_ref() {
767 token.cancel();
768 }
769 self.blocks_task.abort();
770
771 for task in self.dex_event_tasks.values_mut() {
772 task.cancellation_token.cancel();
773 task.task.abort();
774 }
775 }
776}
777
778#[derive(Debug, Clone, PartialEq, Eq)]
779struct DexEventStreamFilter {
780 contract_addresses: Vec<Address>,
781 event_signatures: Vec<String>,
782}
783
784impl DexEventStreamFilter {
785 fn new(mut contract_addresses: Vec<Address>, mut event_signatures: Vec<String>) -> Self {
786 contract_addresses.sort_unstable();
787 contract_addresses.dedup();
788 event_signatures.sort_unstable();
789 event_signatures.dedup();
790 Self {
791 contract_addresses,
792 event_signatures,
793 }
794 }
795
796 fn is_empty(&self) -> bool {
797 self.contract_addresses.is_empty() || self.event_signatures.is_empty()
798 }
799}
800
801#[derive(Debug)]
802struct DexEventStreamTask {
803 filter: DexEventStreamFilter,
804 next_from_block: Arc<AtomicU64>,
805 cancellation_token: tokio_util::sync::CancellationToken,
806 task: TaskSlot<()>,
807}
808
809fn pool_events_from_response(
814 chain: Blockchain,
815 blocks: Vec<Vec<hypersync_client::simple_types::Block>>,
816 logs: Vec<Vec<Log>>,
817) -> Vec<PoolEventStreamItem> {
818 let mut items = Vec::new();
819
820 for block in blocks.into_iter().flatten() {
821 match transform_hypersync_block(chain, block) {
822 Ok(block) => items.push(PoolEventStreamItem::Block(block)),
823 Err(e) => log::error!("Failed to transform block for timestamp: {e}"),
824 }
825 }
826
827 items.extend(logs.into_iter().flatten().map(PoolEventStreamItem::Log));
828 items
829}
830
831#[cfg(test)]
832mod tests {
833 use std::{
834 str::FromStr,
835 sync::{
836 Arc,
837 atomic::{AtomicU64, Ordering},
838 },
839 };
840
841 use hypersync_client::{
842 format::{Address as HypersyncAddress, Hash, Quantity},
843 simple_types::{Block as HypersyncBlock, Log},
844 };
845 use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_SECOND};
846 use nautilus_model::defi::Chain;
847 use rstest::rstest;
848
849 use super::*;
850
851 fn synthetic_block(number: u64, timestamp_secs: u64) -> HypersyncBlock {
852 HypersyncBlock {
853 number: Some(number),
854 hash: Some(
855 Hash::from_str(
856 "0x0000000000000000000000000000000000000000000000000000000000000001",
857 )
858 .unwrap(),
859 ),
860 parent_hash: Some(
861 Hash::from_str(
862 "0x0000000000000000000000000000000000000000000000000000000000000000",
863 )
864 .unwrap(),
865 ),
866 miner: Some(
867 HypersyncAddress::from_str("0x0000000000000000000000000000000000000001").unwrap(),
868 ),
869 gas_limit: Some(Quantity::from(21_000u64)),
870 gas_used: Some(Quantity::from(21_000u64)),
871 timestamp: Some(Quantity::from(timestamp_secs)),
872 ..Default::default()
873 }
874 }
875
876 #[rstest]
877 #[should_panic(expected = "Invalid HyperSync URL")]
878 fn hypersync_rejects_remote_cleartext_before_loading_token() {
879 let mut chain = Chain::new(Blockchain::Arbitrum, 42_161);
880 chain.hypersync_url = "http://localhost:8080".to_string();
881
882 let _ = HyperSyncClient::new(
883 Arc::new(chain),
884 None,
885 tokio_util::sync::CancellationToken::new(),
886 );
887 }
888
889 #[rstest]
890 fn pool_events_yields_blocks_before_logs() {
891 let items = pool_events_from_response(
892 Blockchain::Ethereum,
893 vec![vec![synthetic_block(12, 100)]],
894 vec![vec![Log::default(), Log::default()]],
895 );
896
897 assert_eq!(items.len(), 3);
898 match &items[0] {
899 PoolEventStreamItem::Block(block) => {
900 assert_eq!(block.number, 12);
901 assert_eq!(block.timestamp, UnixNanos::new(100 * NANOSECONDS_IN_SECOND));
902 }
903 other => panic!("expected Block first, was {other:?}"),
904 }
905 assert!(matches!(items[1], PoolEventStreamItem::Log(_)));
906 assert!(matches!(items[2], PoolEventStreamItem::Log(_)));
907 }
908
909 #[rstest]
910 fn pool_events_skips_unparsable_block_but_keeps_logs() {
911 let bad_block = HypersyncBlock {
914 number: Some(7),
915 ..Default::default()
916 };
917 let items = pool_events_from_response(
918 Blockchain::Ethereum,
919 vec![vec![bad_block]],
920 vec![vec![Log::default()]],
921 );
922
923 assert_eq!(items.len(), 1);
924 assert!(matches!(items[0], PoolEventStreamItem::Log(_)));
925 }
926
927 #[rstest]
928 fn construct_block_query_converts_to_block_to_hypersync_exclusive_bound() {
929 let query = HyperSyncClient::construct_block_query(10, Some(12));
930
931 assert_eq!(query.from_block, 10);
932 assert_eq!(query.to_block, Some(13));
933 }
934
935 #[rstest]
936 fn construct_contract_events_query_converts_to_block_to_hypersync_exclusive_bound() {
937 let address = Address::from_str("0x0000000000000000000000000000000000000001").unwrap();
938 let query = HyperSyncClient::construct_contract_events_query(
939 10,
940 Some(12),
941 &[address],
942 &["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"],
943 );
944
945 assert_eq!(query.from_block, 10);
946 assert_eq!(query.to_block, Some(13));
947 }
948
949 #[rstest]
950 fn construct_contract_events_query_single_block_uses_next_block_as_exclusive_bound() {
951 let address = Address::from_str("0x0000000000000000000000000000000000000001").unwrap();
952 let query = HyperSyncClient::construct_contract_events_query(
953 10,
954 Some(10),
955 &[address],
956 &["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"],
957 );
958
959 assert_eq!(query.from_block, 10);
960 assert_eq!(query.to_block, Some(11));
961 }
962
963 #[rstest]
964 fn construct_contract_events_query_open_upper_bound_stays_open() {
965 let address = Address::from_str("0x0000000000000000000000000000000000000001").unwrap();
966 let query = HyperSyncClient::construct_contract_events_query(
967 10,
968 None,
969 &[address],
970 &["0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67"],
971 );
972
973 assert_eq!(query.from_block, 10);
974 assert_eq!(query.to_block, None);
975 }
976
977 #[rstest]
978 fn dex_event_stream_filter_sorts_and_deduplicates_inputs() {
979 let address1 = Address::from_str("0x0000000000000000000000000000000000000001").unwrap();
980 let address2 = Address::from_str("0x0000000000000000000000000000000000000002").unwrap();
981 let filter = DexEventStreamFilter::new(
982 vec![address2, address1, address2],
983 vec![
984 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
985 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
986 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
987 ],
988 );
989
990 assert_eq!(filter.contract_addresses, vec![address1, address2]);
991 assert_eq!(
992 filter.event_signatures,
993 vec![
994 "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
995 "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
996 ]
997 );
998 }
999
1000 #[rstest]
1001 fn dex_event_stream_filter_requires_addresses_and_signatures() {
1002 let address = Address::from_str("0x0000000000000000000000000000000000000001").unwrap();
1003
1004 assert!(DexEventStreamFilter::new(vec![], vec!["0x01".to_string()]).is_empty());
1005 assert!(DexEventStreamFilter::new(vec![address], vec![]).is_empty());
1006 assert!(!DexEventStreamFilter::new(vec![address], vec!["0x01".to_string()]).is_empty());
1007 }
1008
1009 #[tokio::test]
1010 async fn stop_dex_event_task_cancels_and_awaits_task() {
1011 let cancellation_token = tokio_util::sync::CancellationToken::new();
1012 let task_token = cancellation_token.clone();
1013 let next_from_block = Arc::new(AtomicU64::new(42));
1014 let task_next_from_block = next_from_block.clone();
1015
1016 let task = tokio::spawn(async move {
1017 task_token.cancelled().await;
1018 task_next_from_block.store(99, Ordering::Relaxed);
1019 });
1020 let stream_task = DexEventStreamTask {
1021 filter: DexEventStreamFilter::new(vec![], vec![]),
1022 next_from_block,
1023 cancellation_token,
1024 task: TaskSlot::from_handle(task),
1025 };
1026 let mut stream_task = stream_task;
1027
1028 let terminated =
1029 HyperSyncClient::stop_dex_event_task(DexType::UniswapV3, &mut stream_task).await;
1030
1031 assert!(terminated);
1032 assert_eq!(stream_task.next_from_block.load(Ordering::Relaxed), 99);
1033 assert!(stream_task.task.is_none());
1034 }
1035}