openzeppelin_relayer/config/config_file/network/
evm.rs

1//! EVM Network Configuration
2//!
3//! This module provides configuration support for EVM-compatible blockchain networks
4//! such as Ethereum, Polygon, BSC, Avalanche, and other Ethereum-compatible chains.
5//!
6//! ## Key Features
7//!
8//! - **Full inheritance support**: EVM networks can inherit from other EVM networks
9//! - **Feature merging**: Parent and child features are merged preserving unique items
10//! - **Type safety**: Inheritance only allowed between EVM networks
11
12use super::common::{merge_optional_string_vecs, NetworkConfigCommon};
13use crate::config::ConfigFileError;
14use serde::{Deserialize, Serialize};
15
16/// Default value for gas price cache enabled flag
17fn default_gas_cache_enabled() -> bool {
18    false
19}
20
21/// Default value for gas price cache stale after duration in milliseconds
22fn default_gas_cache_stale_after_ms() -> u64 {
23    20_000 // 20 seconds
24}
25
26/// Default value for gas price cache expire after duration in milliseconds
27fn default_gas_cache_expire_after_ms() -> u64 {
28    45_000 // 45 seconds
29}
30
31/// Configuration for gas price caching
32#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
33#[serde(deny_unknown_fields)]
34pub struct GasPriceCacheConfig {
35    /// Enable gas price caching for this network
36    #[serde(default = "default_gas_cache_enabled")]
37    pub enabled: bool,
38
39    /// When data becomes stale (milliseconds)
40    #[serde(default = "default_gas_cache_stale_after_ms")]
41    pub stale_after_ms: u64,
42
43    /// When to expire and force refresh (milliseconds)
44    #[serde(default = "default_gas_cache_expire_after_ms")]
45    pub expire_after_ms: u64,
46}
47
48impl Default for GasPriceCacheConfig {
49    fn default() -> Self {
50        Self {
51            enabled: default_gas_cache_enabled(),
52            stale_after_ms: default_gas_cache_stale_after_ms(),
53            expire_after_ms: default_gas_cache_expire_after_ms(),
54        }
55    }
56}
57
58impl GasPriceCacheConfig {
59    /// Validates the gas price cache configuration
60    ///
61    /// # Returns
62    /// - `Ok(())` if the configuration is valid
63    /// - `Err(ConfigFileError)` if validation fails
64    pub fn validate(&self) -> Result<(), ConfigFileError> {
65        // Check that durations are non-zero
66        if self.stale_after_ms == 0 {
67            return Err(ConfigFileError::InvalidFormat(
68                "Gas price cache stale_after_ms must be greater than zero".into(),
69            ));
70        }
71
72        if self.expire_after_ms == 0 {
73            return Err(ConfigFileError::InvalidFormat(
74                "Gas price cache expire_after_ms must be greater than zero".into(),
75            ));
76        }
77
78        // Check that expire_after_ms > stale_after_ms
79        if self.expire_after_ms <= self.stale_after_ms {
80            return Err(ConfigFileError::InvalidFormat(
81                "Gas price cache expire_after_ms must be greater than stale_after_ms".into(),
82            ));
83        }
84
85        Ok(())
86    }
87}
88
89/// Configuration specific to EVM-compatible networks.
90#[derive(Debug, Serialize, Deserialize, Clone)]
91#[serde(deny_unknown_fields)]
92pub struct EvmNetworkConfig {
93    /// Common network fields.
94    #[serde(flatten)]
95    pub common: NetworkConfigCommon,
96
97    /// The unique chain identifier (Chain ID) for the EVM network.
98    pub chain_id: Option<u64>,
99    /// Number of block confirmations required before a transaction is considered final.
100    pub required_confirmations: Option<u64>,
101    /// List of specific features supported by the network (e.g., "eip1559").
102    pub features: Option<Vec<String>>,
103    /// The symbol of the network's native currency (e.g., "ETH", "MATIC").
104    pub symbol: Option<String>,
105    /// Gas price cache configuration
106    pub gas_price_cache: Option<GasPriceCacheConfig>,
107}
108
109impl EvmNetworkConfig {
110    /// Validates the specific configuration fields for an EVM network.
111    ///
112    /// # Returns
113    /// - `Ok(())` if the EVM configuration is valid.
114    /// - `Err(ConfigFileError)` if validation fails (e.g., missing fields, invalid URLs).
115    pub fn validate(&self) -> Result<(), ConfigFileError> {
116        self.common.validate()?;
117
118        // Chain ID is required for non-inherited networks
119        if self.chain_id.is_none() {
120            return Err(ConfigFileError::MissingField("chain_id".into()));
121        }
122
123        if self.required_confirmations.is_none() {
124            return Err(ConfigFileError::MissingField(
125                "required_confirmations".into(),
126            ));
127        }
128
129        if self.symbol.is_none() || self.symbol.as_ref().unwrap_or(&String::new()).is_empty() {
130            return Err(ConfigFileError::MissingField("symbol".into()));
131        }
132
133        // Validate gas price cache configuration if present
134        if let Some(gas_price_cache) = &self.gas_price_cache {
135            gas_price_cache.validate()?;
136        }
137
138        Ok(())
139    }
140
141    /// Creates a new EVM configuration by merging this config with a parent, where child values override parent defaults.
142    ///
143    /// # Arguments
144    /// * `parent` - The parent EVM configuration to merge with.
145    ///
146    /// # Returns
147    /// A new `EvmNetworkConfig` with merged values where child takes precedence over parent.
148    pub fn merge_with_parent(&self, parent: &Self) -> Self {
149        Self {
150            common: self.common.merge_with_parent(&parent.common),
151            chain_id: self.chain_id.or(parent.chain_id),
152            required_confirmations: self
153                .required_confirmations
154                .or(parent.required_confirmations),
155            features: merge_optional_string_vecs(&self.features, &parent.features),
156            symbol: self.symbol.clone().or_else(|| parent.symbol.clone()),
157            gas_price_cache: self
158                .gas_price_cache
159                .clone()
160                .or_else(|| parent.gas_price_cache.clone()),
161        }
162    }
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::config::config_file::network::test_utils::*;
169    use crate::models::RpcConfig;
170    #[test]
171    fn test_validate_success_complete_config() {
172        let config = create_evm_network("ethereum-mainnet");
173        let result = config.validate();
174        assert!(result.is_ok());
175    }
176
177    #[test]
178    fn test_validate_success_minimal_config() {
179        let mut config = create_evm_network("minimal-evm");
180        config.features = None;
181        let result = config.validate();
182        assert!(result.is_ok());
183    }
184
185    #[test]
186    fn test_validate_missing_chain_id() {
187        let mut config = create_evm_network("ethereum-mainnet");
188        config.chain_id = None;
189
190        let result = config.validate();
191        assert!(result.is_err());
192        assert!(matches!(
193            result.unwrap_err(),
194            ConfigFileError::MissingField(_)
195        ));
196    }
197
198    #[test]
199    fn test_validate_missing_required_confirmations() {
200        let mut config = create_evm_network("ethereum-mainnet");
201        config.required_confirmations = None;
202
203        let result = config.validate();
204        assert!(result.is_err());
205        assert!(matches!(
206            result.unwrap_err(),
207            ConfigFileError::MissingField(_)
208        ));
209    }
210
211    #[test]
212    fn test_validate_missing_symbol() {
213        let mut config = create_evm_network("ethereum-mainnet");
214        config.symbol = None;
215
216        let result = config.validate();
217        assert!(result.is_err());
218        assert!(matches!(
219            result.unwrap_err(),
220            ConfigFileError::MissingField(_)
221        ));
222    }
223
224    #[test]
225    fn test_validate_invalid_common_fields() {
226        let mut config = create_evm_network("ethereum-mainnet");
227        config.common.network = String::new(); // Invalid empty network name
228
229        let result = config.validate();
230        assert!(result.is_err());
231        assert!(matches!(
232            result.unwrap_err(),
233            ConfigFileError::MissingField(_)
234        ));
235    }
236
237    #[test]
238    fn test_validate_invalid_rpc_urls() {
239        let mut config = create_evm_network("ethereum-mainnet");
240        config.common.rpc_urls = Some(vec![RpcConfig::new("invalid-url".to_string())]);
241
242        let result = config.validate();
243        assert!(result.is_err());
244        assert!(matches!(
245            result.unwrap_err(),
246            ConfigFileError::InvalidFormat(_)
247        ));
248    }
249
250    #[test]
251    fn test_validate_with_zero_chain_id() {
252        let mut config = create_evm_network("ethereum-mainnet");
253        config.chain_id = Some(0);
254
255        let result = config.validate();
256        assert!(result.is_ok()); // Zero is a valid chain ID
257    }
258
259    #[test]
260    fn test_validate_with_large_chain_id() {
261        let mut config = create_evm_network("ethereum-mainnet");
262        config.chain_id = Some(u64::MAX);
263
264        let result = config.validate();
265        assert!(result.is_ok());
266    }
267
268    #[test]
269    fn test_validate_with_zero_confirmations() {
270        let mut config = create_evm_network("ethereum-mainnet");
271        config.required_confirmations = Some(0);
272
273        let result = config.validate();
274        assert!(result.is_ok()); // Zero confirmations is valid
275    }
276
277    #[test]
278    fn test_validate_with_empty_features() {
279        let mut config = create_evm_network("ethereum-mainnet");
280        config.features = Some(vec![]);
281
282        let result = config.validate();
283        assert!(result.is_ok());
284    }
285
286    #[test]
287    fn test_validate_with_empty_symbol() {
288        let mut config = create_evm_network("ethereum-mainnet");
289        config.symbol = Some(String::new());
290
291        let result = config.validate();
292        assert!(result.is_err());
293    }
294
295    #[test]
296    fn test_merge_with_parent_child_overrides() {
297        let parent = EvmNetworkConfig {
298            common: NetworkConfigCommon {
299                network: "parent".to_string(),
300                from: Some("parent".to_string()),
301                rpc_urls: Some(vec![RpcConfig::new(
302                    "https://parent-rpc.example.com".to_string(),
303                )]),
304                explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
305                average_blocktime_ms: Some(10000),
306                is_testnet: Some(true),
307                tags: Some(vec!["parent-tag".to_string()]),
308            },
309            chain_id: Some(1),
310            required_confirmations: Some(6),
311            features: Some(vec!["legacy".to_string()]),
312            symbol: Some("PETH".to_string()),
313            gas_price_cache: Some(GasPriceCacheConfig {
314                enabled: true,
315                stale_after_ms: 20_000,
316                expire_after_ms: 100_000,
317            }),
318        };
319
320        let child = EvmNetworkConfig {
321            common: NetworkConfigCommon {
322                network: "child".to_string(),
323                from: Some("parent".to_string()),
324                rpc_urls: Some(vec![RpcConfig::new(
325                    "https://child-rpc.example.com".to_string(),
326                )]),
327                explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]),
328                average_blocktime_ms: Some(15000),
329                is_testnet: Some(false),
330                tags: Some(vec!["child-tag".to_string()]),
331            },
332            chain_id: Some(31337),
333            required_confirmations: Some(1),
334            features: Some(vec!["eip1559".to_string()]),
335            symbol: Some("CETH".to_string()),
336            gas_price_cache: Some(GasPriceCacheConfig {
337                enabled: false,
338                stale_after_ms: 40_000,
339                expire_after_ms: 200_000,
340            }),
341        };
342
343        let result = child.merge_with_parent(&parent);
344
345        // Child values should override parent values
346        assert_eq!(result.common.network, "child");
347        assert_eq!(result.common.from, Some("parent".to_string()));
348        assert_eq!(
349            result.common.rpc_urls,
350            Some(vec![RpcConfig::new(
351                "https://child-rpc.example.com".to_string()
352            )])
353        );
354        assert_eq!(
355            result.common.explorer_urls,
356            Some(vec!["https://child-explorer.example.com".to_string()])
357        );
358        assert_eq!(result.common.average_blocktime_ms, Some(15000));
359        assert_eq!(result.common.is_testnet, Some(false));
360        assert_eq!(
361            result.common.tags,
362            Some(vec!["parent-tag".to_string(), "child-tag".to_string()])
363        );
364        assert_eq!(result.chain_id, Some(31337));
365        assert_eq!(result.required_confirmations, Some(1));
366        assert_eq!(
367            result.features,
368            Some(vec!["legacy".to_string(), "eip1559".to_string()])
369        );
370        assert_eq!(result.symbol, Some("CETH".to_string()));
371        assert_eq!(
372            result.gas_price_cache,
373            Some(GasPriceCacheConfig {
374                enabled: false,
375                stale_after_ms: 40_000,
376                expire_after_ms: 200_000,
377            })
378        );
379    }
380
381    #[test]
382    fn test_merge_with_parent_child_inherits() {
383        let parent = EvmNetworkConfig {
384            common: NetworkConfigCommon {
385                network: "parent".to_string(),
386                from: None,
387                rpc_urls: Some(vec![RpcConfig::new(
388                    "https://parent-rpc.example.com".to_string(),
389                )]),
390                explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
391                average_blocktime_ms: Some(10000),
392                is_testnet: Some(true),
393                tags: Some(vec!["parent-tag".to_string()]),
394            },
395            chain_id: Some(1),
396            required_confirmations: Some(6),
397            features: Some(vec!["eip1559".to_string()]),
398            symbol: Some("ETH".to_string()),
399            gas_price_cache: Some(GasPriceCacheConfig {
400                enabled: true,
401                stale_after_ms: 20_000,
402                expire_after_ms: 100_000,
403            }),
404        };
405
406        let child = create_evm_network_for_inheritance_test("ethereum-testnet", "ethereum-mainnet");
407
408        let result = child.merge_with_parent(&parent);
409
410        // Child should inherit parent values where child has None
411        assert_eq!(result.common.network, "ethereum-testnet");
412        assert_eq!(result.common.from, Some("ethereum-mainnet".to_string()));
413        assert_eq!(
414            result.common.rpc_urls,
415            Some(vec![RpcConfig::new(
416                "https://parent-rpc.example.com".to_string()
417            )])
418        );
419        assert_eq!(
420            result.common.explorer_urls,
421            Some(vec!["https://parent-explorer.example.com".to_string()])
422        );
423        assert_eq!(result.common.average_blocktime_ms, Some(10000));
424        assert_eq!(result.common.is_testnet, Some(true));
425        assert_eq!(result.common.tags, Some(vec!["parent-tag".to_string()]));
426        assert_eq!(result.chain_id, Some(1));
427        assert_eq!(result.required_confirmations, Some(6));
428        assert_eq!(result.features, Some(vec!["eip1559".to_string()]));
429        assert_eq!(result.symbol, Some("ETH".to_string()));
430        assert_eq!(
431            result.gas_price_cache,
432            Some(GasPriceCacheConfig {
433                enabled: true,
434                stale_after_ms: 20_000,
435                expire_after_ms: 100_000,
436            })
437        );
438    }
439
440    #[test]
441    fn test_merge_with_parent_mixed_inheritance() {
442        let parent = EvmNetworkConfig {
443            common: NetworkConfigCommon {
444                network: "parent".to_string(),
445                from: None,
446                rpc_urls: Some(vec![RpcConfig::new(
447                    "https://parent-rpc.example.com".to_string(),
448                )]),
449                explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
450                average_blocktime_ms: Some(10000),
451                is_testnet: Some(true),
452                tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
453            },
454            chain_id: Some(1),
455            required_confirmations: Some(6),
456            features: Some(vec!["eip155".to_string(), "eip1559".to_string()]),
457            symbol: Some("ETH".to_string()),
458            gas_price_cache: Some(GasPriceCacheConfig {
459                enabled: true,
460                stale_after_ms: 20_000,
461                expire_after_ms: 100_000,
462            }),
463        };
464
465        let child = EvmNetworkConfig {
466            common: NetworkConfigCommon {
467                network: "child".to_string(),
468                from: Some("parent".to_string()),
469                rpc_urls: Some(vec![RpcConfig::new(
470                    "https://child-rpc.example.com".to_string(),
471                )]), // Override
472                explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), // Override
473                average_blocktime_ms: None,                // Inherit
474                is_testnet: Some(false),                   // Override
475                tags: Some(vec!["child-tag".to_string()]), // Merge
476            },
477            chain_id: Some(31337),                       // Override
478            required_confirmations: None,                // Inherit
479            features: Some(vec!["eip2930".to_string()]), // Merge
480            symbol: None,                                // Inherit
481            gas_price_cache: Some(GasPriceCacheConfig {
482                enabled: false,
483                stale_after_ms: 40_000,
484                expire_after_ms: 200_000,
485            }),
486        };
487
488        let result = child.merge_with_parent(&parent);
489
490        assert_eq!(result.common.network, "child");
491        assert_eq!(
492            result.common.rpc_urls,
493            Some(vec![RpcConfig::new(
494                "https://child-rpc.example.com".to_string()
495            )])
496        ); // Overridden
497        assert_eq!(
498            result.common.explorer_urls,
499            Some(vec!["https://child-explorer.example.com".to_string()])
500        ); // Overridden
501        assert_eq!(result.common.average_blocktime_ms, Some(10000)); // Inherited
502        assert_eq!(result.common.is_testnet, Some(false)); // Overridden
503        assert_eq!(
504            result.common.tags,
505            Some(vec![
506                "parent-tag1".to_string(),
507                "parent-tag2".to_string(),
508                "child-tag".to_string()
509            ])
510        ); // Merged
511        assert_eq!(result.chain_id, Some(31337)); // Overridden
512        assert_eq!(result.required_confirmations, Some(6)); // Inherited
513        assert_eq!(
514            result.features,
515            Some(vec![
516                "eip155".to_string(),
517                "eip1559".to_string(),
518                "eip2930".to_string()
519            ])
520        ); // Merged
521        assert_eq!(result.symbol, Some("ETH".to_string())); // Inherited
522        assert_eq!(
523            result.gas_price_cache,
524            Some(GasPriceCacheConfig {
525                enabled: false,
526                stale_after_ms: 40_000,
527                expire_after_ms: 200_000,
528            })
529        );
530    }
531
532    #[test]
533    fn test_merge_with_parent_both_empty() {
534        let parent = EvmNetworkConfig {
535            common: NetworkConfigCommon {
536                network: "parent".to_string(),
537                from: None,
538                rpc_urls: None,
539                explorer_urls: None,
540                average_blocktime_ms: None,
541                is_testnet: None,
542                tags: None,
543            },
544            chain_id: None,
545            required_confirmations: None,
546            features: None,
547            symbol: None,
548            gas_price_cache: None,
549        };
550
551        let child = EvmNetworkConfig {
552            common: NetworkConfigCommon {
553                network: "child".to_string(),
554                from: Some("parent".to_string()),
555                rpc_urls: None,
556                explorer_urls: None,
557                average_blocktime_ms: None,
558                is_testnet: None,
559                tags: None,
560            },
561            chain_id: None,
562            required_confirmations: None,
563            features: None,
564            symbol: None,
565            gas_price_cache: None,
566        };
567
568        let result = child.merge_with_parent(&parent);
569
570        assert_eq!(result.common.network, "child");
571        assert_eq!(result.common.from, Some("parent".to_string()));
572        assert_eq!(result.common.rpc_urls, None);
573        assert_eq!(result.common.average_blocktime_ms, None);
574        assert_eq!(result.common.is_testnet, None);
575        assert_eq!(result.common.tags, None);
576        assert_eq!(result.chain_id, None);
577        assert_eq!(result.required_confirmations, None);
578        assert_eq!(result.features, None);
579        assert_eq!(result.symbol, None);
580        assert_eq!(result.gas_price_cache, None);
581    }
582
583    #[test]
584    fn test_merge_with_parent_complex_features_merging() {
585        let parent = EvmNetworkConfig {
586            common: NetworkConfigCommon {
587                network: "parent".to_string(),
588                from: None,
589                rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
590                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
591                average_blocktime_ms: Some(12000),
592                is_testnet: Some(false),
593                tags: None,
594            },
595            chain_id: Some(1),
596            required_confirmations: Some(12),
597            features: Some(vec![
598                "eip155".to_string(),
599                "eip1559".to_string(),
600                "shared".to_string(),
601            ]),
602            symbol: Some("ETH".to_string()),
603            gas_price_cache: Some(GasPriceCacheConfig {
604                enabled: true,
605                stale_after_ms: 20_000,
606                expire_after_ms: 100_000,
607            }),
608        };
609
610        let child = EvmNetworkConfig {
611            common: NetworkConfigCommon {
612                network: "child".to_string(),
613                from: Some("parent".to_string()),
614                rpc_urls: None,
615                explorer_urls: None,
616                average_blocktime_ms: None,
617                is_testnet: None,
618                tags: None,
619            },
620            chain_id: None,
621            required_confirmations: None,
622            features: Some(vec![
623                "shared".to_string(),
624                "eip2930".to_string(),
625                "custom".to_string(),
626            ]),
627            symbol: None,
628            gas_price_cache: None,
629        };
630
631        let result = child.merge_with_parent(&parent);
632
633        // Features should be merged with parent first, then unique child features added
634        let expected_features = vec![
635            "eip155".to_string(),
636            "eip1559".to_string(),
637            "shared".to_string(), // Duplicate should not be added again
638            "eip2930".to_string(),
639            "custom".to_string(),
640        ];
641        assert_eq!(result.features, Some(expected_features));
642        assert_eq!(
643            result.gas_price_cache,
644            Some(GasPriceCacheConfig {
645                enabled: true,
646                stale_after_ms: 20_000,
647                expire_after_ms: 100_000,
648            })
649        );
650    }
651
652    #[test]
653    fn test_merge_with_parent_preserves_child_network_name() {
654        let parent = create_evm_network("ethereum-mainnet");
655        let mut child =
656            create_evm_network_for_inheritance_test("ethereum-testnet", "ethereum-mainnet");
657        child.common.network = "custom-child-name".to_string();
658
659        let result = child.merge_with_parent(&parent);
660
661        // Child network name should always be preserved
662        assert_eq!(result.common.network, "custom-child-name");
663    }
664
665    #[test]
666    fn test_merge_with_parent_preserves_child_from_field() {
667        let parent = EvmNetworkConfig {
668            common: NetworkConfigCommon {
669                network: "parent".to_string(),
670                from: Some("grandparent".to_string()),
671                rpc_urls: Some(vec![RpcConfig::new(
672                    "https://parent.example.com".to_string(),
673                )]),
674                explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
675                average_blocktime_ms: Some(10000),
676                is_testnet: Some(true),
677                tags: None,
678            },
679            chain_id: Some(1),
680            required_confirmations: Some(6),
681            features: None,
682            symbol: Some("ETH".to_string()),
683            gas_price_cache: Some(GasPriceCacheConfig {
684                enabled: true,
685                stale_after_ms: 20_000,
686                expire_after_ms: 100_000,
687            }),
688        };
689
690        let child = EvmNetworkConfig {
691            common: NetworkConfigCommon {
692                network: "child".to_string(),
693                from: Some("parent".to_string()),
694                rpc_urls: None,
695                explorer_urls: None,
696                average_blocktime_ms: None,
697                is_testnet: None,
698                tags: None,
699            },
700            chain_id: None,
701            required_confirmations: None,
702            features: None,
703            symbol: None,
704            gas_price_cache: None,
705        };
706
707        let result = child.merge_with_parent(&parent);
708
709        // Child's 'from' field should be preserved, not inherited from parent
710        assert_eq!(result.common.from, Some("parent".to_string()));
711        assert_eq!(
712            result.gas_price_cache,
713            Some(GasPriceCacheConfig {
714                enabled: true,
715                stale_after_ms: 20_000,
716                expire_after_ms: 100_000,
717            })
718        );
719    }
720
721    #[test]
722    fn test_validate_with_unicode_symbol() {
723        let mut config = create_evm_network("ethereum-mainnet");
724        config.symbol = Some("Ξ".to_string()); // Greek Xi symbol for Ethereum
725
726        let result = config.validate();
727        assert!(result.is_ok());
728    }
729
730    #[test]
731    fn test_validate_with_unicode_features() {
732        let mut config = create_evm_network("ethereum-mainnet");
733        config.features = Some(vec!["eip1559".to_string(), "测试功能".to_string()]);
734
735        let result = config.validate();
736        assert!(result.is_ok());
737    }
738
739    #[test]
740    fn test_merge_with_parent_with_empty_features() {
741        let parent = EvmNetworkConfig {
742            common: NetworkConfigCommon {
743                network: "parent".to_string(),
744                from: None,
745                rpc_urls: Some(vec![RpcConfig::new("https://rpc.example.com".to_string())]),
746                explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
747                average_blocktime_ms: Some(12000),
748                is_testnet: Some(false),
749                tags: None,
750            },
751            chain_id: Some(1),
752            required_confirmations: Some(12),
753            features: Some(vec![]),
754            symbol: Some("ETH".to_string()),
755            gas_price_cache: Some(GasPriceCacheConfig {
756                enabled: true,
757                stale_after_ms: 20_000,
758                expire_after_ms: 100_000,
759            }),
760        };
761
762        let child = EvmNetworkConfig {
763            common: NetworkConfigCommon {
764                network: "child".to_string(),
765                from: Some("parent".to_string()),
766                rpc_urls: None,
767                explorer_urls: None,
768                average_blocktime_ms: None,
769                is_testnet: None,
770                tags: None,
771            },
772            chain_id: None,
773            required_confirmations: None,
774            features: Some(vec!["eip1559".to_string()]),
775            symbol: None,
776            gas_price_cache: None,
777        };
778
779        let result = child.merge_with_parent(&parent);
780
781        // Should merge empty parent features with child features
782        assert_eq!(result.features, Some(vec!["eip1559".to_string()]));
783        assert_eq!(
784            result.gas_price_cache,
785            Some(GasPriceCacheConfig {
786                enabled: true,
787                stale_after_ms: 20_000,
788                expire_after_ms: 100_000,
789            })
790        );
791    }
792
793    #[test]
794    fn test_validate_with_very_large_confirmations() {
795        let mut config = create_evm_network("ethereum-mainnet");
796        config.required_confirmations = Some(u64::MAX);
797
798        let result = config.validate();
799        assert!(result.is_ok());
800    }
801
802    #[test]
803    fn test_merge_with_parent_identical_configs() {
804        let config = create_evm_network("ethereum-mainnet");
805        let result = config.merge_with_parent(&config);
806
807        // Merging identical configs should result in the same config
808        assert_eq!(result.common.network, config.common.network);
809        assert_eq!(result.chain_id, config.chain_id);
810        assert_eq!(result.required_confirmations, config.required_confirmations);
811        assert_eq!(result.features, config.features);
812        assert_eq!(result.symbol, config.symbol);
813        assert_eq!(result.gas_price_cache, config.gas_price_cache);
814    }
815
816    #[test]
817    fn test_validate_propagates_common_validation_errors() {
818        let mut config = create_evm_network("ethereum-mainnet");
819        config.common.rpc_urls = None; // This should cause common validation to fail
820
821        let result = config.validate();
822        assert!(result.is_err());
823        assert!(matches!(
824            result.unwrap_err(),
825            ConfigFileError::MissingField(_)
826        ));
827    }
828
829    #[test]
830    fn test_gas_price_cache_validation_zero_stale_after() {
831        let mut config = create_evm_network("ethereum-mainnet");
832        config.gas_price_cache = Some(GasPriceCacheConfig {
833            enabled: true,
834            stale_after_ms: 0, // Invalid: zero value
835            expire_after_ms: 45_000,
836        });
837
838        let result = config.validate();
839        assert!(result.is_err());
840        assert!(matches!(
841            result.unwrap_err(),
842            ConfigFileError::InvalidFormat(_)
843        ));
844    }
845
846    #[test]
847    fn test_gas_price_cache_validation_zero_expire_after() {
848        let mut config = create_evm_network("ethereum-mainnet");
849        config.gas_price_cache = Some(GasPriceCacheConfig {
850            enabled: true,
851            stale_after_ms: 20_000,
852            expire_after_ms: 0, // Invalid: zero value
853        });
854
855        let result = config.validate();
856        assert!(result.is_err());
857        assert!(matches!(
858            result.unwrap_err(),
859            ConfigFileError::InvalidFormat(_)
860        ));
861    }
862
863    #[test]
864    fn test_gas_price_cache_validation_expire_less_than_stale() {
865        let mut config = create_evm_network("ethereum-mainnet");
866        config.gas_price_cache = Some(GasPriceCacheConfig {
867            enabled: true,
868            stale_after_ms: 45_000,
869            expire_after_ms: 20_000, // Invalid: less than stale_after_ms
870        });
871
872        let result = config.validate();
873        assert!(result.is_err());
874        assert!(matches!(
875            result.unwrap_err(),
876            ConfigFileError::InvalidFormat(_)
877        ));
878    }
879
880    #[test]
881    fn test_gas_price_cache_validation_expire_equal_to_stale() {
882        let mut config = create_evm_network("ethereum-mainnet");
883        config.gas_price_cache = Some(GasPriceCacheConfig {
884            enabled: true,
885            stale_after_ms: 20_000,
886            expire_after_ms: 20_000, // Invalid: equal to stale_after_ms
887        });
888
889        let result = config.validate();
890        assert!(result.is_err());
891        assert!(matches!(
892            result.unwrap_err(),
893            ConfigFileError::InvalidFormat(_)
894        ));
895    }
896
897    #[test]
898    fn test_gas_price_cache_validation_valid_config() {
899        let mut config = create_evm_network("ethereum-mainnet");
900        config.gas_price_cache = Some(GasPriceCacheConfig {
901            enabled: true,
902            stale_after_ms: 20_000,
903            expire_after_ms: 45_000, // Valid: greater than stale_after_ms
904        });
905
906        let result = config.validate();
907        assert!(result.is_ok());
908    }
909
910    #[test]
911    fn test_gas_price_cache_default_values() {
912        let config = GasPriceCacheConfig::default();
913
914        assert!(!config.enabled);
915        assert_eq!(config.stale_after_ms, 20_000);
916        assert_eq!(config.expire_after_ms, 45_000);
917
918        // Validation should pass for default values
919        assert!(config.validate().is_ok());
920    }
921}