use std::time::{Duration, Instant};
use crate::error::{Error, Result};
use crate::evm::EvmClient;
use crate::transport::Transport;
use crate::types::{Filter, Log};
pub const DEFAULT_MAX_BLOCKS: u64 = 10_000;
pub const DEFAULT_CHUNK: u64 = 200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScanProgress {
pub scanned: u64,
pub total: u64,
pub requests: u64,
pub found: usize,
pub window: (u64, u64),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScanPlan {
pub from: u64,
pub to: u64,
pub blocks: u64,
pub max_requests: u64,
pub min_duration: Option<Duration>,
}
type ProgressFn<'a> = Box<dyn FnMut(&ScanProgress) + Send + 'a>;
#[must_use = "a LogScan does nothing until `.run()` is awaited"]
pub struct LogScan<'a, T> {
client: &'a EvmClient<T>,
filters: Vec<Filter>,
newest_first: bool,
limit: Option<usize>,
from: Option<u64>,
to: Option<u64>,
max_blocks: u64,
chunk: u64,
min_interval: Option<Duration>,
progress: Option<ProgressFn<'a>>,
resolved: Option<(u64, u64)>,
}
impl<'a, T: Transport> LogScan<'a, T> {
pub(crate) fn new(client: &'a EvmClient<T>, filter: Filter) -> Self {
Self {
client,
filters: vec![filter],
newest_first: false,
limit: None,
from: None,
to: None,
max_blocks: DEFAULT_MAX_BLOCKS,
chunk: DEFAULT_CHUNK,
min_interval: None,
progress: None,
resolved: None,
}
}
pub fn or_filter(mut self, filter: Filter) -> Self {
self.filters.push(filter);
self
}
pub fn newest_first(mut self) -> Self {
self.newest_first = true;
self
}
pub fn oldest_first(mut self) -> Self {
self.newest_first = false;
self
}
pub fn limit(mut self, n: usize) -> Self {
self.limit = Some(n);
self
}
pub fn from_block(mut self, n: u64) -> Self {
self.from = Some(n);
self.resolved = None;
self
}
pub fn to_block(mut self, n: u64) -> Self {
self.to = Some(n);
self.resolved = None;
self
}
pub fn max_blocks(mut self, n: u64) -> Self {
self.max_blocks = n.max(1);
self.resolved = None;
self
}
pub fn chunk(mut self, blocks: u64) -> Self {
self.chunk = blocks.max(1);
self
}
#[cfg(feature = "http")]
pub fn rate_limit(mut self, requests_per_second: f64) -> Self {
self.min_interval =
(requests_per_second > 0.0).then(|| Duration::from_secs_f64(1.0 / requests_per_second));
self
}
pub fn on_progress(mut self, f: impl FnMut(&ScanProgress) + Send + 'a) -> Self {
self.progress = Some(Box::new(f));
self
}
async fn range(&mut self) -> Result<(u64, u64)> {
if let Some(r) = self.resolved {
return Ok(r);
}
let to = match self.to {
Some(to) => to,
None => self.client.block_number().await?,
};
let from = self
.from
.unwrap_or_else(|| to.saturating_sub(self.max_blocks - 1));
self.resolved = Some((from, to));
Ok((from, to))
}
pub async fn plan(&mut self) -> Result<ScanPlan> {
let (from, to) = self.range().await?;
let blocks = if from <= to { to - from + 1 } else { 0 };
let max_requests = blocks.div_ceil(self.chunk) * self.filters.len() as u64;
let min_duration = self.min_interval.map(|interval| {
interval
.saturating_mul(u32::try_from(max_requests.saturating_sub(1)).unwrap_or(u32::MAX))
});
Ok(ScanPlan {
from,
to,
blocks,
max_requests,
min_duration,
})
}
pub async fn run(mut self) -> Result<Vec<Log>> {
let (from, to) = self.range().await?;
let mut found = Vec::new();
if from > to {
return Ok(found);
}
let total = to - from + 1;
let (mut lo, mut hi) = (from, to);
let mut window = self.chunk;
let (mut scanned, mut requests) = (0u64, 0u64);
let mut last_request: Option<Instant> = None;
loop {
let (start, end) = if self.newest_first {
(hi.saturating_sub(window - 1).max(lo), hi)
} else {
(lo, lo.saturating_add(window - 1).min(hi))
};
let mut batch = Vec::new();
let mut too_large = false;
for filter in &self.filters {
self.throttle(&mut last_request).await;
let mut query = filter.clone();
query.block_hash = None;
let query = query.from_block(start).to_block(end);
requests += 1;
match self.client.get_logs(&query).await {
Ok(logs) => batch.extend(logs),
Err(Error::Rpc { code, .. }) if is_range_error(code) && window > 1 => {
too_large = true;
break;
}
Err(e) => return Err(e),
}
}
if too_large {
window = (window / 2).max(1);
continue;
}
batch.sort_by_key(|l| (l.block_number, l.log_index));
batch.dedup_by_key(|l| (l.block_number, l.log_index));
if self.newest_first {
batch.reverse();
}
found.extend(batch);
scanned += end - start + 1;
if let Some(report) = self.progress.as_mut() {
report(&ScanProgress {
scanned,
total,
requests,
found: found.len(),
window: (start, end),
});
}
if let Some(limit) = self.limit
&& found.len() >= limit
{
found.truncate(limit);
break;
}
if self.newest_first {
if start == lo {
break;
}
hi = start - 1;
} else {
if end == hi {
break;
}
lo = end + 1;
}
}
Ok(found)
}
#[cfg(feature = "http")]
async fn throttle(&self, last: &mut Option<Instant>) {
if let (Some(interval), Some(previous)) = (self.min_interval, *last) {
let elapsed = previous.elapsed();
if elapsed < interval {
tokio::time::sleep(interval - elapsed).await;
}
}
*last = Some(Instant::now());
}
#[cfg(not(feature = "http"))]
async fn throttle(&self, _last: &mut Option<Instant>) {}
}
pub(crate) fn is_range_error(code: i64) -> bool {
matches!(code, -32602 | -32005 | -32600 | -32000)
}