#[path = "support/mock.rs"]
mod mock;
use std::collections::{HashMap, VecDeque};
use std::sync::Arc;
use alloy_network::Ethereum;
use alloy_primitives::{Address, B256, Bytes, Log as PrimitiveLog, U256, keccak256};
use alloy_rpc_types_eth::{Filter, Log};
use anyhow::{Result, anyhow};
use evm_fork_cache::StateUpdate;
use evm_fork_cache::events::StateView;
use evm_fork_cache::reactive::{
BlockRef, ChainStatus, EventSubscriber, HandlerError, HandlerId, HandlerOutcome, InputSource,
InterestOwnerSubscriber, LogInterest, ReactiveConfig, ReactiveContext, ReactiveEffect,
ReactiveEngine, ReactiveHandler, ReactiveInput, ReactiveInputBatch, ReactiveInputRecord,
ReactiveInterest, ReactiveRuntime, RouteKeySpec, StateEffectQuality, SubscriberBackfill,
SubscriberError, SubscriberNextBatch, TrackingPolicy,
};
const POOL_SLOT: u64 = 0;
struct PoolHandler {
id: HandlerId,
pool: Address,
}
impl ReactiveHandler<Ethereum> for PoolHandler {
fn id(&self) -> HandlerId {
self.id.clone()
}
fn interests(&self) -> Vec<ReactiveInterest> {
vec![ReactiveInterest::Logs(LogInterest {
provider_filter: Filter::new().address(self.pool),
local_matcher: None,
route_key: Some(RouteKeySpec::EmitterAddress),
})]
}
fn handle(
&self,
_ctx: &ReactiveContext,
input: &ReactiveInput<Ethereum>,
_state: &dyn StateView,
) -> Result<HandlerOutcome, HandlerError> {
let ReactiveInput::Log(log) = input else {
return Ok(HandlerOutcome::empty(StateEffectQuality::NoStateEffect));
};
let value = U256::from_be_slice(log.data().data.as_ref());
Ok(HandlerOutcome {
effects: vec![ReactiveEffect::StateUpdate(StateUpdate::slot(
log.address(),
U256::from(POOL_SLOT),
value,
))],
quality: StateEffectQuality::ExactFromInput,
tags: vec![],
})
}
}
#[derive(Default)]
struct ScriptedSubscriber {
owners: HashMap<HandlerId, Vec<ReactiveInterest<Ethereum>>>,
backfills: Vec<(HandlerId, SubscriberBackfill)>,
batches: VecDeque<ReactiveInputBatch<Ethereum>>,
}
impl EventSubscriber<Ethereum> for ScriptedSubscriber {
fn register_interests(
&mut self,
_interests: &[ReactiveInterest<Ethereum>],
) -> Result<(), SubscriberError> {
self.owners.clear();
Ok(())
}
fn next_batch(&mut self) -> SubscriberNextBatch<'_, Ethereum> {
Box::pin(async move { Ok(self.batches.pop_front()) })
}
}
impl InterestOwnerSubscriber<Ethereum> for ScriptedSubscriber {
fn add_interest_owner(
&mut self,
owner: HandlerId,
interests: &[ReactiveInterest<Ethereum>],
) -> Result<(), SubscriberError> {
self.owners.insert(owner, interests.to_vec());
Ok(())
}
fn add_interest_owner_with_backfill(
&mut self,
owner: HandlerId,
interests: &[ReactiveInterest<Ethereum>],
backfill: SubscriberBackfill,
) -> Result<(), SubscriberError> {
self.add_interest_owner(owner.clone(), interests)?;
self.backfills.push((owner, backfill));
Ok(())
}
fn remove_interest_owner(
&mut self,
owner: &HandlerId,
) -> Option<Vec<ReactiveInterest<Ethereum>>> {
self.owners.remove(owner)
}
fn owner_interests(&self, owner: &HandlerId) -> Option<&[ReactiveInterest<Ethereum>]> {
self.owners.get(owner).map(Vec::as_slice)
}
}
fn swap_batch(pool: Address, block_number: u64, value: u64) -> ReactiveInputBatch<Ethereum> {
let block = BlockRef {
number: block_number,
hash: B256::repeat_byte(block_number as u8),
parent_hash: Some(B256::repeat_byte(block_number.saturating_sub(1) as u8)),
timestamp: Some(1_700_000_000 + block_number),
};
let log = Log {
inner: PrimitiveLog::new_unchecked(
pool,
vec![keccak256(b"Swap()")],
Bytes::from(U256::from(value).to_be_bytes::<32>().to_vec()),
),
block_hash: Some(block.hash),
block_number: Some(block_number),
block_timestamp: block.timestamp,
transaction_hash: Some(B256::repeat_byte(0xcc)),
transaction_index: Some(0),
log_index: Some(0),
removed: false,
};
let ctx = ReactiveContext {
chain_id: Some(1),
source: InputSource::Subscription,
chain_status: ChainStatus::Included {
block: block.clone(),
confirmations: 0,
},
block: Some(block),
transaction_index: Some(0),
log_index: Some(0),
};
ReactiveInputBatch::new(vec![ReactiveInputRecord::new(ReactiveInput::Log(log), ctx)])
}
#[tokio::main(flavor = "multi_thread")]
async fn main() -> Result<()> {
let pool_a = Address::repeat_byte(0xa1);
let pool_b = Address::repeat_byte(0xb2);
let mut cache = mock::offline_cache().await?;
mock::install_mock_erc20(&mut cache, pool_a);
mock::install_mock_erc20(&mut cache, pool_b);
let runtime = ReactiveRuntime::<Ethereum>::new(ReactiveConfig::default());
let mut engine = ReactiveEngine::new(runtime, ScriptedSubscriber::default());
engine.register_handler(Arc::new(PoolHandler {
id: HandlerId::new("pool-a"),
pool: pool_a,
}))?;
engine
.runtime_mut()
.track_account(pool_a, TrackingPolicy::WholeAccount);
println!(
"1. registered pool-a on a fresh runtime → backfills requested: {} (live-only)",
engine.subscriber().backfills.len()
);
engine
.subscriber_mut()
.batches
.push_back(swap_batch(pool_a, 100, 111));
engine.next_ingest_with_resync(&mut cache).await?;
let head = engine
.runtime()
.last_canonical_block()
.ok_or_else(|| anyhow!("expected a canonical head after ingest"))?;
println!(
"2. ingested block {head}; pool-a slot0 = {}",
cache
.cached_storage_value(pool_a, U256::from(POOL_SLOT))
.unwrap_or_default(),
head = head.number
);
engine.register_handler(Arc::new(PoolHandler {
id: HandlerId::new("pool-b"),
pool: pool_b,
}))?;
let (owner, backfill) = engine
.subscriber()
.backfills
.last()
.cloned()
.ok_or_else(|| anyhow!("expected an auto-anchored backfill for pool-b"))?;
println!(
"3. discovered pool-b → registered with automatic backfill: owner={} from block {}",
owner.as_str(),
backfill.start_block()
);
engine
.subscriber_mut()
.batches
.push_back(swap_batch(pool_b, 101, 222));
engine.next_ingest_with_resync(&mut cache).await?;
println!(
"4. ingested block 101; pool-b slot0 = {}",
cache
.cached_storage_value(pool_b, U256::from(POOL_SLOT))
.unwrap_or_default()
);
let removed = engine.unregister_handler(&HandlerId::new("pool-a"));
let untracked = engine.runtime_mut().untrack_account(pool_a);
let cancelled = engine.runtime_mut().cancel_pending_resyncs(pool_a);
println!(
"5. retired pool-a → handler removed: {}, untracked: {}, resyncs cancelled: {}",
removed.is_some(),
untracked,
cancelled.len()
);
engine
.subscriber_mut()
.batches
.push_back(swap_batch(pool_a, 102, 999));
engine
.subscriber_mut()
.batches
.push_back(swap_batch(pool_b, 102, 333));
while engine.next_ingest_with_resync(&mut cache).await?.is_some() {}
println!(
"6. after teardown, block 102: pool-a slot0 = {} (unchanged — not routed), pool-b slot0 = {}",
cache
.cached_storage_value(pool_a, U256::from(POOL_SLOT))
.unwrap_or_default(),
cache
.cached_storage_value(pool_b, U256::from(POOL_SLOT))
.unwrap_or_default()
);
println!("\nlifecycle complete — registered, auto-anchored, and torn down with no RPC.");
Ok(())
}