use crate::rpc::{
LogRange, RpcError,
backfill::backfill,
config::{HttpRpcSettings, ProviderSettings},
heads::{
collect_window_hashes, fetch_logs_by_hash_range, last_block, resolve_http_provider,
stream_heads_with_logs_with_progress,
},
progress::ScanProgress,
};
use alloy::providers::Provider;
use alloy::rpc::types::{BlockNumberOrTag, Filter};
use async_stream::stream;
use futures_util::{Stream, StreamExt, pin_mut};
use tracing::debug;
pub struct LogStreamConfig {
pub wss_endpoints: Vec<String>,
pub http_options: Vec<HttpRpcSettings>,
pub events: Vec<String>,
pub from_block: u64,
pub to_block: Option<u64>,
}
pub async fn stream_logs_with_progress(
providers: &ProviderSettings,
filter: Option<&Filter>,
progress: Option<&ScanProgress>,
) -> impl Stream<Item = Result<LogRange, RpcError>> + Send {
stream! {
let heads = stream_heads_with_logs_with_progress(providers, filter, progress).await;
pin_mut!(heads);
while let Some(result) = heads.next().await {
yield result;
}
}
}
pub async fn stream_logs(
providers: &ProviderSettings,
filter: Option<&Filter>,
) -> impl Stream<Item = Result<LogRange, RpcError>> + Send {
stream_logs_with_progress(providers, filter, None).await
}
pub async fn backfill_then_watch_logs_with_progress(
providers: &ProviderSettings,
filter: Option<&Filter>,
progress: Option<&ScanProgress>,
) -> impl Stream<Item = Result<LogRange, RpcError>> + Send {
let init_from_block = filter.and_then(|f| f.get_from_block()).unwrap_or(0);
let init_to_block = filter.and_then(|f| f.get_to_block());
let depth = providers.recovery_window_depth;
stream! {
let mut from_block = init_from_block;
let mut tip = match last_block(providers).await {
Ok(b) => {
if let Some(p) = progress {
p.record_observed_tip(b);
}
init_to_block.unwrap_or(b.saturating_sub(2))
}
Err(e) => {
yield Err(e);
return;
}
};
let window_floor = if depth > 0 { tip.saturating_sub(depth) } else { u64::MAX };
while tip >= from_block {
let stop_condition = if depth > 0 {
init_to_block.is_none() && from_block >= window_floor
} else {
tip - from_block < 30 && init_to_block.is_none()
};
if stop_condition {
break;
}
let backfill_to = if depth > 0 && init_to_block.is_none() {
tip.min(window_floor.saturating_sub(1))
} else {
tip
};
if backfill_to < from_block {
break;
}
debug!("Starting backfill for block range {} to {}", from_block, backfill_to);
let batch_filter = filter.cloned().unwrap_or_else(Filter::new).from_block(from_block).to_block(backfill_to);
let stream = backfill(providers, &batch_filter).await;
pin_mut!(stream);
let mut yielded_any = false;
while let Some(result) = stream.next().await {
match result {
Ok(range) => {
if let Some(p) = progress {
p.record_scanned_to(range.to_block);
}
from_block = range.to_block + 1;
yielded_any = true;
yield Ok(range);
}
Err(e) => {
yield Err(e);
return;
}
}
}
if !yielded_any {
from_block = backfill_to + 1;
}
if init_to_block.is_none() {
match last_block(providers).await {
Ok(b) => {
if let Some(p) = progress {
p.record_observed_tip(b);
}
tip = b.saturating_sub(2);
}
Err(e) => {
yield Err(e);
return;
}
}
}
}
if depth > 0 && from_block <= tip && init_to_block.is_none() {
debug!(
"Fetching reorg-exposed window [{}, {}] by canonical block hash",
from_block, tip
);
let reused_http_clients = providers.build_reused_http_clients();
let http_provider = resolve_http_provider(providers, Some(&reused_http_clients), 0);
let tip_block = match http_provider
.get_block_by_number(BlockNumberOrTag::Number(tip))
.await
{
Ok(Some(block)) => block,
Ok(None) => {
yield Err(RpcError::ConnectionError(format!(
"tip block {} not found during window hash walk",
tip
)));
return;
}
Err(e) => {
yield Err(RpcError::ConnectionError(format!(
"failed to fetch tip block {}: {}",
tip, e
)));
return;
}
};
let tip_hash = tip_block.header.hash;
let pairs_desc = match collect_window_hashes(
&http_provider,
providers,
Some(&reused_http_clients),
0,
tip_hash,
tip,
from_block,
)
.await
{
Ok(pairs) => pairs,
Err(e) => {
yield Err(e);
return;
}
};
let mut pairs = pairs_desc;
pairs.reverse();
match fetch_logs_by_hash_range(providers, &pairs, filter).await {
Ok(ranges) => {
for range in ranges {
if let Some(p) = progress {
p.record_scanned_to(range.to_block);
}
from_block = range.to_block + 1;
yield Ok(range);
}
}
Err(e) => {
yield Err(e);
return;
}
}
}
if filter.and_then(|f| f.get_to_block()).is_none() {
debug!("No more backfilling needed, streaming live logs from block {}", from_block);
let live_filter = filter.cloned().unwrap_or_else(Filter::new).from_block(from_block);
let heads = stream_heads_with_logs_with_progress(providers, Some(&live_filter), progress).await;
pin_mut!(heads);
while let Some(result) = heads.next().await {
yield result;
}
}
}
}
pub async fn backfill_then_watch_logs(
providers: &ProviderSettings,
filter: Option<&Filter>,
) -> impl Stream<Item = Result<LogRange, RpcError>> + Send {
backfill_then_watch_logs_with_progress(providers, filter, None).await
}
#[cfg(test)]
mod tests {
#[test]
fn bounded_fetch_with_depth_no_window_floor_stop() {
let tip: u64 = 200;
let depth: u64 = 10;
let init_to_block: Option<u64> = Some(200);
let window_floor = tip.saturating_sub(depth);
let from_block = window_floor;
let stop_condition = if depth > 0 {
init_to_block.is_none() && from_block >= window_floor
} else {
tip - from_block < 30 && init_to_block.is_none()
};
assert!(
!stop_condition,
"bounded fetch must not stop at the window floor (would drop blocks)"
);
let backfill_to = if depth > 0 && init_to_block.is_none() {
tip.min(window_floor.saturating_sub(1))
} else {
tip
};
assert_eq!(
backfill_to, tip,
"bounded fetch must backfill to tip, not window_floor - 1"
);
let unbounded_stop = if depth > 0 {
None::<u64>.is_none() && from_block >= window_floor
} else {
false
};
assert!(unbounded_stop, "unbounded fetch must stop at window floor");
}
}