|
1 | 1 | # Event Scanner |
2 | 2 |
|
3 | 3 | > ⚠️ **WARNING: ACTIVE DEVELOPMENT** ⚠️ |
4 | | -> |
5 | | -> This project is under active development and likely contains many bugs. Use at your own risk! |
6 | | -> APIs and functionality may change without notice. |
| 4 | +> |
| 5 | +> This project is under active development and likely contains bugs. APIs and behaviour may change without notice. Use at your own risk. |
7 | 6 |
|
8 | 7 | ## About |
9 | 8 |
|
10 | | -This is an ethereum L1 event scanner is a Rust-based Ethereum blockchain event monitoring library built on top of the Alloy framework. It provides a flexible and efficient way to: |
| 9 | +Event Scanner is a Rust library for monitoring EVM-based smart contract events. It is built on top of the [`alloy`](https://github.com/alloy-rs/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. |
11 | 10 |
|
12 | | -- Subscribe to and monitor specific smart contract events in real-time |
13 | | -- Process historical events from a specified starting block |
14 | | -- Handle event callbacks with configurable retry logic |
15 | | -- Support both WebSocket and IPC connections to Ethereum nodes |
| 11 | +--- |
16 | 12 |
|
17 | | -The scanner allows you to define custom event filters with associated callbacks, making it easy to build applications that react to on-chain events, specifically with rollups in mind. This is varies from traditional indexers in that all logic is handlded in memory and no database is used. |
| 13 | +## Table of Contents |
18 | 14 |
|
| 15 | +- [Features](#features) |
| 16 | +- [Architecture Overview](#architecture-overview) |
| 17 | +- [Quick Start](#quick-start) |
| 18 | +- [Usage](#usage) |
| 19 | + - [Building a Scanner](#building-a-scanner) |
| 20 | + - [Defining Event Filters](#defining-event-filters) |
| 21 | + - [Scanning Modes](#scanning-modes) |
| 22 | + - [Working with Callbacks](#working-with-callbacks) |
| 23 | +- [Examples](#examples) |
| 24 | +- [Testing](#testing) |
19 | 25 |
|
20 | | -## Status |
| 26 | +--- |
| 27 | + |
| 28 | +## Features |
| 29 | + |
| 30 | +- **Historical replay** – scan block ranges. |
| 31 | +- **Live subscriptions** – stay up to date with latest blocks via WebSocket or IPC transports. |
| 32 | +- **Hybrid flow** – automatically transition from historical catch-up into streaming mode. |
| 33 | +- **Composable filters** – register one or many contract + event signature pairs with their own callbacks. |
| 34 | +- **Retry strategies** – built-in retryable callback backoff strategies |
| 35 | +- **No database** – processing happens in-memory; persistence is left to the host application. |
| 36 | + |
| 37 | +--- |
| 38 | + |
| 39 | +## Architecture Overview |
| 40 | + |
| 41 | +The library exposes two primary layers: |
| 42 | + |
| 43 | +- `EventScannerBuilder` / `EventScanner` – the main module the application will interact with. |
| 44 | +- `BlockScanner` – lower-level component that streams block ranges, handles reorg, batching, and provider subscriptions. This is exposed to the user but has many edge cases which will be documented in the future. For now interact with this via the `EventScanner` |
| 45 | + |
| 46 | +Callbacks implement the `EventCallback` trait. They are executed through a `CallbackStrategy` that performs retries when necessary before reporting failures. |
| 47 | + |
| 48 | +--- |
| 49 | + |
| 50 | +## Quick Start |
| 51 | + |
| 52 | +Add `event-scanner` to your `Cargo.toml`: |
| 53 | + |
| 54 | +```toml |
| 55 | +[dependencies] |
| 56 | +event-scanner = "0.1.0-alpha.1" |
| 57 | +``` |
| 58 | +Create a callback implementing `EventCallback` and register it with the builder: |
| 59 | + |
| 60 | +```rust |
| 61 | +use std::{sync::{Arc, atomic::{AtomicUsize, Ordering}}}; |
| 62 | +use alloy::{eips::BlockNumberOrTag, network::Ethereum, rpc::types::Log, sol_types::SolEvent}; |
| 63 | +use async_trait::async_trait; |
| 64 | +use event_scanner::{event_scanner::EventScannerBuilder, EventCallback, EventFilter}; |
| 65 | + |
| 66 | +struct CounterCallback { processed: Arc<AtomicUsize> } |
| 67 | + |
| 68 | +#[async_trait] |
| 69 | +impl EventCallback for CounterCallback { |
| 70 | + async fn on_event(&self, _log: &Log) -> anyhow::Result<()> { |
| 71 | + self.processed.fetch_add(1, Ordering::SeqCst); |
| 72 | + Ok(()) |
| 73 | + } |
| 74 | +} |
| 75 | + |
| 76 | +async fn run_scanner(ws_url: alloy::transports::http::reqwest::Url, contract: alloy::primitives::Address) -> anyhow::Result<()> { |
| 77 | + let filter = EventFilter { |
| 78 | + contract_address: contract, |
| 79 | + event: MyContract::SomeEvent::SIGNATURE.to_owned(), |
| 80 | + callback: Arc::new(CounterCallback { processed: Arc::new(AtomicUsize::new(0)) }), |
| 81 | + }; |
| 82 | + |
| 83 | + let mut scanner = EventScannerBuilder::new() |
| 84 | + .with_event_filter(filter) |
| 85 | + .connect_ws::<Ethereum>(ws_url) |
| 86 | + .await?; |
| 87 | + |
| 88 | + scanner.start(BlockNumberOrTag::Latest, None).await?; |
| 89 | + Ok(()) |
| 90 | +} |
| 91 | +``` |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +## Usage |
| 96 | + |
| 97 | +### Building a Scanner |
| 98 | + |
| 99 | +`EventScannerBuilder` supports: |
| 100 | + |
| 101 | +- `with_event_filter(s)` – attach [filters](#defining-event-filters). |
| 102 | +- `with_callback_strategy(strategy)` – override retry behaviour (`StateSyncAwareStrategy` by default). |
| 103 | +- `with_blocks_read_per_epoch` - how many blocks are read at a time in a single batch (taken into consideration when fetching historical blocks) |
| 104 | +- `with_reorg_rewind_depth` - how many blocks to rewind when a reorg is detected |
| 105 | +- `with_retry_interval` - how often to retry failed callbacks |
| 106 | +- `with_block_confirmations` - how many confirmations to wait for before considering a block final |
| 107 | + |
| 108 | +Once 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](#scanning-Modes). |
| 109 | + |
| 110 | + |
| 111 | +### Defining Event Filters |
| 112 | + |
| 113 | +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>`. |
| 114 | + |
| 115 | +```rust |
| 116 | +let filter = EventFilter { |
| 117 | + contract_address: *counter_contract.address(), |
| 118 | + event: Counter::CountIncreased::SIGNATURE.to_owned(), |
| 119 | + callback: Arc::new(CounterCallback), |
| 120 | +}; |
| 121 | +``` |
| 122 | + |
| 123 | +Register multiple filters by calling either `with_event_filter` repeatedly or `with_event_filters` once. |
| 124 | + |
| 125 | + |
| 126 | +### Scanning Modes |
| 127 | + |
| 128 | +- **Live mode** – `start(BlockNumberOrTag::Latest, None)` subscribes to new blocks only. |
| 129 | +- **Historical mode** – `start(BlockNumberOrTag::Number(start, Some(BlockNumberOrTag::Number(end)))`, scanner fetches events from a historical block range. |
| 130 | +- **Historical → Live** – `start(BlockNumberOrTag::Number(start, None)` replays from `start` to current head, then streams future blocks. |
| 131 | + |
| 132 | +For now modes are deduced from the `start` and `end` parameters. In the future, we might add explicit commands to select the mode. |
| 133 | + |
| 134 | +See the integration tests under `tests/live_mode`, `tests/historic_mode`, and `tests/historic_to_live` for concrete examples. |
| 135 | + |
| 136 | +### Working with Callbacks |
| 137 | + |
| 138 | +Implement `EventCallback`: |
| 139 | + |
| 140 | +```rust |
| 141 | +#[async_trait] |
| 142 | +impl EventCallback for RollupCallback { |
| 143 | + async fn on_event(&self, log: &Log) -> anyhow::Result<()> { |
| 144 | + // decode event, send to EL etc. |
| 145 | + Ok(()) |
| 146 | + } |
| 147 | +} |
| 148 | +``` |
| 149 | + |
| 150 | +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](https://github.com/taikoxyz/taiko-mono/blob/f4b3a0e830e42e2fee54829326389709dd422098/packages/taiko-client/pkg/chain_iterator/block_batch_iterator.go#L149) from the geth driver) before falling back to a fixed retry policy configured via `FixedRetryConfig`. |
| 151 | + |
| 152 | +```rust |
| 153 | +#[async_trait] |
| 154 | +pub trait CallbackStrategy: Send + Sync { |
| 155 | + async fn execute( |
| 156 | + &self, |
| 157 | + callback: &Arc<dyn EventCallback + Send + Sync>, |
| 158 | + log: &Log, |
| 159 | + ) -> anyhow::Result<()>; |
| 160 | +} |
| 161 | +``` |
| 162 | + |
| 163 | +--- |
| 164 | + |
| 165 | +## Examples |
| 166 | + |
| 167 | +- `examples/simple_counter` – minimal live-mode scanner |
| 168 | +- `examples/historical_scanning` – demonstrates replaying from genesis (block 0) before continuing streaming latest blocks |
| 169 | + |
| 170 | +Run an example with: |
| 171 | + |
| 172 | +```bash |
| 173 | +RUST_LOG=info cargo run -p simple_counter |
| 174 | +# or |
| 175 | +RUST_LOG=info cargo run -p historical_scanning |
| 176 | +``` |
| 177 | + |
| 178 | +Both examples spin up a local `anvil` instance and deploy a demo counter contract before starting the scanner. |
| 179 | + |
| 180 | +--- |
| 181 | + |
| 182 | +## Testing |
| 183 | + |
| 184 | +Integration tests cover live, historical, and hybrid flows: |
| 185 | +(We recommend using [nextest](https://crates.io/crates/cargo-nextest) to run the tests) |
| 186 | + |
| 187 | +```bash |
| 188 | +cargo nextest run |
| 189 | +``` |
21 | 190 |
|
22 | | -This library is in early alpha stage. Expect breaking changes and bugs. |
|
0 commit comments