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        use crate::models::RpcConfig;
166        EvmNetworkConfig {
167            common: NetworkConfigCommon {
168                network: name.to_string(),
169                from: None,
170                rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
171                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
172                average_blocktime_ms: Some(12000),
173                is_testnet: Some(false),
174                tags: Some(vec!["mainnet".to_string()]),
175            },
176            chain_id: Some(chain_id),
177            required_confirmations: Some(12),
178            features: Some(vec!["eip1559".to_string()]),
179            symbol: Some(symbol.to_string()),
180            gas_price_cache: None,
181        }
182    }
183
184    fn create_solana_config(name: &str, is_testnet: bool) -> SolanaNetworkConfig {
185        use crate::models::RpcConfig;
186        SolanaNetworkConfig {
187            common: NetworkConfigCommon {
188                network: name.to_string(),
189                from: None,
190                rpc_urls: Some(vec![RpcConfig::new(
191                    "https://api.mainnet-beta.solana.com".to_string(),
192                )]),
193                explorer_urls: Some(vec!["https://explorer.solana.com".to_string()]),
194                average_blocktime_ms: Some(400),
195                is_testnet: Some(is_testnet),
196                tags: Some(vec!["solana".to_string()]),
197            },
198        }
199    }
200
201    fn create_stellar_config(name: &str, passphrase: Option<&str>) -> StellarNetworkConfig {
202        use crate::models::RpcConfig;
203        StellarNetworkConfig {
204            common: NetworkConfigCommon {
205                network: name.to_string(),
206                from: None,
207                rpc_urls: Some(vec![RpcConfig::new(
208                    "https://horizon.stellar.org".to_string(),
209                )]),
210                explorer_urls: Some(vec!["https://stellarchain.io".to_string()]),
211                average_blocktime_ms: Some(5000),
212                is_testnet: Some(passphrase.is_none()),
213                tags: Some(vec!["stellar".to_string()]),
214            },
215            passphrase: passphrase.map(|s| s.to_string()),
216            horizon_url: Some("https://horizon.stellar.org".to_string()),
217        }
218    }
219
220    #[test]
221    fn test_network_config_data_evm() {
222        let config = create_evm_config("mainnet", 1, "ETH");
223        let config_data = NetworkConfigData::Evm(config);
224
225        assert_eq!(config_data.network_name(), "mainnet");
226        assert_eq!(config_data.network_type(), NetworkType::Evm);
227        assert_eq!(config_data.common().network, "mainnet");
228        assert_eq!(config_data.common().is_testnet, Some(false));
229    }
230
231    #[test]
232    fn test_network_config_data_solana() {
233        let config = create_solana_config("devnet", true);
234        let config_data = NetworkConfigData::Solana(config);
235
236        assert_eq!(config_data.network_name(), "devnet");
237        assert_eq!(config_data.network_type(), NetworkType::Solana);
238        assert_eq!(config_data.common().is_testnet, Some(true));
239    }
240
241    #[test]
242    fn test_network_config_data_stellar() {
243        let config = create_stellar_config("testnet", None);
244        let config_data = NetworkConfigData::Stellar(config);
245
246        assert_eq!(config_data.network_name(), "testnet");
247        assert_eq!(config_data.network_type(), NetworkType::Stellar);
248        assert_eq!(config_data.common().is_testnet, Some(true));
249    }
250
251    #[test]
252    fn test_new_evm() {
253        let config = create_evm_config("mainnet", 1, "ETH");
254        let network_repo = NetworkRepoModel::new_evm(config);
255
256        assert_eq!(network_repo.name, "mainnet");
257        assert_eq!(network_repo.network_type, NetworkType::Evm);
258        assert_eq!(network_repo.id, "evm:mainnet");
259
260        match network_repo.config() {
261            NetworkConfigData::Evm(evm_config) => {
262                assert_eq!(evm_config.chain_id, Some(1));
263                assert_eq!(evm_config.symbol, Some("ETH".to_string()));
264            }
265            _ => panic!("Expected EVM config"),
266        }
267    }
268
269    #[test]
270    fn test_new_solana() {
271        let config = create_solana_config("devnet", true);
272        let network_repo = NetworkRepoModel::new_solana(config);
273
274        assert_eq!(network_repo.name, "devnet");
275        assert_eq!(network_repo.network_type, NetworkType::Solana);
276        assert_eq!(network_repo.id, "solana:devnet");
277
278        match network_repo.config() {
279            NetworkConfigData::Solana(solana_config) => {
280                assert_eq!(solana_config.common.is_testnet, Some(true));
281            }
282            _ => panic!("Expected Solana config"),
283        }
284    }
285
286    #[test]
287    fn test_new_stellar() {
288        let config = create_stellar_config(
289            "mainnet",
290            Some("Public Global Stellar Network ; September 2015"),
291        );
292        let network_repo = NetworkRepoModel::new_stellar(config);
293
294        assert_eq!(network_repo.name, "mainnet");
295        assert_eq!(network_repo.network_type, NetworkType::Stellar);
296        assert_eq!(network_repo.id, "stellar:mainnet");
297
298        match network_repo.config() {
299            NetworkConfigData::Stellar(stellar_config) => {
300                assert_eq!(
301                    stellar_config.passphrase,
302                    Some("Public Global Stellar Network ; September 2015".to_string())
303                );
304            }
305            _ => panic!("Expected Stellar config"),
306        }
307    }
308
309    #[test]
310    fn test_create_id() {
311        assert_eq!(
312            NetworkRepoModel::create_id(NetworkType::Evm, "Mainnet"),
313            "evm:mainnet"
314        );
315        assert_eq!(
316            NetworkRepoModel::create_id(NetworkType::Solana, "DEVNET"),
317            "solana:devnet"
318        );
319        assert_eq!(
320            NetworkRepoModel::create_id(NetworkType::Stellar, "TestNet"),
321            "stellar:testnet"
322        );
323    }
324
325    #[test]
326    fn test_create_id_with_special_characters() {
327        assert_eq!(
328            NetworkRepoModel::create_id(NetworkType::Evm, "My-Network_123"),
329            "evm:my-network_123"
330        );
331        assert_eq!(
332            NetworkRepoModel::create_id(NetworkType::Solana, "Test Network"),
333            "solana:test network"
334        );
335    }
336
337    #[test]
338    fn test_common_method() {
339        let config = create_evm_config("mainnet", 1, "ETH");
340        let network_repo = NetworkRepoModel::new_evm(config);
341
342        let common = network_repo.common();
343        assert_eq!(common.network, "mainnet");
344        assert_eq!(common.is_testnet, Some(false));
345        assert_eq!(common.average_blocktime_ms, Some(12000));
346        use crate::models::RpcConfig;
347        assert_eq!(
348            common.rpc_urls,
349            Some(vec![RpcConfig::new("https://rpc.example.com".to_string())])
350        );
351    }
352
353    #[test]
354    fn test_config_method() {
355        let config = create_evm_config("mainnet", 1, "ETH");
356        let network_repo = NetworkRepoModel::new_evm(config);
357
358        let config_data = network_repo.config();
359        assert!(matches!(config_data, NetworkConfigData::Evm(_)));
360        assert_eq!(config_data.network_type(), NetworkType::Evm);
361        assert_eq!(config_data.network_name(), "mainnet");
362    }
363
364    #[test]
365    fn test_try_from_evm() {
366        let evm_config = create_evm_config("mainnet", 1, "ETH");
367        let network_file_config = NetworkFileConfig::Evm(evm_config);
368
369        let result = NetworkRepoModel::try_from(network_file_config);
370        assert!(result.is_ok());
371
372        let network_repo = result.unwrap();
373        assert_eq!(network_repo.name, "mainnet");
374        assert_eq!(network_repo.network_type, NetworkType::Evm);
375        assert_eq!(network_repo.id, "evm:mainnet");
376    }
377
378    #[test]
379    fn test_try_from_solana() {
380        let solana_config = create_solana_config("devnet", true);
381        let network_file_config = NetworkFileConfig::Solana(solana_config);
382
383        let result = NetworkRepoModel::try_from(network_file_config);
384        assert!(result.is_ok());
385
386        let network_repo = result.unwrap();
387        assert_eq!(network_repo.name, "devnet");
388        assert_eq!(network_repo.network_type, NetworkType::Solana);
389        assert_eq!(network_repo.id, "solana:devnet");
390    }
391
392    #[test]
393    fn test_try_from_stellar() {
394        let stellar_config = create_stellar_config("testnet", None);
395        let network_file_config = NetworkFileConfig::Stellar(stellar_config);
396
397        let result = NetworkRepoModel::try_from(network_file_config);
398        assert!(result.is_ok());
399
400        let network_repo = result.unwrap();
401        assert_eq!(network_repo.name, "testnet");
402        assert_eq!(network_repo.network_type, NetworkType::Stellar);
403        assert_eq!(network_repo.id, "stellar:testnet");
404    }
405
406    #[test]
407    fn test_serialization_roundtrip() {
408        let config = create_evm_config("mainnet", 1, "ETH");
409        let network_repo = NetworkRepoModel::new_evm(config);
410
411        let serialized = serde_json::to_string(&network_repo).unwrap();
412        let deserialized: NetworkRepoModel = serde_json::from_str(&serialized).unwrap();
413
414        assert_eq!(network_repo.id, deserialized.id);
415        assert_eq!(network_repo.name, deserialized.name);
416        assert_eq!(network_repo.network_type, deserialized.network_type);
417    }
418
419    #[test]
420    fn test_clone() {
421        let config = create_evm_config("mainnet", 1, "ETH");
422        let network_repo = NetworkRepoModel::new_evm(config);
423        let cloned = network_repo.clone();
424
425        assert_eq!(network_repo.id, cloned.id);
426        assert_eq!(network_repo.name, cloned.name);
427        assert_eq!(network_repo.network_type, cloned.network_type);
428    }
429
430    #[test]
431    fn test_debug() {
432        let config = create_evm_config("mainnet", 1, "ETH");
433        let network_repo = NetworkRepoModel::new_evm(config);
434
435        let debug_str = format!("{network_repo:?}");
436        assert!(debug_str.contains("NetworkRepoModel"));
437        assert!(debug_str.contains("mainnet"));
438        assert!(debug_str.contains("Evm"));
439    }
440
441    #[test]
442    fn test_network_types_consistency() {
443        let evm_config = create_evm_config("mainnet", 1, "ETH");
444        let solana_config = create_solana_config("devnet", true);
445        let stellar_config = create_stellar_config("testnet", None);
446
447        let evm_repo = NetworkRepoModel::new_evm(evm_config);
448        let solana_repo = NetworkRepoModel::new_solana(solana_config);
449        let stellar_repo = NetworkRepoModel::new_stellar(stellar_config);
450
451        assert_eq!(evm_repo.network_type, evm_repo.config().network_type());
452        assert_eq!(
453            solana_repo.network_type,
454            solana_repo.config().network_type()
455        );
456        assert_eq!(
457            stellar_repo.network_type,
458            stellar_repo.config().network_type()
459        );
460    }
461
462    #[test]
463    fn test_empty_optional_fields() {
464        use crate::models::RpcConfig;
465        let minimal_config = EvmNetworkConfig {
466            common: NetworkConfigCommon {
467                network: "minimal".to_string(),
468                from: None,
469                rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
470                explorer_urls: None,
471                average_blocktime_ms: None,
472                is_testnet: None,
473                tags: None,
474            },
475            chain_id: Some(1),
476            required_confirmations: Some(1),
477            features: None,
478            symbol: Some("ETH".to_string()),
479            gas_price_cache: None,
480        };
481
482        let network_repo = NetworkRepoModel::new_evm(minimal_config);
483        assert_eq!(network_repo.name, "minimal");
484        assert_eq!(network_repo.common().explorer_urls, None);
485        assert_eq!(network_repo.common().tags, None);
486    }
487}