⚠️ WARNING: ACTIVE DEVELOPMENT⚠️ This project is under active development and likely contains bugs. APIs and behaviour may change without notice. Use at your own risk.
Event Scanner is a Rust library for streaming EVM-based smart contract events. It is built on top of the alloy ecosystem and focuses on in-memory scanning without a backing database. Applications provide event filters; the scanner takes care of fetching historical ranges, bridging into live streaming mode, all whilst delivering the events as streams of data.
- Historical replay – stream events from past block ranges.
- Live subscriptions – stay up to date with latest events via WebSocket or IPC transports.
- Hybrid flow – automatically transition from historical catch-up into streaming mode.
- Latest events fetch – one-shot rewind to collect the most recent matching logs.
- Composable filters – register one or many contract + event signature pairs.
- No database – processing happens in-memory; persistence is left to the host application.
The library exposes two primary layers:
- EventScanner– the main scanner type the application will interact with.
- BlockRangeScanner– lower-level component that streams block ranges, handles reorg, batching, and provider subscriptions.
Add event-scanner to your Cargo.toml:
[dependencies]
event-scanner = "0.4.0-alpha"Create an event stream for the given event filters registered with the EventScanner:
use alloy::{network::Ethereum, sol_types::SolEvent};
use event_scanner::{EventFilter, EventScannerBuilder, Message};
use tokio_stream::StreamExt;
use crate::MyContract;
async fn run_scanner(
    ws_url: alloy::transports::http::reqwest::Url,
    contract: alloy::primitives::Address,
) -> Result<(), Box<dyn std::error::Error>> {
    // Configure scanner with custom batch size (optional)
    let mut scanner = EventScannerBuilder::live()
        .max_block_range(500)  // Process up to 500 blocks per batch
        .connect_ws::<Ethereum>(ws_url).await?;
    // Register an event listener
    let filter = EventFilter::new()
        .contract_address(contract)
        .event(MyContract::SomeEvent::SIGNATURE);
    let mut stream = scanner.subscribe(filter);
    // Start the scanner
    scanner.start().await?;
    // Process messages from the stream
    while let Some(message) = stream.next().await {
        match message {
            Message::Data(logs) => {
                println!("Received {} logs: {logs:?}", logs.len());
            }
            Message::Status(status) => {
                println!("Status update: {status:?}");
            }
            Message::Error(err) => {
                eprintln!("Error: {err}");
            }
        }
    }
    Ok(())
}EventScannerBuilder provides mode-specific constructors and a functions to configure settings before connecting.
Once configured, connect using one of:
- connect_ws::<Ethereum>(ws_url)
- connect_ipc::<Ethereum>(path)
- connect::<Ethereum>(provider)
This will connect the EventScanner and allow you to create event streams and start scanning in various modes.
// Live streaming mode
let scanner = EventScannerBuilder::live()
    .max_block_range(500)  // Optional: set max blocks per read (default: 1000)
    .block_confirmations(12)  // Optional: set block confirmations (default: 12)
    .connect_ws::<Ethereum>(ws_url).await?;
// Historical block range mode
let scanner = EventScannerBuilder::historic()
    .from_block(1_000_000)
    .to_block(2_000_000)
    .max_block_range(500)
    .connect_ws::<Ethereum>(ws_url).await?;
// Latest events mode
let scanner = EventScannerBuilder::latest(100)
    // .from_block(1_000_000)  // Optional: set start of search range
    // .to_block(2_000_000)    // Optional: set end of search range
    .max_block_range(500)
    .connect_ws::<Ethereum>(ws_url).await?;
// Sync from block then switch to live mode
let scanner = EventScannerBuilder::sync()
    .from_block(100)
    .max_block_range(500)
    .block_confirmations(12)
    .connect_ws::<Ethereum>(ws_url).await?;
// Sync the latest 60 events then switch to live mode
let scanner = EventScannerBuilder::sync()
    .from_latest(60)
    .block_confirmations(12)
    .connect_ws::<Ethereum>(ws_url).await?;Invoking scanner.start() starts the scanner in the specified mode.
Create an EventFilter for each event stream you wish to process. The filter specifies the contract address where events originated, and event signatures (tip: you can use the value stored in SolEvent::SIGNATURE).
// Track a SPECIFIC event from a SPECIFIC contract
let specific_filter = EventFilter::new()
    .contract_address(*counter_contract.address())
    .event(Counter::CountIncreased::SIGNATURE);
// Track a multiple events from a SPECIFIC contract
let specific_filter = EventFilter::new()
    .contract_address(*counter_contract.address())
    .event(Counter::CountIncreased::SIGNATURE)
    .event(Counter::CountDecreased::SIGNATURE);
// Track a SPECIFIC event from a ALL contracts
let specific_filter = EventFilter::new()
    .event(Counter::CountIncreased::SIGNATURE);
// Track ALL events from a SPECIFIC contracts
let all_contract_events_filter = EventFilter::new()
    .contract_address(*counter_contract.address())
    .contract_address(*other_counter_contract.address());
// Track ALL events from ALL contracts in the block range
let all_events_filter = EventFilter::new();Register multiple filters by invoking subscribe repeatedly.
The flexibility provided by EventFilter allows you to build sophisticated event monitoring systems that can track events at different granularities depending on your application's needs.
Batch builder examples:
// Multiple contract addresses at once
let multi_addr = EventFilter::new()
    .contract_addresses([*counter_contract.address(), *other_counter_contract.address()]);
// Multiple event names at once
let multi_events = EventFilter::new()
    .events([Counter::CountIncreased::SIGNATURE, Counter::CountDecreased::SIGNATURE]);
// Multiple event signature hashes at once
let multi_sigs = EventFilter::new()
    .event_signatures([
        Counter::CountIncreased::SIGNATURE_HASH,
        Counter::CountDecreased::SIGNATURE_HASH,
    ]);The scanner delivers three types of messages through the event stream:
- Message::Data(Vec<Log>)– Contains a batch of matching event logs. Each log includes the raw event data, transaction hash, block number, and other metadata.
- Message::Status(ScannerStatus)– Status notifications from the scanner:
- Message::Error(ScannerError)– Error notifications if the scanner encounters issues (e.g., RPC failures, connection problems)
Always handle all message types in your stream processing loop to ensure robust error handling and proper reorg detection.
- Live – scanner that streams new blocks as they arrive.
- Historic – scanner for streaming events from a past block range (default: genesis..=latest).
- Latest Events – scanner that collects up to countmost recent events per listener. Final delivery is in chronological order (oldest to newest).
- Sync from Block – scanner that streams events from a given start block up to the current confirmed tip, then automatically transitions to live streaming.
- Sync from Latest Events - scanner that collects the most recent countevents, then automatically transitions to live streaming.
- Set max_block_rangebased on your RPC provider's limits (e.g., Alchemy, Infura may limit queries to 2000 blocks). Default is 1000 blocks.
- The modes come with sensible defaults; for example, not specifying a start block for historic mode automatically sets it to the genesis block.
- For live mode, if the WebSocket subscription lags significantly (e.g., >2000 blocks), ranges are automatically capped to prevent RPC errors.
- examples/live_scanning– minimal live-mode scanner using- EventScannerBuilder::live()
- examples/historical_scanning– demonstrates replaying historical data using- EventScannerBuilder::historic()
- examples/sync_from_block_scanning– demonstrates replaying from genesis (block 0) before continuing to stream the latest blocks using- EventScannerBuilder::sync().from_block(block_tag_or_number)
- examples/latest_events_scanning– demonstrates scanning the latest events using- EventScannerBuilder::latest()
- examples/sync_from_latest_scanning– demonstrates scanning the latest events before switching to live mode using- EventScannerBuilder::sync().from_latest(count).
Run an example with:
RUST_LOG=info cargo run -p live_scanningAll examples spin up a local anvil instance, deploy a demo counter contract, and demonstrate using event streams to process events.
(We recommend using nextest to run the tests)
Integration tests cover all modes:
cargo nextest run --features test-utils