Skip to main content

chainindex_core/
factory.rs

1//! Factory contract support — dynamic address tracking for protocols
2//! that deploy child contracts (e.g., Uniswap V3 Factory -> Pool).
3//!
4//! # Overview
5//!
6//! Many DeFi protocols use a factory pattern: a single factory contract
7//! deploys child contracts (pools, vaults, markets) via events. The
8//! indexer needs to automatically start tracking these new contracts.
9//!
10//! [`FactoryRegistry`] solves this by:
11//! 1. Accepting factory configurations ([`FactoryConfig`]) that describe
12//!    which event signals a child deployment and which field holds the address.
13//! 2. Processing incoming [`DecodedEvent`]s and extracting child addresses.
14//! 3. Maintaining a thread-safe set of all discovered addresses.
15//! 4. Supporting snapshot/restore for persistence across restarts.
16//!
17//! # Example
18//!
19//! ```rust
20//! use chainindex_core::factory::{FactoryConfig, FactoryRegistry};
21//!
22//! let registry = FactoryRegistry::new();
23//!
24//! // Register Uniswap V3 factory
25//! registry.register(FactoryConfig {
26//!     factory_address: "0x1f98431c8ad98523631ae4a59f267346ea31f984".into(),
27//!     creation_event_topic0: "0x783cca1c0412dd0d695e784568c96da2e9c22ff989357a2e8b1d9b2b4e6b7118".into(),
28//!     child_address_field: "pool".into(),
29//!     name: Some("Uniswap V3 Factory".into()),
30//! });
31//! ```
32
33use std::collections::{HashMap, HashSet};
34use std::sync::{Arc, Mutex};
35
36use serde::{Deserialize, Serialize};
37
38use crate::handler::DecodedEvent;
39
40// ─── FactoryConfig ──────────────────────────────────────────────────────────
41
42/// Configuration for a single factory contract.
43///
44/// Describes the factory's address, which event signals child creation,
45/// and which field in the event's `fields_json` contains the new child address.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct FactoryConfig {
48    /// Address of the factory contract (lowercase hex, `0x…`).
49    pub factory_address: String,
50    /// Event signature hash (topic0) that signals a child contract creation.
51    /// For example, Uniswap V3's `PoolCreated` topic0.
52    pub creation_event_topic0: String,
53    /// Field name within `fields_json` that contains the child contract address.
54    /// Supports dot-separated paths for nested fields (e.g. `"args.pool"`).
55    pub child_address_field: String,
56    /// Optional human-readable name for this factory (e.g. `"Uniswap V3 Factory"`).
57    pub name: Option<String>,
58}
59
60// ─── DiscoveredChild ────────────────────────────────────────────────────────
61
62/// A child contract discovered from a factory creation event.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct DiscoveredChild {
65    /// Address of the newly deployed child contract.
66    pub address: String,
67    /// Address of the factory that deployed this child.
68    pub factory_address: String,
69    /// Block number where the creation event was emitted.
70    pub discovered_at_block: u64,
71    /// Transaction hash of the creation event.
72    pub discovered_at_tx: String,
73    /// The raw creation event payload (preserved for debugging/auditing).
74    pub creation_event: serde_json::Value,
75}
76
77// ─── FactorySnapshot ────────────────────────────────────────────────────────
78
79/// A serializable snapshot of the factory registry state.
80///
81/// Used for persistence — serialize this to disk or storage on shutdown,
82/// and restore it on restart to avoid re-scanning the entire chain.
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct FactorySnapshot {
85    /// All factory configurations.
86    pub configs: Vec<FactoryConfig>,
87    /// All discovered child contracts, keyed by factory address.
88    pub children: HashMap<String, Vec<DiscoveredChild>>,
89}
90
91// ─── Internal State ─────────────────────────────────────────────────────────
92
93/// Internal mutable state behind the lock.
94#[derive(Debug)]
95struct RegistryInner {
96    /// Factory configs keyed by (factory_address, topic0) for fast lookup.
97    configs: HashMap<(String, String), FactoryConfig>,
98    /// All factory addresses for membership checks.
99    factory_addresses: HashSet<String>,
100    /// Children keyed by factory address.
101    children: HashMap<String, Vec<DiscoveredChild>>,
102    /// Set of all child addresses for dedup.
103    child_addresses: HashSet<String>,
104}
105
106impl RegistryInner {
107    fn new() -> Self {
108        Self {
109            configs: HashMap::new(),
110            factory_addresses: HashSet::new(),
111            children: HashMap::new(),
112            child_addresses: HashSet::new(),
113        }
114    }
115}
116
117// ─── FactoryRegistry ────────────────────────────────────────────────────────
118
119/// Thread-safe registry of factory contracts and their discovered children.
120///
121/// Call [`register`](Self::register) to add factory configurations, then feed
122/// incoming events through [`process_event`](Self::process_event). When a
123/// creation event is detected, the child address is extracted and tracked.
124///
125/// Use [`get_all_addresses`](Self::get_all_addresses) to build an
126/// [`EventFilter`](crate::types::EventFilter) that covers all factory and
127/// child addresses.
128pub struct FactoryRegistry {
129    inner: Arc<Mutex<RegistryInner>>,
130}
131
132impl FactoryRegistry {
133    /// Create a new, empty factory registry.
134    pub fn new() -> Self {
135        Self {
136            inner: Arc::new(Mutex::new(RegistryInner::new())),
137        }
138    }
139
140    /// Register a factory contract configuration.
141    ///
142    /// After registration, any [`DecodedEvent`] from this factory with the
143    /// matching topic0 will be checked for child address extraction.
144    pub fn register(&self, config: FactoryConfig) {
145        let mut inner = self.inner.lock().expect("factory registry lock poisoned");
146        let key = (
147            config.factory_address.to_lowercase(),
148            config.creation_event_topic0.to_lowercase(),
149        );
150        inner
151            .factory_addresses
152            .insert(config.factory_address.to_lowercase());
153        inner.configs.insert(key, config);
154    }
155
156    /// Process an incoming event, checking if it is a factory creation event.
157    ///
158    /// Returns `Some(DiscoveredChild)` if the event matched a registered
159    /// factory and a new child address was extracted. Returns `None` if:
160    /// - The event is not from a registered factory.
161    /// - The event topic0 does not match a creation event.
162    /// - The child address was already discovered (dedup).
163    /// - The child address field could not be found in `fields_json`.
164    pub fn process_event(&self, event: &DecodedEvent) -> Option<DiscoveredChild> {
165        let mut inner = self.inner.lock().expect("factory registry lock poisoned");
166        let addr = event.address.to_lowercase();
167
168        // Quick check: is this from a known factory?
169        if !inner.factory_addresses.contains(&addr) {
170            return None;
171        }
172
173        // We need to find any config that matches this factory address.
174        // The event's schema or topic0 might be encoded in the event itself;
175        // we iterate configs matching the factory address.
176        let matching_config = inner
177            .configs
178            .iter()
179            .find(|((fa, _), _)| fa == &addr)
180            .map(|(_, cfg)| cfg.clone());
181
182        let config = matching_config?;
183
184        // Extract the child address from fields_json using the configured field path.
185        let child_addr = extract_field(&event.fields_json, &config.child_address_field)?;
186        let child_addr_lower = child_addr.to_lowercase();
187
188        // Dedup: skip if already known.
189        if inner.child_addresses.contains(&child_addr_lower) {
190            return None;
191        }
192
193        let child = DiscoveredChild {
194            address: child_addr_lower.clone(),
195            factory_address: addr.clone(),
196            discovered_at_block: event.block_number,
197            discovered_at_tx: event.tx_hash.clone(),
198            creation_event: event.fields_json.clone(),
199        };
200
201        inner.child_addresses.insert(child_addr_lower);
202        inner.children.entry(addr).or_default().push(child.clone());
203
204        Some(child)
205    }
206
207    /// Get all tracked addresses (factories + discovered children).
208    ///
209    /// Useful for building an [`EventFilter`](crate::types::EventFilter)
210    /// that covers all contracts the indexer should watch.
211    pub fn get_all_addresses(&self) -> Vec<String> {
212        let inner = self.inner.lock().expect("factory registry lock poisoned");
213        let mut addrs: Vec<String> = inner.factory_addresses.iter().cloned().collect();
214        addrs.extend(inner.child_addresses.iter().cloned());
215        addrs.sort();
216        addrs
217    }
218
219    /// Get all children discovered from a specific factory.
220    ///
221    /// Returns an empty vec if the factory has no children or is not registered.
222    pub fn children_of(&self, factory_address: &str) -> Vec<DiscoveredChild> {
223        let inner = self.inner.lock().expect("factory registry lock poisoned");
224        inner
225            .children
226            .get(&factory_address.to_lowercase())
227            .cloned()
228            .unwrap_or_default()
229    }
230
231    /// Create a serializable snapshot of the current registry state.
232    ///
233    /// Use this to persist discovered children across restarts.
234    pub fn snapshot(&self) -> FactorySnapshot {
235        let inner = self.inner.lock().expect("factory registry lock poisoned");
236        let configs: Vec<FactoryConfig> = inner.configs.values().cloned().collect();
237        let children = inner.children.clone();
238        FactorySnapshot { configs, children }
239    }
240
241    /// Restore the registry from a previously saved snapshot.
242    ///
243    /// This re-registers all factory configs and re-populates the children
244    /// sets. Any existing state is merged (not replaced).
245    pub fn restore(&self, snapshot: FactorySnapshot) {
246        let mut inner = self.inner.lock().expect("factory registry lock poisoned");
247
248        // Restore configs.
249        for config in snapshot.configs {
250            let key = (
251                config.factory_address.to_lowercase(),
252                config.creation_event_topic0.to_lowercase(),
253            );
254            inner
255                .factory_addresses
256                .insert(config.factory_address.to_lowercase());
257            inner.configs.insert(key, config);
258        }
259
260        // Restore children.
261        for (factory_addr, children) in snapshot.children {
262            let factory_lower = factory_addr.to_lowercase();
263            for child in children {
264                let child_lower = child.address.to_lowercase();
265                if inner.child_addresses.insert(child_lower) {
266                    inner
267                        .children
268                        .entry(factory_lower.clone())
269                        .or_default()
270                        .push(child);
271                }
272            }
273        }
274    }
275
276    /// Returns the number of registered factories.
277    pub fn factory_count(&self) -> usize {
278        let inner = self.inner.lock().expect("factory registry lock poisoned");
279        inner.factory_addresses.len()
280    }
281
282    /// Returns the total number of discovered children across all factories.
283    pub fn child_count(&self) -> usize {
284        let inner = self.inner.lock().expect("factory registry lock poisoned");
285        inner.child_addresses.len()
286    }
287}
288
289impl Default for FactoryRegistry {
290    fn default() -> Self {
291        Self::new()
292    }
293}
294
295impl Clone for FactoryRegistry {
296    fn clone(&self) -> Self {
297        let inner = self.inner.lock().expect("factory registry lock poisoned");
298        let new_inner = RegistryInner {
299            configs: inner.configs.clone(),
300            factory_addresses: inner.factory_addresses.clone(),
301            children: inner.children.clone(),
302            child_addresses: inner.child_addresses.clone(),
303        };
304        Self {
305            inner: Arc::new(Mutex::new(new_inner)),
306        }
307    }
308}
309
310// ─── Helpers ────────────────────────────────────────────────────────────────
311
312/// Extract a string value from a JSON object by a dot-separated field path.
313///
314/// Supports paths like `"pool"` (top-level) or `"args.pool"` (nested).
315/// Returns `None` if any segment is missing or the final value is not a string.
316fn extract_field(json: &serde_json::Value, path: &str) -> Option<String> {
317    let mut current = json;
318    for segment in path.split('.') {
319        current = current.get(segment)?;
320    }
321    match current {
322        serde_json::Value::String(s) => Some(s.clone()),
323        // Also handle case where address is a number (unlikely but defensive).
324        _ => None,
325    }
326}
327
328// ─── Tests ──────────────────────────────────────────────────────────────────
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    const FACTORY_ADDR: &str = "0xfactory";
335    const TOPIC0: &str = "0xpoolcreated";
336
337    fn make_config() -> FactoryConfig {
338        FactoryConfig {
339            factory_address: FACTORY_ADDR.into(),
340            creation_event_topic0: TOPIC0.into(),
341            child_address_field: "pool".into(),
342            name: Some("Test Factory".into()),
343        }
344    }
345
346    fn make_event(factory: &str, pool_addr: &str, block: u64) -> DecodedEvent {
347        DecodedEvent {
348            chain: "ethereum".into(),
349            schema: "PoolCreated".into(),
350            address: factory.into(),
351            tx_hash: format!("0xtx{block}"),
352            block_number: block,
353            log_index: 0,
354            fields_json: serde_json::json!({ "pool": pool_addr }),
355        }
356    }
357
358    #[test]
359    fn register_factory() {
360        let registry = FactoryRegistry::new();
361        assert_eq!(registry.factory_count(), 0);
362
363        registry.register(make_config());
364        assert_eq!(registry.factory_count(), 1);
365
366        let addrs = registry.get_all_addresses();
367        assert!(addrs.contains(&FACTORY_ADDR.to_lowercase().to_string()));
368    }
369
370    #[test]
371    fn discover_child_from_event() {
372        let registry = FactoryRegistry::new();
373        registry.register(make_config());
374
375        let event = make_event(FACTORY_ADDR, "0xchild1", 100);
376        let child = registry.process_event(&event);
377
378        assert!(child.is_some());
379        let child = child.unwrap();
380        assert_eq!(child.address, "0xchild1");
381        assert_eq!(child.factory_address, FACTORY_ADDR.to_lowercase());
382        assert_eq!(child.discovered_at_block, 100);
383    }
384
385    #[test]
386    fn duplicate_child_ignored() {
387        let registry = FactoryRegistry::new();
388        registry.register(make_config());
389
390        let event = make_event(FACTORY_ADDR, "0xchild1", 100);
391        assert!(registry.process_event(&event).is_some());
392
393        // Same child again — should be ignored.
394        let event2 = make_event(FACTORY_ADDR, "0xchild1", 101);
395        assert!(registry.process_event(&event2).is_none());
396
397        assert_eq!(registry.child_count(), 1);
398    }
399
400    #[test]
401    fn event_from_unknown_factory_ignored() {
402        let registry = FactoryRegistry::new();
403        registry.register(make_config());
404
405        let event = make_event("0xunknown", "0xchild1", 100);
406        assert!(registry.process_event(&event).is_none());
407    }
408
409    #[test]
410    fn multiple_factories() {
411        let registry = FactoryRegistry::new();
412
413        registry.register(FactoryConfig {
414            factory_address: "0xfactory_a".into(),
415            creation_event_topic0: "0xtopic_a".into(),
416            child_address_field: "pool".into(),
417            name: Some("Factory A".into()),
418        });
419
420        registry.register(FactoryConfig {
421            factory_address: "0xfactory_b".into(),
422            creation_event_topic0: "0xtopic_b".into(),
423            child_address_field: "vault".into(),
424            name: Some("Factory B".into()),
425        });
426
427        assert_eq!(registry.factory_count(), 2);
428
429        // Child from factory A.
430        let ev_a = DecodedEvent {
431            chain: "ethereum".into(),
432            schema: "PoolCreated".into(),
433            address: "0xfactory_a".into(),
434            tx_hash: "0xtx1".into(),
435            block_number: 50,
436            log_index: 0,
437            fields_json: serde_json::json!({ "pool": "0xchild_a" }),
438        };
439        assert!(registry.process_event(&ev_a).is_some());
440
441        // Child from factory B.
442        let ev_b = DecodedEvent {
443            chain: "ethereum".into(),
444            schema: "VaultCreated".into(),
445            address: "0xfactory_b".into(),
446            tx_hash: "0xtx2".into(),
447            block_number: 55,
448            log_index: 0,
449            fields_json: serde_json::json!({ "vault": "0xchild_b" }),
450        };
451        assert!(registry.process_event(&ev_b).is_some());
452
453        assert_eq!(registry.child_count(), 2);
454        assert_eq!(registry.children_of("0xfactory_a").len(), 1);
455        assert_eq!(registry.children_of("0xfactory_b").len(), 1);
456    }
457
458    #[test]
459    fn get_all_addresses_includes_factories_and_children() {
460        let registry = FactoryRegistry::new();
461        registry.register(make_config());
462
463        let event = make_event(FACTORY_ADDR, "0xchild1", 100);
464        registry.process_event(&event);
465
466        let event2 = make_event(FACTORY_ADDR, "0xchild2", 101);
467        registry.process_event(&event2);
468
469        let addrs = registry.get_all_addresses();
470        assert_eq!(addrs.len(), 3); // factory + 2 children
471        assert!(addrs.contains(&FACTORY_ADDR.to_lowercase().to_string()));
472        assert!(addrs.contains(&"0xchild1".to_string()));
473        assert!(addrs.contains(&"0xchild2".to_string()));
474    }
475
476    #[test]
477    fn snapshot_and_restore() {
478        let registry = FactoryRegistry::new();
479        registry.register(make_config());
480
481        let event = make_event(FACTORY_ADDR, "0xchild1", 100);
482        registry.process_event(&event);
483        let event2 = make_event(FACTORY_ADDR, "0xchild2", 101);
484        registry.process_event(&event2);
485
486        // Take snapshot.
487        let snap = registry.snapshot();
488        assert_eq!(snap.children.len(), 1); // one factory key
489        let children = snap.children.get(&FACTORY_ADDR.to_lowercase()).unwrap();
490        assert_eq!(children.len(), 2);
491
492        // Restore into a fresh registry.
493        let registry2 = FactoryRegistry::new();
494        registry2.restore(snap);
495
496        assert_eq!(registry2.factory_count(), 1);
497        assert_eq!(registry2.child_count(), 2);
498        assert_eq!(registry2.get_all_addresses().len(), 3);
499        assert_eq!(registry2.children_of(FACTORY_ADDR).len(), 2);
500    }
501
502    #[test]
503    fn snapshot_restore_roundtrip_json() {
504        let registry = FactoryRegistry::new();
505        registry.register(make_config());
506        registry.process_event(&make_event(FACTORY_ADDR, "0xchild1", 100));
507
508        // Serialize to JSON and back.
509        let snap = registry.snapshot();
510        let json = serde_json::to_string(&snap).unwrap();
511        let restored: FactorySnapshot = serde_json::from_str(&json).unwrap();
512
513        let registry2 = FactoryRegistry::new();
514        registry2.restore(restored);
515
516        assert_eq!(registry2.child_count(), 1);
517        assert_eq!(registry2.children_of(FACTORY_ADDR).len(), 1);
518    }
519
520    #[test]
521    fn nested_field_extraction() {
522        let registry = FactoryRegistry::new();
523        registry.register(FactoryConfig {
524            factory_address: "0xnested_factory".into(),
525            creation_event_topic0: "0xtopic".into(),
526            child_address_field: "args.pool".into(),
527            name: Some("Nested Factory".into()),
528        });
529
530        let event = DecodedEvent {
531            chain: "ethereum".into(),
532            schema: "PoolCreated".into(),
533            address: "0xnested_factory".into(),
534            tx_hash: "0xtx1".into(),
535            block_number: 200,
536            log_index: 0,
537            fields_json: serde_json::json!({ "args": { "pool": "0xdeep_child" } }),
538        };
539
540        let child = registry.process_event(&event);
541        assert!(child.is_some());
542        assert_eq!(child.unwrap().address, "0xdeep_child");
543    }
544
545    #[test]
546    fn missing_field_returns_none() {
547        let registry = FactoryRegistry::new();
548        registry.register(make_config());
549
550        // Event without the expected "pool" field.
551        let event = DecodedEvent {
552            chain: "ethereum".into(),
553            schema: "PoolCreated".into(),
554            address: FACTORY_ADDR.into(),
555            tx_hash: "0xtx1".into(),
556            block_number: 100,
557            log_index: 0,
558            fields_json: serde_json::json!({ "token0": "0xabc" }),
559        };
560
561        assert!(registry.process_event(&event).is_none());
562    }
563
564    #[test]
565    fn case_insensitive_address_matching() {
566        let registry = FactoryRegistry::new();
567        registry.register(FactoryConfig {
568            factory_address: "0xAbCdEf".into(),
569            creation_event_topic0: TOPIC0.into(),
570            child_address_field: "pool".into(),
571            name: None,
572        });
573
574        // Event with different case.
575        let event = DecodedEvent {
576            chain: "ethereum".into(),
577            schema: "PoolCreated".into(),
578            address: "0xabcdef".into(),
579            tx_hash: "0xtx1".into(),
580            block_number: 100,
581            log_index: 0,
582            fields_json: serde_json::json!({ "pool": "0xchild_case" }),
583        };
584
585        let child = registry.process_event(&event);
586        assert!(child.is_some());
587        assert_eq!(child.unwrap().address, "0xchild_case");
588    }
589
590    #[test]
591    fn children_of_unknown_factory_returns_empty() {
592        let registry = FactoryRegistry::new();
593        assert!(registry.children_of("0xnonexistent").is_empty());
594    }
595}