openzeppelin_relayer/config/config_file/network/
common.rs

1//! Common Network Configuration Components
2//!
3//! This module defines shared configuration structures and utilities common across
4//! all network types (EVM, Solana, Stellar) with inheritance and merging support.
5//!
6//! ## Key Features
7//!
8//! - **Inheritance support**: Child networks inherit from parents with override capability
9//! - **Smart merging**: Collections merge preserving unique items, primitives override
10//! - **Validation**: Required field checks and URL format validation
11
12use crate::config::ConfigFileError;
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Serialize, Deserialize, Clone)]
16pub struct NetworkConfigCommon {
17    /// Unique network identifier (e.g., "mainnet", "sepolia", "custom-devnet").
18    pub network: String,
19    /// Optional name of an existing network to inherit configuration from.
20    /// If set, this network will use the `from` network's settings as a base,
21    /// overriding specific fields as needed.
22    pub from: Option<String>,
23    /// List of RPC endpoint URLs for connecting to the network.
24    pub rpc_urls: Option<Vec<String>>,
25    /// List of Explorer endpoint URLs for connecting to the network.
26    pub explorer_urls: Option<Vec<String>>,
27    /// Estimated average time between blocks in milliseconds.
28    pub average_blocktime_ms: Option<u64>,
29    /// Flag indicating if the network is a testnet.
30    pub is_testnet: Option<bool>,
31    /// List of arbitrary tags for categorizing or filtering networks.
32    pub tags: Option<Vec<String>>,
33}
34
35impl NetworkConfigCommon {
36    /// Validates the common fields for a network configuration.
37    ///
38    /// # Returns
39    /// - `Ok(())` if common fields are valid.
40    /// - `Err(ConfigFileError)` if validation fails.
41    pub fn validate(&self) -> Result<(), ConfigFileError> {
42        // Validate network name
43        if self.network.is_empty() {
44            return Err(ConfigFileError::MissingField("network name".into()));
45        }
46
47        // If this is a base network (not inheriting), validate required fields
48        if self.from.is_none() {
49            // RPC URLs are required for base networks
50            if self.rpc_urls.is_none() || self.rpc_urls.as_ref().unwrap().is_empty() {
51                return Err(ConfigFileError::MissingField("rpc_urls".into()));
52            }
53        }
54
55        // Validate RPC URLs format if provided
56        if let Some(urls) = &self.rpc_urls {
57            for url in urls {
58                reqwest::Url::parse(url).map_err(|_| {
59                    ConfigFileError::InvalidFormat(format!("Invalid RPC URL: {}", url))
60                })?;
61            }
62        }
63
64        if let Some(urls) = &self.explorer_urls {
65            for url in urls {
66                reqwest::Url::parse(url).map_err(|_| {
67                    ConfigFileError::InvalidFormat(format!("Invalid Explorer URL: {}", url))
68                })?;
69            }
70        }
71
72        Ok(())
73    }
74
75    /// Creates a new configuration by merging this config with a parent, where child values override parent defaults.
76    ///
77    /// # Arguments
78    /// * `parent` - The parent configuration to merge with.
79    ///
80    /// # Returns
81    /// A new `NetworkConfigCommon` with merged values where child takes precedence over parent.
82    pub fn merge_with_parent(&self, parent: &Self) -> Self {
83        Self {
84            network: self.network.clone(),
85            from: self.from.clone(),
86            rpc_urls: self.rpc_urls.clone().or_else(|| parent.rpc_urls.clone()),
87            explorer_urls: self
88                .explorer_urls
89                .clone()
90                .or_else(|| parent.explorer_urls.clone()),
91            average_blocktime_ms: self.average_blocktime_ms.or(parent.average_blocktime_ms),
92            is_testnet: self.is_testnet.or(parent.is_testnet),
93            tags: merge_tags(&self.tags, &parent.tags),
94        }
95    }
96}
97
98/// Combines child and parent string vectors, preserving all unique items with child items taking precedence.
99///
100/// # Arguments
101/// * `child` - Optional vector of child items.
102/// * `parent` - Optional vector of parent items.
103///
104/// # Returns
105/// An optional vector containing all unique items from both sources, or `None` if both inputs are `None`.
106pub fn merge_optional_string_vecs(
107    child: &Option<Vec<String>>,
108    parent: &Option<Vec<String>>,
109) -> Option<Vec<String>> {
110    match (child, parent) {
111        (Some(child), Some(parent)) => {
112            let mut merged = parent.clone();
113            for item in child {
114                if !merged.contains(item) {
115                    merged.push(item.clone());
116                }
117            }
118            Some(merged)
119        }
120        (Some(items), None) => Some(items.clone()),
121        (None, Some(items)) => Some(items.clone()),
122        (None, None) => None,
123    }
124}
125
126/// Combines child and parent tag vectors, preserving all unique tags with child tags taking precedence.
127///
128/// # Arguments
129/// * `child_tags` - Optional vector of child tags.
130/// * `parent_tags` - Optional vector of parent tags.
131///
132/// # Returns
133/// An optional vector containing all unique tags from both sources, or `None` if both inputs are `None`.
134fn merge_tags(
135    child_tags: &Option<Vec<String>>,
136    parent_tags: &Option<Vec<String>>,
137) -> Option<Vec<String>> {
138    merge_optional_string_vecs(child_tags, parent_tags)
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::config::config_file::network::test_utils::*;
145
146    #[test]
147    fn test_validate_success_base_network() {
148        let config = create_network_common("test-network");
149        let result = config.validate();
150        assert!(result.is_ok());
151    }
152
153    #[test]
154    fn test_validate_success_inheriting_network() {
155        let config = create_network_common_with_parent("child-network", "parent-network");
156        let result = config.validate();
157        assert!(result.is_ok());
158    }
159
160    #[test]
161    fn test_validate_empty_network_name() {
162        let mut config = create_network_common("test-network");
163        config.network = String::new();
164
165        let result = config.validate();
166        assert!(result.is_err());
167        assert!(matches!(
168            result.unwrap_err(),
169            ConfigFileError::MissingField(_)
170        ));
171    }
172
173    #[test]
174    fn test_validate_base_network_missing_rpc_urls() {
175        let mut config = create_network_common("test-network");
176        config.rpc_urls = None;
177
178        let result = config.validate();
179        assert!(result.is_err());
180        assert!(matches!(
181            result.unwrap_err(),
182            ConfigFileError::MissingField(_)
183        ));
184    }
185
186    #[test]
187    fn test_validate_base_network_empty_rpc_urls() {
188        let mut config = create_network_common("test-network");
189        config.rpc_urls = Some(vec![]);
190
191        let result = config.validate();
192        assert!(result.is_err());
193        assert!(matches!(
194            result.unwrap_err(),
195            ConfigFileError::MissingField(_)
196        ));
197    }
198
199    #[test]
200    fn test_validate_invalid_rpc_url_format() {
201        let mut config = create_network_common("test-network");
202        config.rpc_urls = Some(vec!["invalid-url".to_string()]);
203
204        let result = config.validate();
205        assert!(result.is_err());
206        assert!(matches!(
207            result.unwrap_err(),
208            ConfigFileError::InvalidFormat(_)
209        ));
210    }
211
212    #[test]
213    fn test_validate_multiple_invalid_rpc_urls() {
214        let mut config = create_network_common("test-network");
215        config.rpc_urls = Some(vec![
216            "https://valid.example.com".to_string(),
217            "invalid-url".to_string(),
218            "also-invalid".to_string(),
219        ]);
220
221        let result = config.validate();
222        assert!(result.is_err());
223        assert!(matches!(
224            result.unwrap_err(),
225            ConfigFileError::InvalidFormat(_)
226        ));
227    }
228
229    #[test]
230    fn test_validate_various_valid_rpc_url_formats() {
231        let mut config = create_network_common("test-network");
232        config.rpc_urls = Some(vec![
233            "https://mainnet.infura.io/v3/key".to_string(),
234            "http://localhost:8545".to_string(),
235            "wss://ws.example.com".to_string(),
236            "https://rpc.example.com:8080/path".to_string(),
237        ]);
238
239        let result = config.validate();
240        assert!(result.is_ok());
241    }
242
243    #[test]
244    fn test_validate_inheriting_network_with_rpc_urls() {
245        let mut config = create_network_common_with_parent("child-network", "parent-network");
246        config.rpc_urls = Some(vec!["https://override.example.com".to_string()]);
247
248        let result = config.validate();
249        assert!(result.is_ok());
250    }
251
252    #[test]
253    fn test_validate_inheriting_network_with_invalid_rpc_urls() {
254        let mut config = create_network_common_with_parent("child-network", "parent-network");
255        config.rpc_urls = Some(vec!["invalid-url".to_string()]);
256
257        let result = config.validate();
258        assert!(result.is_err());
259        assert!(matches!(
260            result.unwrap_err(),
261            ConfigFileError::InvalidFormat(_)
262        ));
263    }
264
265    #[test]
266    fn test_merge_with_parent_child_overrides() {
267        let parent = NetworkConfigCommon {
268            network: "parent".to_string(),
269            from: None,
270            rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
271            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
272            average_blocktime_ms: Some(10000),
273            is_testnet: Some(true),
274            tags: Some(vec!["parent-tag".to_string()]),
275        };
276
277        let child = NetworkConfigCommon {
278            network: "child".to_string(),
279            from: Some("parent".to_string()),
280            rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]),
281            explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]),
282            average_blocktime_ms: Some(15000),
283            is_testnet: Some(false),
284            tags: Some(vec!["child-tag".to_string()]),
285        };
286
287        let result = child.merge_with_parent(&parent);
288
289        assert_eq!(result.network, "child");
290        assert_eq!(result.from, Some("parent".to_string()));
291        assert_eq!(
292            result.rpc_urls,
293            Some(vec!["https://child-rpc.example.com".to_string()])
294        );
295        assert_eq!(result.average_blocktime_ms, Some(15000));
296        assert_eq!(result.is_testnet, Some(false));
297        assert_eq!(
298            result.tags,
299            Some(vec!["parent-tag".to_string(), "child-tag".to_string()])
300        );
301    }
302
303    #[test]
304    fn test_merge_with_parent_child_inherits() {
305        let parent = NetworkConfigCommon {
306            network: "parent".to_string(),
307            from: None,
308            rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
309            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
310            average_blocktime_ms: Some(10000),
311            is_testnet: Some(true),
312            tags: Some(vec!["parent-tag".to_string()]),
313        };
314
315        let child = NetworkConfigCommon {
316            network: "child".to_string(),
317            from: Some("parent".to_string()),
318            rpc_urls: None,             // Will inherit
319            explorer_urls: None,        // Will inherit
320            average_blocktime_ms: None, // Will inherit
321            is_testnet: None,           // Will inherit
322            tags: None,                 // Will inherit
323        };
324
325        let result = child.merge_with_parent(&parent);
326
327        assert_eq!(result.network, "child");
328        assert_eq!(result.from, Some("parent".to_string()));
329        assert_eq!(
330            result.rpc_urls,
331            Some(vec!["https://parent-rpc.example.com".to_string()])
332        );
333        assert_eq!(
334            result.explorer_urls,
335            Some(vec!["https://parent-explorer.example.com".to_string()])
336        );
337        assert_eq!(result.average_blocktime_ms, Some(10000));
338        assert_eq!(result.is_testnet, Some(true));
339        assert_eq!(result.tags, Some(vec!["parent-tag".to_string()]));
340    }
341
342    #[test]
343    fn test_merge_with_parent_mixed_inheritance() {
344        let parent = NetworkConfigCommon {
345            network: "parent".to_string(),
346            from: None,
347            rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
348            explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
349            average_blocktime_ms: Some(10000),
350            is_testnet: Some(true),
351            tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
352        };
353
354        let child = NetworkConfigCommon {
355            network: "child".to_string(),
356            from: Some("parent".to_string()),
357            rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]), // Override
358            explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
359            average_blocktime_ms: None,                                        // Inherit
360            is_testnet: Some(false),                                           // Override
361            tags: Some(vec!["child-tag".to_string()]),                         // Merge
362        };
363
364        let result = child.merge_with_parent(&parent);
365
366        assert_eq!(result.network, "child");
367        assert_eq!(
368            result.rpc_urls,
369            Some(vec!["https://child-rpc.example.com".to_string()])
370        );
371        assert_eq!(
372            result.explorer_urls,
373            Some(vec!["https://child-explorer.example.com".to_string()])
374        );
375        assert_eq!(result.average_blocktime_ms, Some(10000)); // Inherited
376        assert_eq!(result.is_testnet, Some(false)); // Overridden
377        assert_eq!(
378            result.tags,
379            Some(vec![
380                "parent-tag1".to_string(),
381                "parent-tag2".to_string(),
382                "child-tag".to_string()
383            ])
384        );
385    }
386
387    #[test]
388    fn test_merge_with_parent_both_empty() {
389        let parent = NetworkConfigCommon {
390            network: "parent".to_string(),
391            from: None,
392            rpc_urls: None,
393            explorer_urls: None,
394            average_blocktime_ms: None,
395            is_testnet: None,
396            tags: None,
397        };
398
399        let child = NetworkConfigCommon {
400            network: "child".to_string(),
401            from: Some("parent".to_string()),
402            rpc_urls: None,
403            explorer_urls: None,
404            average_blocktime_ms: None,
405            is_testnet: None,
406            tags: None,
407        };
408
409        let result = child.merge_with_parent(&parent);
410
411        assert_eq!(result.network, "child");
412        assert_eq!(result.from, Some("parent".to_string()));
413        assert_eq!(result.rpc_urls, None);
414        assert_eq!(result.explorer_urls, None);
415        assert_eq!(result.average_blocktime_ms, None);
416        assert_eq!(result.is_testnet, None);
417        assert_eq!(result.tags, None);
418    }
419
420    #[test]
421    fn test_merge_with_parent_complex_tag_merging() {
422        let parent = NetworkConfigCommon {
423            network: "parent".to_string(),
424            from: None,
425            rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
426            explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
427            average_blocktime_ms: Some(12000),
428            is_testnet: Some(true),
429            tags: Some(vec![
430                "production".to_string(),
431                "mainnet".to_string(),
432                "shared".to_string(),
433            ]),
434        };
435
436        let child = NetworkConfigCommon {
437            network: "child".to_string(),
438            from: Some("parent".to_string()),
439            rpc_urls: None,
440            explorer_urls: None,
441            average_blocktime_ms: None,
442            is_testnet: None,
443            tags: Some(vec![
444                "shared".to_string(),
445                "custom".to_string(),
446                "override".to_string(),
447            ]),
448        };
449
450        let result = child.merge_with_parent(&parent);
451
452        // Tags should be merged with parent first, then unique child tags added
453        let expected_tags = vec![
454            "production".to_string(),
455            "mainnet".to_string(),
456            "shared".to_string(), // Duplicate should not be added again
457            "custom".to_string(),
458            "override".to_string(),
459        ];
460        assert_eq!(result.tags, Some(expected_tags));
461    }
462
463    #[test]
464    fn test_merge_optional_string_vecs_both_some() {
465        let child = Some(vec!["child1".to_string(), "child2".to_string()]);
466        let parent = Some(vec!["parent1".to_string(), "parent2".to_string()]);
467        let result = merge_optional_string_vecs(&child, &parent);
468        assert_eq!(
469            result,
470            Some(vec![
471                "parent1".to_string(),
472                "parent2".to_string(),
473                "child1".to_string(),
474                "child2".to_string()
475            ])
476        );
477    }
478
479    #[test]
480    fn test_merge_optional_string_vecs_child_some_parent_none() {
481        let child = Some(vec!["child1".to_string()]);
482        let parent = None;
483        let result = merge_optional_string_vecs(&child, &parent);
484        assert_eq!(result, Some(vec!["child1".to_string()]));
485    }
486
487    #[test]
488    fn test_merge_optional_string_vecs_child_none_parent_some() {
489        let child = None;
490        let parent = Some(vec!["parent1".to_string()]);
491        let result = merge_optional_string_vecs(&child, &parent);
492        assert_eq!(result, Some(vec!["parent1".to_string()]));
493    }
494
495    #[test]
496    fn test_merge_optional_string_vecs_both_none() {
497        let child = None;
498        let parent = None;
499        let result = merge_optional_string_vecs(&child, &parent);
500        assert_eq!(result, None);
501    }
502
503    #[test]
504    fn test_merge_optional_string_vecs_duplicate_handling() {
505        // Test duplicate handling
506        let child = Some(vec!["duplicate".to_string(), "child1".to_string()]);
507        let parent = Some(vec!["duplicate".to_string(), "parent1".to_string()]);
508        let result = merge_optional_string_vecs(&child, &parent);
509        assert_eq!(
510            result,
511            Some(vec![
512                "duplicate".to_string(),
513                "parent1".to_string(),
514                "child1".to_string()
515            ])
516        );
517    }
518
519    #[test]
520    fn test_merge_optional_string_vecs_empty_vectors() {
521        // Test empty child vector
522        let child = Some(vec![]);
523        let parent = Some(vec!["parent1".to_string()]);
524        let result = merge_optional_string_vecs(&child, &parent);
525        assert_eq!(result, Some(vec!["parent1".to_string()]));
526
527        // Test empty parent vector
528        let child = Some(vec!["child1".to_string()]);
529        let parent = Some(vec![]);
530        let result = merge_optional_string_vecs(&child, &parent);
531        assert_eq!(result, Some(vec!["child1".to_string()]));
532
533        // Test both empty vectors
534        let child = Some(vec![]);
535        let parent = Some(vec![]);
536        let result = merge_optional_string_vecs(&child, &parent);
537        assert_eq!(result, Some(vec![]));
538    }
539
540    #[test]
541    fn test_merge_optional_string_vecs_multiple_duplicates() {
542        let child = Some(vec![
543            "a".to_string(),
544            "b".to_string(),
545            "c".to_string(),
546            "a".to_string(),
547        ]);
548        let parent = Some(vec!["b".to_string(), "d".to_string(), "a".to_string()]);
549        let result = merge_optional_string_vecs(&child, &parent);
550
551        // Should preserve parent order, then add unique child items
552        let expected = vec![
553            "b".to_string(),
554            "d".to_string(),
555            "a".to_string(),
556            "c".to_string(),
557        ];
558        assert_eq!(result, Some(expected));
559    }
560
561    #[test]
562    fn test_merge_optional_string_vecs_single_item_vectors() {
563        let child = Some(vec!["child".to_string()]);
564        let parent = Some(vec!["parent".to_string()]);
565        let result = merge_optional_string_vecs(&child, &parent);
566        assert_eq!(
567            result,
568            Some(vec!["parent".to_string(), "child".to_string()])
569        );
570    }
571
572    #[test]
573    fn test_merge_optional_string_vecs_identical_vectors() {
574        let child = Some(vec!["same1".to_string(), "same2".to_string()]);
575        let parent = Some(vec!["same1".to_string(), "same2".to_string()]);
576        let result = merge_optional_string_vecs(&child, &parent);
577        assert_eq!(result, Some(vec!["same1".to_string(), "same2".to_string()]));
578    }
579
580    // Edge Cases and Integration Tests
581    #[test]
582    fn test_network_config_common_clone() {
583        let config = create_network_common("test-network");
584        let cloned = config.clone();
585
586        assert_eq!(config.network, cloned.network);
587        assert_eq!(config.from, cloned.from);
588        assert_eq!(config.rpc_urls, cloned.rpc_urls);
589        assert_eq!(config.average_blocktime_ms, cloned.average_blocktime_ms);
590        assert_eq!(config.is_testnet, cloned.is_testnet);
591        assert_eq!(config.tags, cloned.tags);
592    }
593
594    #[test]
595    fn test_network_config_common_debug() {
596        let config = create_network_common("test-network");
597        let debug_str = format!("{:?}", config);
598
599        assert!(debug_str.contains("NetworkConfigCommon"));
600        assert!(debug_str.contains("test-network"));
601    }
602
603    #[test]
604    fn test_validate_with_unicode_network_name() {
605        let mut config = create_network_common("test-network");
606        config.network = "测试网络".to_string();
607
608        let result = config.validate();
609        assert!(result.is_ok());
610    }
611
612    #[test]
613    fn test_validate_with_unicode_rpc_urls() {
614        let mut config = create_network_common("test-network");
615        config.rpc_urls = Some(vec!["https://测试.example.com".to_string()]);
616
617        let result = config.validate();
618        assert!(result.is_ok());
619    }
620
621    #[test]
622    fn test_merge_with_parent_preserves_child_network_name() {
623        let parent = NetworkConfigCommon {
624            network: "parent-name".to_string(),
625            from: None,
626            rpc_urls: Some(vec!["https://parent.example.com".to_string()]),
627            explorer_urls: Some(vec!["https://parent.example.com".to_string()]),
628            average_blocktime_ms: Some(10000),
629            is_testnet: Some(true),
630            tags: None,
631        };
632
633        let child = NetworkConfigCommon {
634            network: "child-name".to_string(),
635            from: Some("parent-name".to_string()),
636            rpc_urls: None,
637            explorer_urls: None,
638            average_blocktime_ms: None,
639            is_testnet: None,
640            tags: None,
641        };
642
643        let result = child.merge_with_parent(&parent);
644
645        // Child network name should always be preserved
646        assert_eq!(result.network, "child-name");
647        assert_eq!(result.from, Some("parent-name".to_string()));
648    }
649
650    #[test]
651    fn test_merge_with_parent_preserves_child_from_field() {
652        let parent = NetworkConfigCommon {
653            network: "parent".to_string(),
654            from: Some("grandparent".to_string()),
655            rpc_urls: Some(vec!["https://parent.example.com".to_string()]),
656            explorer_urls: Some(vec!["https://parent.example.com".to_string()]),
657            average_blocktime_ms: Some(10000),
658            is_testnet: Some(true),
659            tags: None,
660        };
661
662        let child = NetworkConfigCommon {
663            network: "child".to_string(),
664            from: Some("parent".to_string()),
665            rpc_urls: None,
666            explorer_urls: None,
667            average_blocktime_ms: None,
668            is_testnet: None,
669            tags: None,
670        };
671
672        let result = child.merge_with_parent(&parent);
673
674        // Child's 'from' field should be preserved, not inherited from parent
675        assert_eq!(result.from, Some("parent".to_string()));
676    }
677}