1use super::common::{merge_optional_string_vecs, NetworkConfigCommon};
13use crate::config::ConfigFileError;
14use serde::{Deserialize, Serialize};
15
16#[derive(Debug, Serialize, Deserialize, Clone)]
18#[serde(deny_unknown_fields)]
19pub struct EvmNetworkConfig {
20 #[serde(flatten)]
22 pub common: NetworkConfigCommon,
23
24 pub chain_id: Option<u64>,
26 pub required_confirmations: Option<u64>,
28 pub features: Option<Vec<String>>,
30 pub symbol: Option<String>,
32}
33
34impl EvmNetworkConfig {
35 pub fn validate(&self) -> Result<(), ConfigFileError> {
41 self.common.validate()?;
42
43 if self.chain_id.is_none() {
45 return Err(ConfigFileError::MissingField("chain_id".into()));
46 }
47
48 if self.required_confirmations.is_none() {
49 return Err(ConfigFileError::MissingField(
50 "required_confirmations".into(),
51 ));
52 }
53
54 if self.symbol.is_none() || self.symbol.as_ref().unwrap_or(&String::new()).is_empty() {
55 return Err(ConfigFileError::MissingField("symbol".into()));
56 }
57
58 Ok(())
59 }
60
61 pub fn merge_with_parent(&self, parent: &Self) -> Self {
69 Self {
70 common: self.common.merge_with_parent(&parent.common),
71 chain_id: self.chain_id.or(parent.chain_id),
72 required_confirmations: self
73 .required_confirmations
74 .or(parent.required_confirmations),
75 features: merge_optional_string_vecs(&self.features, &parent.features),
76 symbol: self.symbol.clone().or_else(|| parent.symbol.clone()),
77 }
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84 use crate::config::config_file::network::test_utils::*;
85
86 #[test]
87 fn test_validate_success_complete_config() {
88 let config = create_evm_network("ethereum-mainnet");
89 let result = config.validate();
90 assert!(result.is_ok());
91 }
92
93 #[test]
94 fn test_validate_success_minimal_config() {
95 let mut config = create_evm_network("minimal-evm");
96 config.features = None;
97 let result = config.validate();
98 assert!(result.is_ok());
99 }
100
101 #[test]
102 fn test_validate_missing_chain_id() {
103 let mut config = create_evm_network("ethereum-mainnet");
104 config.chain_id = None;
105
106 let result = config.validate();
107 assert!(result.is_err());
108 assert!(matches!(
109 result.unwrap_err(),
110 ConfigFileError::MissingField(_)
111 ));
112 }
113
114 #[test]
115 fn test_validate_missing_required_confirmations() {
116 let mut config = create_evm_network("ethereum-mainnet");
117 config.required_confirmations = None;
118
119 let result = config.validate();
120 assert!(result.is_err());
121 assert!(matches!(
122 result.unwrap_err(),
123 ConfigFileError::MissingField(_)
124 ));
125 }
126
127 #[test]
128 fn test_validate_missing_symbol() {
129 let mut config = create_evm_network("ethereum-mainnet");
130 config.symbol = None;
131
132 let result = config.validate();
133 assert!(result.is_err());
134 assert!(matches!(
135 result.unwrap_err(),
136 ConfigFileError::MissingField(_)
137 ));
138 }
139
140 #[test]
141 fn test_validate_invalid_common_fields() {
142 let mut config = create_evm_network("ethereum-mainnet");
143 config.common.network = String::new(); let result = config.validate();
146 assert!(result.is_err());
147 assert!(matches!(
148 result.unwrap_err(),
149 ConfigFileError::MissingField(_)
150 ));
151 }
152
153 #[test]
154 fn test_validate_invalid_rpc_urls() {
155 let mut config = create_evm_network("ethereum-mainnet");
156 config.common.rpc_urls = Some(vec!["invalid-url".to_string()]);
157
158 let result = config.validate();
159 assert!(result.is_err());
160 assert!(matches!(
161 result.unwrap_err(),
162 ConfigFileError::InvalidFormat(_)
163 ));
164 }
165
166 #[test]
167 fn test_validate_with_zero_chain_id() {
168 let mut config = create_evm_network("ethereum-mainnet");
169 config.chain_id = Some(0);
170
171 let result = config.validate();
172 assert!(result.is_ok()); }
174
175 #[test]
176 fn test_validate_with_large_chain_id() {
177 let mut config = create_evm_network("ethereum-mainnet");
178 config.chain_id = Some(u64::MAX);
179
180 let result = config.validate();
181 assert!(result.is_ok());
182 }
183
184 #[test]
185 fn test_validate_with_zero_confirmations() {
186 let mut config = create_evm_network("ethereum-mainnet");
187 config.required_confirmations = Some(0);
188
189 let result = config.validate();
190 assert!(result.is_ok()); }
192
193 #[test]
194 fn test_validate_with_empty_features() {
195 let mut config = create_evm_network("ethereum-mainnet");
196 config.features = Some(vec![]);
197
198 let result = config.validate();
199 assert!(result.is_ok());
200 }
201
202 #[test]
203 fn test_validate_with_empty_symbol() {
204 let mut config = create_evm_network("ethereum-mainnet");
205 config.symbol = Some(String::new());
206
207 let result = config.validate();
208 assert!(result.is_err());
209 }
210
211 #[test]
212 fn test_merge_with_parent_child_overrides() {
213 let parent = EvmNetworkConfig {
214 common: NetworkConfigCommon {
215 network: "parent".to_string(),
216 from: None,
217 rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
218 explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
219 average_blocktime_ms: Some(10000),
220 is_testnet: Some(true),
221 tags: Some(vec!["parent-tag".to_string()]),
222 },
223 chain_id: Some(1),
224 required_confirmations: Some(6),
225 features: Some(vec!["legacy".to_string()]),
226 symbol: Some("PETH".to_string()),
227 };
228
229 let child = EvmNetworkConfig {
230 common: NetworkConfigCommon {
231 network: "child".to_string(),
232 from: Some("parent".to_string()),
233 rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]),
234 explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]),
235 average_blocktime_ms: Some(15000),
236 is_testnet: Some(false),
237 tags: Some(vec!["child-tag".to_string()]),
238 },
239 chain_id: Some(31337),
240 required_confirmations: Some(1),
241 features: Some(vec!["eip1559".to_string()]),
242 symbol: Some("CETH".to_string()),
243 };
244
245 let result = child.merge_with_parent(&parent);
246
247 assert_eq!(result.common.network, "child");
249 assert_eq!(result.common.from, Some("parent".to_string()));
250 assert_eq!(
251 result.common.rpc_urls,
252 Some(vec!["https://child-rpc.example.com".to_string()])
253 );
254 assert_eq!(
255 result.common.explorer_urls,
256 Some(vec!["https://child-explorer.example.com".to_string()])
257 );
258 assert_eq!(result.common.average_blocktime_ms, Some(15000));
259 assert_eq!(result.common.is_testnet, Some(false));
260 assert_eq!(
261 result.common.tags,
262 Some(vec!["parent-tag".to_string(), "child-tag".to_string()])
263 );
264 assert_eq!(result.chain_id, Some(31337));
265 assert_eq!(result.required_confirmations, Some(1));
266 assert_eq!(
267 result.features,
268 Some(vec!["legacy".to_string(), "eip1559".to_string()])
269 );
270 assert_eq!(result.symbol, Some("CETH".to_string()));
271 }
272
273 #[test]
274 fn test_merge_with_parent_child_inherits() {
275 let parent = EvmNetworkConfig {
276 common: NetworkConfigCommon {
277 network: "parent".to_string(),
278 from: None,
279 rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
280 explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
281 average_blocktime_ms: Some(10000),
282 is_testnet: Some(true),
283 tags: Some(vec!["parent-tag".to_string()]),
284 },
285 chain_id: Some(1),
286 required_confirmations: Some(6),
287 features: Some(vec!["eip1559".to_string()]),
288 symbol: Some("ETH".to_string()),
289 };
290
291 let child = create_evm_network_for_inheritance_test("ethereum-testnet", "ethereum-mainnet");
292
293 let result = child.merge_with_parent(&parent);
294
295 assert_eq!(result.common.network, "ethereum-testnet");
297 assert_eq!(result.common.from, Some("ethereum-mainnet".to_string()));
298 assert_eq!(
299 result.common.rpc_urls,
300 Some(vec!["https://parent-rpc.example.com".to_string()])
301 );
302 assert_eq!(
303 result.common.explorer_urls,
304 Some(vec!["https://parent-explorer.example.com".to_string()])
305 );
306 assert_eq!(result.common.average_blocktime_ms, Some(10000));
307 assert_eq!(result.common.is_testnet, Some(true));
308 assert_eq!(result.common.tags, Some(vec!["parent-tag".to_string()]));
309 assert_eq!(result.chain_id, Some(1));
310 assert_eq!(result.required_confirmations, Some(6));
311 assert_eq!(result.features, Some(vec!["eip1559".to_string()]));
312 assert_eq!(result.symbol, Some("ETH".to_string()));
313 }
314
315 #[test]
316 fn test_merge_with_parent_mixed_inheritance() {
317 let parent = EvmNetworkConfig {
318 common: NetworkConfigCommon {
319 network: "parent".to_string(),
320 from: None,
321 rpc_urls: Some(vec!["https://parent-rpc.example.com".to_string()]),
322 explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
323 average_blocktime_ms: Some(10000),
324 is_testnet: Some(true),
325 tags: Some(vec!["parent-tag1".to_string(), "parent-tag2".to_string()]),
326 },
327 chain_id: Some(1),
328 required_confirmations: Some(6),
329 features: Some(vec!["eip155".to_string(), "eip1559".to_string()]),
330 symbol: Some("ETH".to_string()),
331 };
332
333 let child = EvmNetworkConfig {
334 common: NetworkConfigCommon {
335 network: "child".to_string(),
336 from: Some("parent".to_string()),
337 rpc_urls: Some(vec!["https://child-rpc.example.com".to_string()]), explorer_urls: Some(vec!["https://child-explorer.example.com".to_string()]), average_blocktime_ms: None, is_testnet: Some(false), tags: Some(vec!["child-tag".to_string()]), },
343 chain_id: Some(31337), required_confirmations: None, features: Some(vec!["eip2930".to_string()]), symbol: None, };
348
349 let result = child.merge_with_parent(&parent);
350
351 assert_eq!(result.common.network, "child");
352 assert_eq!(
353 result.common.rpc_urls,
354 Some(vec!["https://child-rpc.example.com".to_string()])
355 ); assert_eq!(
357 result.common.explorer_urls,
358 Some(vec!["https://child-explorer.example.com".to_string()])
359 ); assert_eq!(result.common.average_blocktime_ms, Some(10000)); assert_eq!(result.common.is_testnet, Some(false)); assert_eq!(
363 result.common.tags,
364 Some(vec![
365 "parent-tag1".to_string(),
366 "parent-tag2".to_string(),
367 "child-tag".to_string()
368 ])
369 ); assert_eq!(result.chain_id, Some(31337)); assert_eq!(result.required_confirmations, Some(6)); assert_eq!(
373 result.features,
374 Some(vec![
375 "eip155".to_string(),
376 "eip1559".to_string(),
377 "eip2930".to_string()
378 ])
379 ); assert_eq!(result.symbol, Some("ETH".to_string())); }
382
383 #[test]
384 fn test_merge_with_parent_both_empty() {
385 let parent = EvmNetworkConfig {
386 common: NetworkConfigCommon {
387 network: "parent".to_string(),
388 from: None,
389 rpc_urls: None,
390 explorer_urls: None,
391 average_blocktime_ms: None,
392 is_testnet: None,
393 tags: None,
394 },
395 chain_id: None,
396 required_confirmations: None,
397 features: None,
398 symbol: None,
399 };
400
401 let child = EvmNetworkConfig {
402 common: NetworkConfigCommon {
403 network: "child".to_string(),
404 from: Some("parent".to_string()),
405 rpc_urls: None,
406 explorer_urls: None,
407 average_blocktime_ms: None,
408 is_testnet: None,
409 tags: None,
410 },
411 chain_id: None,
412 required_confirmations: None,
413 features: None,
414 symbol: None,
415 };
416
417 let result = child.merge_with_parent(&parent);
418
419 assert_eq!(result.common.network, "child");
420 assert_eq!(result.common.from, Some("parent".to_string()));
421 assert_eq!(result.common.rpc_urls, None);
422 assert_eq!(result.common.average_blocktime_ms, None);
423 assert_eq!(result.common.is_testnet, None);
424 assert_eq!(result.common.tags, None);
425 assert_eq!(result.chain_id, None);
426 assert_eq!(result.required_confirmations, None);
427 assert_eq!(result.features, None);
428 assert_eq!(result.symbol, None);
429 }
430
431 #[test]
432 fn test_merge_with_parent_complex_features_merging() {
433 let parent = EvmNetworkConfig {
434 common: NetworkConfigCommon {
435 network: "parent".to_string(),
436 from: None,
437 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
438 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
439 average_blocktime_ms: Some(12000),
440 is_testnet: Some(false),
441 tags: None,
442 },
443 chain_id: Some(1),
444 required_confirmations: Some(12),
445 features: Some(vec![
446 "eip155".to_string(),
447 "eip1559".to_string(),
448 "shared".to_string(),
449 ]),
450 symbol: Some("ETH".to_string()),
451 };
452
453 let child = EvmNetworkConfig {
454 common: NetworkConfigCommon {
455 network: "child".to_string(),
456 from: Some("parent".to_string()),
457 rpc_urls: None,
458 explorer_urls: None,
459 average_blocktime_ms: None,
460 is_testnet: None,
461 tags: None,
462 },
463 chain_id: None,
464 required_confirmations: None,
465 features: Some(vec![
466 "shared".to_string(),
467 "eip2930".to_string(),
468 "custom".to_string(),
469 ]),
470 symbol: None,
471 };
472
473 let result = child.merge_with_parent(&parent);
474
475 let expected_features = vec![
477 "eip155".to_string(),
478 "eip1559".to_string(),
479 "shared".to_string(), "eip2930".to_string(),
481 "custom".to_string(),
482 ];
483 assert_eq!(result.features, Some(expected_features));
484 }
485
486 #[test]
487 fn test_merge_with_parent_preserves_child_network_name() {
488 let parent = create_evm_network("ethereum-mainnet");
489 let mut child =
490 create_evm_network_for_inheritance_test("ethereum-testnet", "ethereum-mainnet");
491 child.common.network = "custom-child-name".to_string();
492
493 let result = child.merge_with_parent(&parent);
494
495 assert_eq!(result.common.network, "custom-child-name");
497 }
498
499 #[test]
500 fn test_merge_with_parent_preserves_child_from_field() {
501 let parent = EvmNetworkConfig {
502 common: NetworkConfigCommon {
503 network: "parent".to_string(),
504 from: Some("grandparent".to_string()),
505 rpc_urls: Some(vec!["https://parent.example.com".to_string()]),
506 explorer_urls: Some(vec!["https://parent-explorer.example.com".to_string()]),
507 average_blocktime_ms: Some(10000),
508 is_testnet: Some(true),
509 tags: None,
510 },
511 chain_id: Some(1),
512 required_confirmations: Some(6),
513 features: None,
514 symbol: Some("ETH".to_string()),
515 };
516
517 let child = EvmNetworkConfig {
518 common: NetworkConfigCommon {
519 network: "child".to_string(),
520 from: Some("parent".to_string()),
521 rpc_urls: None,
522 explorer_urls: None,
523 average_blocktime_ms: None,
524 is_testnet: None,
525 tags: None,
526 },
527 chain_id: None,
528 required_confirmations: None,
529 features: None,
530 symbol: None,
531 };
532
533 let result = child.merge_with_parent(&parent);
534
535 assert_eq!(result.common.from, Some("parent".to_string()));
537 }
538
539 #[test]
540 fn test_validate_with_unicode_symbol() {
541 let mut config = create_evm_network("ethereum-mainnet");
542 config.symbol = Some("Ξ".to_string()); let result = config.validate();
545 assert!(result.is_ok());
546 }
547
548 #[test]
549 fn test_validate_with_unicode_features() {
550 let mut config = create_evm_network("ethereum-mainnet");
551 config.features = Some(vec!["eip1559".to_string(), "测试功能".to_string()]);
552
553 let result = config.validate();
554 assert!(result.is_ok());
555 }
556
557 #[test]
558 fn test_merge_with_parent_with_empty_features() {
559 let parent = EvmNetworkConfig {
560 common: NetworkConfigCommon {
561 network: "parent".to_string(),
562 from: None,
563 rpc_urls: Some(vec!["https://rpc.example.com".to_string()]),
564 explorer_urls: Some(vec!["https://explorer.example.com".to_string()]),
565 average_blocktime_ms: Some(12000),
566 is_testnet: Some(false),
567 tags: None,
568 },
569 chain_id: Some(1),
570 required_confirmations: Some(12),
571 features: Some(vec![]),
572 symbol: Some("ETH".to_string()),
573 };
574
575 let child = EvmNetworkConfig {
576 common: NetworkConfigCommon {
577 network: "child".to_string(),
578 from: Some("parent".to_string()),
579 rpc_urls: None,
580 explorer_urls: None,
581 average_blocktime_ms: None,
582 is_testnet: None,
583 tags: None,
584 },
585 chain_id: None,
586 required_confirmations: None,
587 features: Some(vec!["eip1559".to_string()]),
588 symbol: None,
589 };
590
591 let result = child.merge_with_parent(&parent);
592
593 assert_eq!(result.features, Some(vec!["eip1559".to_string()]));
595 }
596
597 #[test]
598 fn test_validate_with_very_large_confirmations() {
599 let mut config = create_evm_network("ethereum-mainnet");
600 config.required_confirmations = Some(u64::MAX);
601
602 let result = config.validate();
603 assert!(result.is_ok());
604 }
605
606 #[test]
607 fn test_merge_with_parent_identical_configs() {
608 let config = create_evm_network("ethereum-mainnet");
609 let result = config.merge_with_parent(&config);
610
611 assert_eq!(result.common.network, config.common.network);
613 assert_eq!(result.chain_id, config.chain_id);
614 assert_eq!(result.required_confirmations, config.required_confirmations);
615 assert_eq!(result.features, config.features);
616 assert_eq!(result.symbol, config.symbol);
617 }
618
619 #[test]
620 fn test_validate_propagates_common_validation_errors() {
621 let mut config = create_evm_network("ethereum-mainnet");
622 config.common.rpc_urls = None; let result = config.validate();
625 assert!(result.is_err());
626 assert!(matches!(
627 result.unwrap_err(),
628 ConfigFileError::MissingField(_)
629 ));
630 }
631}