Skip to main content

txgate_chain/
registry.rs

1//! Chain registry for runtime chain lookup.
2//!
3//! This module provides the [`ChainRegistry`] struct that holds all chain parsers
4//! and provides runtime chain lookup by identifier.
5//!
6//! # Design
7//!
8//! The registry is designed to be:
9//! - **Thread-safe**: Uses `Arc` internally for cheap cloning across async tasks
10//! - **Immutable in production**: The `new()` constructor registers all supported chains
11//! - **Testable**: Provides `empty()` and `register()` for testing with mock chains
12//!
13//! # Example
14//!
15//! ```
16//! use txgate_chain::ChainRegistry;
17//!
18//! let registry = ChainRegistry::new();
19//!
20//! // List supported chains
21//! println!("Supported chains: {:?}", registry.supported_chains());
22//!
23//! // Look up a chain parser
24//! if let Some(parser) = registry.get("ethereum") {
25//!     println!("Found ethereum parser: {}", parser.id());
26//! }
27//!
28//! // Check if a chain is supported
29//! if registry.supports("ethereum") {
30//!     println!("Ethereum is supported!");
31//! }
32//! ```
33//!
34//! # Thread Safety
35//!
36//! The registry can be safely shared across threads and async tasks:
37//!
38//! ```
39//! use txgate_chain::ChainRegistry;
40//! use std::sync::Arc;
41//!
42//! let registry = ChainRegistry::new();
43//!
44//! // Clone is cheap (Arc internally)
45//! let registry_clone = registry.clone();
46//!
47//! // Both can be used concurrently
48//! std::thread::spawn(move || {
49//!     let _ = registry_clone.supported_chains();
50//! });
51//! ```
52
53use std::collections::HashMap;
54use std::sync::Arc;
55
56use crate::Chain;
57
58/// Registry of supported blockchain parsers.
59///
60/// The registry provides runtime lookup of chain parsers by their identifier.
61/// It is designed to be cloned cheaply (via [`Arc`]) for use across async tasks.
62///
63/// # Construction
64///
65/// Use [`ChainRegistry::new()`] to create a registry with all production chains,
66/// or [`ChainRegistry::empty()`] for testing.
67///
68/// # Example
69///
70/// ```
71/// use txgate_chain::ChainRegistry;
72///
73/// let registry = ChainRegistry::new();
74/// println!("Supported chains: {:?}", registry.supported_chains());
75///
76/// if let Some(parser) = registry.get("ethereum") {
77///     // Use parser...
78///     println!("Found: {}", parser.id());
79/// }
80/// ```
81#[derive(Clone)]
82pub struct ChainRegistry {
83    chains: Arc<HashMap<String, Arc<dyn Chain>>>,
84}
85
86impl ChainRegistry {
87    /// Create a new registry with all supported chain parsers.
88    ///
89    /// Currently supported chains:
90    /// - `ethereum` - Ethereum and EVM-compatible chains
91    /// - `bitcoin` - Bitcoin (Legacy, `SegWit`, Taproot)
92    /// - `solana` - Solana (Legacy and Versioned messages)
93    ///
94    /// # Example
95    ///
96    /// ```
97    /// use txgate_chain::ChainRegistry;
98    ///
99    /// let registry = ChainRegistry::new();
100    /// assert_eq!(registry.len(), 3);
101    /// assert!(registry.supports("ethereum"));
102    /// assert!(registry.supports("bitcoin"));
103    /// assert!(registry.supports("solana"));
104    /// ```
105    #[must_use]
106    pub fn new() -> Self {
107        let mut chains: HashMap<String, Arc<dyn Chain>> = HashMap::new();
108
109        // Register supported chains
110        chains.insert(
111            "ethereum".to_string(),
112            Arc::new(crate::EthereumParser::new()),
113        );
114        chains.insert(
115            "bitcoin".to_string(),
116            Arc::new(crate::BitcoinParser::mainnet()),
117        );
118        chains.insert("solana".to_string(), Arc::new(crate::SolanaParser::new()));
119
120        Self {
121            chains: Arc::new(chains),
122        }
123    }
124
125    /// Create an empty registry (for testing).
126    ///
127    /// This is useful when you need a registry without any production chains,
128    /// typically for unit tests where you want to register mock chains.
129    ///
130    /// # Example
131    ///
132    /// ```
133    /// use txgate_chain::ChainRegistry;
134    ///
135    /// let registry = ChainRegistry::empty();
136    /// assert!(registry.is_empty());
137    /// assert_eq!(registry.len(), 0);
138    /// ```
139    #[must_use]
140    pub fn empty() -> Self {
141        Self {
142            chains: Arc::new(HashMap::new()),
143        }
144    }
145
146    /// Register a chain parser.
147    ///
148    /// This is primarily used for testing with mock parsers.
149    /// In production, use [`ChainRegistry::new()`] which registers all supported chains.
150    ///
151    /// # Arguments
152    ///
153    /// * `chain` - The chain parser to register
154    ///
155    /// # Note
156    ///
157    /// If a chain with the same ID is already registered, it will be replaced.
158    ///
159    /// # Example
160    ///
161    /// ```ignore
162    /// use txgate_chain::{ChainRegistry, MockChain};
163    ///
164    /// let mut registry = ChainRegistry::empty();
165    ///
166    /// let mock = MockChain {
167    ///     id: "test-chain",
168    ///     ..Default::default()
169    /// };
170    ///
171    /// registry.register(mock);
172    ///
173    /// assert!(registry.supports("test-chain"));
174    /// assert_eq!(registry.len(), 1);
175    /// ```
176    pub fn register<C: Chain + 'static>(&mut self, chain: C) {
177        let chains = Arc::make_mut(&mut self.chains);
178        chains.insert(chain.id().to_string(), Arc::new(chain));
179    }
180
181    /// Look up a chain parser by ID.
182    ///
183    /// # Arguments
184    ///
185    /// * `chain_id` - The chain identifier (e.g., "ethereum", "bitcoin")
186    ///
187    /// # Returns
188    ///
189    /// * `Some(&dyn Chain)` if the chain is supported
190    /// * `None` if the chain is not supported
191    ///
192    /// # Example
193    ///
194    /// ```
195    /// use txgate_chain::ChainRegistry;
196    ///
197    /// let registry = ChainRegistry::new();
198    ///
199    /// // Look up a chain (returns None if not registered)
200    /// let parser = registry.get("ethereum");
201    /// // Currently None - parsers will be added in future tasks
202    ///
203    /// // Not found
204    /// let missing = registry.get("nonexistent");
205    /// assert!(missing.is_none());
206    /// ```
207    #[must_use]
208    pub fn get(&self, chain_id: &str) -> Option<&dyn Chain> {
209        self.chains.get(chain_id).map(AsRef::as_ref)
210    }
211
212    /// List all supported chain IDs.
213    ///
214    /// Returns a sorted list of chain identifiers for consistency.
215    ///
216    /// # Example
217    ///
218    /// ```
219    /// use txgate_chain::ChainRegistry;
220    ///
221    /// let registry = ChainRegistry::new();
222    ///
223    /// // Get list of all supported chains (sorted alphabetically)
224    /// let chains = registry.supported_chains();
225    /// assert_eq!(chains, vec!["bitcoin", "ethereum", "solana"]);
226    /// ```
227    #[must_use]
228    pub fn supported_chains(&self) -> Vec<&str> {
229        let mut chains: Vec<&str> = self.chains.keys().map(String::as_str).collect();
230        chains.sort_unstable();
231        chains
232    }
233
234    /// Check if a chain is supported.
235    ///
236    /// # Arguments
237    ///
238    /// * `chain_id` - The chain identifier to check
239    ///
240    /// # Returns
241    ///
242    /// * `true` if the chain is registered
243    /// * `false` otherwise
244    ///
245    /// # Example
246    ///
247    /// ```
248    /// use txgate_chain::ChainRegistry;
249    ///
250    /// let registry = ChainRegistry::new();
251    ///
252    /// // Check if a chain is supported
253    /// assert!(registry.supports("ethereum"));
254    /// assert!(registry.supports("bitcoin"));
255    /// assert!(registry.supports("solana"));
256    /// assert!(!registry.supports("unknown"));
257    /// ```
258    #[must_use]
259    pub fn supports(&self, chain_id: &str) -> bool {
260        self.chains.contains_key(chain_id)
261    }
262
263    /// Get the number of registered chains.
264    ///
265    /// # Example
266    ///
267    /// ```
268    /// use txgate_chain::ChainRegistry;
269    ///
270    /// let registry = ChainRegistry::new();
271    /// assert_eq!(registry.len(), 3);  // ethereum, bitcoin, solana
272    ///
273    /// let registry = ChainRegistry::empty();
274    /// assert_eq!(registry.len(), 0);
275    /// ```
276    #[must_use]
277    pub fn len(&self) -> usize {
278        self.chains.len()
279    }
280
281    /// Check if the registry is empty.
282    ///
283    /// # Example
284    ///
285    /// ```
286    /// use txgate_chain::ChainRegistry;
287    ///
288    /// let registry = ChainRegistry::new();
289    /// assert!(!registry.is_empty());  // has 3 chains registered
290    ///
291    /// let registry = ChainRegistry::empty();
292    /// assert!(registry.is_empty());
293    /// ```
294    #[must_use]
295    pub fn is_empty(&self) -> bool {
296        self.chains.is_empty()
297    }
298}
299
300impl Default for ChainRegistry {
301    fn default() -> Self {
302        Self::new()
303    }
304}
305
306impl std::fmt::Debug for ChainRegistry {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        f.debug_struct("ChainRegistry")
309            .field("chains", &self.supported_chains())
310            .finish()
311    }
312}
313
314// ============================================================================
315// Tests
316// ============================================================================
317
318#[cfg(test)]
319mod tests {
320    #![allow(
321        clippy::expect_used,
322        clippy::unwrap_used,
323        clippy::panic,
324        clippy::indexing_slicing,
325        clippy::similar_names,
326        clippy::redundant_clone,
327        clippy::manual_string_new,
328        clippy::needless_raw_string_hashes,
329        clippy::needless_collect,
330        clippy::unreadable_literal
331    )]
332
333    use super::*;
334    use crate::{MockChain, MockParseError};
335    use txgate_core::TxType;
336    use txgate_crypto::CurveType;
337
338    // ------------------------------------------------------------------------
339    // Construction Tests
340    // ------------------------------------------------------------------------
341
342    #[test]
343    fn test_new_registry() {
344        let registry = ChainRegistry::new();
345        // Should have ethereum, bitcoin, and solana
346        assert_eq!(registry.len(), 3);
347        assert!(registry.supports("ethereum"));
348        assert!(registry.supports("bitcoin"));
349        assert!(registry.supports("solana"));
350    }
351
352    #[test]
353    fn test_empty_registry() {
354        let registry = ChainRegistry::empty();
355        assert!(registry.is_empty());
356        assert_eq!(registry.len(), 0);
357        assert!(registry.supported_chains().is_empty());
358    }
359
360    #[test]
361    fn test_default_registry() {
362        let registry = ChainRegistry::default();
363        // Default is same as new()
364        assert_eq!(registry.len(), 3);
365    }
366
367    // ------------------------------------------------------------------------
368    // Registration Tests
369    // ------------------------------------------------------------------------
370
371    #[test]
372    fn test_register_chain() {
373        let mut registry = ChainRegistry::empty();
374
375        let mock = MockChain {
376            id: "test-chain",
377            curve: CurveType::Secp256k1,
378            ..Default::default()
379        };
380
381        registry.register(mock);
382
383        assert!(!registry.is_empty());
384        assert_eq!(registry.len(), 1);
385        assert!(registry.supports("test-chain"));
386    }
387
388    #[test]
389    fn test_register_multiple_chains() {
390        let mut registry = ChainRegistry::empty();
391
392        registry.register(MockChain {
393            id: "ethereum",
394            curve: CurveType::Secp256k1,
395            ..Default::default()
396        });
397        registry.register(MockChain {
398            id: "solana",
399            curve: CurveType::Ed25519,
400            ..Default::default()
401        });
402        registry.register(MockChain {
403            id: "bitcoin",
404            curve: CurveType::Secp256k1,
405            ..Default::default()
406        });
407
408        assert_eq!(registry.len(), 3);
409        assert!(registry.supports("ethereum"));
410        assert!(registry.supports("solana"));
411        assert!(registry.supports("bitcoin"));
412    }
413
414    #[test]
415    fn test_register_overwrites_existing() {
416        let mut registry = ChainRegistry::empty();
417
418        registry.register(MockChain {
419            id: "ethereum",
420            curve: CurveType::Secp256k1,
421            parse_error: Some(MockParseError::UnknownTxType),
422            ..Default::default()
423        });
424
425        // Verify first registration
426        let parser = registry.get("ethereum").unwrap();
427        assert!(parser.parse(&[]).is_err());
428
429        // Overwrite with new parser
430        registry.register(MockChain {
431            id: "ethereum",
432            curve: CurveType::Secp256k1,
433            parse_error: None,
434            ..Default::default()
435        });
436
437        // Verify overwrite
438        assert_eq!(registry.len(), 1); // Still only 1
439        let parser = registry.get("ethereum").unwrap();
440        assert!(parser.parse(&[]).is_ok()); // Now succeeds
441    }
442
443    // ------------------------------------------------------------------------
444    // Lookup Tests
445    // ------------------------------------------------------------------------
446
447    #[test]
448    fn test_get_existing_chain() {
449        let mut registry = ChainRegistry::empty();
450        registry.register(MockChain {
451            id: "ethereum",
452            curve: CurveType::Secp256k1,
453            ..Default::default()
454        });
455
456        let parser = registry.get("ethereum");
457        assert!(parser.is_some());
458
459        let parser = parser.unwrap();
460        assert_eq!(parser.id(), "ethereum");
461        assert_eq!(parser.curve(), CurveType::Secp256k1);
462    }
463
464    #[test]
465    fn test_get_nonexistent_chain() {
466        let registry = ChainRegistry::empty();
467
468        let parser = registry.get("ethereum");
469        assert!(parser.is_none());
470
471        let parser = registry.get("nonexistent");
472        assert!(parser.is_none());
473    }
474
475    #[test]
476    fn test_get_and_parse() {
477        let mut registry = ChainRegistry::empty();
478
479        let expected_tx = txgate_core::ParsedTx {
480            chain: "ethereum".to_string(),
481            tx_type: TxType::Transfer,
482            recipient: Some("0x1234".to_string()),
483            ..Default::default()
484        };
485
486        registry.register(MockChain {
487            id: "ethereum",
488            curve: CurveType::Secp256k1,
489            parse_result: Some(expected_tx.clone()),
490            parse_error: None,
491        });
492
493        let parser = registry.get("ethereum").unwrap();
494        let result = parser.parse(&[0x02, 0x01, 0x02, 0x03]);
495
496        assert!(result.is_ok());
497        let parsed = result.unwrap();
498        assert_eq!(parsed.chain, "ethereum");
499        assert_eq!(parsed.tx_type, TxType::Transfer);
500        assert_eq!(parsed.recipient, Some("0x1234".to_string()));
501    }
502
503    // ------------------------------------------------------------------------
504    // Supported Chains Tests
505    // ------------------------------------------------------------------------
506
507    #[test]
508    fn test_supported_chains_empty() {
509        let registry = ChainRegistry::empty();
510        assert!(registry.supported_chains().is_empty());
511    }
512
513    #[test]
514    fn test_supported_chains_sorted() {
515        let mut registry = ChainRegistry::empty();
516
517        // Register in non-alphabetical order
518        registry.register(MockChain {
519            id: "solana",
520            ..Default::default()
521        });
522        registry.register(MockChain {
523            id: "ethereum",
524            ..Default::default()
525        });
526        registry.register(MockChain {
527            id: "bitcoin",
528            ..Default::default()
529        });
530        registry.register(MockChain {
531            id: "arbitrum",
532            ..Default::default()
533        });
534
535        let chains = registry.supported_chains();
536
537        // Should be sorted alphabetically
538        assert_eq!(chains, vec!["arbitrum", "bitcoin", "ethereum", "solana"]);
539    }
540
541    // ------------------------------------------------------------------------
542    // Supports Tests
543    // ------------------------------------------------------------------------
544
545    #[test]
546    fn test_supports_registered_chain() {
547        let mut registry = ChainRegistry::empty();
548        registry.register(MockChain {
549            id: "ethereum",
550            ..Default::default()
551        });
552
553        assert!(registry.supports("ethereum"));
554    }
555
556    #[test]
557    fn test_supports_unregistered_chain() {
558        let registry = ChainRegistry::empty();
559        assert!(!registry.supports("ethereum"));
560        assert!(!registry.supports("bitcoin"));
561        assert!(!registry.supports(""));
562    }
563
564    // ------------------------------------------------------------------------
565    // Clone Tests (Arc sharing)
566    // ------------------------------------------------------------------------
567
568    #[test]
569    fn test_clone_shares_arc() {
570        let mut registry = ChainRegistry::empty();
571        registry.register(MockChain {
572            id: "ethereum",
573            ..Default::default()
574        });
575
576        let clone = registry.clone();
577
578        // Both should see the same chains
579        assert_eq!(registry.len(), clone.len());
580        assert!(registry.supports("ethereum"));
581        assert!(clone.supports("ethereum"));
582
583        // Arc should be shared (same pointer)
584        assert!(Arc::ptr_eq(&registry.chains, &clone.chains));
585    }
586
587    #[test]
588    fn test_clone_independent_mutation() {
589        let mut registry = ChainRegistry::empty();
590        registry.register(MockChain {
591            id: "ethereum",
592            ..Default::default()
593        });
594
595        let mut clone = registry.clone();
596
597        // Mutate clone
598        clone.register(MockChain {
599            id: "bitcoin",
600            ..Default::default()
601        });
602
603        // Original should be unaffected (Arc::make_mut creates new allocation)
604        assert_eq!(registry.len(), 1);
605        assert!(registry.supports("ethereum"));
606        assert!(!registry.supports("bitcoin"));
607
608        // Clone should have both
609        assert_eq!(clone.len(), 2);
610        assert!(clone.supports("ethereum"));
611        assert!(clone.supports("bitcoin"));
612
613        // Arcs should no longer be shared
614        assert!(!Arc::ptr_eq(&registry.chains, &clone.chains));
615    }
616
617    // ------------------------------------------------------------------------
618    // Thread Safety Tests (Send + Sync)
619    // ------------------------------------------------------------------------
620
621    #[test]
622    fn test_registry_is_send() {
623        fn assert_send<T: Send>() {}
624        assert_send::<ChainRegistry>();
625    }
626
627    #[test]
628    fn test_registry_is_sync() {
629        fn assert_sync<T: Sync>() {}
630        assert_sync::<ChainRegistry>();
631    }
632
633    #[test]
634    fn test_registry_across_threads() {
635        let mut registry = ChainRegistry::empty();
636        registry.register(MockChain {
637            id: "ethereum",
638            ..Default::default()
639        });
640
641        let clone = registry.clone();
642
643        let handle = std::thread::spawn(move || {
644            assert!(clone.supports("ethereum"));
645            clone.len()
646        });
647
648        let result = handle.join().unwrap();
649        assert_eq!(result, 1);
650
651        // Original still works
652        assert!(registry.supports("ethereum"));
653    }
654
655    // ------------------------------------------------------------------------
656    // Debug Tests
657    // ------------------------------------------------------------------------
658
659    #[test]
660    fn test_debug_format() {
661        let mut registry = ChainRegistry::empty();
662        registry.register(MockChain {
663            id: "ethereum",
664            ..Default::default()
665        });
666        registry.register(MockChain {
667            id: "bitcoin",
668            ..Default::default()
669        });
670
671        let debug_str = format!("{registry:?}");
672        assert!(debug_str.contains("ChainRegistry"));
673        assert!(debug_str.contains("bitcoin"));
674        assert!(debug_str.contains("ethereum"));
675    }
676
677    // ------------------------------------------------------------------------
678    // Edge Cases
679    // ------------------------------------------------------------------------
680
681    #[test]
682    fn test_empty_chain_id() {
683        let mut registry = ChainRegistry::empty();
684        registry.register(MockChain {
685            id: "",
686            ..Default::default()
687        });
688
689        assert!(registry.supports(""));
690        assert!(registry.get("").is_some());
691        assert_eq!(registry.len(), 1);
692    }
693
694    #[test]
695    fn test_chain_with_special_characters() {
696        let mut registry = ChainRegistry::empty();
697        registry.register(MockChain {
698            id: "arbitrum-one",
699            ..Default::default()
700        });
701        registry.register(MockChain {
702            id: "polygon_pos",
703            ..Default::default()
704        });
705
706        assert!(registry.supports("arbitrum-one"));
707        assert!(registry.supports("polygon_pos"));
708    }
709
710    #[test]
711    fn test_len_and_is_empty_consistency() {
712        let mut registry = ChainRegistry::empty();
713
714        // Empty
715        assert!(registry.is_empty());
716        assert_eq!(registry.len(), 0);
717
718        // Add one
719        registry.register(MockChain {
720            id: "eth",
721            ..Default::default()
722        });
723        assert!(!registry.is_empty());
724        assert_eq!(registry.len(), 1);
725
726        // Add another
727        registry.register(MockChain {
728            id: "btc",
729            ..Default::default()
730        });
731        assert!(!registry.is_empty());
732        assert_eq!(registry.len(), 2);
733    }
734}