Skip to content

Commit 2dc61bd

Browse files
committed
chore: merge with main
2 parents 31f2a6a + ce77c52 commit 2dc61bd

File tree

36 files changed

+2164
-1406
lines changed

36 files changed

+2164
-1406
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ cargo build --locked --all-targets --all-features
6060
Run the test suite (we recommend nextest):
6161

6262
```bash
63-
cargo nextest run
63+
cargo nextest run --features test-utils
6464
# or
65-
cargo test
65+
cargo test --features test-utils
6666
```
6767

6868
Run examples:
@@ -123,7 +123,7 @@ cargo clippy --all-targets --all-features -- -D warnings -D clippy::pedantic
123123
typos
124124
125125
# Tests (prefer nextest)
126-
cargo nextest run
126+
cargo nextest run --features test-utils
127127
```
128128

129129
---
@@ -160,7 +160,7 @@ Key implementation details, trade-offs, and alternatives considered.
160160
- [ ] `cargo +nightly fmt --all --check`
161161
- [ ] `cargo clippy --all-targets --all-features -- -D warnings -D clippy::pedantic`
162162
- [ ] `typos`
163-
- [ ] Tests pass (`cargo nextest run`)
163+
- [ ] Tests pass (`cargo nextest run --features test-utils`)
164164
- [ ] Docs/README/examples updated (if applicable)
165165
```
166166

Cargo.lock

Lines changed: 28 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
members = [
33
".",
44
"examples/historical_scanning",
5-
"examples/simple_counter",
5+
"examples/live_scanning",
66
"examples/latest_events_scanning",
7+
"examples/sync_scanning"
78
]
89
resolver = "2"
910

@@ -71,8 +72,3 @@ workspace = true
7172

7273
[features]
7374
test-utils = []
74-
75-
[[test]]
76-
# see tests/mod.rs
77-
name = "mod"
78-
required-features = ["test-utils"]

README.md

Lines changed: 88 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
Event Scanner is a Rust library for streaming 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; the scanner takes care of fetching historical ranges, bridging into live streaming mode, all whilst delivering the events as streams of data.
1212

1313
---
14-
1514

1615
## Table of Contents
1716

@@ -60,27 +59,29 @@ event-scanner = "0.3.0-alpha"
6059
Create an event stream for the given event filters registered with the `EventScanner`:
6160

6261
```rust
63-
use alloy::{eips::BlockNumberOrTag, network::Ethereum, sol_types::SolEvent};
64-
use event_scanner::{EventFilter, EventScanner, EventScannerError, EventScannerMessage};
62+
use alloy::{network::Ethereum, sol_types::SolEvent};
63+
use event_scanner::{EventFilter, EventScanner, EventScannerMessage};
6564
use tokio_stream::StreamExt;
6665

6766
use crate::MyContract;
6867

6968
async fn run_scanner(
7069
ws_url: alloy::transports::http::reqwest::Url,
7170
contract: alloy::primitives::Address,
72-
) -> Result<(), EventScannerError> {
73-
let mut client = EventScanner::new().connect_ws::<Ethereum>(ws_url).await?;
71+
) -> Result<(), Box<dyn std::error::Error>> {
72+
// Configure scanner with custom batch size (optional)
73+
let mut scanner = EventScanner::live()
74+
.block_read_limit(500) // Process up to 500 blocks per batch
75+
.connect_ws::<Ethereum>(ws_url).await?;
7476

7577
let filter = EventFilter::new()
76-
.with_contract_address(contract)
77-
.with_event(MyContract::SomeEvent::SIGNATURE);
78+
.contract_address(contract)
79+
.event(MyContract::SomeEvent::SIGNATURE);
7880

79-
let mut stream = client.create_event_stream(filter);
81+
let mut stream = scanner.subscribe(filter);
8082

81-
tokio::spawn(async move {
82-
client.start_scanner(BlockNumberOrTag::Earliest, Some(BlockNumberOrTag::Latest)).await
83-
});
83+
// Start the scanner
84+
tokio::spawn(async move { scanner.stream().await });
8485

8586
while let Some(EventScannerMessage::Data(logs)) = stream.next().await {
8687
println!("Fetched logs: {logs:?}");
@@ -96,13 +97,43 @@ async fn run_scanner(
9697

9798
### Building a Scanner
9899

99-
`EventScanner` supports:
100+
`EventScanner` provides mode-specific constructors and a builder pattern to configure settings before connecting:
100101

101-
- `with_blocks_read_per_epoch` - how many blocks are read at a time in a single batch (taken into consideration when fetching historical blocks)
102-
- `with_reorg_rewind_depth` - how many blocks to rewind when a reorg is detected (NOTE ⚠️: still WIP)
103-
- `with_block_confirmations` - how many confirmations to wait for before considering a block final
102+
```rust
103+
// Live streaming mode
104+
let scanner = EventScanner::live()
105+
.block_read_limit(500) // Optional: set max blocks per read (default: 1000)
106+
.connect_ws::<Ethereum>(ws_url).await?;
107+
108+
// Historical scanning mode
109+
let scanner = EventScanner::historic()
110+
.block_read_limit(500)
111+
.connect_ws::<Ethereum>(ws_url).await?;
112+
113+
// Sync mode (historical + live)
114+
let scanner = EventScanner::sync()
115+
.block_read_limit(500)
116+
.connect_ws::<Ethereum>(ws_url).await?;
117+
118+
// Latest mode (recent blocks only)
119+
let scanner = EventScanner::latest()
120+
.count(100)
121+
.block_read_limit(500)
122+
.connect_ws::<Ethereum>(ws_url).await?;
123+
```
124+
125+
**Available Modes:**
126+
- `EventScanner::live()` – Streams new blocks as they arrive
127+
- `EventScanner::historic()` – Processes historical block ranges
128+
- `EventScanner::sync()` – Processes historical data then transitions to live streaming
129+
- `EventScanner::latest()` – Processes a specific number of events then optionally switches to live scanning mode
104130

105-
Once configured, connect using either `connect_ws::<Ethereum>(ws_url)` or `connect_ipc::<Ethereum>(path)`. This will `connect` the `EventScanner` and allow you to create event streams and start scanning in various [modes](#scanning-modes).
131+
**Global Configuration Options:**
132+
- `block_read_limit(usize)` – Sets the maximum number of blocks to process per read operation. This prevents RPC provider errors from overly large block range queries.
133+
- Connect with `connect_ws::<Ethereum>(url)`, `connect_ipc::<Ethereum>(path)`, or `connect(provider)`.
134+
135+
**Starting the Scanner:**
136+
Invoking `scanner.start()` starts the scanner in the specified mode.
106137

107138
### Defining Event Filters
108139

@@ -111,70 +142,74 @@ Create an `EventFilter` for each event stream you wish to process. The filter sp
111142
```rust
112143
// Track a SPECIFIC event from a SPECIFIC contract
113144
let specific_filter = EventFilter::new()
114-
.with_contract_address(*counter_contract.address())
115-
.with_event(Counter::CountIncreased::SIGNATURE);
145+
.contract_address(*counter_contract.address())
146+
.event(Counter::CountIncreased::SIGNATURE);
116147

117148
// Track a multiple events from a SPECIFIC contract
118149
let specific_filter = EventFilter::new()
119-
.with_contract_address(*counter_contract.address())
120-
.with_event(Counter::CountIncreased::SIGNATURE)
121-
.with_event(Counter::CountDecreased::SIGNATURE);
150+
.contract_address(*counter_contract.address())
151+
.event(Counter::CountIncreased::SIGNATURE)
152+
.event(Counter::CountDecreased::SIGNATURE);
122153

123154
// Track a SPECIFIC event from a ALL contracts
124155
let specific_filter = EventFilter::new()
125-
.with_event(Counter::CountIncreased::SIGNATURE);
156+
.event(Counter::CountIncreased::SIGNATURE);
126157

127158
// Track ALL events from a SPECIFIC contracts
128159
let all_contract_events_filter = EventFilter::new()
129-
.with_contract_address(*counter_contract.address())
130-
.with_contract_address(*other_counter_contract.address());
160+
.contract_address(*counter_contract.address())
161+
.contract_address(*other_counter_contract.address());
131162

132163
// Track ALL events from ALL contracts in the block range
133164
let all_events_filter = EventFilter::new();
134165
```
135166

136-
Register multiple filters by invoking `create_event_stream` repeatedly.
167+
Register multiple filters by invoking `subscribe` repeatedly.
137168

138169
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.
139170

140171
### Scanning Modes
141172

142-
- **Live mode** - `start_scanner(BlockNumberOrTag::Latest, None)` subscribes to new blocks only. On detecting a reorg, the scanner emits `ScannerStatus::ReorgDetected` and recalculates the confirmed window, streaming logs from the corrected confirmed block range.
143-
- **Historical mode** - `start_scanner(BlockNumberOrTag::Number(start), Some(BlockNumberOrTag::Number(end)))`, scanner fetches events from a historical block range. Currently no reorg logic has been implemented (NOTE ⚠️: still WIP). In the case that the end block > finalized block and you need reorg resistance, we recommend to use sync mode.
144-
- **Historical → Live** - `start_scanner(BlockNumberOrTag::Number(start), None)` replays from `start` to current head, then streams future blocks. Reorgs are handled as per the particular mode phase the scanner is in (historical or live).
173+
- **Live mode**`EventScanner::live()` creates a scanner that subscribes to new blocks as they arrive.
174+
- **Historical mode**`EventScanner::historic()` creates a scanner for processing historical block ranges.
175+
- **Sync mode**`EventScanner::sync()` creates a scanner that processes historical data then automatically transitions to live streaming.
176+
- **Latest mode**`EventScanner::latest()` creates a scanner that processes a set number of events.
145177

146-
For now modes are deduced from the `start` and `end` parameters. In the future, we might add explicit commands to select the mode.
178+
**Configuration Tips:**
179+
- Set `block_read_limit` based on your RPC provider's limits (e.g., Alchemy, Infura may limit queries to 2000 blocks)
180+
- For live mode, if the WebSocket subscription lags significantly (e.g., >2000 blocks), ranges are automatically capped to prevent RPC errors
181+
- Each mode has its own configuration options for start block, end block, confirmations, etc. where it makes sense
182+
- The modes come with sensible defaults for example not specify a start block for historic mode automatically sets the start block to the earliest one
147183

148-
See the integration tests under `tests/live_mode`, `tests/historic_mode`, and `tests/historic_to_live` for concrete examples.
184+
See integration tests under `tests/live_mode`, `tests/historic_mode`, and `tests/historic_to_live` for concrete examples.
149185

150186
### Scanning Latest Events
151187

152-
`scan_latest` collects the most recent matching events for each registered stream.
188+
Scanner mode that collects a specified number of the most recent matching events for each registered stream.
153189

154190
- It does not enter live mode; it scans a block range and then returns.
155191
- Each registered stream receives at most `count` logs in a single message, chronologically ordered.
156192

157193
Basic usage:
158194

159195
```rust
160-
use alloy::{eips::BlockNumberOrTag, network::Ethereum};
161-
use event_scanner::{EventFilter, event_scanner::{EventScanner, EventScannerMessage}};
196+
use alloy::{network::Ethereum, primitives::Address, transports::http::reqwest::Url};
197+
use event_scanner::{EventFilter, EventScanner, Message};
162198
use tokio_stream::StreamExt;
163199

164-
async fn latest_example(ws_url: alloy::transports::http::reqwest::Url, addr: alloy::primitives::Address) -> eyre::Result<()> {
165-
let mut client = EventScanner::new().connect_ws::<Ethereum>(ws_url).await?;
200+
async fn latest_events(ws_url: Url, addr: Address) -> anyhow::Result<()> {
201+
let mut scanner = EventScanner::latest().count(10).connect_ws::<Ethereum>(ws_url).await?;
202+
203+
let filter = EventFilter::new().contract_address(addr);
166204

167-
let filter = EventFilter::new().with_contract_address(addr);
168-
let mut stream = client.create_event_stream(filter);
205+
let mut stream = scanner.subscribe(filter);
169206

170207
// Collect the latest 10 events across Earliest..=Latest
171-
client.scan_latest(10).await?;
208+
scanner.start().await?;
172209

173210
// Expect a single message with up to 10 logs, then the stream ends
174-
while let Some(msg) = stream.next().await {
175-
if let EventScannerMessage::Data(logs) = msg {
176-
println!("Latest logs: {}", logs.len());
177-
}
211+
while let Some(Message::Data(logs)) = stream.next().await {
212+
println!("Latest logs: {}", logs.len());
178213
}
179214

180215
Ok(())
@@ -185,30 +220,33 @@ Restricting to a specific block range:
185220

186221
```rust
187222
// Collect the latest 5 events between blocks [1_000_000, 1_100_000]
188-
client
189-
.scan_latest_in_range(5, BlockNumberOrTag::Number(1_000_000), BlockNumberOrTag::Number(1_100_000))
223+
let mut scanner = EventScanner::latest()
224+
.count(5)
225+
.from_block(1_000_000)
226+
.to_block(1_100_000)
227+
.connect_ws::<Ethereum>(ws_url).await?;
190228
.await?;
191229
```
192230

193231
The scanner periodically checks the tip to detect reorgs. On reorg, the scanner emits `ScannerStatus::ReorgDetected`, resets to the updated tip, and restarts the scan. Final delivery to log listeners is in chronological order.
194232

195233
Notes:
196234

197-
- Ensure you create streams via `create_event_stream()` before calling `scan_latest*` so listeners are registered.
235+
- Ensure you create streams via `subscribe()` before calling `start` so listeners are registered.
198236
<!-- TODO: uncomment once implemented - The function returns after delivering the messages; to continuously stream new blocks, use `scan_latest_then_live`. -->
199237

200238
---
201239

202240
## Examples
203241

204-
- `examples/simple_counter` – minimal live-mode scanner
205-
- `examples/historical_scanning` – demonstrates replaying from genesis (block 0) before continuing streaming latest blocks
206-
- `examples/latest_events_scanning` – demonstrates scanning the latest events
242+
- `examples/live_scanning` – minimal live-mode scanner using `EventScanner::live()`
243+
- `examples/historical_scanning` – demonstrates replaying historical data using `EventScanner::historic()`
244+
- `examples/latest_events_scanning` – demonstrates scanning the latest events using `EventScanner::latest()`
207245

208246
Run an example with:
209247

210248
```bash
211-
RUST_LOG=info cargo run -p simple_counter
249+
RUST_LOG=info cargo run -p live_scanning
212250
# or
213251
RUST_LOG=info cargo run -p historical_scanning
214252
```
@@ -224,6 +262,6 @@ Both examples spin up a local `anvil` instance, deploy a demo counter contract,
224262
Integration tests cover all modes:
225263

226264
```bash
227-
cargo nextest run
265+
cargo nextest run --features test-utils
228266
```
229267

0 commit comments

Comments
 (0)