openzeppelin_relayer/config/config_file/network/
mod.rs

1//! Network Configuration Module
2//!
3//! This module provides network configuration support for EVM, Solana, and Stellar networks
4//! with inheritance, validation, and flexible loading mechanisms.
5//!
6//! ## Key Features
7//!
8//! - **Multi-blockchain support**: EVM, Solana, and Stellar network configurations
9//! - **Inheritance system**: Networks can inherit from parents with type safety
10//! - **Flexible loading**: JSON arrays or directory-based configuration files
11//! - **Comprehensive validation**: URL validation, required fields, inheritance integrity
12//!
13//! ## Core Types
14//!
15//! - [`NetworkFileConfig`] - Unified enum for all network types
16//! - [`NetworksFileConfig`] - Collection managing multiple networks
17//! - [`NetworkConfigCommon`] - Shared configuration fields
18//! - [`InheritanceResolver`] - Handles inheritance resolution
19//! - [`NetworkFileLoader`] - Loads configurations from files/directories
20
21pub mod collection;
22pub mod common;
23pub mod evm;
24pub mod file_loading;
25pub mod inheritance;
26pub mod solana;
27pub mod stellar;
28#[cfg(test)]
29pub mod test_utils;
30
31pub use collection::*;
32pub use common::*;
33pub use evm::*;
34pub use file_loading::*;
35pub use inheritance::*;
36pub use solana::*;
37pub use stellar::*;
38
39use super::ConfigFileNetworkType;
40use crate::config::ConfigFileError;
41use serde::{Deserialize, Serialize};
42
43/// Represents the configuration for a specific network, which can be EVM, Solana, or Stellar.
44///
45/// During deserialization, the `type` field in the configuration source determines which variant is expected.
46#[derive(Debug, Serialize, Deserialize, Clone)]
47#[serde(tag = "type", rename_all = "lowercase")]
48pub enum NetworkFileConfig {
49    /// Configuration for an EVM-compatible network.
50    Evm(EvmNetworkConfig),
51    /// Configuration for a Solana network.
52    Solana(SolanaNetworkConfig),
53    /// Configuration for a Stellar network.
54    Stellar(StellarNetworkConfig),
55}
56
57impl NetworkFileConfig {
58    /// Validates the network configuration based on its type.
59    ///
60    /// # Returns
61    /// - `Ok(())` if the configuration is valid.
62    /// - `Err(ConfigFileError)` with details about the validation failure.
63    pub fn validate(&self) -> Result<(), ConfigFileError> {
64        match self {
65            NetworkFileConfig::Evm(network) => network.validate(),
66            NetworkFileConfig::Solana(network) => network.validate(),
67            NetworkFileConfig::Stellar(network) => network.validate(),
68        }
69    }
70
71    /// Returns the unique identifier (name) of the network.
72    ///
73    /// # Returns
74    /// - `&str` containing the network name.
75    pub fn network_name(&self) -> &str {
76        match self {
77            NetworkFileConfig::Evm(network) => &network.common.network,
78            NetworkFileConfig::Solana(network) => &network.common.network,
79            NetworkFileConfig::Stellar(network) => &network.common.network,
80        }
81    }
82
83    /// Returns the type of the network (EVM, Solana, or Stellar).
84    ///
85    /// # Returns
86    /// - `ConfigFileNetworkType` enum variant corresponding to the network type.
87    pub fn network_type(&self) -> ConfigFileNetworkType {
88        match self {
89            NetworkFileConfig::Evm(_) => ConfigFileNetworkType::Evm,
90            NetworkFileConfig::Solana(_) => ConfigFileNetworkType::Solana,
91            NetworkFileConfig::Stellar(_) => ConfigFileNetworkType::Stellar,
92        }
93    }
94
95    /// Returns true if the network is a testnet, false otherwise.
96    ///
97    /// # Returns
98    /// - `true` if the network is a testnet.
99    /// - `false` if the network is a mainnet.
100    pub fn is_testnet(&self) -> bool {
101        match self {
102            NetworkFileConfig::Evm(network) => network.common.is_testnet.unwrap_or(false),
103            NetworkFileConfig::Solana(network) => network.common.is_testnet.unwrap_or(false),
104            NetworkFileConfig::Stellar(network) => network.common.is_testnet.unwrap_or(false),
105        }
106    }
107
108    /// Returns the name of the network this configuration inherits from, if any.
109    ///
110    /// # Returns
111    /// - `Some(&str)` containing the source network name if the `from` field is set.
112    /// - `None` otherwise.
113    pub fn inherits_from(&self) -> Option<&str> {
114        match self {
115            NetworkFileConfig::Evm(network) => network.common.from.as_deref(),
116            NetworkFileConfig::Solana(network) => network.common.from.as_deref(),
117            NetworkFileConfig::Stellar(network) => network.common.from.as_deref(),
118        }
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use crate::config::config_file::network::test_utils::*;
126
127    #[test]
128    fn test_validate_evm_network_success() {
129        let config = create_evm_network_wrapped("test-evm");
130        let result = config.validate();
131        assert!(result.is_ok());
132    }
133
134    #[test]
135    fn test_validate_solana_network_success() {
136        let config = create_solana_network_wrapped("test-solana");
137        let result = config.validate();
138        assert!(result.is_ok());
139    }
140
141    #[test]
142    fn test_validate_stellar_network_success() {
143        let config = create_stellar_network_wrapped("test-stellar");
144        let result = config.validate();
145        assert!(result.is_ok());
146    }
147
148    #[test]
149    fn test_validate_evm_network_failure() {
150        let mut config = create_evm_network_wrapped("test-evm");
151        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
152            evm_config.common.network = "".to_string(); // Invalid empty network name
153        }
154
155        let result = config.validate();
156        assert!(result.is_err());
157        assert!(matches!(
158            result.unwrap_err(),
159            ConfigFileError::MissingField(_)
160        ));
161    }
162
163    #[test]
164    fn test_validate_solana_network_failure() {
165        let mut config = create_solana_network_wrapped("test-solana");
166        if let NetworkFileConfig::Solana(ref mut solana_config) = config {
167            solana_config.common.rpc_urls = None; // Missing required RPC URLs
168        }
169
170        let result = config.validate();
171        assert!(result.is_err());
172        assert!(matches!(
173            result.unwrap_err(),
174            ConfigFileError::MissingField(_)
175        ));
176    }
177
178    #[test]
179    fn test_validate_stellar_network_failure() {
180        let mut config = create_stellar_network_wrapped("test-stellar");
181        if let NetworkFileConfig::Stellar(ref mut stellar_config) = config {
182            stellar_config.common.network = "".to_string(); // Invalid empty network name
183        }
184
185        let result = config.validate();
186        assert!(result.is_err());
187        assert!(matches!(
188            result.unwrap_err(),
189            ConfigFileError::MissingField(_)
190        ));
191    }
192
193    #[test]
194    fn test_validate_evm_network_missing_chain_id() {
195        let mut config = create_evm_network_wrapped("test-evm");
196        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
197            evm_config.chain_id = None; // Missing required chain_id
198        }
199
200        let result = config.validate();
201        assert!(result.is_err());
202        assert!(matches!(
203            result.unwrap_err(),
204            ConfigFileError::MissingField(_)
205        ));
206    }
207
208    #[test]
209    fn test_validate_evm_network_missing_confirmations() {
210        let mut config = create_evm_network_wrapped("test-evm");
211        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
212            evm_config.required_confirmations = None; // Missing required confirmations
213        }
214
215        let result = config.validate();
216        assert!(result.is_err());
217        assert!(matches!(
218            result.unwrap_err(),
219            ConfigFileError::MissingField(_)
220        ));
221    }
222
223    #[test]
224    fn test_validate_evm_network_missing_symbol() {
225        let mut config = create_evm_network_wrapped("test-evm");
226        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
227            evm_config.symbol = None; // Missing required symbol
228        }
229
230        let result = config.validate();
231        assert!(result.is_err());
232        assert!(matches!(
233            result.unwrap_err(),
234            ConfigFileError::MissingField(_)
235        ));
236    }
237
238    // NetworkFileConfig::network_name() tests
239    #[test]
240    fn test_network_name_evm() {
241        let config = create_evm_network_wrapped("test-evm");
242        assert_eq!(config.network_name(), "test-evm");
243    }
244
245    #[test]
246    fn test_network_name_solana() {
247        let config = create_solana_network_wrapped("test-solana");
248        assert_eq!(config.network_name(), "test-solana");
249    }
250
251    #[test]
252    fn test_network_name_stellar() {
253        let config = create_stellar_network_wrapped("test-stellar");
254        assert_eq!(config.network_name(), "test-stellar");
255    }
256
257    #[test]
258    fn test_network_name_with_unicode() {
259        let mut config = create_evm_network_wrapped("test-evm");
260        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
261            evm_config.common.network = "测试网络".to_string();
262        }
263        assert_eq!(config.network_name(), "测试网络");
264    }
265
266    #[test]
267    fn test_network_name_with_special_characters() {
268        let mut config = create_solana_network_wrapped("test-solana");
269        if let NetworkFileConfig::Solana(ref mut solana_config) = config {
270            solana_config.common.network = "test-network_123-dev".to_string();
271        }
272        assert_eq!(config.network_name(), "test-network_123-dev");
273    }
274
275    #[test]
276    fn test_network_name_empty_string() {
277        let mut config = create_stellar_network_wrapped("test-stellar");
278        if let NetworkFileConfig::Stellar(ref mut stellar_config) = config {
279            stellar_config.common.network = "".to_string();
280        }
281        assert_eq!(config.network_name(), "");
282    }
283
284    #[test]
285    fn test_network_type_evm() {
286        let config = create_evm_network_wrapped("test-evm");
287        assert_eq!(config.network_type(), ConfigFileNetworkType::Evm);
288    }
289
290    #[test]
291    fn test_network_type_solana() {
292        let config = create_solana_network_wrapped("test-solana");
293        assert_eq!(config.network_type(), ConfigFileNetworkType::Solana);
294    }
295
296    #[test]
297    fn test_network_type_stellar() {
298        let config = create_stellar_network_wrapped("test-stellar");
299        assert_eq!(config.network_type(), ConfigFileNetworkType::Stellar);
300    }
301
302    #[test]
303    fn test_network_type_consistency() {
304        let evm_config = create_evm_network_wrapped("test-evm");
305        let solana_config = create_solana_network_wrapped("test-solana");
306        let stellar_config = create_stellar_network_wrapped("test-stellar");
307
308        // Ensure each type returns the correct enum variant
309        assert!(matches!(
310            evm_config.network_type(),
311            ConfigFileNetworkType::Evm
312        ));
313        assert!(matches!(
314            solana_config.network_type(),
315            ConfigFileNetworkType::Solana
316        ));
317        assert!(matches!(
318            stellar_config.network_type(),
319            ConfigFileNetworkType::Stellar
320        ));
321    }
322
323    #[test]
324    fn test_inherits_from_none() {
325        let config = create_evm_network_wrapped("test-evm");
326        assert_eq!(config.inherits_from(), None);
327    }
328
329    #[test]
330    fn test_inherits_from_some_evm() {
331        let config = create_evm_network_wrapped_with_parent("child-evm", "parent-evm");
332        assert_eq!(config.inherits_from(), Some("parent-evm"));
333    }
334
335    #[test]
336    fn test_inherits_from_some_solana() {
337        let mut config = create_solana_network_wrapped("test-solana");
338        if let NetworkFileConfig::Solana(ref mut solana_config) = config {
339            solana_config.common.from = Some("parent-solana".to_string());
340        }
341        assert_eq!(config.inherits_from(), Some("parent-solana"));
342    }
343
344    #[test]
345    fn test_inherits_from_some_stellar() {
346        let mut config = create_stellar_network_wrapped("test-stellar");
347        if let NetworkFileConfig::Stellar(ref mut stellar_config) = config {
348            stellar_config.common.from = Some("parent-stellar".to_string());
349        }
350        assert_eq!(config.inherits_from(), Some("parent-stellar"));
351    }
352
353    #[test]
354    fn test_inherits_from_empty_string() {
355        let mut config = create_evm_network_wrapped("test-evm");
356        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
357            evm_config.common.from = Some("".to_string());
358        }
359        assert_eq!(config.inherits_from(), Some(""));
360    }
361
362    #[test]
363    fn test_inherits_from_with_unicode() {
364        let mut config = create_solana_network_wrapped("test-solana");
365        if let NetworkFileConfig::Solana(ref mut solana_config) = config {
366            solana_config.common.from = Some("父网络".to_string());
367        }
368        assert_eq!(config.inherits_from(), Some("父网络"));
369    }
370
371    #[test]
372    fn test_serialize_deserialize_evm() {
373        let original = create_evm_network_wrapped("test-evm");
374        let serialized = serde_json::to_string(&original).unwrap();
375        let deserialized: NetworkFileConfig = serde_json::from_str(&serialized).unwrap();
376
377        assert_eq!(original.network_name(), deserialized.network_name());
378        assert_eq!(original.network_type(), deserialized.network_type());
379        assert_eq!(original.inherits_from(), deserialized.inherits_from());
380    }
381
382    #[test]
383    fn test_serialize_deserialize_solana() {
384        let original = create_solana_network_wrapped("test-solana");
385        let serialized = serde_json::to_string(&original).unwrap();
386        let deserialized: NetworkFileConfig = serde_json::from_str(&serialized).unwrap();
387
388        assert_eq!(original.network_name(), deserialized.network_name());
389        assert_eq!(original.network_type(), deserialized.network_type());
390        assert_eq!(original.inherits_from(), deserialized.inherits_from());
391    }
392
393    #[test]
394    fn test_serialize_deserialize_stellar() {
395        let original = create_stellar_network_wrapped("test-stellar");
396        let serialized = serde_json::to_string(&original).unwrap();
397        let deserialized: NetworkFileConfig = serde_json::from_str(&serialized).unwrap();
398
399        assert_eq!(original.network_name(), deserialized.network_name());
400        assert_eq!(original.network_type(), deserialized.network_type());
401        assert_eq!(original.inherits_from(), deserialized.inherits_from());
402    }
403
404    #[test]
405    fn test_deserialize_evm_from_json() {
406        let json = r#"{
407            "type": "evm",
408            "network": "test-evm-json",
409            "chain_id": 1337,
410            "required_confirmations": 2,
411            "symbol": "ETH",
412            "rpc_urls": ["https://rpc.example.com"]
413        }"#;
414
415        let config: NetworkFileConfig = serde_json::from_str(json).unwrap();
416        assert_eq!(config.network_name(), "test-evm-json");
417        assert_eq!(config.network_type(), ConfigFileNetworkType::Evm);
418        assert_eq!(config.inherits_from(), None);
419    }
420
421    #[test]
422    fn test_deserialize_solana_from_json() {
423        let json = r#"{
424            "type": "solana",
425            "network": "test-solana-json",
426            "rpc_urls": ["https://api.devnet.solana.com"]
427        }"#;
428
429        let config: NetworkFileConfig = serde_json::from_str(json).unwrap();
430        assert_eq!(config.network_name(), "test-solana-json");
431        assert_eq!(config.network_type(), ConfigFileNetworkType::Solana);
432        assert_eq!(config.inherits_from(), None);
433    }
434
435    #[test]
436    fn test_deserialize_stellar_from_json() {
437        let json = r#"{
438            "type": "stellar",
439            "network": "test-stellar-json",
440            "rpc_urls": ["https://horizon-testnet.stellar.org"]
441        }"#;
442
443        let config: NetworkFileConfig = serde_json::from_str(json).unwrap();
444        assert_eq!(config.network_name(), "test-stellar-json");
445        assert_eq!(config.network_type(), ConfigFileNetworkType::Stellar);
446        assert_eq!(config.inherits_from(), None);
447    }
448
449    #[test]
450    fn test_deserialize_with_inheritance() {
451        let json = r#"{
452            "type": "evm",
453            "network": "child-network",
454            "from": "parent-network",
455            "chain_id": 1337,
456            "required_confirmations": 1,
457            "symbol": "ETH"
458        }"#;
459
460        let config: NetworkFileConfig = serde_json::from_str(json).unwrap();
461        assert_eq!(config.network_name(), "child-network");
462        assert_eq!(config.inherits_from(), Some("parent-network"));
463    }
464
465    #[test]
466    fn test_deserialize_invalid_type() {
467        let json = r#"{
468            "type": "invalid",
469            "network": "test-network"
470        }"#;
471
472        let result: Result<NetworkFileConfig, _> = serde_json::from_str(json);
473        assert!(result.is_err());
474    }
475
476    #[test]
477    fn test_deserialize_missing_type() {
478        let json = r#"{
479            "network": "test-network",
480            "chain_id": 1337
481        }"#;
482
483        let result: Result<NetworkFileConfig, _> = serde_json::from_str(json);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_deserialize_missing_network_field() {
489        let json = r#"{
490            "type": "evm",
491            "chain_id": 1337
492        }"#;
493
494        let result: Result<NetworkFileConfig, _> = serde_json::from_str(json);
495        assert!(result.is_err());
496    }
497
498    #[test]
499    fn test_all_network_types_in_collection() {
500        let configs = vec![
501            create_evm_network_wrapped("test-evm"),
502            create_solana_network_wrapped("test-solana"),
503            create_stellar_network_wrapped("test-stellar"),
504        ];
505
506        let types: Vec<ConfigFileNetworkType> = configs.iter().map(|c| c.network_type()).collect();
507        assert!(types.contains(&ConfigFileNetworkType::Evm));
508        assert!(types.contains(&ConfigFileNetworkType::Solana));
509        assert!(types.contains(&ConfigFileNetworkType::Stellar));
510    }
511
512    #[test]
513    fn test_network_names_uniqueness() {
514        let configs = vec![
515            create_evm_network_wrapped("test-evm"),
516            create_solana_network_wrapped("test-solana"),
517            create_stellar_network_wrapped("test-stellar"),
518        ];
519
520        let names: Vec<&str> = configs.iter().map(|c| c.network_name()).collect();
521        assert_eq!(names.len(), 3);
522        assert!(names.contains(&"test-evm"));
523        assert!(names.contains(&"test-solana"));
524        assert!(names.contains(&"test-stellar"));
525    }
526
527    #[test]
528    fn test_inheritance_patterns() {
529        let mut configs = vec![
530            create_evm_network_wrapped("test-evm"),
531            create_evm_network_wrapped_with_parent("child-evm", "parent-evm"),
532        ];
533
534        let mut solana_with_inheritance = create_solana_network_wrapped("test-solana");
535        if let NetworkFileConfig::Solana(ref mut solana_config) = solana_with_inheritance {
536            solana_config.common.from = Some("parent-solana".to_string());
537        }
538        configs.push(solana_with_inheritance);
539
540        let inheritance_info: Vec<Option<&str>> =
541            configs.iter().map(|c| c.inherits_from()).collect();
542        assert_eq!(inheritance_info[0], None); // Base EVM config
543        assert_eq!(inheritance_info[1], Some("parent-evm")); // Child EVM config
544        assert_eq!(inheritance_info[2], Some("parent-solana")); // Child Solana config
545    }
546
547    #[test]
548    fn test_validation_error_propagation() {
549        let mut config = create_evm_network_wrapped("test-evm");
550        if let NetworkFileConfig::Evm(ref mut evm_config) = config {
551            evm_config.common.rpc_urls = Some(vec!["invalid-url".to_string()]);
552        }
553
554        let result = config.validate();
555        assert!(result.is_err());
556        assert!(matches!(
557            result.unwrap_err(),
558            ConfigFileError::InvalidFormat(_)
559        ));
560    }
561
562    #[test]
563    fn test_serialization_preserves_all_fields() {
564        let config = create_evm_network_wrapped("test-evm");
565        let serialized = serde_json::to_string(&config).unwrap();
566
567        // Check that important fields are present in serialized JSON
568        assert!(serialized.contains("\"type\":\"evm\""));
569        assert!(serialized.contains("\"network\":\"test-evm\""));
570        assert!(serialized.contains("\"chain_id\":31337"));
571        assert!(serialized.contains("\"required_confirmations\":1"));
572        assert!(serialized.contains("\"symbol\":\"ETH\""));
573    }
574
575    #[test]
576    fn test_deserialization_with_extra_fields() {
577        let json = r#"{
578            "type": "evm",
579            "network": "test-network",
580            "chain_id": 1337,
581            "required_confirmations": 1,
582            "symbol": "ETH",
583            "rpc_urls": ["https://rpc.example.com"],
584            "extra_field": "should_be_ignored"
585        }"#;
586
587        let result: Result<NetworkFileConfig, _> = serde_json::from_str(json);
588        assert!(result.is_err());
589    }
590
591    #[test]
592    fn test_method_consistency_across_types() {
593        let configs = vec![
594            create_evm_network_wrapped("test-evm"),
595            create_solana_network_wrapped("test-solana"),
596            create_stellar_network_wrapped("test-stellar"),
597        ];
598
599        // Ensure all methods work consistently across all network types
600        for config in configs {
601            // All should have non-empty network names
602            assert!(!config.network_name().is_empty());
603
604            // All should have valid network types
605            let network_type = config.network_type();
606            assert!(matches!(
607                network_type,
608                ConfigFileNetworkType::Evm
609                    | ConfigFileNetworkType::Solana
610                    | ConfigFileNetworkType::Stellar
611            ));
612
613            // All should validate successfully
614            assert!(config.validate().is_ok());
615
616            // All should have None inheritance by default
617            assert_eq!(config.inherits_from(), None);
618        }
619    }
620}