openzeppelin_relayer/models/network/evm/
network.rs

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