openzeppelin_relayer/models/network/evm/
network.rs

1use crate::models::{NetworkConfigData, NetworkRepoModel, RepositoryError};
2use std::time::Duration;
3
4#[derive(Clone, PartialEq, Eq, Hash, Debug)]
5pub struct EvmNetwork {
6    // Common network fields (flattened from NetworkConfigCommon)
7    /// Unique network identifier (e.g., "mainnet", "sepolia", "custom-devnet").
8    pub network: String,
9    /// List of RPC endpoint URLs for connecting to the network.
10    pub rpc_urls: Vec<String>,
11    /// List of Explorer endpoint URLs for connecting to the network.
12    pub explorer_urls: Option<Vec<String>>,
13    /// Estimated average time between blocks in milliseconds.
14    pub average_blocktime_ms: u64,
15    /// Flag indicating if the network is a testnet.
16    pub is_testnet: bool,
17    /// List of arbitrary tags for categorizing or filtering networks.
18    pub tags: Vec<String>,
19    /// The unique chain identifier (Chain ID) for the EVM network.
20    pub chain_id: u64,
21    /// Number of block confirmations required before a transaction is considered final.
22    pub required_confirmations: u64,
23    /// List of specific features supported by the network (e.g., "eip1559").
24    pub features: Vec<String>,
25    /// The symbol of the network's native currency (e.g., "ETH", "MATIC").
26    pub symbol: String,
27}
28
29impl TryFrom<NetworkRepoModel> for EvmNetwork {
30    type Error = RepositoryError;
31
32    /// Converts a NetworkRepoModel to an EvmNetwork.
33    ///
34    /// # Arguments
35    /// * `network_repo` - The repository model to convert
36    ///
37    /// # Returns
38    /// Result containing the EvmNetwork if successful, or a RepositoryError
39    fn try_from(network_repo: NetworkRepoModel) -> Result<Self, Self::Error> {
40        match &network_repo.config {
41            NetworkConfigData::Evm(evm_config) => {
42                let common = &evm_config.common;
43
44                let chain_id = evm_config.chain_id.ok_or_else(|| {
45                    RepositoryError::InvalidData(format!(
46                        "EVM network '{}' has no chain_id",
47                        network_repo.name
48                    ))
49                })?;
50
51                let required_confirmations =
52                    evm_config.required_confirmations.ok_or_else(|| {
53                        RepositoryError::InvalidData(format!(
54                            "EVM network '{}' has no required_confirmations",
55                            network_repo.name
56                        ))
57                    })?;
58
59                let symbol = evm_config.symbol.clone().ok_or_else(|| {
60                    RepositoryError::InvalidData(format!(
61                        "EVM network '{}' has no symbol",
62                        network_repo.name
63                    ))
64                })?;
65
66                let average_blocktime_ms = common.average_blocktime_ms.ok_or_else(|| {
67                    RepositoryError::InvalidData(format!(
68                        "EVM network '{}' has no average_blocktime_ms",
69                        network_repo.name
70                    ))
71                })?;
72
73                Ok(EvmNetwork {
74                    network: common.network.clone(),
75                    rpc_urls: common.rpc_urls.clone().unwrap_or_default(),
76                    explorer_urls: common.explorer_urls.clone(),
77                    average_blocktime_ms,
78                    is_testnet: common.is_testnet.unwrap_or(false),
79                    tags: common.tags.clone().unwrap_or_default(),
80                    chain_id,
81                    required_confirmations,
82                    features: evm_config.features.clone().unwrap_or_default(),
83                    symbol,
84                })
85            }
86            _ => Err(RepositoryError::InvalidData(format!(
87                "Network '{}' is not an EVM network",
88                network_repo.name
89            ))),
90        }
91    }
92}
93
94impl EvmNetwork {
95    pub fn is_optimism(&self) -> bool {
96        self.tags.contains(&"optimism".to_string())
97    }
98
99    pub fn is_rollup(&self) -> bool {
100        self.tags.contains(&"rollup".to_string())
101    }
102
103    pub fn lacks_mempool(&self) -> bool {
104        self.tags.contains(&"no-mempool".to_string())
105    }
106
107    pub fn is_testnet(&self) -> bool {
108        self.is_testnet
109    }
110
111    /// Returns the recommended number of confirmations needed for this network.
112    pub fn required_confirmations(&self) -> u64 {
113        self.required_confirmations
114    }
115
116    pub fn id(&self) -> u64 {
117        self.chain_id
118    }
119
120    pub fn average_blocktime(&self) -> Option<Duration> {
121        Some(Duration::from_millis(self.average_blocktime_ms))
122    }
123
124    pub fn is_legacy(&self) -> bool {
125        !self.features.contains(&"eip1559".to_string())
126    }
127
128    pub fn explorer_urls(&self) -> Option<&[String]> {
129        self.explorer_urls.as_deref()
130    }
131
132    pub fn public_rpc_urls(&self) -> Option<&[String]> {
133        if self.rpc_urls.is_empty() {
134            None
135        } else {
136            Some(&self.rpc_urls)
137        }
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
145    use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType};
146
147    fn create_test_evm_network_with_tags(tags: Vec<&str>) -> EvmNetwork {
148        EvmNetwork {
149            network: "test-network".to_string(),
150            rpc_urls: vec!["https://rpc.example.com".to_string()],
151            explorer_urls: None,
152            average_blocktime_ms: 12000,
153            is_testnet: false,
154            tags: tags.into_iter().map(|s| s.to_string()).collect(),
155            chain_id: 1,
156            required_confirmations: 1,
157            features: vec!["eip1559".to_string()],
158            symbol: "ETH".to_string(),
159        }
160    }
161
162    #[test]
163    fn test_is_optimism_with_optimism_tag() {
164        let network = create_test_evm_network_with_tags(vec!["optimism", "rollup"]);
165        assert!(network.is_optimism());
166    }
167
168    #[test]
169    fn test_is_optimism_without_optimism_tag() {
170        let network = create_test_evm_network_with_tags(vec!["rollup", "mainnet"]);
171        assert!(!network.is_optimism());
172    }
173
174    #[test]
175    fn test_is_rollup_with_rollup_tag() {
176        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
177        assert!(network.is_rollup());
178    }
179
180    #[test]
181    fn test_is_rollup_without_rollup_tag() {
182        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
183        assert!(!network.is_rollup());
184    }
185
186    #[test]
187    fn test_lacks_mempool_with_no_mempool_tag() {
188        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
189        assert!(network.lacks_mempool());
190    }
191
192    #[test]
193    fn test_lacks_mempool_without_no_mempool_tag() {
194        let network = create_test_evm_network_with_tags(vec!["rollup", "optimism"]);
195        assert!(!network.lacks_mempool());
196    }
197
198    #[test]
199    fn test_arbitrum_like_network() {
200        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
201        assert!(network.is_rollup());
202        assert!(network.lacks_mempool());
203        assert!(!network.is_optimism());
204    }
205
206    #[test]
207    fn test_optimism_like_network() {
208        let network = create_test_evm_network_with_tags(vec!["rollup", "optimism"]);
209        assert!(network.is_rollup());
210        assert!(network.is_optimism());
211        assert!(!network.lacks_mempool());
212    }
213
214    #[test]
215    fn test_ethereum_mainnet_like_network() {
216        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
217        assert!(!network.is_rollup());
218        assert!(!network.is_optimism());
219        assert!(!network.lacks_mempool());
220    }
221
222    #[test]
223    fn test_empty_tags() {
224        let network = create_test_evm_network_with_tags(vec![]);
225        assert!(!network.is_rollup());
226        assert!(!network.is_optimism());
227        assert!(!network.lacks_mempool());
228    }
229
230    #[test]
231    fn test_try_from_with_tags() {
232        let config = EvmNetworkConfig {
233            common: NetworkConfigCommon {
234                network: "test-network".to_string(),
235                from: None,
236                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
237                explorer_urls: None,
238                average_blocktime_ms: Some(12000),
239                is_testnet: Some(false),
240                tags: Some(vec!["rollup".to_string(), "optimism".to_string()]),
241            },
242            chain_id: Some(10),
243            required_confirmations: Some(1),
244            features: Some(vec!["eip1559".to_string()]),
245            symbol: Some("ETH".to_string()),
246        };
247
248        let repo_model = NetworkRepoModel {
249            id: "evm:test-network".to_string(),
250            name: "test-network".to_string(),
251            network_type: NetworkType::Evm,
252            config: NetworkConfigData::Evm(config),
253        };
254
255        let network = EvmNetwork::try_from(repo_model).unwrap();
256        assert!(network.is_optimism());
257        assert!(network.is_rollup());
258        assert!(!network.lacks_mempool());
259    }
260}