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