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_arbitrum(&self) -> bool {
108        self.tags.contains(&"arbitrum-based".to_string())
109    }
110
111    pub fn is_testnet(&self) -> bool {
112        self.is_testnet
113    }
114
115    /// Returns the recommended number of confirmations needed for this network.
116    pub fn required_confirmations(&self) -> u64 {
117        self.required_confirmations
118    }
119
120    pub fn id(&self) -> u64 {
121        self.chain_id
122    }
123
124    pub fn average_blocktime(&self) -> Option<Duration> {
125        Some(Duration::from_millis(self.average_blocktime_ms))
126    }
127
128    pub fn is_legacy(&self) -> bool {
129        !self.features.contains(&"eip1559".to_string())
130    }
131
132    pub fn explorer_urls(&self) -> Option<&[String]> {
133        self.explorer_urls.as_deref()
134    }
135
136    pub fn public_rpc_urls(&self) -> Option<&[String]> {
137        if self.rpc_urls.is_empty() {
138            None
139        } else {
140            Some(&self.rpc_urls)
141        }
142    }
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148    use crate::config::{EvmNetworkConfig, NetworkConfigCommon};
149    use crate::models::{NetworkConfigData, NetworkRepoModel, NetworkType};
150
151    fn create_test_evm_network_with_tags(tags: Vec<&str>) -> EvmNetwork {
152        EvmNetwork {
153            network: "test-network".to_string(),
154            rpc_urls: vec!["https://rpc.example.com".to_string()],
155            explorer_urls: None,
156            average_blocktime_ms: 12000,
157            is_testnet: false,
158            tags: tags.into_iter().map(|s| s.to_string()).collect(),
159            chain_id: 1,
160            required_confirmations: 1,
161            features: vec!["eip1559".to_string()],
162            symbol: "ETH".to_string(),
163        }
164    }
165
166    #[test]
167    fn test_is_optimism_with_optimism_tag() {
168        let network = create_test_evm_network_with_tags(vec!["optimism", "rollup"]);
169        assert!(network.is_optimism());
170    }
171
172    #[test]
173    fn test_is_optimism_without_optimism_tag() {
174        let network = create_test_evm_network_with_tags(vec!["rollup", "mainnet"]);
175        assert!(!network.is_optimism());
176    }
177
178    #[test]
179    fn test_is_rollup_with_rollup_tag() {
180        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
181        assert!(network.is_rollup());
182    }
183
184    #[test]
185    fn test_is_rollup_without_rollup_tag() {
186        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
187        assert!(!network.is_rollup());
188    }
189
190    #[test]
191    fn test_lacks_mempool_with_no_mempool_tag() {
192        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
193        assert!(network.lacks_mempool());
194    }
195
196    #[test]
197    fn test_lacks_mempool_without_no_mempool_tag() {
198        let network = create_test_evm_network_with_tags(vec!["rollup", "optimism"]);
199        assert!(!network.lacks_mempool());
200    }
201
202    #[test]
203    fn test_arbitrum_like_network() {
204        let network = create_test_evm_network_with_tags(vec!["rollup", "no-mempool"]);
205        assert!(network.is_rollup());
206        assert!(network.lacks_mempool());
207        assert!(!network.is_optimism());
208    }
209
210    #[test]
211    fn test_optimism_like_network() {
212        let network = create_test_evm_network_with_tags(vec!["rollup", "optimism"]);
213        assert!(network.is_rollup());
214        assert!(network.is_optimism());
215        assert!(!network.lacks_mempool());
216    }
217
218    #[test]
219    fn test_ethereum_mainnet_like_network() {
220        let network = create_test_evm_network_with_tags(vec!["mainnet", "ethereum"]);
221        assert!(!network.is_rollup());
222        assert!(!network.is_optimism());
223        assert!(!network.lacks_mempool());
224    }
225
226    #[test]
227    fn test_empty_tags() {
228        let network = create_test_evm_network_with_tags(vec![]);
229        assert!(!network.is_rollup());
230        assert!(!network.is_optimism());
231        assert!(!network.lacks_mempool());
232    }
233
234    #[test]
235    fn test_try_from_with_tags() {
236        let config = EvmNetworkConfig {
237            common: NetworkConfigCommon {
238                network: "test-network".to_string(),
239                from: None,
240                rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
241                explorer_urls: None,
242                average_blocktime_ms: Some(12000),
243                is_testnet: Some(false),
244                tags: Some(vec!["rollup".to_string(), "optimism".to_string()]),
245            },
246            chain_id: Some(10),
247            required_confirmations: Some(1),
248            features: Some(vec!["eip1559".to_string()]),
249            symbol: Some("ETH".to_string()),
250        };
251
252        let repo_model = NetworkRepoModel {
253            id: "evm:test-network".to_string(),
254            name: "test-network".to_string(),
255            network_type: NetworkType::Evm,
256            config: NetworkConfigData::Evm(config),
257        };
258
259        let network = EvmNetwork::try_from(repo_model).unwrap();
260        assert!(network.is_optimism());
261        assert!(network.is_rollup());
262        assert!(!network.lacks_mempool());
263    }
264}