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, RpcConfig};
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 configurations for connecting to the network.
15    pub rpc_urls: Vec<RpcConfig>,
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<&[RpcConfig]> {
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        use crate::models::RpcConfig;
178        EvmNetwork {
179            network: "test-network".to_string(),
180            rpc_urls: vec![RpcConfig::new("https://rpc.example.com".to_string())],
181            explorer_urls: None,
182            average_blocktime_ms: 12000,
183            is_testnet: false,
184            tags: tags.into_iter().map(|s| s.to_string()).collect(),
185            chain_id: 1,
186            required_confirmations: 1,
187            features: vec!["eip1559".to_string()],
188            symbol: "ETH".to_string(),
189            gas_price_cache: None,
190        }
191    }
192
193    #[test]
194    fn test_is_optimism_with_optimism_tag() {
195        let network = create_test_evm_network_with_tags(vec![OPTIMISM_BASED_TAG, ROLLUP_TAG]);
196        assert!(network.is_optimism());
197    }
198
199    #[test]
200    fn test_is_optimism_without_optimism_tag() {
201        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, "mainnet"]);
202        assert!(!network.is_optimism());
203    }
204
205    #[test]
206    fn test_is_optimism_with_deprecated_optimism_tag() {
207        let network = create_test_evm_network_with_tags(vec![OPTIMISM_TAG, ROLLUP_TAG]);
208        assert!(network.is_optimism());
209    }
210
211    #[test]
212    fn test_lacks_mempool_with_deprecated_optimism_tag() {
213        let network = create_test_evm_network_with_tags(vec![OPTIMISM_TAG, ROLLUP_TAG]);
214        assert!(network.lacks_mempool());
215    }
216
217    #[test]
218    fn test_is_rollup_with_rollup_tag() {
219        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, NO_MEMPOOL_TAG]);
220        assert!(network.is_rollup());
221    }
222
223    #[test]
224    fn test_is_rollup_without_rollup_tag() {
225        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
226        assert!(!network.is_rollup());
227    }
228
229    #[test]
230    fn test_lacks_mempool_with_no_mempool_tag() {
231        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, NO_MEMPOOL_TAG]);
232        assert!(network.lacks_mempool());
233    }
234
235    #[test]
236    fn test_lacks_mempool_without_no_mempool_tag() {
237        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG]);
238        assert!(!network.lacks_mempool());
239    }
240
241    #[test]
242    fn test_arbitrum_like_network() {
243        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, ARBITRUM_BASED_TAG]);
244        assert!(network.is_rollup());
245        assert!(network.is_arbitrum());
246        assert!(network.lacks_mempool());
247        assert!(!network.is_optimism());
248    }
249
250    #[test]
251    fn test_optimism_like_network() {
252        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, OPTIMISM_BASED_TAG]);
253        assert!(network.is_rollup());
254        assert!(network.is_optimism());
255        assert!(network.lacks_mempool());
256    }
257
258    #[test]
259    fn test_polygon_zkevm_network() {
260        let network = create_test_evm_network_with_tags(vec![ROLLUP_TAG, POLYGON_ZKEVM_TAG]);
261        assert!(network.is_rollup());
262        assert!(network.is_polygon_zkevm());
263        assert!(!network.lacks_mempool());
264        assert!(!network.is_optimism());
265        assert!(!network.is_arbitrum());
266    }
267
268    #[test]
269    fn test_ethereum_mainnet_like_network() {
270        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
271        assert!(!network.is_rollup());
272        assert!(!network.is_optimism());
273        assert!(!network.lacks_mempool());
274    }
275
276    #[test]
277    fn test_empty_tags() {
278        let network = create_test_evm_network_with_tags(vec![]);
279        assert!(!network.is_rollup());
280        assert!(!network.is_optimism());
281        assert!(!network.lacks_mempool());
282    }
283
284    #[test]
285    fn test_try_from_with_tags() {
286        use crate::models::RpcConfig;
287        let config = EvmNetworkConfig {
288            common: NetworkConfigCommon {
289                network: "test-network".to_string(),
290                from: None,
291                rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
292                explorer_urls: None,
293                average_blocktime_ms: Some(12000),
294                is_testnet: Some(false),
295                tags: Some(vec![ROLLUP_TAG.to_string(), OPTIMISM_BASED_TAG.to_string()]),
296            },
297            chain_id: Some(10),
298            required_confirmations: Some(1),
299            features: Some(vec!["eip1559".to_string()]),
300            symbol: Some("ETH".to_string()),
301            gas_price_cache: None,
302        };
303
304        let repo_model = NetworkRepoModel {
305            id: "evm:test-network".to_string(),
306            name: "test-network".to_string(),
307            network_type: NetworkType::Evm,
308            config: NetworkConfigData::Evm(config),
309        };
310
311        let network = EvmNetwork::try_from(repo_model).unwrap();
312        assert!(network.is_optimism());
313        assert!(network.is_rollup());
314        assert!(network.lacks_mempool());
315    }
316}