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        };
425
426        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "parent");
427        assert!(result.is_ok());
428
429        let resolved = result.unwrap();
430        assert_eq!(resolved.common.network, "child");
431        assert_eq!(resolved.common.rpc_urls, parent_config.common.rpc_urls); // Inherited
432        assert_eq!(resolved.common.average_blocktime_ms, Some(6000)); // Overridden
433        assert_eq!(resolved.passphrase, parent_config.passphrase); // Inherited
434    }
435
436    #[test]
437    fn test_resolve_stellar_inheritance_type_mismatch() {
438        let mut networks = HashMap::new();
439
440        // Create a Solana parent network
441        let parent_config = create_solana_network("parent");
442        networks.insert(
443            "parent".to_string(),
444            NetworkFileConfig::Solana(parent_config),
445        );
446
447        let lookup_fn = |name: &str| networks.get(name);
448        let resolver = InheritanceResolver::new(&lookup_fn);
449
450        let child_config = create_stellar_network_with_parent("child", "parent");
451
452        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "parent");
453        assert!(result.is_err());
454
455        assert!(matches!(
456            result.unwrap_err(),
457            ConfigFileError::IncompatibleInheritanceType(_)
458        ));
459    }
460
461    #[test]
462    fn test_resolve_stellar_inheritance_nonexistent_parent() {
463        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
464        let lookup_fn = |name: &str| networks.get(name);
465        let resolver = InheritanceResolver::new(&lookup_fn);
466
467        let child_config = create_stellar_network_with_parent("child", "nonexistent");
468
469        let result = resolver.resolve_stellar_inheritance(&child_config, "child", "nonexistent");
470        assert!(result.is_err());
471
472        assert!(matches!(
473            result.unwrap_err(),
474            ConfigFileError::InvalidReference(_)
475        ));
476    }
477
478    #[test]
479    fn test_resolve_inheritance_deep_chain() {
480        let mut networks = HashMap::new();
481
482        // Create a 4-level inheritance chain: great-grandparent -> grandparent -> parent -> child
483        let great_grandparent_config = create_evm_network("great-grandparent");
484        networks.insert(
485            "great-grandparent".to_string(),
486            NetworkFileConfig::Evm(great_grandparent_config.clone()),
487        );
488
489        let grandparent_config = EvmNetworkConfig {
490            common: NetworkConfigCommon {
491                network: "grandparent".to_string(),
492                from: Some("great-grandparent".to_string()),
493                rpc_urls: None,
494                explorer_urls: None,
495                average_blocktime_ms: Some(11000),
496                is_testnet: None,
497                tags: None,
498            },
499            chain_id: None,
500            required_confirmations: None,
501            features: Some(vec!["eip1559".to_string(), "london".to_string()]),
502            symbol: None,
503            gas_price_cache: None,
504        };
505        networks.insert(
506            "grandparent".to_string(),
507            NetworkFileConfig::Evm(grandparent_config),
508        );
509
510        let parent_config = EvmNetworkConfig {
511            common: NetworkConfigCommon {
512                network: "parent".to_string(),
513                from: Some("grandparent".to_string()),
514                rpc_urls: None,
515                explorer_urls: None,
516                average_blocktime_ms: None,
517                is_testnet: Some(false),
518                tags: Some(vec!["production".to_string()]),
519            },
520            chain_id: Some(100),
521            required_confirmations: None,
522            features: None,
523            symbol: None,
524            gas_price_cache: None,
525        };
526        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
527
528        let lookup_fn = |name: &str| networks.get(name);
529        let resolver = InheritanceResolver::new(&lookup_fn);
530
531        let child_config = EvmNetworkConfig {
532            common: NetworkConfigCommon {
533                network: "child".to_string(),
534                from: Some("parent".to_string()),
535                rpc_urls: Some(vec!["https://custom-rpc.example.com".to_string()]),
536                explorer_urls: Some(vec!["https://custom-explorer.example.com".to_string()]),
537                average_blocktime_ms: None,
538                is_testnet: None,
539                tags: None,
540            },
541            chain_id: None,
542            required_confirmations: Some(5),
543            features: None,
544            symbol: Some("CUSTOM".to_string()),
545            gas_price_cache: None,
546        };
547
548        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
549        assert!(result.is_ok());
550
551        let resolved = result.unwrap();
552        assert_eq!(resolved.common.network, "child");
553        assert_eq!(
554            resolved.common.rpc_urls,
555            Some(vec!["https://custom-rpc.example.com".to_string()])
556        ); // From child
557        assert_eq!(
558            resolved.common.explorer_urls,
559            Some(vec!["https://custom-explorer.example.com".to_string()])
560        ); // From child
561        assert_eq!(resolved.common.average_blocktime_ms, Some(11000)); // From grandparent
562        assert_eq!(resolved.common.is_testnet, Some(false)); // From parent
563        assert_eq!(
564            resolved.common.tags,
565            Some(vec!["test".to_string(), "production".to_string()])
566        ); // Merged from great-grandparent and parent
567        assert_eq!(resolved.chain_id, Some(100)); // From parent
568        assert_eq!(resolved.required_confirmations, Some(5)); // From child
569        assert_eq!(resolved.symbol, Some("CUSTOM".to_string())); // From child
570        assert_eq!(
571            resolved.features,
572            Some(vec!["eip1559".to_string(), "london".to_string()])
573        );
574    }
575
576    #[test]
577    fn test_resolve_inheritance_with_empty_network_name() {
578        let mut networks = HashMap::new();
579        let parent_config = create_evm_network("parent");
580        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
581
582        let lookup_fn = |name: &str| networks.get(name);
583        let resolver = InheritanceResolver::new(&lookup_fn);
584
585        let child_config = create_evm_network_with_parent("child", "parent");
586
587        // Test with empty network name - this should succeed since parent exists
588        let result = resolver.resolve_evm_inheritance(&child_config, "", "parent");
589        assert!(result.is_ok());
590
591        // The resolved config should have the child's network name (empty string in this case)
592        let resolved = result.unwrap();
593        assert_eq!(resolved.common.network, "child"); // Network name comes from the child config, not the parameter
594    }
595
596    #[test]
597    fn test_resolve_inheritance_with_empty_parent_name() {
598        let networks: HashMap<String, NetworkFileConfig> = HashMap::new();
599        let lookup_fn = |name: &str| networks.get(name);
600        let resolver = InheritanceResolver::new(&lookup_fn);
601
602        let child_config = create_evm_network_with_parent("child", "");
603
604        // Test with empty parent name
605        let result = resolver.resolve_evm_inheritance(&child_config, "child", "");
606        assert!(result.is_err());
607
608        assert!(matches!(
609            result.unwrap_err(),
610            ConfigFileError::InvalidReference(_)
611        ));
612    }
613
614    #[test]
615    fn test_all_network_types_coverage() {
616        let mut networks = HashMap::new();
617
618        // Create parent networks for all types
619        let evm_parent = create_evm_network("evm-parent");
620        let solana_parent = create_solana_network("solana-parent");
621        let stellar_parent = create_stellar_network("stellar-parent");
622
623        networks.insert("evm-parent".to_string(), NetworkFileConfig::Evm(evm_parent));
624        networks.insert(
625            "solana-parent".to_string(),
626            NetworkFileConfig::Solana(solana_parent),
627        );
628        networks.insert(
629            "stellar-parent".to_string(),
630            NetworkFileConfig::Stellar(stellar_parent),
631        );
632
633        let lookup_fn = |name: &str| networks.get(name);
634        let resolver = InheritanceResolver::new(&lookup_fn);
635
636        // Test EVM inheritance
637        let evm_child = create_evm_network_with_parent("evm-child", "evm-parent");
638        let evm_result = resolver.resolve_evm_inheritance(&evm_child, "evm-child", "evm-parent");
639        assert!(evm_result.is_ok());
640
641        // Test Solana inheritance
642        let solana_child = create_solana_network_with_parent("solana-child", "solana-parent");
643        let solana_result =
644            resolver.resolve_solana_inheritance(&solana_child, "solana-child", "solana-parent");
645        assert!(solana_result.is_ok());
646
647        // Test Stellar inheritance
648        let stellar_child = create_stellar_network_with_parent("stellar-child", "stellar-parent");
649        let stellar_result =
650            resolver.resolve_stellar_inheritance(&stellar_child, "stellar-child", "stellar-parent");
651        assert!(stellar_result.is_ok());
652    }
653
654    #[test]
655    fn test_inheritance_with_complex_merging() {
656        let mut networks = HashMap::new();
657
658        // Create parent with comprehensive configuration
659        let parent_config = EvmNetworkConfig {
660            common: NetworkConfigCommon {
661                network: "parent".to_string(),
662                from: None,
663                rpc_urls: Some(vec![
664                    "https://parent-rpc1.example.com".to_string(),
665                    "https://parent-rpc2.example.com".to_string(),
666                ]),
667                explorer_urls: Some(vec![
668                    "https://parent-explorer1.example.com".to_string(),
669                    "https://parent-explorer2.example.com".to_string(),
670                ]),
671                average_blocktime_ms: Some(12000),
672                is_testnet: Some(true),
673                tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
674            },
675            chain_id: Some(1),
676            required_confirmations: Some(1),
677            features: Some(vec!["eip1559".to_string(), "london".to_string()]),
678            symbol: Some("ETH".to_string()),
679            gas_price_cache: None,
680        };
681        networks.insert("parent".to_string(), NetworkFileConfig::Evm(parent_config));
682
683        let lookup_fn = |name: &str| networks.get(name);
684        let resolver = InheritanceResolver::new(&lookup_fn);
685
686        // Create child that partially overrides parent
687        let child_config = EvmNetworkConfig {
688            common: NetworkConfigCommon {
689                network: "child".to_string(),
690                from: Some("parent".to_string()),
691                rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]), // Override
692                explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
693                average_blocktime_ms: None,                // Inherit
694                is_testnet: Some(false),                   // Override
695                tags: Some(vec!["child-tag".to_string()]), // Override (merge behavior depends on implementation)
696            },
697            chain_id: Some(42),                         // Override
698            required_confirmations: None,               // Inherit
699            features: Some(vec!["berlin".to_string()]), // Override (merge behavior depends on implementation)
700            symbol: None,                               // Inherit
701            gas_price_cache: None,
702        };
703
704        let result = resolver.resolve_evm_inheritance(&child_config, "child", "parent");
705        assert!(result.is_ok());
706
707        let resolved = result.unwrap();
708        assert_eq!(resolved.common.network, "child");
709        assert_eq!(
710            resolved.common.rpc_urls,
711            Some(vec!["https://child-rpc.example.com".to_string()])
712        ); // Child override
713        assert_eq!(
714            resolved.common.explorer_urls,
715            Some(vec!["https://child-explorer.example.com".to_string()])
716        ); // Child override
717        assert_eq!(resolved.common.average_blocktime_ms, Some(12000)); // Inherited from parent
718        assert_eq!(resolved.common.is_testnet, Some(false)); // Child override
719        assert_eq!(resolved.chain_id, Some(42)); // Child override
720        assert_eq!(resolved.required_confirmations, Some(1)); // Inherited from parent
721        assert_eq!(resolved.symbol, Some("ETH".to_string())); // Inherited from parent
722    }
723}