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