Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
b16d0f2
feat: temp md for input validation
LeoPatOZ Nov 5, 2025
c78d1ba
Merge branch 'main' into input-validation
LeoPatOZ Nov 5, 2025
f45b794
feat: input validtion readme
LeoPatOZ Nov 5, 2025
721e1f1
feat: qquestion
LeoPatOZ Nov 5, 2025
9a431cb
Merge branch 'main' into input-validation
LeoPatOZ Nov 5, 2025
e2243db
Merge branch 'main' into input-validation
LeoPatOZ Nov 6, 2025
94bff10
feat: add assert non zero to count and max block range
LeoPatOZ Nov 6, 2025
2c25629
Merge branch 'main' into input-validation
LeoPatOZ Nov 7, 2025
a0ef4ca
Merge branch 'main' into input-validation
LeoPatOZ Nov 7, 2025
986d6fb
Merge branch 'main' into input-validation
LeoPatOZ Nov 9, 2025
4520dbd
Merge remote-tracking branch 'refs/remotes/origin/input-validation' i…
LeoPatOZ Nov 9, 2025
e8e160b
Merge branch 'main' into input-validation
LeoPatOZ Nov 11, 2025
a862926
feat: shared internal build and expose connect. Validate block range …
LeoPatOZ Nov 11, 2025
c9c191d
test: add test for block validation
LeoPatOZ Nov 11, 2025
737e6d5
Merge branch 'main' into input-validation
LeoPatOZ Nov 12, 2025
04f6c2e
ref: remove panic and add error
LeoPatOZ Nov 12, 2025
14d3e9f
ref: make count assertion return err + test
LeoPatOZ Nov 12, 2025
8b15c6e
ref: add invalid block range error + test
LeoPatOZ Nov 12, 2025
96783cd
Merge branch 'main' into input-validation
LeoPatOZ Nov 13, 2025
575349e
ref: better error
LeoPatOZ Nov 13, 2025
edbf2f5
ref: simplify validation error
LeoPatOZ Nov 13, 2025
c62a75d
ref: remove primitve import
LeoPatOZ Nov 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ pub enum ScannerError {

#[error("Operation timed out")]
Timeout,

#[error("{0} {1} exceeds the latest block {2}")]
BlockExceedsLatest(&'static str, u64, u64),

#[error("Event count must be greater than 0")]
InvalidEventCount,

#[error("Max block range must be greater than 0")]
InvalidMaxBlockRange,
}

impl From<RobustProviderError> for ScannerError {
Expand Down
118 changes: 118 additions & 0 deletions src/event_scanner/scanner/historic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::common::{ConsumerMode, handle_stream};
use crate::{
EventScannerBuilder, ScannerError,
event_scanner::scanner::{EventScanner, Historic},
robust_provider::IntoRobustProvider,
};

impl EventScannerBuilder<Historic> {
Expand All @@ -18,6 +19,40 @@ impl EventScannerBuilder<Historic> {
self.config.to_block = block.into();
self
}

/// Connects to an existing provider with block range validation.
///
/// Validates that the maximum of `from_block` and `to_block` does not exceed
/// the latest block on the chain.
///
/// # Errors
///
/// Returns an error if:
/// * The provider connection fails
/// * The specified block range exceeds the latest block on the chain
/// * The max block range is zero
pub async fn connect<N: Network>(
self,
provider: impl IntoRobustProvider<N>,
) -> Result<EventScanner<Historic, N>, ScannerError> {
let scanner = self.build(provider).await?;

let provider = scanner.block_range_scanner.provider();
let latest_block = provider.get_block_number().await?;

let from_num = scanner.config.from_block.as_number().unwrap_or(0);
let to_num = scanner.config.to_block.as_number().unwrap_or(0);

if from_num > latest_block {
Err(ScannerError::BlockExceedsLatest("from_block", from_num, latest_block))?;
}

if to_num > latest_block {
Err(ScannerError::BlockExceedsLatest("to_block", to_num, latest_block))?;
}

Ok(scanner)
}
}

impl<N: Network> EventScanner<Historic, N> {
Expand Down Expand Up @@ -52,6 +87,12 @@ impl<N: Network> EventScanner<Historic, N> {
#[cfg(test)]
mod tests {
use super::*;
use alloy::{
network::Ethereum,
providers::{Provider, ProviderBuilder, RootProvider, mock::Asserter},
rpc::client::RpcClient,
};
use alloy_node_bindings::Anvil;

#[test]
fn test_historic_scanner_builder_pattern() {
Expand Down Expand Up @@ -88,4 +129,81 @@ mod tests {
assert!(matches!(builder.config.from_block, BlockNumberOrTag::Number(2)));
assert!(matches!(builder.config.to_block, BlockNumberOrTag::Number(200)));
}

#[tokio::test]
async fn test_from_block_above_latest_returns_error() {
let anvil = Anvil::new().try_spawn().unwrap();
let provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());

let latest_block = provider.get_block_number().await.unwrap();

let result = EventScannerBuilder::historic()
.from_block(latest_block + 100)
.to_block(latest_block)
.connect(provider)
.await;

match result {
Err(ScannerError::BlockExceedsLatest("from_block", max, latest)) => {
assert_eq!(max, latest_block + 100);
assert_eq!(latest, latest_block);
}
_ => panic!("Expected BlockExceedsLatest error"),
}
}

#[tokio::test]
async fn test_to_block_above_latest_returns_error() {
let anvil = Anvil::new().try_spawn().unwrap();
let provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());

let latest_block = provider.get_block_number().await.unwrap();

let result = EventScannerBuilder::historic()
.from_block(0)
.to_block(latest_block + 100)
.connect(provider)
.await;

match result {
Err(ScannerError::BlockExceedsLatest("to_block", max, latest)) => {
assert_eq!(max, latest_block + 100);
assert_eq!(latest, latest_block);
}
_ => panic!("Expected BlockExceedsLatest error"),
}
}

#[tokio::test]
async fn test_to_and_from_block_above_latest_returns_error() {
let anvil = Anvil::new().try_spawn().unwrap();
let provider = ProviderBuilder::new().connect_http(anvil.endpoint_url());

let latest_block = provider.get_block_number().await.unwrap();

let result = EventScannerBuilder::historic()
.from_block(latest_block + 50)
.to_block(latest_block + 100)
.connect(provider)
.await;

match result {
Err(ScannerError::BlockExceedsLatest("from_block", max, latest)) => {
assert_eq!(max, latest_block + 50);
assert_eq!(latest, latest_block);
}
_ => panic!("Expected BlockExceedsLatest error for 'from_block'"),
}
}

#[tokio::test]
async fn test_historic_returns_error_with_zero_max_block_range() {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let result = EventScannerBuilder::historic().max_block_range(0).connect(provider).await;

match result {
Err(ScannerError::InvalidMaxBlockRange) => {}
_ => panic!("Expected InvalidMaxBlockRange error"),
}
}
}
47 changes: 47 additions & 0 deletions src/event_scanner/scanner/latest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::common::{ConsumerMode, handle_stream};
use crate::{
EventScannerBuilder, ScannerError,
event_scanner::{EventScanner, LatestEvents},
robust_provider::IntoRobustProvider,
};

impl EventScannerBuilder<LatestEvents> {
Expand All @@ -24,6 +25,24 @@ impl EventScannerBuilder<LatestEvents> {
self.config.to_block = block.into();
self
}

/// Connects to an existing provider.
///
/// # Errors
///
/// Returns an error if:
/// * The provider connection fails
/// * The event count is zero
/// * The max block range is zero
pub async fn connect<N: Network>(
self,
provider: impl IntoRobustProvider<N>,
) -> Result<EventScanner<LatestEvents, N>, ScannerError> {
if self.config.count == 0 {
return Err(ScannerError::InvalidEventCount);
}
self.build(provider).await
}
}

impl<N: Network> EventScanner<LatestEvents, N> {
Expand Down Expand Up @@ -63,6 +82,12 @@ impl<N: Network> EventScanner<LatestEvents, N> {

#[cfg(test)]
mod tests {
use alloy::{
network::Ethereum,
providers::{RootProvider, mock::Asserter},
rpc::client::RpcClient,
};

use super::*;

#[test]
Expand Down Expand Up @@ -111,4 +136,26 @@ mod tests {
assert_eq!(builder.config.block_confirmations, 7);
assert_eq!(builder.block_range_scanner.max_block_range, 60);
}

#[tokio::test]
async fn test_latest_returns_error_with_zero_count() {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let result = EventScannerBuilder::latest(0).connect(provider).await;

match result {
Err(ScannerError::InvalidEventCount) => {}
_ => panic!("Expected InvalidEventCount error"),
}
}

#[tokio::test]
async fn test_latest_returns_error_with_zero_max_block_range() {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let result = EventScannerBuilder::latest(10).max_block_range(0).connect(provider).await;

match result {
Err(ScannerError::InvalidMaxBlockRange) => {}
_ => panic!("Expected InvalidMaxBlockRange error"),
}
}
}
32 changes: 32 additions & 0 deletions src/event_scanner/scanner/live.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use super::common::{ConsumerMode, handle_stream};
use crate::{
EventScannerBuilder, ScannerError,
event_scanner::{EventScanner, scanner::Live},
robust_provider::IntoRobustProvider,
};

impl EventScannerBuilder<Live> {
Expand All @@ -12,6 +13,20 @@ impl EventScannerBuilder<Live> {
self.config.block_confirmations = confirmations;
self
}

/// Connects to an existing provider.
///
/// # Errors
///
/// Returns an error if:
/// * The provider connection fails
/// * The max block range is zero
pub async fn connect<N: Network>(
self,
provider: impl IntoRobustProvider<N>,
) -> Result<EventScanner<Live, N>, ScannerError> {
self.build(provider).await
}
}

impl<N: Network> EventScanner<Live, N> {
Expand Down Expand Up @@ -45,6 +60,12 @@ impl<N: Network> EventScanner<Live, N> {

#[cfg(test)]
mod tests {
use alloy::{
network::Ethereum,
providers::{RootProvider, mock::Asserter},
rpc::client::RpcClient,
};

use super::*;

#[test]
Expand Down Expand Up @@ -76,4 +97,15 @@ mod tests {
assert_eq!(builder.block_range_scanner.max_block_range, 105);
assert_eq!(builder.config.block_confirmations, 8);
}

#[tokio::test]
async fn test_live_returns_error_with_zero_max_block_range() {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let result = EventScannerBuilder::live().max_block_range(0).connect(provider).await;

match result {
Err(ScannerError::InvalidMaxBlockRange) => {}
_ => panic!("Expected InvalidMaxBlockRange error"),
}
}
}
39 changes: 27 additions & 12 deletions src/event_scanner/scanner/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ impl EventScannerBuilder<Unspecified> {
/// # Example
///
/// ```no_run
/// # use alloy::{network::Ethereum, primitives::Address, providers::{Provider, ProviderBuilder}};
/// # use alloy::{network::Ethereum, providers::{Provider, ProviderBuilder}};
/// # use event_scanner::{EventFilter, EventScannerBuilder, Message, robust_provider::RobustProviderBuilder};
/// # use tokio_stream::StreamExt;
/// #
Expand Down Expand Up @@ -298,7 +298,7 @@ impl EventScannerBuilder<Unspecified> {
///
/// # Arguments
///
/// * `count` - Maximum number of recent events to collect per listener
/// * `count` - Maximum number of recent events to collect per listener (must be greater than 0)
///
/// # Reorg behavior
///
Expand Down Expand Up @@ -373,7 +373,7 @@ impl<M> EventScannerBuilder<M> {
///
/// # Arguments
///
/// * `max_block_range` - Maximum number of blocks to process per batch.
/// * `max_block_range` - Maximum number of blocks to process per batch (must be greater than 0)
///
/// # Example
///
Expand All @@ -388,17 +388,16 @@ impl<M> EventScannerBuilder<M> {
self
}

/// Connects to an existing provider.
///
/// Final builder method: consumes the builder and returns the built [`EventScanner`].
///
/// # Errors
/// Builds the scanner by connecting to an existing provider.
///
/// Returns an error if the provider connection fails.
pub async fn connect<N: Network>(
/// This is a shared method used internally by scanner-specific `connect()` methods.
async fn build<N: Network>(
self,
provider: impl IntoRobustProvider<N>,
) -> Result<EventScanner<M, N>, ScannerError> {
if self.block_range_scanner.max_block_range == 0 {
return Err(ScannerError::InvalidMaxBlockRange);
}
let block_range_scanner = self.block_range_scanner.connect::<N>(provider).await?;
Ok(EventScanner { config: self.config, block_range_scanner, listeners: Vec::new() })
}
Expand Down Expand Up @@ -458,7 +457,7 @@ mod tests {
#[tokio::test]
async fn test_historic_event_stream_listeners_vector_updates() -> anyhow::Result<()> {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let mut scanner = EventScannerBuilder::historic().connect(provider).await?;
let mut scanner = EventScannerBuilder::historic().build(provider).await?;

assert!(scanner.listeners.is_empty());

Expand All @@ -475,7 +474,7 @@ mod tests {
#[tokio::test]
async fn test_historic_event_stream_channel_capacity() -> anyhow::Result<()> {
let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let mut scanner = EventScannerBuilder::historic().connect(provider).await?;
let mut scanner = EventScannerBuilder::historic().build(provider).await?;

let _ = scanner.subscribe(EventFilter::new());

Expand All @@ -484,4 +483,20 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn test_latest_returns_error_with_zero_count() {
use alloy::{
providers::{RootProvider, mock::Asserter},
rpc::client::RpcClient,
};

let provider = RootProvider::<Ethereum>::new(RpcClient::mocked(Asserter::new()));
let result = EventScannerBuilder::latest(0).connect(provider).await;

match result {
Err(ScannerError::InvalidEventCount) => {}
_ => panic!("Expected InvalidEventCount error"),
}
}
}
Loading