use alloy_chains::NamedChain;
use alloy_primitives::BlockNumber;
use alloy_provider::Provider;
use chrono::{DateTime, Datelike, NaiveDate, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use std::path::Path;
use tracing::{debug, info};
use crate::blocks::cache::{BlockWindowCache, CacheKey, DiskCache};
use crate::errors::{BlockWindowError, RpcError};
use crate::tracing::spans;
use crate::types::config::BlockCount;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct UnixTimestamp(pub i64);
impl UnixTimestamp {
pub fn from_datetime(dt: DateTime<Utc>) -> Self {
Self(dt.timestamp())
}
pub fn from_u64(ts: u64) -> Self {
Self(ts as i64)
}
pub fn as_u64(&self) -> u64 {
self.0 as u64
}
pub fn pred(&self) -> Self {
Self(self.0 - 1)
}
}
impl std::fmt::Display for UnixTimestamp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DailyBlockWindow {
pub start_block: BlockNumber,
pub end_block: BlockNumber,
pub start_ts: UnixTimestamp,
pub end_ts_exclusive: UnixTimestamp,
}
impl DailyBlockWindow {
pub fn new(
start_block: BlockNumber,
end_block: BlockNumber,
start_ts: UnixTimestamp,
end_ts_exclusive: UnixTimestamp,
) -> Result<Self, BlockWindowError> {
if end_block < start_block {
return Err(BlockWindowError::invalid_range(start_block, end_block));
}
if end_ts_exclusive.0 <= start_ts.0 {
return Err(BlockWindowError::invalid_timestamp_range(
start_ts,
end_ts_exclusive,
));
}
Ok(Self {
start_block,
end_block,
start_ts,
end_ts_exclusive,
})
}
pub fn block_count(&self) -> BlockCount {
let count = self
.end_block
.saturating_sub(self.start_block)
.saturating_add(1);
BlockCount::new(count)
}
}
pub struct BlockWindowCalculator<P> {
provider: P,
cache: Box<dyn BlockWindowCache>,
}
impl<P: Provider> BlockWindowCalculator<P> {
pub fn new(provider: P, cache: Box<dyn BlockWindowCache>) -> Self {
Self { provider, cache }
}
pub fn with_disk_cache(
provider: P,
cache_path: impl AsRef<Path>,
) -> Result<Self, BlockWindowError> {
let cache = DiskCache::new(cache_path.as_ref()).validate()?;
Ok(Self::new(provider, Box::new(cache)))
}
pub fn with_memory_cache(provider: P) -> Self {
use crate::blocks::cache::MemoryCache;
Self::new(provider, Box::new(MemoryCache::new()))
}
pub fn without_cache(provider: P) -> Self {
use crate::blocks::cache::NoOpCache;
Self::new(provider, Box::new(NoOpCache))
}
pub async fn cache_stats(&self) -> crate::blocks::cache::CacheStats {
self.cache.stats().await
}
async fn get_block_timestamp(
&self,
block_number: BlockNumber,
) -> Result<UnixTimestamp, BlockWindowError> {
let span = spans::get_block_timestamp(block_number);
let _guard = span.enter();
let block = self
.provider
.get_block_by_number(block_number.into())
.await
.map_err(|e| RpcError::get_block_failed(block_number, e))?
.ok_or_else(|| RpcError::BlockNotFound { block_number })?;
Ok(UnixTimestamp::from_u64(block.header.timestamp))
}
async fn find_first_block_at_or_after(
&self,
target_ts: UnixTimestamp,
latest_block: BlockNumber,
) -> Result<BlockNumber, BlockWindowError> {
let span = spans::find_first_block_at_or_after(target_ts.as_u64(), latest_block);
let _guard = span.enter();
let mut lo = 0u64;
let mut hi = latest_block;
let mut result = latest_block;
while lo <= hi {
let mid = (lo + hi) / 2;
let ts = self.get_block_timestamp(mid).await?;
if ts >= target_ts {
result = mid;
if mid == 0 {
break;
}
hi = mid - 1;
} else {
lo = mid + 1;
}
}
debug!(target_ts = %target_ts, result, "Found first block at or after timestamp");
Ok(result)
}
async fn find_last_block_at_or_before(
&self,
target_ts: UnixTimestamp,
latest_block: BlockNumber,
) -> Result<BlockNumber, BlockWindowError> {
let span = spans::find_last_block_at_or_before(target_ts.as_u64(), latest_block);
let _guard = span.enter();
let mut lo = 0u64;
let mut hi = latest_block;
let mut result = 0u64;
while lo <= hi {
let mid = (lo + hi) / 2;
let ts = self.get_block_timestamp(mid).await?;
if ts <= target_ts {
result = mid;
lo = mid + 1;
} else {
if mid == 0 {
break;
}
hi = mid - 1;
}
}
debug!(target_ts = %target_ts, result, "Found last block at or before timestamp");
Ok(result)
}
pub async fn get_daily_window(
&self,
chain: NamedChain,
date: NaiveDate,
) -> Result<DailyBlockWindow, BlockWindowError> {
let span = spans::get_daily_window(chain, date);
let _guard = span.enter();
let key = CacheKey::new(chain, date);
if let Some(window) = self.cache.get(&key).await {
info!(
chain = %chain,
date = %date,
cache = %self.cache.name(),
cached = true,
"Retrieved daily block window from cache"
);
return Ok(window);
}
let start_dt = Utc
.with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0)
.single()
.ok_or_else(|| BlockWindowError::invalid_date_conversion(date))?;
let end_dt = start_dt
.checked_add_signed(chrono::TimeDelta::days(1))
.ok_or_else(|| BlockWindowError::date_arithmetic_overflow(date))?;
let start_ts = UnixTimestamp::from_datetime(start_dt);
let end_ts_exclusive = UnixTimestamp::from_datetime(end_dt);
let latest_block = self
.provider
.get_block_number()
.await
.map_err(RpcError::get_block_number_failed)?;
info!(
chain = %chain,
date = %date,
start_ts = %start_ts,
end_ts_exclusive = %end_ts_exclusive,
latest_block,
"Computing daily block window"
);
let start_block = self
.find_first_block_at_or_after(start_ts, latest_block)
.await?;
let end_block = self
.find_last_block_at_or_before(end_ts_exclusive.pred(), latest_block)
.await?;
let window = DailyBlockWindow::new(start_block, end_block, start_ts, end_ts_exclusive)?;
info!(
chain = %chain,
date = %date,
start_block = window.start_block,
end_block = window.end_block,
block_count = window.block_count().as_u64(),
cache = %self.cache.name(),
"Computed daily block window"
);
if let Err(e) = self.cache.insert(key, window.clone()).await {
debug!(error = %e, "Failed to cache block window (continuing anyway)");
}
Ok(window)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cache_key_display() {
let key = CacheKey::new(
NamedChain::Arbitrum,
NaiveDate::from_ymd_opt(2025, 10, 10).unwrap(),
);
let serialized = key.to_string();
assert_eq!(serialized, "42161:2025-10-10");
}
#[test]
fn test_daily_block_window_validation() {
let start_ts = UnixTimestamp(1728518400);
let end_ts = UnixTimestamp(1728604800);
let window = DailyBlockWindow::new(1000, 2000, start_ts, end_ts);
assert!(window.is_ok());
assert_eq!(window.unwrap().block_count().as_u64(), 1001);
let invalid = DailyBlockWindow::new(2000, 1000, start_ts, end_ts);
assert!(invalid.is_err());
let invalid = DailyBlockWindow::new(1000, 2000, end_ts, start_ts);
assert!(invalid.is_err());
}
#[test]
fn test_block_window_edge_cases() {
let single = DailyBlockWindow {
start_block: 1000,
end_block: 1000,
start_ts: UnixTimestamp(1697328000),
end_ts_exclusive: UnixTimestamp(1697414400),
};
assert_eq!(single.block_count().as_u64(), 1);
let large = DailyBlockWindow {
start_block: 100_000_000,
end_block: 100_040_000,
start_ts: UnixTimestamp(1697328000),
end_ts_exclusive: UnixTimestamp(1697414400),
};
assert_eq!(large.block_count().as_u64(), 40_001);
let window = DailyBlockWindow {
start_block: 1000,
end_block: 2000,
start_ts: UnixTimestamp(1697328000),
end_ts_exclusive: UnixTimestamp(1697414400),
};
assert_eq!(window.block_count().as_u64(), 1001);
}
#[test]
fn test_block_window_validation_errors() {
let start_ts = UnixTimestamp(1728518400);
let end_ts = UnixTimestamp(1728604800);
let result = DailyBlockWindow::new(2000, 1000, start_ts, end_ts);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid block range"));
let result = DailyBlockWindow::new(1000, 2000, start_ts, start_ts);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid timestamp range"));
let result = DailyBlockWindow::new(1000, 2000, end_ts, start_ts);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid timestamp range"));
}
#[test]
fn test_block_window_zero_values() {
let start_ts = UnixTimestamp(1728518400);
let end_ts = UnixTimestamp(1728604800);
let window = DailyBlockWindow::new(0, 100, start_ts, end_ts);
assert!(window.is_ok());
assert_eq!(window.unwrap().block_count().as_u64(), 101);
let window = DailyBlockWindow::new(0, 0, start_ts, end_ts);
assert!(window.is_ok());
assert_eq!(window.unwrap().block_count().as_u64(), 1);
}
#[test]
fn test_block_window_large_values() {
let start_ts = UnixTimestamp(1728518400);
let end_ts = UnixTimestamp(1728604800);
let window = DailyBlockWindow::new(100_000_000, 100_040_000, start_ts, end_ts);
assert!(window.is_ok());
assert_eq!(window.unwrap().block_count().as_u64(), 40_001);
let window = DailyBlockWindow::new(1_000_000_000, 1_001_000_000, start_ts, end_ts);
assert!(window.is_ok());
assert_eq!(window.unwrap().block_count().as_u64(), 1_000_001);
}
#[test]
fn test_block_window_count_overflow_protection() {
let start_ts = UnixTimestamp(1728518400);
let end_ts = UnixTimestamp(1728604800);
let window = DailyBlockWindow::new(u64::MAX - 100, u64::MAX, start_ts, end_ts);
assert!(window.is_ok());
let count = window.unwrap().block_count();
assert_eq!(count.as_u64(), 101);
}
}