openzeppelin_relayer/models/network/
repository.rs

1use crate::{
2    config::{
3        EvmNetworkConfig, NetworkConfigCommon, NetworkFileConfig, SolanaNetworkConfig,
4        StellarNetworkConfig,
5    },
6    models::NetworkType,
7};
8use eyre;
9use serde::{Deserialize, Serialize};
10
11/// Network configuration data enum that can hold different network types.
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub enum NetworkConfigData {
14    /// EVM network configuration
15    Evm(EvmNetworkConfig),
16    /// Solana network configuration
17    Solana(SolanaNetworkConfig),
18    /// Stellar network configuration
19    Stellar(StellarNetworkConfig),
20}
21
22impl NetworkConfigData {
23    /// Returns the common network configuration shared by all network types.
24    pub fn common(&self) -> &NetworkConfigCommon {
25        match self {
26            NetworkConfigData::Evm(config) => &config.common,
27            NetworkConfigData::Solana(config) => &config.common,
28            NetworkConfigData::Stellar(config) => &config.common,
29        }
30    }
31
32    /// Returns the network type based on the configuration variant.
33    pub fn network_type(&self) -> NetworkType {
34        match self {
35            NetworkConfigData::Evm(_) => NetworkType::Evm,
36            NetworkConfigData::Solana(_) => NetworkType::Solana,
37            NetworkConfigData::Stellar(_) => NetworkType::Stellar,
38        }
39    }
40
41    /// Returns the network name from the common configuration.
42    pub fn network_name(&self) -> &str {
43        &self.common().network
44    }
45}
46
47/// Network repository model representing a network configuration stored in the repository.
48///
49/// This model is used to store network configurations that have been processed from
50/// the configuration file and are ready to be used by the application.
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct NetworkRepoModel {
53    /// Unique identifier composed of network_type and name, e.g., "evm:mainnet"
54    pub id: String,
55    /// Name of the network (e.g., "mainnet", "sepolia")
56    pub name: String,
57    /// Type of the network (EVM, Solana, Stellar)
58    pub network_type: NetworkType,
59    /// Network configuration data specific to the network type
60    pub config: NetworkConfigData,
61}
62
63impl NetworkRepoModel {
64    /// Creates a new NetworkRepoModel with EVM configuration.
65    ///
66    /// # Arguments
67    /// * `config` - The EVM network configuration
68    ///
69    /// # Returns
70    /// A new NetworkRepoModel instance
71    pub fn new_evm(config: EvmNetworkConfig) -> Self {
72        let name = config.common.network.clone();
73        let id = format!("evm:{name}").to_lowercase();
74        Self {
75            id,
76            name,
77            network_type: NetworkType::Evm,
78            config: NetworkConfigData::Evm(config),
79        }
80    }
81
82    /// Creates a new NetworkRepoModel with Solana configuration.
83    ///
84    /// # Arguments
85    /// * `config` - The Solana network configuration
86    ///
87    /// # Returns
88    /// A new NetworkRepoModel instance
89    pub fn new_solana(config: SolanaNetworkConfig) -> Self {
90        let name = config.common.network.clone();
91        let id = format!("solana:{name}").to_lowercase();
92        Self {
93            id,
94            name,
95            network_type: NetworkType::Solana,
96            config: NetworkConfigData::Solana(config),
97        }
98    }
99
100    /// Creates a new NetworkRepoModel with Stellar configuration.
101    ///
102    /// # Arguments
103    /// * `config` - The Stellar network configuration
104    ///
105    /// # Returns
106    /// A new NetworkRepoModel instance
107    pub fn new_stellar(config: StellarNetworkConfig) -> Self {
108        let name = config.common.network.clone();
109        let id = format!("stellar:{name}").to_lowercase();
110        Self {
111            id,
112            name,
113            network_type: NetworkType::Stellar,
114            config: NetworkConfigData::Stellar(config),
115        }
116    }
117
118    /// Creates an ID string from network type and name.
119    ///
120    /// # Arguments
121    /// * `network_type` - The type of network
122    /// * `name` - The name of the network
123    ///
124    /// # Returns
125    /// A lowercase string ID in format "network_type:name"
126    pub fn create_id(network_type: NetworkType, name: &str) -> String {
127        format!("{network_type:?}:{name}").to_lowercase()
128    }
129
130    /// Returns the common network configuration.
131    pub fn common(&self) -> &NetworkConfigCommon {
132        self.config.common()
133    }
134
135    /// Returns the network configuration data.
136    pub fn config(&self) -> &NetworkConfigData {
137        &self.config
138    }
139}
140
141impl TryFrom<NetworkFileConfig> for NetworkRepoModel {
142    type Error = eyre::Report;
143
144    /// Converts a NetworkFileConfig into a NetworkRepoModel.
145    ///
146    /// # Arguments
147    /// * `network_config` - The network file configuration to convert
148    ///
149    /// # Returns
150    /// Result containing the NetworkRepoModel or an error
151    fn try_from(network_config: NetworkFileConfig) -> Result<Self, Self::Error> {
152        match network_config {
153            NetworkFileConfig::Evm(evm_config) => Ok(Self::new_evm(evm_config)),
154            NetworkFileConfig::Solana(solana_config) => Ok(Self::new_solana(solana_config)),
155            NetworkFileConfig::Stellar(stellar_config) => Ok(Self::new_stellar(stellar_config)),
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    fn create_evm_config(name: &str, chain_id: u64, symbol: &str) -> EvmNetworkConfig {
165        EvmNetworkConfig {
166            common: NetworkConfigCommon {
167                network: name.to_string(),
168                from: None,
169                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
170                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
171                average_blocktime_ms: Some(12000),
172                is_testnet: Some(false),
173                tags: Some(vec!["mainnet".to_string()]),
174            },
175            chain_id: Some(chain_id),
176            required_confirmations: Some(12),
177            features: Some(vec!["eip1559".to_string()]),
178            symbol: Some(symbol.to_string()),
179            gas_price_cache: None,
180        }
181    }
182
183    fn create_solana_config(name: &str, is_testnet: bool) -> SolanaNetworkConfig {
184        SolanaNetworkConfig {
185            common: NetworkConfigCommon {
186                network: name.to_string(),
187                from: None,
188                rpc_urls: Some(vec!["https://api.mainnet-beta.solana.com".to_string()]),
189                explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
190                average_blocktime_ms: Some(400),
191                is_testnet: Some(is_testnet),
192                tags: Some(vec!["solana".to_string()]),
193            },
194        }
195    }
196
197    fn create_stellar_config(name: &str, passphrase: Option<&str>) -> StellarNetworkConfig {
198        StellarNetworkConfig {
199            common: NetworkConfigCommon {
200                network: name.to_string(),
201                from: None,
202                rpc_urls: Some(vec!["https://horizon.stellar.org".to_string()]),
203                explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
204                average_blocktime_ms: Some(5000),
205                is_testnet: Some(passphrase.is_none()),
206                tags: Some(vec!["stellar".to_string()]),
207            },
208            passphrase: passphrase.map(|s| s.to_string()),
209            horizon_url: Some("https://horizon.stellar.org".to_string()),
210        }
211    }
212
213    #[test]
214    fn test_network_config_data_evm() {
215        let config = create_evm_config("mainnet", 1, "ETH");
216        let config_data = NetworkConfigData::Evm(config);
217
218        assert_eq!(config_data.network_name(), "mainnet");
219        assert_eq!(config_data.network_type(), NetworkType::Evm);
220        assert_eq!(config_data.common().network, "mainnet");
221        assert_eq!(config_data.common().is_testnet, Some(false));
222    }
223
224    #[test]
225    fn test_network_config_data_solana() {
226        let config = create_solana_config("devnet", true);
227        let config_data = NetworkConfigData::Solana(config);
228
229        assert_eq!(config_data.network_name(), "devnet");
230        assert_eq!(config_data.network_type(), NetworkType::Solana);
231        assert_eq!(config_data.common().is_testnet, Some(true));
232    }
233
234    #[test]
235    fn test_network_config_data_stellar() {
236        let config = create_stellar_config("testnet", None);
237        let config_data = NetworkConfigData::Stellar(config);
238
239        assert_eq!(config_data.network_name(), "testnet");
240        assert_eq!(config_data.network_type(), NetworkType::Stellar);
241        assert_eq!(config_data.common().is_testnet, Some(true));
242    }
243
244    #[test]
245    fn test_new_evm() {
246        let config = create_evm_config("mainnet", 1, "ETH");
247        let network_repo = NetworkRepoModel::new_evm(config);
248
249        assert_eq!(network_repo.name, "mainnet");
250        assert_eq!(network_repo.network_type, NetworkType::Evm);
251        assert_eq!(network_repo.id, "evm:mainnet");
252
253        match network_repo.config() {
254            NetworkConfigData::Evm(evm_config) => {
255                assert_eq!(evm_config.chain_id, Some(1));
256                assert_eq!(evm_config.symbol, Some("ETH".to_string()));
257            }
258            _ => panic!("Expected EVM config"),
259        }
260    }
261
262    #[test]
263    fn test_new_solana() {
264        let config = create_solana_config("devnet", true);
265        let network_repo = NetworkRepoModel::new_solana(config);
266
267        assert_eq!(network_repo.name, "devnet");
268        assert_eq!(network_repo.network_type, NetworkType::Solana);
269        assert_eq!(network_repo.id, "solana:devnet");
270
271        match network_repo.config() {
272            NetworkConfigData::Solana(solana_config) => {
273                assert_eq!(solana_config.common.is_testnet, Some(true));
274            }
275            _ => panic!("Expected Solana config"),
276        }
277    }
278
279    #[test]
280    fn test_new_stellar() {
281        let config = create_stellar_config(
282            "mainnet",
283            Some("Public Global Stellar Network ; September 2015"),
284        );
285        let network_repo = NetworkRepoModel::new_stellar(config);
286
287        assert_eq!(network_repo.name, "mainnet");
288        assert_eq!(network_repo.network_type, NetworkType::Stellar);
289        assert_eq!(network_repo.id, "stellar:mainnet");
290
291        match network_repo.config() {
292            NetworkConfigData::Stellar(stellar_config) => {
293                assert_eq!(
294                    stellar_config.passphrase,
295                    Some("Public Global Stellar Network ; September 2015".to_string())
296                );
297            }
298            _ => panic!("Expected Stellar config"),
299        }
300    }
301
302    #[test]
303    fn test_create_id() {
304        assert_eq!(
305            NetworkRepoModel::create_id(NetworkType::Evm, "Mainnet"),
306            "evm:mainnet"
307        );
308        assert_eq!(
309            NetworkRepoModel::create_id(NetworkType::Solana, "DEVNET"),
310            "solana:devnet"
311        );
312        assert_eq!(
313            NetworkRepoModel::create_id(NetworkType::Stellar, "TestNet"),
314            "stellar:testnet"
315        );
316    }
317
318    #[test]
319    fn test_create_id_with_special_characters() {
320        assert_eq!(
321            NetworkRepoModel::create_id(NetworkType::Evm, "My-Network_123"),
322            "evm:my-network_123"
323        );
324        assert_eq!(
325            NetworkRepoModel::create_id(NetworkType::Solana, "Test Network"),
326            "solana:test network"
327        );
328    }
329
330    #[test]
331    fn test_common_method() {
332        let config = create_evm_config("mainnet", 1, "ETH");
333        let network_repo = NetworkRepoModel::new_evm(config);
334
335        let common = network_repo.common();
336        assert_eq!(common.network, "mainnet");
337        assert_eq!(common.is_testnet, Some(false));
338        assert_eq!(common.average_blocktime_ms, Some(12000));
339        assert_eq!(
340            common.rpc_urls,
341            Some(vec!["https://rpc.example.com".to_string()])
342        );
343    }
344
345    #[test]
346    fn test_config_method() {
347        let config = create_evm_config("mainnet", 1, "ETH");
348        let network_repo = NetworkRepoModel::new_evm(config);
349
350        let config_data = network_repo.config();
351        assert!(matches!(config_data, NetworkConfigData::Evm(_)));
352        assert_eq!(config_data.network_type(), NetworkType::Evm);
353        assert_eq!(config_data.network_name(), "mainnet");
354    }
355
356    #[test]
357    fn test_try_from_evm() {
358        let evm_config = create_evm_config("mainnet", 1, "ETH");
359        let network_file_config = NetworkFileConfig::Evm(evm_config);
360
361        let result = NetworkRepoModel::try_from(network_file_config);
362        assert!(result.is_ok());
363
364        let network_repo = result.unwrap();
365        assert_eq!(network_repo.name, "mainnet");
366        assert_eq!(network_repo.network_type, NetworkType::Evm);
367        assert_eq!(network_repo.id, "evm:mainnet");
368    }
369
370    #[test]
371    fn test_try_from_solana() {
372        let solana_config = create_solana_config("devnet", true);
373        let network_file_config = NetworkFileConfig::Solana(solana_config);
374
375        let result = NetworkRepoModel::try_from(network_file_config);
376        assert!(result.is_ok());
377
378        let network_repo = result.unwrap();
379        assert_eq!(network_repo.name, "devnet");
380        assert_eq!(network_repo.network_type, NetworkType::Solana);
381        assert_eq!(network_repo.id, "solana:devnet");
382    }
383
384    #[test]
385    fn test_try_from_stellar() {
386        let stellar_config = create_stellar_config("testnet", None);
387        let network_file_config = NetworkFileConfig::Stellar(stellar_config);
388
389        let result = NetworkRepoModel::try_from(network_file_config);
390        assert!(result.is_ok());
391
392        let network_repo = result.unwrap();
393        assert_eq!(network_repo.name, "testnet");
394        assert_eq!(network_repo.network_type, NetworkType::Stellar);
395        assert_eq!(network_repo.id, "stellar:testnet");
396    }
397
398    #[test]
399    fn test_serialization_roundtrip() {
400        let config = create_evm_config("mainnet", 1, "ETH");
401        let network_repo = NetworkRepoModel::new_evm(config);
402
403        let serialized = serde_json::to_string(&network_repo).unwrap();
404        let deserialized: NetworkRepoModel = serde_json::from_str(&serialized).unwrap();
405
406        assert_eq!(network_repo.id, deserialized.id);
407        assert_eq!(network_repo.name, deserialized.name);
408        assert_eq!(network_repo.network_type, deserialized.network_type);
409    }
410
411    #[test]
412    fn test_clone() {
413        let config = create_evm_config("mainnet", 1, "ETH");
414        let network_repo = NetworkRepoModel::new_evm(config);
415        let cloned = network_repo.clone();
416
417        assert_eq!(network_repo.id, cloned.id);
418        assert_eq!(network_repo.name, cloned.name);
419        assert_eq!(network_repo.network_type, cloned.network_type);
420    }
421
422    #[test]
423    fn test_debug() {
424        let config = create_evm_config("mainnet", 1, "ETH");
425        let network_repo = NetworkRepoModel::new_evm(config);
426
427        let debug_str = format!("{:?}", network_repo);
428        assert!(debug_str.contains("NetworkRepoModel"));
429        assert!(debug_str.contains("mainnet"));
430        assert!(debug_str.contains("Evm"));
431    }
432
433    #[test]
434    fn test_network_types_consistency() {
435        let evm_config = create_evm_config("mainnet", 1, "ETH");
436        let solana_config = create_solana_config("devnet", true);
437        let stellar_config = create_stellar_config("testnet", None);
438
439        let evm_repo = NetworkRepoModel::new_evm(evm_config);
440        let solana_repo = NetworkRepoModel::new_solana(solana_config);
441        let stellar_repo = NetworkRepoModel::new_stellar(stellar_config);
442
443        assert_eq!(evm_repo.network_type, evm_repo.config().network_type());
444        assert_eq!(
445            solana_repo.network_type,
446            solana_repo.config().network_type()
447        );
448        assert_eq!(
449            stellar_repo.network_type,
450            stellar_repo.config().network_type()
451        );
452    }
453
454    #[test]
455    fn test_empty_optional_fields() {
456        let minimal_config = EvmNetworkConfig {
457            common: NetworkConfigCommon {
458                network: "minimal".to_string(),
459                from: None,
460                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
461                explorer_urls: None,
462                average_blocktime_ms: None,
463                is_testnet: None,
464                tags: None,
465            },
466            chain_id: Some(1),
467            required_confirmations: Some(1),
468            features: None,
469            symbol: Some("ETH".to_string()),
470            gas_price_cache: None,
471        };
472
473        let network_repo = NetworkRepoModel::new_evm(minimal_config);
474        assert_eq!(network_repo.name, "minimal");
475        assert_eq!(network_repo.common().explorer_urls, None);
476        assert_eq!(network_repo.common().tags, None);
477    }
478}