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