1use super::{
21 ConfigFileNetworkType, EvmNetworkConfig, NetworkFileConfig, SolanaNetworkConfig,
22 StellarNetworkConfig,
23};
24use crate::config::ConfigFileError;
25
26pub struct InheritanceResolver<'a> {
28 network_lookup: &'a dyn Fn(&str) -> Option<&'a NetworkFileConfig>,
30}
31
32macro_rules! impl_inheritance_resolver {
37 ($method_name:ident, $config_type:ty, $network_type:ident, $variant:ident, $type_name:expr) => {
38 pub fn $method_name(&self, config: &$config_type, network_name: &str, parent_name: &str) -> Result<$config_type, ConfigFileError> {
48 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 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 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 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 Ok(config.merge_with_parent(&resolved_parent))
80 }
81 };
82}
83
84impl<'a> InheritanceResolver<'a> {
85 pub fn new(network_lookup: &'a dyn Fn(&str) -> Option<&'a NetworkFileConfig>) -> Self {
93 Self { network_lookup }
94 }
95
96 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 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 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 let child_config = EvmNetworkConfig {
148 common: NetworkConfigCommon {
149 network: "child".to_string(),
150 from: Some("parent".to_string()),
151 rpc_urls: None, explorer_urls: None, average_blocktime_ms: Some(15000), is_testnet: Some(false), tags: None,
156 },
157 chain_id: None, required_confirmations: Some(2), features: None,
160 symbol: None, 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); assert_eq!(
171 resolved.common.explorer_urls,
172 parent_config.common.explorer_urls
173 ); assert_eq!(resolved.common.average_blocktime_ms, Some(15000)); assert_eq!(resolved.common.is_testnet, Some(false)); assert_eq!(resolved.chain_id, parent_config.chain_id); assert_eq!(resolved.required_confirmations, Some(2)); assert_eq!(resolved.symbol, parent_config.symbol); }
180
181 #[test]
182 fn test_resolve_evm_inheritance_multi_level() {
183 let mut networks = HashMap::new();
184
185 let grandparent_config = create_evm_network("grandparent");
187 networks.insert(
188 "grandparent".to_string(),
189 NetworkFileConfig::Evm(grandparent_config.clone()),
190 );
191
192 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), is_testnet: None,
201 tags: None,
202 },
203 chain_id: None,
204 required_confirmations: Some(3), 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 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), tags: None,
227 },
228 chain_id: Some(42), 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); assert_eq!(
242 resolved.common.explorer_urls,
243 grandparent_config.common.explorer_urls
244 ); assert_eq!(resolved.common.average_blocktime_ms, Some(10000)); assert_eq!(resolved.common.is_testnet, Some(false)); assert_eq!(resolved.chain_id, Some(42)); assert_eq!(resolved.required_confirmations, Some(3)); assert_eq!(resolved.symbol, grandparent_config.symbol); }
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 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 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 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 #[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, explorer_urls: None, average_blocktime_ms: Some(500), 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); assert_eq!(
355 resolved.common.explorer_urls,
356 parent_config.common.explorer_urls
357 ); assert_eq!(resolved.common.average_blocktime_ms, Some(500)); }
360
361 #[test]
362 fn test_resolve_solana_inheritance_type_mismatch() {
363 let mut networks = HashMap::new();
364
365 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, explorer_urls: None, average_blocktime_ms: Some(6000), is_testnet: None,
421 tags: None,
422 },
423 passphrase: None, horizon_url: None, };
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); assert_eq!(resolved.common.average_blocktime_ms, Some(6000)); assert_eq!(resolved.passphrase, parent_config.passphrase); }
436
437 #[test]
438 fn test_resolve_stellar_inheritance_type_mismatch() {
439 let mut networks = HashMap::new();
440
441 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 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 ); assert_eq!(
563 resolved.common.explorer_urls,
564 Some(vec!["https://custom-explorer.example.com".to_string()])
565 ); assert_eq!(resolved.common.average_blocktime_ms, Some(11000)); assert_eq!(resolved.common.is_testnet, Some(false)); assert_eq!(
569 resolved.common.tags,
570 Some(vec!["test".to_string(), "production".to_string()])
571 ); assert_eq!(resolved.chain_id, Some(100)); assert_eq!(resolved.required_confirmations, Some(5)); assert_eq!(resolved.symbol, Some("CUSTOM".to_string())); 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 let result = resolver.resolve_evm_inheritance(&child_config, "", "parent");
594 assert!(result.is_ok());
595
596 let resolved = result.unwrap();
598 assert_eq!(resolved.common.network, "child"); }
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 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 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 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 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 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 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 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 )]), explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), average_blocktime_ms: None, is_testnet: Some(false), tags: Some(vec!["child-tag".to_string()]), },
704 chain_id: Some(42), required_confirmations: None, features: Some(vec!["berlin".to_string()]), symbol: None, 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 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 ); assert_eq!(resolved.common.average_blocktime_ms, Some(12000)); assert_eq!(resolved.common.is_testnet, Some(false)); assert_eq!(resolved.chain_id, Some(42)); assert_eq!(resolved.required_confirmations, Some(1)); assert_eq!(resolved.symbol, Some("ETH".to_string())); }
733}