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![crate::models::RpcConfig::new(
537                    "https://custom-rpc.example.com".to_string(),
538                )]),
539                explorer_urls: Some(vec!["https://custom-explorer.example.com".to_string()]),
540                average_blocktime_ms: None,
541                is_testnet: None,
542                tags: None,
543            },
544            chain_id: None,
545            required_confirmations: Some(5),
546            features: None,
547            symbol: Some("CUSTOM".to_string()),
548            gas_price_cache: None,
549        };
550
551        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
552        assert!(result.is_ok());
553
554        let resolved = result.unwrap();
555        assert_eq!(resolved.common.network, "child");
556        assert_eq!(
557            resolved.common.rpc_urls,
558            Some(vec![crate::models::RpcConfig::new(
559                "https://custom-rpc.example.com".to_string()
560            )])
561        ); // From child
562        assert_eq!(
563            resolved.common.explorer_urls,
564            Some(vec!["https://custom-explorer.example.com".to_string()])
565        ); // From child
566        assert_eq!(resolved.common.average_blocktime_ms, Some(11000)); // From grandparent
567        assert_eq!(resolved.common.is_testnet, Some(false)); // From parent
568        assert_eq!(
569            resolved.common.tags,
570            Some(vec!["test".to_string(), "production".to_string()])
571        ); // Merged from great-grandparent and parent
572        assert_eq!(resolved.chain_id, Some(100)); // From parent
573        assert_eq!(resolved.required_confirmations, Some(5)); // From child
574        assert_eq!(resolved.symbol, Some("CUSTOM".to_string())); // From child
575        assert_eq!(
576            resolved.features,
577            Some(vec!["eip1559".to_string(), "london".to_string()])
578        );
579    }
580
581    #[test]
582    fn test_resolve_inheritance_with_empty_network_name() {
583        let mut networks = HashMap::new();
584        let parent_config = create_evm_network("parent");
585        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
586
587        let lookup_fn = |name: &str| networks.get(name);
588        let resolver = InheritanceResolver::new(&lookup_fn);
589
590        let child_config = create_evm_network_with_parent("child", "parent");
591
592        // Test with empty network name - this should succeed since parent exists
593        let result = resolver.resolve_evm_inheritance(&child_config, "", "parent");
594        assert!(result.is_ok());
595
596        // The resolved config should have the child's network name (empty string in this case)
597        let resolved = result.unwrap();
598        assert_eq!(resolved.common.network, "child"); // Network name comes from the child config, not the parameter
599    }
600
601    #[test]
602    fn test_resolve_inheritance_with_empty_parent_name() {
603        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
604        let lookup_fn = |name: &str| networks.get(name);
605        let resolver = InheritanceResolver::new(&lookup_fn);
606
607        let child_config = create_evm_network_with_parent("child", "");
608
609        // Test with empty parent name
610        let result = resolver.resolve_evm_inheritance(&child_config, "child", "");
611        assert!(result.is_err());
612
613        assert!(matches!(
614            result.unwrap_err(),
615            ConfigFileError::InvalidReference(_)
616        ));
617    }
618
619    #[test]
620    fn test_all_network_types_coverage() {
621        let mut networks = HashMap::new();
622
623        // Create parent networks for all types
624        let evm_parent = create_evm_network("evm-parent");
625        let solana_parent = create_solana_network("solana-parent");
626        let stellar_parent = create_stellar_network("stellar-parent");
627
628        networks.insert("evm-parent".to_string(), NetworkFileConfig::Evm(evm_parent));
629        networks.insert(
630            "solana-parent".to_string(),
631            NetworkFileConfig::Solana(solana_parent),
632        );
633        networks.insert(
634            "stellar-parent".to_string(),
635            NetworkFileConfig::Stellar(stellar_parent),
636        );
637
638        let lookup_fn = |name: &str| networks.get(name);
639        let resolver = InheritanceResolver::new(&lookup_fn);
640
641        // Test EVM inheritance
642        let evm_child = create_evm_network_with_parent("evm-child", "evm-parent");
643        let evm_result = resolver.resolve_evm_inheritance(&evm_child, "evm-child", "evm-parent");
644        assert!(evm_result.is_ok());
645
646        // Test Solana inheritance
647        let solana_child = create_solana_network_with_parent("solana-child", "solana-parent");
648        let solana_result =
649            resolver.resolve_solana_inheritance(&solana_child, "solana-child", "solana-parent");
650        assert!(solana_result.is_ok());
651
652        // Test Stellar inheritance
653        let stellar_child = create_stellar_network_with_parent("stellar-child", "stellar-parent");
654        let stellar_result =
655            resolver.resolve_stellar_inheritance(&stellar_child, "stellar-child", "stellar-parent");
656        assert!(stellar_result.is_ok());
657    }
658
659    #[test]
660    fn test_inheritance_with_complex_merging() {
661        let mut networks = HashMap::new();
662
663        // Create parent with comprehensive configuration
664        let parent_config = EvmNetworkConfig {
665            common: NetworkConfigCommon {
666                network: "parent".to_string(),
667                from: None,
668                rpc_urls: Some(vec![
669                    crate::models::RpcConfig::new("https://parent-rpc1.example.com".to_string()),
670                    crate::models::RpcConfig::new("https://parent-rpc2.example.com".to_string()),
671                ]),
672                explorer_urls: Some(vec![
673                    "https://parent-explorer1.example.com".to_string(),
674                    "https://parent-explorer2.example.com".to_string(),
675                ]),
676                average_blocktime_ms: Some(12000),
677                is_testnet: Some(true),
678                tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
679            },
680            chain_id: Some(1),
681            required_confirmations: Some(1),
682            features: Some(vec!["eip1559".to_string(), "london".to_string()]),
683            symbol: Some("ETH".to_string()),
684            gas_price_cache: None,
685        };
686        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
687
688        let lookup_fn = |name: &str| networks.get(name);
689        let resolver = InheritanceResolver::new(&lookup_fn);
690
691        // Create child that partially overrides parent
692        let child_config = EvmNetworkConfig {
693            common: NetworkConfigCommon {
694                network: "child".to_string(),
695                from: Some("parent".to_string()),
696                rpc_urls: Some(vec![crate::models::RpcConfig::new(
697                    "https://child-rpc.example.com".to_string(),
698                )]), // Override
699                explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
700                average_blocktime_ms: None,                // Inherit
701                is_testnet: Some(false),                   // Override
702                tags: Some(vec!["child-tag".to_string()]), // Override (merge behavior depends on implementation)
703            },
704            chain_id: Some(42),                         // Override
705            required_confirmations: None,               // Inherit
706            features: Some(vec!["berlin".to_string()]), // Override (merge behavior depends on implementation)
707            symbol: None,                               // Inherit
708            gas_price_cache: None,
709        };
710
711        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
712        assert!(result.is_ok());
713
714        let resolved = result.unwrap();
715        assert_eq!(resolved.common.network, "child");
716        // Child's RPC URLs override parent's
717        assert_eq!(
718            resolved.common.rpc_urls,
719            Some(vec![crate::models::RpcConfig::new(
720                "https://child-rpc.example.com".to_string()
721            )])
722        );
723        assert_eq!(
724            resolved.common.explorer_urls,
725            Some(vec!["https://child-explorer.example.com".to_string()])
726        ); // Child override
727        assert_eq!(resolved.common.average_blocktime_ms, Some(12000)); // Inherited from parent
728        assert_eq!(resolved.common.is_testnet, Some(false)); // Child override
729        assert_eq!(resolved.chain_id, Some(42)); // Child override
730        assert_eq!(resolved.required_confirmations, Some(1)); // Inherited from parent
731        assert_eq!(resolved.symbol, Some("ETH".to_string())); // Inherited from parent
732    }
733}