use alloy::providers::{Provider, ProviderBuilder};
use alloy::rpc::types::Filter;
use alloy::transports::mock::Asserter;
use anyhow::Result;
use ethl::rpc::RpcError;
use ethl::rpc::config::ProviderSettings;
use ethl::rpc::progress::ScanProgress;
use ethl::rpc::stream::backfill_then_watch_logs;
use ethl::rpc::stream::backfill_then_watch_logs_with_progress;
use futures_util::pin_mut;
use futures_util::stream::StreamExt;
use std::sync::mpsc;
use std::thread;
use std::time::Duration;
use tokio::runtime::Builder;
#[tokio::test]
async fn test_single_block_bounded_range() -> Result<()> {
let logs: serde_json::Value = serde_json::from_str(include_str!(
"fixtures/transfer_getlogs_34762687_34762688.json"
))?;
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2")); asserter.push_success(&logs);
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(34762687).to_block(34762687);
let stream = backfill_then_watch_logs(&settings, Some(&filter)).await;
pin_mut!(stream);
let result = stream.next().await;
assert!(
result.is_some(),
"single-block range should yield at least one result"
);
let log_range_result = result.unwrap();
assert!(
log_range_result.is_ok(),
"result should be Ok, not an error"
);
let (from, to, logs) = log_range_result?;
assert!(
from <= 34762687 && to >= 34762687,
"returned range [{}, {}] should contain requested block 34762687",
from,
to
);
assert!(
!logs.is_empty(),
"single-block range should return logs from the fixture"
);
Ok(())
}
#[tokio::test]
async fn test_two_block_bounded_range() -> Result<()> {
let logs: serde_json::Value = serde_json::from_str(include_str!(
"fixtures/transfer_getlogs_34762687_34762688.json"
))?;
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2")); asserter.push_success(&logs);
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(34762687).to_block(34762688);
let stream = backfill_then_watch_logs(&settings, Some(&filter)).await;
pin_mut!(stream);
let result = stream.next().await;
assert!(
result.is_some(),
"two-block range should yield at least one result"
);
let log_range_result = result.unwrap();
assert!(log_range_result.is_ok(), "result should be Ok");
let (from, to, logs) = log_range_result?;
assert!(
from <= 34762688 && to >= 34762687,
"range should cover blocks 34762687-34762688"
);
assert!(!logs.is_empty(), "two-block range should return logs");
Ok(())
}
#[tokio::test]
async fn bounded_range_with_no_matching_events_still_records_scan_progress() -> Result<()> {
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2")); asserter.push_success(&serde_json::json!([])); asserter.push_success(&serde_json::json!("0x2126fc2"));
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(34762687).to_block(34762688);
let progress = ScanProgress::default();
assert_eq!(
progress.scanned_to(),
None,
"progress must be unset before the stream is driven"
);
let stream =
backfill_then_watch_logs_with_progress(&settings, Some(&filter), Some(&progress)).await;
pin_mut!(stream);
let result = stream.next().await;
assert!(
result.is_some(),
"a zero-event range must still reach the consumer as (from, to, [])"
);
let (from, to, logs) = result.unwrap()?;
assert_eq!((from, to), (34762687, 34762688));
assert!(logs.is_empty(), "the fixture returns zero matching events");
assert_eq!(
progress.scanned_to(),
Some(34762688),
"scanned_to() must report the top of the range even though nothing matched the filter"
);
Ok(())
}
#[test]
fn zero_to_zero_bounded_range_progress_stays_unset() {
let (tx, rx) = mpsc::channel::<(Vec<Result<(u64, u64, usize), RpcError>>, Option<u64>)>();
thread::spawn(move || {
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build current-thread runtime");
let (collected, scanned_to) = runtime.block_on(async {
let logs: serde_json::Value = serde_json::from_str(include_str!(
"fixtures/transfer_getlogs_34762687_34762688.json"
))
.expect("fixture parses as JSON");
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2"));
asserter.push_success(&logs);
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(0u64).to_block(0u64);
let progress = ScanProgress::default();
let stream =
backfill_then_watch_logs_with_progress(&settings, Some(&filter), Some(&progress))
.await;
pin_mut!(stream);
let mut collected = Vec::new();
while let Some(item) = stream.next().await {
collected.push(item.map(|(from, to, logs)| (from, to, logs.len())));
}
(collected, progress.scanned_to())
});
let _ = tx.send((collected, scanned_to));
});
let (collected, scanned_to) = rx
.recv_timeout(Duration::from_secs(10))
.expect("bounded [0, 0] must terminate");
assert_eq!(
collected.len(),
0,
"bounded [0, 0] should terminate yielding zero ranges, got {collected:?}"
);
assert_eq!(
scanned_to, None,
"block 0 is never scanned; scanned_to() must stay None, not Some(0)"
);
}
#[tokio::test]
async fn test_unbounded_filter_unchanged() -> Result<()> {
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2"));
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(0);
let stream = backfill_then_watch_logs(&settings, Some(&filter)).await;
pin_mut!(stream);
let result = stream.next().await;
assert!(
result.is_some(),
"unbounded stream should yield a result (error or data)"
);
let item = result.unwrap();
assert!(
item.is_err(),
"unbounded stream without WSS should yield AllProvidersSuspended error"
);
Ok(())
}
#[test]
fn zero_to_zero_bounded_range_terminates() {
let (tx, rx) = mpsc::channel::<Vec<Result<(u64, u64, usize), RpcError>>>();
thread::spawn(move || {
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.expect("failed to build current-thread runtime");
let collected = runtime.block_on(async {
let logs: serde_json::Value = serde_json::from_str(include_str!(
"fixtures/transfer_getlogs_34762687_34762688.json"
))
.expect("fixture parses as JSON");
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2"));
asserter.push_success(&logs);
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(0u64).to_block(0u64);
let stream = backfill_then_watch_logs(&settings, Some(&filter)).await;
pin_mut!(stream);
let mut collected = Vec::new();
while let Some(item) = stream.next().await {
collected.push(item.map(|(from, to, logs)| (from, to, logs.len())));
}
collected
});
let _ = tx.send(collected);
});
let collected = rx
.recv_timeout(Duration::from_secs(10))
.expect("bounded [0, 0] must terminate");
assert_eq!(
collected.len(),
0,
"bounded [0, 0] should terminate yielding zero ranges, got {collected:?}"
);
}
#[tokio::test]
async fn bounded_range_from_zero_starts_at_block_one() -> Result<()> {
let logs: serde_json::Value = serde_json::from_str(include_str!(
"fixtures/transfer_getlogs_34762687_34762688.json"
))?;
let asserter = Asserter::new();
asserter.push_success(&serde_json::json!("0x2126fc2"));
asserter.push_success(&logs);
let provider = ProviderBuilder::new()
.disable_recommended_fillers()
.connect_mocked_client(asserter)
.erased();
let settings = ProviderSettings::from_mock(provider);
let filter = Filter::new().from_block(0u64).to_block(2u64);
let stream = backfill_then_watch_logs(&settings, Some(&filter)).await;
pin_mut!(stream);
let result = stream.next().await;
assert!(
result.is_some(),
"range starting at block 0 should yield at least one result"
);
let (from, _to, _logs) = result.unwrap()?;
assert_eq!(
from, 1,
"the first yielded range must start at block 1, skipping genesis"
);
Ok(())
}