use std::collections::{HashMap, HashSet};
use std::sync::{Arc, Mutex};
use serde::{Deserialize, Serialize};
use crate::handler::DecodedEvent;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactoryConfig {
pub factory_address: String,
pub creation_event_topic0: String,
pub child_address_field: String,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiscoveredChild {
pub address: String,
pub factory_address: String,
pub discovered_at_block: u64,
pub discovered_at_tx: String,
pub creation_event: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FactorySnapshot {
pub configs: Vec<FactoryConfig>,
pub children: HashMap<String, Vec<DiscoveredChild>>,
}
#[derive(Debug)]
struct RegistryInner {
configs: HashMap<(String, String), FactoryConfig>,
factory_addresses: HashSet<String>,
children: HashMap<String, Vec<DiscoveredChild>>,
child_addresses: HashSet<String>,
}
impl RegistryInner {
fn new() -> Self {
Self {
configs: HashMap::new(),
factory_addresses: HashSet::new(),
children: HashMap::new(),
child_addresses: HashSet::new(),
}
}
}
pub struct FactoryRegistry {
inner: Arc<Mutex<RegistryInner>>,
}
impl FactoryRegistry {
pub fn new() -> Self {
Self {
inner: Arc::new(Mutex::new(RegistryInner::new())),
}
}
pub fn register(&self, config: FactoryConfig) {
let mut inner = self.inner.lock().expect("factory registry lock poisoned");
let key = (
config.factory_address.to_lowercase(),
config.creation_event_topic0.to_lowercase(),
);
inner
.factory_addresses
.insert(config.factory_address.to_lowercase());
inner.configs.insert(key, config);
}
pub fn process_event(&self, event: &DecodedEvent) -> Option<DiscoveredChild> {
let mut inner = self.inner.lock().expect("factory registry lock poisoned");
let addr = event.address.to_lowercase();
if !inner.factory_addresses.contains(&addr) {
return None;
}
let matching_config = inner
.configs
.iter()
.find(|((fa, _), _)| fa == &addr)
.map(|(_, cfg)| cfg.clone());
let config = matching_config?;
let child_addr = extract_field(&event.fields_json, &config.child_address_field)?;
let child_addr_lower = child_addr.to_lowercase();
if inner.child_addresses.contains(&child_addr_lower) {
return None;
}
let child = DiscoveredChild {
address: child_addr_lower.clone(),
factory_address: addr.clone(),
discovered_at_block: event.block_number,
discovered_at_tx: event.tx_hash.clone(),
creation_event: event.fields_json.clone(),
};
inner.child_addresses.insert(child_addr_lower);
inner.children.entry(addr).or_default().push(child.clone());
Some(child)
}
pub fn get_all_addresses(&self) -> Vec<String> {
let inner = self.inner.lock().expect("factory registry lock poisoned");
let mut addrs: Vec<String> = inner.factory_addresses.iter().cloned().collect();
addrs.extend(inner.child_addresses.iter().cloned());
addrs.sort();
addrs
}
pub fn children_of(&self, factory_address: &str) -> Vec<DiscoveredChild> {
let inner = self.inner.lock().expect("factory registry lock poisoned");
inner
.children
.get(&factory_address.to_lowercase())
.cloned()
.unwrap_or_default()
}
pub fn snapshot(&self) -> FactorySnapshot {
let inner = self.inner.lock().expect("factory registry lock poisoned");
let configs: Vec<FactoryConfig> = inner.configs.values().cloned().collect();
let children = inner.children.clone();
FactorySnapshot { configs, children }
}
pub fn restore(&self, snapshot: FactorySnapshot) {
let mut inner = self.inner.lock().expect("factory registry lock poisoned");
for config in snapshot.configs {
let key = (
config.factory_address.to_lowercase(),
config.creation_event_topic0.to_lowercase(),
);
inner
.factory_addresses
.insert(config.factory_address.to_lowercase());
inner.configs.insert(key, config);
}
for (factory_addr, children) in snapshot.children {
let factory_lower = factory_addr.to_lowercase();
for child in children {
let child_lower = child.address.to_lowercase();
if inner.child_addresses.insert(child_lower) {
inner
.children
.entry(factory_lower.clone())
.or_default()
.push(child);
}
}
}
}
pub fn factory_count(&self) -> usize {
let inner = self.inner.lock().expect("factory registry lock poisoned");
inner.factory_addresses.len()
}
pub fn child_count(&self) -> usize {
let inner = self.inner.lock().expect("factory registry lock poisoned");
inner.child_addresses.len()
}
}
impl Default for FactoryRegistry {
fn default() -> Self {
Self::new()
}
}
impl Clone for FactoryRegistry {
fn clone(&self) -> Self {
let inner = self.inner.lock().expect("factory registry lock poisoned");
let new_inner = RegistryInner {
configs: inner.configs.clone(),
factory_addresses: inner.factory_addresses.clone(),
children: inner.children.clone(),
child_addresses: inner.child_addresses.clone(),
};
Self {
inner: Arc::new(Mutex::new(new_inner)),
}
}
}
fn extract_field(json: &serde_json::Value, path: &str) -> Option<String> {
let mut current = json;
for segment in path.split('.') {
current = current.get(segment)?;
}
match current {
serde_json::Value::String(s) => Some(s.clone()),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
const FACTORY_ADDR: &str = "0xfactory";
const TOPIC0: &str = "0xpoolcreated";
fn make_config() -> FactoryConfig {
FactoryConfig {
factory_address: FACTORY_ADDR.into(),
creation_event_topic0: TOPIC0.into(),
child_address_field: "pool".into(),
name: Some("Test Factory".into()),
}
}
fn make_event(factory: &str, pool_addr: &str, block: u64) -> DecodedEvent {
DecodedEvent {
chain: "ethereum".into(),
schema: "PoolCreated".into(),
address: factory.into(),
tx_hash: format!("0xtx{block}"),
block_number: block,
log_index: 0,
fields_json: serde_json::json!({ "pool": pool_addr }),
}
}
#[test]
fn register_factory() {
let registry = FactoryRegistry::new();
assert_eq!(registry.factory_count(), 0);
registry.register(make_config());
assert_eq!(registry.factory_count(), 1);
let addrs = registry.get_all_addresses();
assert!(addrs.contains(&FACTORY_ADDR.to_lowercase().to_string()));
}
#[test]
fn discover_child_from_event() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = make_event(FACTORY_ADDR, "0xchild1", 100);
let child = registry.process_event(&event);
assert!(child.is_some());
let child = child.unwrap();
assert_eq!(child.address, "0xchild1");
assert_eq!(child.factory_address, FACTORY_ADDR.to_lowercase());
assert_eq!(child.discovered_at_block, 100);
}
#[test]
fn duplicate_child_ignored() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = make_event(FACTORY_ADDR, "0xchild1", 100);
assert!(registry.process_event(&event).is_some());
let event2 = make_event(FACTORY_ADDR, "0xchild1", 101);
assert!(registry.process_event(&event2).is_none());
assert_eq!(registry.child_count(), 1);
}
#[test]
fn event_from_unknown_factory_ignored() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = make_event("0xunknown", "0xchild1", 100);
assert!(registry.process_event(&event).is_none());
}
#[test]
fn multiple_factories() {
let registry = FactoryRegistry::new();
registry.register(FactoryConfig {
factory_address: "0xfactory_a".into(),
creation_event_topic0: "0xtopic_a".into(),
child_address_field: "pool".into(),
name: Some("Factory A".into()),
});
registry.register(FactoryConfig {
factory_address: "0xfactory_b".into(),
creation_event_topic0: "0xtopic_b".into(),
child_address_field: "vault".into(),
name: Some("Factory B".into()),
});
assert_eq!(registry.factory_count(), 2);
let ev_a = DecodedEvent {
chain: "ethereum".into(),
schema: "PoolCreated".into(),
address: "0xfactory_a".into(),
tx_hash: "0xtx1".into(),
block_number: 50,
log_index: 0,
fields_json: serde_json::json!({ "pool": "0xchild_a" }),
};
assert!(registry.process_event(&ev_a).is_some());
let ev_b = DecodedEvent {
chain: "ethereum".into(),
schema: "VaultCreated".into(),
address: "0xfactory_b".into(),
tx_hash: "0xtx2".into(),
block_number: 55,
log_index: 0,
fields_json: serde_json::json!({ "vault": "0xchild_b" }),
};
assert!(registry.process_event(&ev_b).is_some());
assert_eq!(registry.child_count(), 2);
assert_eq!(registry.children_of("0xfactory_a").len(), 1);
assert_eq!(registry.children_of("0xfactory_b").len(), 1);
}
#[test]
fn get_all_addresses_includes_factories_and_children() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = make_event(FACTORY_ADDR, "0xchild1", 100);
registry.process_event(&event);
let event2 = make_event(FACTORY_ADDR, "0xchild2", 101);
registry.process_event(&event2);
let addrs = registry.get_all_addresses();
assert_eq!(addrs.len(), 3); assert!(addrs.contains(&FACTORY_ADDR.to_lowercase().to_string()));
assert!(addrs.contains(&"0xchild1".to_string()));
assert!(addrs.contains(&"0xchild2".to_string()));
}
#[test]
fn snapshot_and_restore() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = make_event(FACTORY_ADDR, "0xchild1", 100);
registry.process_event(&event);
let event2 = make_event(FACTORY_ADDR, "0xchild2", 101);
registry.process_event(&event2);
let snap = registry.snapshot();
assert_eq!(snap.children.len(), 1); let children = snap.children.get(&FACTORY_ADDR.to_lowercase()).unwrap();
assert_eq!(children.len(), 2);
let registry2 = FactoryRegistry::new();
registry2.restore(snap);
assert_eq!(registry2.factory_count(), 1);
assert_eq!(registry2.child_count(), 2);
assert_eq!(registry2.get_all_addresses().len(), 3);
assert_eq!(registry2.children_of(FACTORY_ADDR).len(), 2);
}
#[test]
fn snapshot_restore_roundtrip_json() {
let registry = FactoryRegistry::new();
registry.register(make_config());
registry.process_event(&make_event(FACTORY_ADDR, "0xchild1", 100));
let snap = registry.snapshot();
let json = serde_json::to_string(&snap).unwrap();
let restored: FactorySnapshot = serde_json::from_str(&json).unwrap();
let registry2 = FactoryRegistry::new();
registry2.restore(restored);
assert_eq!(registry2.child_count(), 1);
assert_eq!(registry2.children_of(FACTORY_ADDR).len(), 1);
}
#[test]
fn nested_field_extraction() {
let registry = FactoryRegistry::new();
registry.register(FactoryConfig {
factory_address: "0xnested_factory".into(),
creation_event_topic0: "0xtopic".into(),
child_address_field: "args.pool".into(),
name: Some("Nested Factory".into()),
});
let event = DecodedEvent {
chain: "ethereum".into(),
schema: "PoolCreated".into(),
address: "0xnested_factory".into(),
tx_hash: "0xtx1".into(),
block_number: 200,
log_index: 0,
fields_json: serde_json::json!({ "args": { "pool": "0xdeep_child" } }),
};
let child = registry.process_event(&event);
assert!(child.is_some());
assert_eq!(child.unwrap().address, "0xdeep_child");
}
#[test]
fn missing_field_returns_none() {
let registry = FactoryRegistry::new();
registry.register(make_config());
let event = DecodedEvent {
chain: "ethereum".into(),
schema: "PoolCreated".into(),
address: FACTORY_ADDR.into(),
tx_hash: "0xtx1".into(),
block_number: 100,
log_index: 0,
fields_json: serde_json::json!({ "token0": "0xabc" }),
};
assert!(registry.process_event(&event).is_none());
}
#[test]
fn case_insensitive_address_matching() {
let registry = FactoryRegistry::new();
registry.register(FactoryConfig {
factory_address: "0xAbCdEf".into(),
creation_event_topic0: TOPIC0.into(),
child_address_field: "pool".into(),
name: None,
});
let event = DecodedEvent {
chain: "ethereum".into(),
schema: "PoolCreated".into(),
address: "0xabcdef".into(),
tx_hash: "0xtx1".into(),
block_number: 100,
log_index: 0,
fields_json: serde_json::json!({ "pool": "0xchild_case" }),
};
let child = registry.process_event(&event);
assert!(child.is_some());
assert_eq!(child.unwrap().address, "0xchild_case");
}
#[test]
fn children_of_unknown_factory_returns_empty() {
let registry = FactoryRegistry::new();
assert!(registry.children_of("0xnonexistent").is_empty());
}
}