openzeppelin_relayer/config/config_file/network/
inheritance.rs

1//! Network Configuration Inheritance Resolution
2//!
3//! This module provides inheritance resolution for network configurations, enabling
4//! hierarchical configuration management where child networks inherit and override
5//! properties from parent networks.
6//!
7//! ## Key Features
8//!
9//! - **Type safety**: Ensures inheritance only between compatible network types
10//! - **Recursive resolution**: Supports multi-level inheritance chains
11//! - **Smart merging**: Child values override parents, collections merge intelligently
12//! - **Error handling**: Detailed errors for circular references and type mismatches
13//!
14//! ## Resolution Process
15//!
16//! 1. **Validation**: Verify parent exists and types are compatible
17//! 2. **Recursive resolution**: Resolve parent's inheritance chain first
18//! 3. **Merging**: Combine child with resolved parent configuration
19
20use super::{
21    ConfigFileNetworkType, EvmNetworkConfig, NetworkFileConfig, SolanaNetworkConfig,
22    StellarNetworkConfig,
23};
24use crate::config::ConfigFileError;
25
26/// Resolves network configuration inheritance by recursively merging child configurations with their parents.
27pub struct InheritanceResolver<'a> {
28    /// Function to lookup network configurations by name
29    network_lookup: &'a dyn Fn(&str) -> Option<&'a NetworkFileConfig>,
30}
31
32/// Macro to generate inheritance resolution methods for different network types.
33///
34/// Generates: resolve_evm_inheritance, resolve_solana_inheritance, resolve_stellar_inheritance
35/// This eliminates code duplication while maintaining type safety across all network types.
36macro_rules! impl_inheritance_resolver {
37    ($method_name:ident, $config_type:ty, $network_type:ident, $variant:ident, $type_name:expr) => {
38        /// Resolves inheritance for network configurations by recursively merging with parent configurations.
39        ///
40        /// # Arguments
41        /// * `config` - The child network configuration to resolve inheritance for
42        /// * `network_name` - The name of the child network (used for error reporting)
43        /// * `parent_name` - The name of the parent network to inherit from
44        ///
45        /// # Returns
46        /// Configuration with all inheritance applied, or an error if resolution fails
47        pub fn $method_name(&self, config: &$config_type, network_name: &str, parent_name: &str) -> Result<$config_type, ConfigFileError> {
48            // Get the parent network
49            let parent_network = (self.network_lookup)(parent_name).ok_or_else(|| {
50                ConfigFileError::InvalidReference(format!(
51                    "Network '{}' inherits from non-existent network '{}' in inheritance chain",
52                    network_name, parent_name
53                ))
54            })?;
55
56            // Verify parent is the same type
57            if parent_network.network_type() != ConfigFileNetworkType::$network_type {
58                return Err(ConfigFileError::IncompatibleInheritanceType(format!(
59                    "Network '{}' (type {}) tries to inherit from '{}' (type {:?}) - inheritance chain broken due to type mismatch",
60                    network_name, $type_name, parent_name, parent_network.network_type()
61                )));
62            }
63
64            // Extract the parent configuration
65            let parent_config = match parent_network {
66                NetworkFileConfig::$variant(config) => config,
67                _ => return Err(ConfigFileError::InvalidFormat(format!("Expected {} network configuration", $type_name))),
68            };
69
70            // Recursively resolve parent inheritance first
71            let resolved_parent = if parent_network.inherits_from().is_some() {
72                let grandparent_name = parent_network.inherits_from().unwrap();
73                self.$method_name(parent_config, parent_name, grandparent_name)?
74            } else {
75                parent_config.clone()
76            };
77
78            // Merge child with resolved parent
79            Ok(config.merge_with_parent(&resolved_parent))
80        }
81    };
82}
83
84impl<'a> InheritanceResolver<'a> {
85    /// Creates a new inheritance resolver.
86    ///
87    /// # Arguments
88    /// * `network_lookup` - Function to lookup network configurations by name
89    ///
90    /// # Returns
91    /// A new `InheritanceResolver` instance
92    pub fn new(network_lookup: &'a dyn Fn(&str) -> Option<&'a NetworkFileConfig>) -> Self {
93        Self { network_lookup }
94    }
95
96    // Generate the three inheritance resolution methods using the macro
97    impl_inheritance_resolver!(resolve_evm_inheritance, EvmNetworkConfig, Evm, Evm, "EVM");
98    impl_inheritance_resolver!(
99        resolve_solana_inheritance,
100        SolanaNetworkConfig,
101        Solana,
102        Solana,
103        "Solana"
104    );
105    impl_inheritance_resolver!(
106        resolve_stellar_inheritance,
107        StellarNetworkConfig,
108        Stellar,
109        Stellar,
110        "Stellar"
111    );
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117    use crate::config::config_file::network::common::NetworkConfigCommon;
118    use crate::config::config_file::network::test_utils::*;
119    use std::collections::HashMap;
120
121    #[test]
122    fn test_inheritance_resolver_new() {
123        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
124        let lookup_fn = |name: &str| networks.get(name);
125        let resolver = InheritanceResolver::new(&lookup_fn);
126
127        // Test that the resolver was created successfully
128        // We can't directly test the function pointer, but we can test that it works
129        assert!((resolver.network_lookup)("nonexistent").is_none());
130    }
131
132    #[test]
133    fn test_resolve_evm_inheritance_simple_success() {
134        let mut networks = HashMap::new();
135
136        // Create parent network
137        let parent_config = create_evm_network("parent");
138        networks.insert(
139            "parent".to_string(),
140            NetworkFileConfig::Evm(parent_config.clone()),
141        );
142
143        let lookup_fn = |name: &str| networks.get(name);
144        let resolver = InheritanceResolver::new(&lookup_fn);
145
146        // Create child network that inherits from parent
147        let child_config = EvmNetworkConfig {
148            common: NetworkConfigCommon {
149                network: "child".to_string(),
150                from: Some("parent".to_string()),
151                rpc_urls: None,                    // Will inherit from parent
152                explorer_urls: None,               // Will inherit from parent
153                average_blocktime_ms: Some(15000), // Override parent value
154                is_testnet: Some(false),           // Override parent value
155                tags: None,
156            },
157            chain_id: None,                  // Will inherit from parent
158            required_confirmations: Some(2), // Override parent value
159            features: None,
160            symbol: None, // Will inherit from parent
161            gas_price_cache: None,
162        };
163
164        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
165        assert!(result.is_ok());
166
167        let resolved = result.unwrap();
168        assert_eq!(resolved.common.network, "child");
169        assert_eq!(resolved.common.rpc_urls, parent_config.common.rpc_urls); // Inherited
170        assert_eq!(
171            resolved.common.explorer_urls,
172            parent_config.common.explorer_urls
173        ); // Inherited
174        assert_eq!(resolved.common.average_blocktime_ms, Some(15000)); // Overridden
175        assert_eq!(resolved.common.is_testnet, Some(false)); // Overridden
176        assert_eq!(resolved.chain_id, parent_config.chain_id); // Inherited
177        assert_eq!(resolved.required_confirmations, Some(2)); // Overridden
178        assert_eq!(resolved.symbol, parent_config.symbol); // Inherited
179    }
180
181    #[test]
182    fn test_resolve_evm_inheritance_multi_level() {
183        let mut networks = HashMap::new();
184
185        // Create grandparent network
186        let grandparent_config = create_evm_network("grandparent");
187        networks.insert(
188            "grandparent".to_string(),
189            NetworkFileConfig::Evm(grandparent_config.clone()),
190        );
191
192        // Create parent network that inherits from grandparent
193        let parent_config = EvmNetworkConfig {
194            common: NetworkConfigCommon {
195                network: "parent".to_string(),
196                from: Some("grandparent".to_string()),
197                rpc_urls: None,
198                explorer_urls: None,
199                average_blocktime_ms: Some(10000), // Override grandparent
200                is_testnet: None,
201                tags: None,
202            },
203            chain_id: None,
204            required_confirmations: Some(3), // Override grandparent
205            features: None,
206            symbol: None,
207            gas_price_cache: None,
208        };
209        networks.insert(
210            "parent".to_string(),
211            NetworkFileConfig::Evm(parent_config.clone()),
212        );
213
214        let lookup_fn = |name: &str| networks.get(name);
215        let resolver = InheritanceResolver::new(&lookup_fn);
216
217        // Create child network that inherits from parent
218        let child_config = EvmNetworkConfig {
219            common: NetworkConfigCommon {
220                network: "child".to_string(),
221                from: Some("parent".to_string()),
222                rpc_urls: None,
223                explorer_urls: None,
224                average_blocktime_ms: None,
225                is_testnet: Some(false), // Override
226                tags: None,
227            },
228            chain_id: Some(42), // Override
229            required_confirmations: None,
230            features: None,
231            symbol: None,
232            gas_price_cache: None,
233        };
234
235        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
236        assert!(result.is_ok());
237
238        let resolved = result.unwrap();
239        assert_eq!(resolved.common.network, "child");
240        assert_eq!(resolved.common.rpc_urls, grandparent_config.common.rpc_urls); // From grandparent
241        assert_eq!(
242            resolved.common.explorer_urls,
243            grandparent_config.common.explorer_urls
244        ); // From grandparent
245        assert_eq!(resolved.common.average_blocktime_ms, Some(10000)); // From parent
246        assert_eq!(resolved.common.is_testnet, Some(false)); // From child
247        assert_eq!(resolved.chain_id, Some(42)); // From child
248        assert_eq!(resolved.required_confirmations, Some(3)); // From parent
249        assert_eq!(resolved.symbol, grandparent_config.symbol); // From grandparent
250    }
251
252    #[test]
253    fn test_resolve_evm_inheritance_nonexistent_parent() {
254        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
255        let lookup_fn = |name: &str| networks.get(name);
256        let resolver = InheritanceResolver::new(&lookup_fn);
257
258        let child_config = create_evm_network_with_parent("child", "nonexistent");
259
260        let result = resolver.resolve_evm_inheritance(&child_config, "child", "nonexistent");
261        assert!(result.is_err());
262
263        assert!(matches!(
264            result.unwrap_err(),
265            ConfigFileError::InvalidReference(_)
266        ));
267    }
268
269    #[test]
270    fn test_resolve_evm_inheritance_type_mismatch() {
271        let mut networks = HashMap::new();
272
273        // Create a Solana parent network
274        let parent_config = create_solana_network("parent");
275        networks.insert(
276            "parent".to_string(),
277            NetworkFileConfig::Solana(parent_config),
278        );
279
280        let lookup_fn = |name: &str| networks.get(name);
281        let resolver = InheritanceResolver::new(&lookup_fn);
282
283        let child_config = create_evm_network_with_parent("child", "parent");
284
285        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
286        assert!(result.is_err());
287
288        assert!(matches!(
289            result.unwrap_err(),
290            ConfigFileError::IncompatibleInheritanceType(_)
291        ));
292    }
293
294    #[test]
295    fn test_resolve_evm_inheritance_no_inheritance() {
296        let mut networks = HashMap::new();
297
298        // Create parent network with no inheritance
299        let parent_config = create_evm_network("parent");
300        networks.insert(
301            "parent".to_string(),
302            NetworkFileConfig::Evm(parent_config.clone()),
303        );
304
305        let lookup_fn = |name: &str| networks.get(name);
306        let resolver = InheritanceResolver::new(&lookup_fn);
307
308        let child_config = create_evm_network_with_parent("child", "parent");
309
310        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
311        assert!(result.is_ok());
312
313        let resolved = result.unwrap();
314        // Should merge child with parent (parent has no inheritance)
315        assert_eq!(resolved.common.network, "child");
316        assert_eq!(
317            resolved.chain_id,
318            child_config.chain_id.or(parent_config.chain_id)
319        );
320    }
321
322    // Solana Inheritance Tests
323    #[test]
324    fn test_resolve_solana_inheritance_simple_success() {
325        let mut networks = HashMap::new();
326
327        let parent_config = create_solana_network("parent");
328        networks.insert(
329            "parent".to_string(),
330            NetworkFileConfig::Solana(parent_config.clone()),
331        );
332
333        let lookup_fn = |name: &str| networks.get(name);
334        let resolver = InheritanceResolver::new(&lookup_fn);
335
336        let child_config = SolanaNetworkConfig {
337            common: NetworkConfigCommon {
338                network: "child".to_string(),
339                from: Some("parent".to_string()),
340                rpc_urls: None,                  // Will inherit
341                explorer_urls: None,             // Will inherit
342                average_blocktime_ms: Some(500), // Override
343                is_testnet: None,
344                tags: None,
345            },
346        };
347
348        let result = resolver.resolve_solana_inheritance(&child_config, "child", "parent");
349        assert!(result.is_ok());
350
351        let resolved = result.unwrap();
352        assert_eq!(resolved.common.network, "child");
353        assert_eq!(resolved.common.rpc_urls, parent_config.common.rpc_urls); // Inherited
354        assert_eq!(
355            resolved.common.explorer_urls,
356            parent_config.common.explorer_urls
357        ); // Inherited
358        assert_eq!(resolved.common.average_blocktime_ms, Some(500)); // Overridden
359    }
360
361    #[test]
362    fn test_resolve_solana_inheritance_type_mismatch() {
363        let mut networks = HashMap::new();
364
365        // Create an EVM parent network
366        let parent_config = create_evm_network("parent");
367        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
368
369        let lookup_fn = |name: &str| networks.get(name);
370        let resolver = InheritanceResolver::new(&lookup_fn);
371
372        let child_config = create_solana_network_with_parent("child", "parent");
373
374        let result = resolver.resolve_solana_inheritance(&child_config, "child", "parent");
375        assert!(result.is_err());
376
377        assert!(matches!(
378            result.unwrap_err(),
379            ConfigFileError::IncompatibleInheritanceType(_)
380        ));
381    }
382
383    #[test]
384    fn test_resolve_solana_inheritance_nonexistent_parent() {
385        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
386        let lookup_fn = |name: &str| networks.get(name);
387        let resolver = InheritanceResolver::new(&lookup_fn);
388
389        let child_config = create_solana_network_with_parent("child", "nonexistent");
390
391        let result = resolver.resolve_solana_inheritance(&child_config, "child", "nonexistent");
392        assert!(result.is_err());
393
394        assert!(matches!(
395            result.unwrap_err(),
396            ConfigFileError::InvalidReference(_)
397        ));
398    }
399
400    #[test]
401    fn test_resolve_stellar_inheritance_simple_success() {
402        let mut networks = HashMap::new();
403
404        let parent_config = create_stellar_network("parent");
405        networks.insert(
406            "parent".to_string(),
407            NetworkFileConfig::Stellar(parent_config.clone()),
408        );
409
410        let lookup_fn = |name: &str| networks.get(name);
411        let resolver = InheritanceResolver::new(&lookup_fn);
412
413        let child_config = StellarNetworkConfig {
414            common: NetworkConfigCommon {
415                network: "child".to_string(),
416                from: Some("parent".to_string()),
417                rpc_urls: None,                   // Will inherit
418                explorer_urls: None,              // Will inherit
419                average_blocktime_ms: Some(6000), // Override
420                is_testnet: None,
421                tags: None,
422            },
423            passphrase: None,  // Will inherit from parent
424            horizon_url: None, // Will inherit from parent
425        };
426
427        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "parent");
428        assert!(result.is_ok());
429
430        let resolved = result.unwrap();
431        assert_eq!(resolved.common.network, "child");
432        assert_eq!(resolved.common.rpc_urls, parent_config.common.rpc_urls); // Inherited
433        assert_eq!(resolved.common.average_blocktime_ms, Some(6000)); // Overridden
434        assert_eq!(resolved.passphrase, parent_config.passphrase); // Inherited
435    }
436
437    #[test]
438    fn test_resolve_stellar_inheritance_type_mismatch() {
439        let mut networks = HashMap::new();
440
441        // Create a Solana parent network
442        let parent_config = create_solana_network("parent");
443        networks.insert(
444            "parent".to_string(),
445            NetworkFileConfig::Solana(parent_config),
446        );
447
448        let lookup_fn = |name: &str| networks.get(name);
449        let resolver = InheritanceResolver::new(&lookup_fn);
450
451        let child_config = create_stellar_network_with_parent("child", "parent");
452
453        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "parent");
454        assert!(result.is_err());
455
456        assert!(matches!(
457            result.unwrap_err(),
458            ConfigFileError::IncompatibleInheritanceType(_)
459        ));
460    }
461
462    #[test]
463    fn test_resolve_stellar_inheritance_nonexistent_parent() {
464        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
465        let lookup_fn = |name: &str| networks.get(name);
466        let resolver = InheritanceResolver::new(&lookup_fn);
467
468        let child_config = create_stellar_network_with_parent("child", "nonexistent");
469
470        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "nonexistent");
471        assert!(result.is_err());
472
473        assert!(matches!(
474            result.unwrap_err(),
475            ConfigFileError::InvalidReference(_)
476        ));
477    }
478
479    #[test]
480    fn test_resolve_inheritance_deep_chain() {
481        let mut networks = HashMap::new();
482
483        // Create a 4-level inheritance chain: great-grandparent -> grandparent -> parent -> child
484        let great_grandparent_config = create_evm_network("great-grandparent");
485        networks.insert(
486            "great-grandparent".to_string(),
487            NetworkFileConfig::Evm(great_grandparent_config.clone()),
488        );
489
490        let grandparent_config = EvmNetworkConfig {
491            common: NetworkConfigCommon {
492                network: "grandparent".to_string(),
493                from: Some("great-grandparent".to_string()),
494                rpc_urls: None,
495                explorer_urls: None,
496                average_blocktime_ms: Some(11000),
497                is_testnet: None,
498                tags: None,
499            },
500            chain_id: None,
501            required_confirmations: None,
502            features: Some(vec!["eip1559".to_string(), "london".to_string()]),
503            symbol: None,
504            gas_price_cache: None,
505        };
506        networks.insert(
507            "grandparent".to_string(),
508            NetworkFileConfig::Evm(grandparent_config),
509        );
510
511        let parent_config = EvmNetworkConfig {
512            common: NetworkConfigCommon {
513                network: "parent".to_string(),
514                from: Some("grandparent".to_string()),
515                rpc_urls: None,
516                explorer_urls: None,
517                average_blocktime_ms: None,
518                is_testnet: Some(false),
519                tags: Some(vec!["production".to_string()]),
520            },
521            chain_id: Some(100),
522            required_confirmations: None,
523            features: None,
524            symbol: None,
525            gas_price_cache: None,
526        };
527        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
528
529        let lookup_fn = |name: &str| networks.get(name);
530        let resolver = InheritanceResolver::new(&lookup_fn);
531
532        let child_config = EvmNetworkConfig {
533            common: NetworkConfigCommon {
534                network: "child".to_string(),
535                from: Some("parent".to_string()),
536                rpc_urls: Some(vec!["https://custom-rpc.example.com".to_string()]),
537                explorer_urls: Some(vec!["https://custom-explorer.example.com".to_string()]),
538                average_blocktime_ms: None,
539                is_testnet: None,
540                tags: None,
541            },
542            chain_id: None,
543            required_confirmations: Some(5),
544            features: None,
545            symbol: Some("CUSTOM".to_string()),
546            gas_price_cache: None,
547        };
548
549        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
550        assert!(result.is_ok());
551
552        let resolved = result.unwrap();
553        assert_eq!(resolved.common.network, "child");
554        assert_eq!(
555            resolved.common.rpc_urls,
556            Some(vec!["https://custom-rpc.example.com".to_string()])
557        ); // From child
558        assert_eq!(
559            resolved.common.explorer_urls,
560            Some(vec!["https://custom-explorer.example.com".to_string()])
561        ); // From child
562        assert_eq!(resolved.common.average_blocktime_ms, Some(11000)); // From grandparent
563        assert_eq!(resolved.common.is_testnet, Some(false)); // From parent
564        assert_eq!(
565            resolved.common.tags,
566            Some(vec!["test".to_string(), "production".to_string()])
567        ); // Merged from great-grandparent and parent
568        assert_eq!(resolved.chain_id, Some(100)); // From parent
569        assert_eq!(resolved.required_confirmations, Some(5)); // From child
570        assert_eq!(resolved.symbol, Some("CUSTOM".to_string())); // From child
571        assert_eq!(
572            resolved.features,
573            Some(vec!["eip1559".to_string(), "london".to_string()])
574        );
575    }
576
577    #[test]
578    fn test_resolve_inheritance_with_empty_network_name() {
579        let mut networks = HashMap::new();
580        let parent_config = create_evm_network("parent");
581        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
582
583        let lookup_fn = |name: &str| networks.get(name);
584        let resolver = InheritanceResolver::new(&lookup_fn);
585
586        let child_config = create_evm_network_with_parent("child", "parent");
587
588        // Test with empty network name - this should succeed since parent exists
589        let result = resolver.resolve_evm_inheritance(&child_config, "", "parent");
590        assert!(result.is_ok());
591
592        // The resolved config should have the child's network name (empty string in this case)
593        let resolved = result.unwrap();
594        assert_eq!(resolved.common.network, "child"); // Network name comes from the child config, not the parameter
595    }
596
597    #[test]
598    fn test_resolve_inheritance_with_empty_parent_name() {
599        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
600        let lookup_fn = |name: &str| networks.get(name);
601        let resolver = InheritanceResolver::new(&lookup_fn);
602
603        let child_config = create_evm_network_with_parent("child", "");
604
605        // Test with empty parent name
606        let result = resolver.resolve_evm_inheritance(&child_config, "child", "");
607        assert!(result.is_err());
608
609        assert!(matches!(
610            result.unwrap_err(),
611            ConfigFileError::InvalidReference(_)
612        ));
613    }
614
615    #[test]
616    fn test_all_network_types_coverage() {
617        let mut networks = HashMap::new();
618
619        // Create parent networks for all types
620        let evm_parent = create_evm_network("evm-parent");
621        let solana_parent = create_solana_network("solana-parent");
622        let stellar_parent = create_stellar_network("stellar-parent");
623
624        networks.insert("evm-parent".to_string(), NetworkFileConfig::Evm(evm_parent));
625        networks.insert(
626            "solana-parent".to_string(),
627            NetworkFileConfig::Solana(solana_parent),
628        );
629        networks.insert(
630            "stellar-parent".to_string(),
631            NetworkFileConfig::Stellar(stellar_parent),
632        );
633
634        let lookup_fn = |name: &str| networks.get(name);
635        let resolver = InheritanceResolver::new(&lookup_fn);
636
637        // Test EVM inheritance
638        let evm_child = create_evm_network_with_parent("evm-child", "evm-parent");
639        let evm_result = resolver.resolve_evm_inheritance(&evm_child, "evm-child", "evm-parent");
640        assert!(evm_result.is_ok());
641
642        // Test Solana inheritance
643        let solana_child = create_solana_network_with_parent("solana-child", "solana-parent");
644        let solana_result =
645            resolver.resolve_solana_inheritance(&solana_child, "solana-child", "solana-parent");
646        assert!(solana_result.is_ok());
647
648        // Test Stellar inheritance
649        let stellar_child = create_stellar_network_with_parent("stellar-child", "stellar-parent");
650        let stellar_result =
651            resolver.resolve_stellar_inheritance(&stellar_child, "stellar-child", "stellar-parent");
652        assert!(stellar_result.is_ok());
653    }
654
655    #[test]
656    fn test_inheritance_with_complex_merging() {
657        let mut networks = HashMap::new();
658
659        // Create parent with comprehensive configuration
660        let parent_config = EvmNetworkConfig {
661            common: NetworkConfigCommon {
662                network: "parent".to_string(),
663                from: None,
664                rpc_urls: Some(vec![
665                    "https://parent-rpc1.example.com".to_string(),
666                    "https://parent-rpc2.example.com".to_string(),
667                ]),
668                explorer_urls: Some(vec![
669                    "https://parent-explorer1.example.com".to_string(),
670                    "https://parent-explorer2.example.com".to_string(),
671                ]),
672                average_blocktime_ms: Some(12000),
673                is_testnet: Some(true),
674                tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
675            },
676            chain_id: Some(1),
677            required_confirmations: Some(1),
678            features: Some(vec!["eip1559".to_string(), "london".to_string()]),
679            symbol: Some("ETH".to_string()),
680            gas_price_cache: None,
681        };
682        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
683
684        let lookup_fn = |name: &str| networks.get(name);
685        let resolver = InheritanceResolver::new(&lookup_fn);
686
687        // Create child that partially overrides parent
688        let child_config = EvmNetworkConfig {
689            common: NetworkConfigCommon {
690                network: "child".to_string(),
691                from: Some("parent".to_string()),
692                rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]), // Override
693                explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
694                average_blocktime_ms: None,                // Inherit
695                is_testnet: Some(false),                   // Override
696                tags: Some(vec!["child-tag".to_string()]), // Override (merge behavior depends on implementation)
697            },
698            chain_id: Some(42),                         // Override
699            required_confirmations: None,               // Inherit
700            features: Some(vec!["berlin".to_string()]), // Override (merge behavior depends on implementation)
701            symbol: None,                               // Inherit
702            gas_price_cache: None,
703        };
704
705        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
706        assert!(result.is_ok());
707
708        let resolved = result.unwrap();
709        assert_eq!(resolved.common.network, "child");
710        assert_eq!(
711            resolved.common.rpc_urls,
712            Some(vec!["https://child-rpc.example.com".to_string()])
713        ); // Child override
714        assert_eq!(
715            resolved.common.explorer_urls,
716            Some(vec!["https://child-explorer.example.com".to_string()])
717        ); // Child override
718        assert_eq!(resolved.common.average_blocktime_ms, Some(12000)); // Inherited from parent
719        assert_eq!(resolved.common.is_testnet, Some(false)); // Child override
720        assert_eq!(resolved.chain_id, Some(42)); // Child override
721        assert_eq!(resolved.required_confirmations, Some(1)); // Inherited from parent
722        assert_eq!(resolved.symbol, Some("ETH".to_string())); // Inherited from parent
723    }
724}