Crates.io | event-scanner |
lib.rs | event-scanner |
version | 0.1.0-alpha.1 |
created_at | 2025-09-19 19:15:05.726576+00 |
updated_at | 2025-09-19 19:15:05.726576+00 |
description | Event Scanner is a library for scanning events from any EVM-based blockchain. |
homepage | |
repository | https://github.com/OpenZeppelin/Event-Scanner |
max_upload_size | |
id | 1846864 |
size | 245,851 |
⚠️ 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 monitoring 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 and callback implementations; the scanner takes care of subscribing to historical ranges, bridging into live mode, and delivering events with retry-aware execution strategies.
The library exposes two primary layers:
EventScannerBuilder
/ EventScanner
– the main module the application will interact with.BlockRangeScanner
– lower-level component that streams block ranges, handles reorg, batching, and provider subscriptions.Callbacks implement the EventCallback
trait. They are executed through a CallbackStrategy
that performs retries when necessary before reporting failures.
Add event-scanner
to your Cargo.toml
:
[dependencies]
event-scanner = "0.1.0-alpha.1"
Create a callback implementing EventCallback
and register it with the builder:
use std::{sync::{Arc, atomic::{AtomicUsize, Ordering}}};
use alloy::{eips::BlockNumberOrTag, network::Ethereum, rpc::types::Log, sol_types::SolEvent};
use async_trait::async_trait;
use event_scanner::{event_scanner::EventScannerBuilder, EventCallback, EventFilter};
struct CounterCallback { processed: Arc<AtomicUsize> }
#[async_trait]
impl EventCallback for CounterCallback {
async fn on_event(&self, _log: &Log) -> anyhow::Result<()> {
self.processed.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
async fn run_scanner(ws_url: alloy::transports::http::reqwest::Url, contract: alloy::primitives::Address) -> anyhow::Result<()> {
let filter = EventFilter {
contract_address: contract,
event: MyContract::SomeEvent::SIGNATURE.to_owned(),
callback: Arc::new(CounterCallback { processed: Arc::new(AtomicUsize::new(0)) }),
};
let mut scanner = EventScannerBuilder::new()
.with_event_filter(filter)
.connect_ws::<Ethereum>(ws_url)
.await?;
scanner.start(BlockNumberOrTag::Latest, None).await?;
Ok(())
}
EventScannerBuilder
supports:
with_event_filter(s)
– attach filters.with_callback_strategy(strategy)
– override retry behaviour (StateSyncAwareStrategy
by default).with_blocks_read_per_epoch
- how many blocks are read at a time in a single batch (taken into consideration when fetching historical blocks)with_reorg_rewind_depth
- how many blocks to rewind when a reorg is detectedwith_retry_interval
- how often to retry failed callbackswith_block_confirmations
- how many confirmations to wait for before considering a block finalOnce configured, connect using either connect_ws::<Ethereum>(ws_url)
or connect_ipc::<Ethereum>(path)
. This will build
the EventScanner
and allow you to call run to start in various modes.
Create an EventFilter
for each contract/event pair you want to track. The filter bundles the contract address, the event signature (from SolEvent::SIGNATURE
), and an Arc<dyn EventCallback + Send + Sync>
.
let filter = EventFilter {
contract_address: *counter_contract.address(),
event: Counter::CountIncreased::SIGNATURE.to_owned(),
callback: Arc::new(CounterCallback),
};
Register multiple filters by calling either with_event_filter
repeatedly or with_event_filters
once.
start(BlockNumberOrTag::Latest, None)
subscribes to new blocks only.start(BlockNumberOrTag::Number(start, Some(BlockNumberOrTag::Number(end)))
, scanner fetches events from a historical block range.start(BlockNumberOrTag::Number(start, None)
replays from start
to current head, then streams future blocks.For now modes are deduced from the start
and end
parameters. In the future, we might add explicit commands to select the mode.
See the integration tests under tests/live_mode
, tests/historic_mode
, and tests/historic_to_live
for concrete examples.
Implement EventCallback
:
#[async_trait]
impl EventCallback for RollupCallback {
async fn on_event(&self, log: &Log) -> anyhow::Result<()> {
// decode event, send to EL etc.
Ok(())
}
}
Advanced users can write custom retry behaviour by implementing the CallbackStrategy
trait. The default StateSyncAwareStrategy
automatically detects state-sync errors and performs exponential backoff (smart retry mechanism from the geth driver) before falling back to a fixed retry policy configured via FixedRetryConfig
.
#[async_trait]
pub trait CallbackStrategy: Send + Sync {
async fn execute(
&self,
callback: &Arc<dyn EventCallback + Send + Sync>,
log: &Log,
) -> anyhow::Result<()>;
}
examples/simple_counter
– minimal live-mode scannerexamples/historical_scanning
– demonstrates replaying from genesis (block 0) before continuing streaming latest blocksRun an example with:
RUST_LOG=info cargo run -p simple_counter
# or
RUST_LOG=info cargo run -p historical_scanning
Both examples spin up a local anvil
instance and deploy a demo counter contract before starting the scanner.
Integration tests cover live, historical, and hybrid flows: (We recommend using nextest to run the tests)
cargo nextest run