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(®istry.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(®istry.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}