1use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, path::PathBuf, rc::Rc};
2
3use multiaddr::Multiaddr;
4use serde::{ser::SerializeStruct, Deserialize, Serialize};
5
6use super::{
7 errors::FieldError,
8 helpers::{
9 ensure_port_unique, ensure_value_is_not_empty, generate_unique_node_name,
10 generate_unique_node_name_from_names, merge_errors, merge_errors_vecs,
11 },
12 macros::states,
13 resources::ResourcesBuilder,
14 types::{AssetLocation, ChainDefaultContext, Command, Image, ValidationContext, U128},
15};
16use crate::{
17 shared::{
18 resources::Resources,
19 types::{Arg, Port},
20 },
21 utils::{default_as_true, default_initial_balance},
22};
23
24states! {
25 Buildable,
26 Initial
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
48pub struct EnvVar {
49 pub name: String,
51
52 pub value: String,
54}
55
56impl From<(&str, &str)> for EnvVar {
57 fn from((name, value): (&str, &str)) -> Self {
58 Self {
59 name: name.to_owned(),
60 value: value.to_owned(),
61 }
62 }
63}
64
65#[derive(Debug, Clone, Default, PartialEq, Deserialize)]
67pub struct NodeConfig {
68 pub(crate) name: String,
69 pub(crate) image: Option<Image>,
70 pub(crate) command: Option<Command>,
71 pub(crate) subcommand: Option<Command>,
72 #[serde(default)]
73 args: Vec<Arg>,
74 #[serde(alias = "validator", default = "default_as_true")]
75 pub(crate) is_validator: bool,
76 #[serde(alias = "invulnerable", default = "default_as_true")]
77 pub(crate) is_invulnerable: bool,
78 #[serde(alias = "bootnode", default)]
79 pub(crate) is_bootnode: bool,
80 #[serde(alias = "balance")]
81 #[serde(default = "default_initial_balance")]
82 initial_balance: U128,
83 #[serde(default)]
84 env: Vec<EnvVar>,
85 #[serde(default)]
86 bootnodes_addresses: Vec<Multiaddr>,
87 pub(crate) resources: Option<Resources>,
88 ws_port: Option<Port>,
89 rpc_port: Option<Port>,
90 prometheus_port: Option<Port>,
91 p2p_port: Option<Port>,
92 p2p_cert_hash: Option<String>,
93 pub(crate) db_snapshot: Option<AssetLocation>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
97 override_eth_key: Option<String>,
98 #[serde(default)]
99 pub(crate) chain_context: ChainDefaultContext,
101 pub(crate) node_log_path: Option<PathBuf>,
102 keystore_path: Option<PathBuf>,
104 #[serde(default)]
108 keystore_key_types: Vec<String>,
109}
110
111impl Serialize for NodeConfig {
112 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
113 where
114 S: serde::Serializer,
115 {
116 let mut state = serializer.serialize_struct("NodeConfig", 19)?;
117 state.serialize_field("name", &self.name)?;
118
119 if self.image == self.chain_context.default_image {
120 state.skip_field("image")?;
121 } else {
122 state.serialize_field("image", &self.image)?;
123 }
124
125 if self.command == self.chain_context.default_command {
126 state.skip_field("command")?;
127 } else {
128 state.serialize_field("command", &self.command)?;
129 }
130
131 if self.subcommand.is_none() {
132 state.skip_field("subcommand")?;
133 } else {
134 state.serialize_field("subcommand", &self.subcommand)?;
135 }
136
137 if self.args.is_empty() || self.args == self.chain_context.default_args {
138 state.skip_field("args")?;
139 } else {
140 state.serialize_field("args", &self.args)?;
141 }
142
143 state.serialize_field("validator", &self.is_validator)?;
144 state.serialize_field("invulnerable", &self.is_invulnerable)?;
145 state.serialize_field("bootnode", &self.is_bootnode)?;
146 state.serialize_field("balance", &self.initial_balance)?;
147
148 if self.env.is_empty() {
149 state.skip_field("env")?;
150 } else {
151 state.serialize_field("env", &self.env)?;
152 }
153
154 if self.bootnodes_addresses.is_empty() {
155 state.skip_field("bootnodes_addresses")?;
156 } else {
157 state.serialize_field("bootnodes_addresses", &self.bootnodes_addresses)?;
158 }
159
160 if self.resources == self.chain_context.default_resources {
161 state.skip_field("resources")?;
162 } else {
163 state.serialize_field("resources", &self.resources)?;
164 }
165
166 state.serialize_field("ws_port", &self.ws_port)?;
167 state.serialize_field("rpc_port", &self.rpc_port)?;
168 state.serialize_field("prometheus_port", &self.prometheus_port)?;
169 state.serialize_field("p2p_port", &self.p2p_port)?;
170 state.serialize_field("p2p_cert_hash", &self.p2p_cert_hash)?;
171 state.serialize_field("override_eth_key", &self.override_eth_key)?;
172
173 if self.db_snapshot == self.chain_context.default_db_snapshot {
174 state.skip_field("db_snapshot")?;
175 } else {
176 state.serialize_field("db_snapshot", &self.db_snapshot)?;
177 }
178
179 if self.node_log_path.is_none() {
180 state.skip_field("node_log_path")?;
181 } else {
182 state.serialize_field("node_log_path", &self.node_log_path)?;
183 }
184
185 if self.keystore_path.is_none() {
186 state.skip_field("keystore_path")?;
187 } else {
188 state.serialize_field("keystore_path", &self.keystore_path)?;
189 }
190
191 if self.keystore_key_types.is_empty() {
192 state.skip_field("keystore_key_types")?;
193 } else {
194 state.serialize_field("keystore_key_types", &self.keystore_key_types)?;
195 }
196
197 state.skip_field("chain_context")?;
198 state.end()
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Deserialize)]
204pub struct GroupNodeConfig {
205 #[serde(flatten)]
206 pub(crate) base_config: NodeConfig,
207 pub(crate) count: usize,
208}
209
210impl GroupNodeConfig {
211 pub fn expand_group_configs(&self) -> Vec<NodeConfig> {
214 let mut used_names = std::collections::HashSet::new();
215
216 (0..self.count)
217 .map(|_index| {
218 let mut node = self.base_config.clone();
219
220 let unique_name = generate_unique_node_name_from_names(node.name, &mut used_names);
221 node.name = unique_name;
222
223 if let Some(ref base_log_path) = node.node_log_path {
225 let unique_log_path = if let Some(parent) = base_log_path.parent() {
226 parent.join(format!("{}.log", node.name))
227 } else {
228 PathBuf::from(format!("{}.log", node.name))
229 };
230 node.node_log_path = Some(unique_log_path);
231 }
232
233 node
234 })
235 .collect()
236 }
237}
238
239impl Serialize for GroupNodeConfig {
240 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
241 where
242 S: serde::Serializer,
243 {
244 let mut state = serializer.serialize_struct("GroupNodeConfig", 18)?;
245 state.serialize_field("NodeConfig", &self.base_config)?;
246 state.serialize_field("count", &self.count)?;
247 state.end()
248 }
249}
250
251impl NodeConfig {
252 pub fn name(&self) -> &str {
254 &self.name
255 }
256
257 pub fn image(&self) -> Option<&Image> {
259 self.image.as_ref()
260 }
261
262 pub fn command(&self) -> Option<&Command> {
264 self.command.as_ref()
265 }
266
267 pub fn subcommand(&self) -> Option<&Command> {
269 self.subcommand.as_ref()
270 }
271
272 pub fn args(&self) -> Vec<&Arg> {
274 self.args.iter().collect()
275 }
276
277 pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
279 self.args = args;
280 }
281
282 pub fn is_validator(&self) -> bool {
284 self.is_validator
285 }
286
287 pub fn is_invulnerable(&self) -> bool {
289 self.is_invulnerable
290 }
291
292 pub fn is_bootnode(&self) -> bool {
294 self.is_bootnode
295 }
296
297 pub fn initial_balance(&self) -> u128 {
299 self.initial_balance.0
300 }
301
302 pub fn env(&self) -> Vec<&EnvVar> {
304 self.env.iter().collect()
305 }
306
307 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
309 self.bootnodes_addresses.iter().collect()
310 }
311
312 pub fn resources(&self) -> Option<&Resources> {
314 self.resources.as_ref()
315 }
316
317 pub fn ws_port(&self) -> Option<u16> {
319 self.ws_port
320 }
321
322 pub fn rpc_port(&self) -> Option<u16> {
324 self.rpc_port
325 }
326
327 pub fn prometheus_port(&self) -> Option<u16> {
329 self.prometheus_port
330 }
331
332 pub fn p2p_port(&self) -> Option<u16> {
334 self.p2p_port
335 }
336
337 pub fn p2p_cert_hash(&self) -> Option<&str> {
339 self.p2p_cert_hash.as_deref()
340 }
341
342 pub fn db_snapshot(&self) -> Option<&AssetLocation> {
344 self.db_snapshot.as_ref()
345 }
346
347 pub fn node_log_path(&self) -> Option<&PathBuf> {
349 self.node_log_path.as_ref()
350 }
351
352 pub fn keystore_path(&self) -> Option<&PathBuf> {
354 self.keystore_path.as_ref()
355 }
356
357 pub fn override_eth_key(&self) -> Option<&str> {
359 self.override_eth_key.as_deref()
360 }
361
362 pub fn keystore_key_types(&self) -> Vec<&str> {
365 self.keystore_key_types.iter().map(String::as_str).collect()
366 }
367}
368
369pub struct NodeConfigBuilder<S> {
371 config: NodeConfig,
372 validation_context: Rc<RefCell<ValidationContext>>,
373 errors: Vec<anyhow::Error>,
374 _state: PhantomData<S>,
375}
376
377impl Default for NodeConfigBuilder<Initial> {
378 fn default() -> Self {
379 Self {
380 config: NodeConfig {
381 name: "".into(),
382 image: None,
383 command: None,
384 subcommand: None,
385 args: vec![],
386 is_validator: true,
387 is_invulnerable: true,
388 is_bootnode: false,
389 initial_balance: 2_000_000_000_000.into(),
390 env: vec![],
391 bootnodes_addresses: vec![],
392 resources: None,
393 ws_port: None,
394 rpc_port: None,
395 prometheus_port: None,
396 p2p_port: None,
397 p2p_cert_hash: None,
398 db_snapshot: None,
399 override_eth_key: None,
400 chain_context: Default::default(),
401 node_log_path: None,
402 keystore_path: None,
403 keystore_key_types: vec![],
404 },
405 validation_context: Default::default(),
406 errors: vec![],
407 _state: PhantomData,
408 }
409 }
410}
411
412impl<A> NodeConfigBuilder<A> {
413 fn transition<B>(
414 config: NodeConfig,
415 validation_context: Rc<RefCell<ValidationContext>>,
416 errors: Vec<anyhow::Error>,
417 ) -> NodeConfigBuilder<B> {
418 NodeConfigBuilder {
419 config,
420 validation_context,
421 errors,
422 _state: PhantomData,
423 }
424 }
425}
426
427impl NodeConfigBuilder<Initial> {
428 pub fn new(
429 chain_context: ChainDefaultContext,
430 validation_context: Rc<RefCell<ValidationContext>>,
431 ) -> Self {
432 Self::transition(
433 NodeConfig {
434 command: chain_context.default_command.clone(),
435 image: chain_context.default_image.clone(),
436 resources: chain_context.default_resources.clone(),
437 db_snapshot: chain_context.default_db_snapshot.clone(),
438 args: chain_context.default_args.clone(),
439 chain_context,
440 ..Self::default().config
441 },
442 validation_context,
443 vec![],
444 )
445 }
446
447 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
449 let name: String = generate_unique_node_name(name, self.validation_context.clone());
450
451 match ensure_value_is_not_empty(&name) {
452 Ok(_) => Self::transition(
453 NodeConfig {
454 name,
455 ..self.config
456 },
457 self.validation_context,
458 self.errors,
459 ),
460 Err(e) => Self::transition(
461 NodeConfig {
462 name,
464 ..self.config
465 },
466 self.validation_context,
467 merge_errors(self.errors, FieldError::Name(e).into()),
468 ),
469 }
470 }
471}
472
473impl NodeConfigBuilder<Buildable> {
474 pub fn with_command<T>(self, command: T) -> Self
476 where
477 T: TryInto<Command>,
478 T::Error: Error + Send + Sync + 'static,
479 {
480 match command.try_into() {
481 Ok(command) => Self::transition(
482 NodeConfig {
483 command: Some(command),
484 ..self.config
485 },
486 self.validation_context,
487 self.errors,
488 ),
489 Err(error) => Self::transition(
490 self.config,
491 self.validation_context,
492 merge_errors(self.errors, FieldError::Command(error.into()).into()),
493 ),
494 }
495 }
496
497 pub fn with_subcommand<T>(self, subcommand: T) -> Self
499 where
500 T: TryInto<Command>,
501 T::Error: Error + Send + Sync + 'static,
502 {
503 match subcommand.try_into() {
504 Ok(subcommand) => Self::transition(
505 NodeConfig {
506 subcommand: Some(subcommand),
507 ..self.config
508 },
509 self.validation_context,
510 self.errors,
511 ),
512 Err(error) => Self::transition(
513 self.config,
514 self.validation_context,
515 merge_errors(self.errors, FieldError::Command(error.into()).into()),
516 ),
517 }
518 }
519
520 pub fn with_image<T>(self, image: T) -> Self
522 where
523 T: TryInto<Image>,
524 T::Error: Error + Send + Sync + 'static,
525 {
526 match image.try_into() {
527 Ok(image) => Self::transition(
528 NodeConfig {
529 image: Some(image),
530 ..self.config
531 },
532 self.validation_context,
533 self.errors,
534 ),
535 Err(error) => Self::transition(
536 self.config,
537 self.validation_context,
538 merge_errors(self.errors, FieldError::Image(error.into()).into()),
539 ),
540 }
541 }
542
543 pub fn with_args(self, args: Vec<Arg>) -> Self {
545 Self::transition(
546 NodeConfig {
547 args,
548 ..self.config
549 },
550 self.validation_context,
551 self.errors,
552 )
553 }
554
555 pub fn validator(self, choice: bool) -> Self {
557 Self::transition(
558 NodeConfig {
559 is_validator: choice,
560 ..self.config
561 },
562 self.validation_context,
563 self.errors,
564 )
565 }
566
567 pub fn invulnerable(self, choice: bool) -> Self {
569 Self::transition(
570 NodeConfig {
571 is_invulnerable: choice,
572 ..self.config
573 },
574 self.validation_context,
575 self.errors,
576 )
577 }
578
579 pub fn bootnode(self, choice: bool) -> Self {
581 Self::transition(
582 NodeConfig {
583 is_bootnode: choice,
584 ..self.config
585 },
586 self.validation_context,
587 self.errors,
588 )
589 }
590
591 pub fn with_override_eth_key(self, session_key: impl Into<String>) -> Self {
593 Self::transition(
594 NodeConfig {
595 override_eth_key: Some(session_key.into()),
596 ..self.config
597 },
598 self.validation_context,
599 self.errors,
600 )
601 }
602
603 pub fn with_initial_balance(self, initial_balance: u128) -> Self {
605 Self::transition(
606 NodeConfig {
607 initial_balance: initial_balance.into(),
608 ..self.config
609 },
610 self.validation_context,
611 self.errors,
612 )
613 }
614
615 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
617 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
618
619 Self::transition(
620 NodeConfig { env, ..self.config },
621 self.validation_context,
622 self.errors,
623 )
624 }
625
626 pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
631 where
632 T: TryInto<Multiaddr> + Display + Copy,
633 T::Error: Error + Send + Sync + 'static,
634 {
635 let mut addrs = vec![];
636 let mut errors = vec![];
637
638 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
639 match addr.try_into() {
640 Ok(addr) => addrs.push(addr),
641 Err(error) => errors.push(
642 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
643 ),
644 }
645 }
646
647 Self::transition(
648 NodeConfig {
649 bootnodes_addresses: addrs,
650 ..self.config
651 },
652 self.validation_context,
653 merge_errors_vecs(self.errors, errors),
654 )
655 }
656
657 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
659 match f(ResourcesBuilder::new()).build() {
660 Ok(resources) => Self::transition(
661 NodeConfig {
662 resources: Some(resources),
663 ..self.config
664 },
665 self.validation_context,
666 self.errors,
667 ),
668 Err(errors) => Self::transition(
669 self.config,
670 self.validation_context,
671 merge_errors_vecs(
672 self.errors,
673 errors
674 .into_iter()
675 .map(|error| FieldError::Resources(error).into())
676 .collect::<Vec<_>>(),
677 ),
678 ),
679 }
680 }
681
682 pub fn with_ws_port(self, ws_port: Port) -> Self {
684 match ensure_port_unique(ws_port, self.validation_context.clone()) {
685 Ok(_) => Self::transition(
686 NodeConfig {
687 ws_port: Some(ws_port),
688 ..self.config
689 },
690 self.validation_context,
691 self.errors,
692 ),
693 Err(error) => Self::transition(
694 self.config,
695 self.validation_context,
696 merge_errors(self.errors, FieldError::WsPort(error).into()),
697 ),
698 }
699 }
700
701 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
703 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
704 Ok(_) => Self::transition(
705 NodeConfig {
706 rpc_port: Some(rpc_port),
707 ..self.config
708 },
709 self.validation_context,
710 self.errors,
711 ),
712 Err(error) => Self::transition(
713 self.config,
714 self.validation_context,
715 merge_errors(self.errors, FieldError::RpcPort(error).into()),
716 ),
717 }
718 }
719
720 pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
722 match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
723 Ok(_) => Self::transition(
724 NodeConfig {
725 prometheus_port: Some(prometheus_port),
726 ..self.config
727 },
728 self.validation_context,
729 self.errors,
730 ),
731 Err(error) => Self::transition(
732 self.config,
733 self.validation_context,
734 merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
735 ),
736 }
737 }
738
739 pub fn with_p2p_port(self, p2p_port: Port) -> Self {
741 match ensure_port_unique(p2p_port, self.validation_context.clone()) {
742 Ok(_) => Self::transition(
743 NodeConfig {
744 p2p_port: Some(p2p_port),
745 ..self.config
746 },
747 self.validation_context,
748 self.errors,
749 ),
750 Err(error) => Self::transition(
751 self.config,
752 self.validation_context,
753 merge_errors(self.errors, FieldError::P2pPort(error).into()),
754 ),
755 }
756 }
757
758 pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
761 Self::transition(
762 NodeConfig {
763 p2p_cert_hash: Some(p2p_cert_hash.into()),
764 ..self.config
765 },
766 self.validation_context,
767 self.errors,
768 )
769 }
770
771 pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
773 Self::transition(
774 NodeConfig {
775 db_snapshot: Some(location.into()),
776 ..self.config
777 },
778 self.validation_context,
779 self.errors,
780 )
781 }
782
783 pub fn with_log_path(self, log_path: impl Into<PathBuf>) -> Self {
785 Self::transition(
786 NodeConfig {
787 node_log_path: Some(log_path.into()),
788 ..self.config
789 },
790 self.validation_context,
791 self.errors,
792 )
793 }
794
795 pub fn with_keystore_path(self, keystore_path: impl Into<PathBuf>) -> Self {
797 Self::transition(
798 NodeConfig {
799 keystore_path: Some(keystore_path.into()),
800 ..self.config
801 },
802 self.validation_context,
803 self.errors,
804 )
805 }
806
807 pub fn with_keystore_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
827 Self::transition(
828 NodeConfig {
829 keystore_key_types: key_types.into_iter().map(|k| k.into()).collect(),
830 ..self.config
831 },
832 self.validation_context,
833 self.errors,
834 )
835 }
836
837 pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
839 if !self.errors.is_empty() {
840 return Err((self.config.name.clone(), self.errors));
841 }
842
843 Ok(self.config)
844 }
845}
846
847pub struct GroupNodeConfigBuilder<S> {
849 base_config: NodeConfig,
850 count: usize,
851 validation_context: Rc<RefCell<ValidationContext>>,
852 errors: Vec<anyhow::Error>,
853 _state: PhantomData<S>,
854}
855
856impl GroupNodeConfigBuilder<Initial> {
857 pub fn new(
858 chain_context: ChainDefaultContext,
859 validation_context: Rc<RefCell<ValidationContext>>,
860 ) -> Self {
861 let (errors, base_config) = match NodeConfigBuilder::new(
862 chain_context.clone(),
863 validation_context.clone(),
864 )
865 .with_name(" ") .build()
867 {
868 Ok(base_config) => (vec![], base_config),
869 Err((_name, errors)) => (errors, NodeConfig::default()),
870 };
871
872 Self {
873 base_config,
874 count: 1,
875 validation_context,
876 errors,
877 _state: PhantomData,
878 }
879 }
880
881 pub fn with_base_node(
883 mut self,
884 f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
885 ) -> GroupNodeConfigBuilder<Buildable> {
886 match f(NodeConfigBuilder::new(
887 ChainDefaultContext::default(),
888 self.validation_context.clone(),
889 ))
890 .build()
891 {
892 Ok(node) => {
893 self.base_config = node;
894 GroupNodeConfigBuilder {
895 base_config: self.base_config,
896 count: self.count,
897 validation_context: self.validation_context,
898 errors: self.errors,
899 _state: PhantomData,
900 }
901 },
902 Err((_name, errors)) => {
903 self.errors.extend(errors);
904 GroupNodeConfigBuilder {
905 base_config: self.base_config,
906 count: self.count,
907 validation_context: self.validation_context,
908 errors: self.errors,
909 _state: PhantomData,
910 }
911 },
912 }
913 }
914
915 pub fn with_count(mut self, count: usize) -> Self {
917 self.count = count;
918 self
919 }
920}
921
922impl GroupNodeConfigBuilder<Buildable> {
923 pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
924 if self.count == 0 {
925 return Err((
926 self.base_config.name().to_string(),
927 vec![anyhow::anyhow!("Count cannot be zero")],
928 ));
929 }
930
931 if !self.errors.is_empty() {
932 return Err((self.base_config.name().to_string(), self.errors));
933 }
934
935 Ok(GroupNodeConfig {
936 base_config: self.base_config,
937 count: self.count,
938 })
939 }
940}
941
942#[cfg(test)]
943mod tests {
944 use std::collections::HashSet;
945
946 use super::*;
947
948 #[test]
949 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
950 let node_config =
951 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
952 .with_name("node")
953 .with_command("mycommand")
954 .with_image("myrepo:myimage")
955 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
956 .validator(true)
957 .invulnerable(true)
958 .bootnode(true)
959 .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
960 .with_initial_balance(100_000_042)
961 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
962 .with_raw_bootnodes_addresses(vec![
963 "/ip4/10.41.122.55/tcp/45421",
964 "/ip4/51.144.222.10/tcp/2333",
965 ])
966 .with_resources(|resources| {
967 resources
968 .with_request_cpu("200M")
969 .with_request_memory("500M")
970 .with_limit_cpu("1G")
971 .with_limit_memory("2G")
972 })
973 .with_ws_port(5000)
974 .with_rpc_port(6000)
975 .with_prometheus_port(7000)
976 .with_p2p_port(8000)
977 .with_p2p_cert_hash(
978 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
979 )
980 .with_db_snapshot("/tmp/mysnapshot")
981 .with_keystore_path("/tmp/mykeystore")
982 .build()
983 .unwrap();
984
985 assert_eq!(node_config.name(), "node");
986 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
987 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
988 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
989 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
990 assert!(node_config.is_validator());
991 assert!(node_config.is_invulnerable());
992 assert!(node_config.is_bootnode());
993 assert_eq!(
994 node_config.override_eth_key(),
995 Some("0x0123456789abcdef0123456789abcdef01234567")
996 );
997 assert_eq!(node_config.initial_balance(), 100_000_042);
998 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
999 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1000 let bootnodes_addresses: Vec<Multiaddr> = vec![
1001 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1002 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1003 ];
1004 assert_eq!(
1005 node_config.bootnodes_addresses(),
1006 bootnodes_addresses.iter().collect::<Vec<_>>()
1007 );
1008 let resources = node_config.resources().unwrap();
1009 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1010 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1011 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1012 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1013 assert_eq!(node_config.ws_port().unwrap(), 5000);
1014 assert_eq!(node_config.rpc_port().unwrap(), 6000);
1015 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1016 assert_eq!(node_config.p2p_port().unwrap(), 8000);
1017 assert_eq!(
1018 node_config.p2p_cert_hash().unwrap(),
1019 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1020 );
1021 assert!(matches!(
1022 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1023 ));
1024 assert!(matches!(
1025 node_config.keystore_path().unwrap().to_str().unwrap(),
1026 "/tmp/mykeystore"
1027 ));
1028 }
1029
1030 #[test]
1031 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1032 let mut used_nodes_names = HashSet::new();
1033 used_nodes_names.insert("mynode".into());
1034 let validation_context = Rc::new(RefCell::new(ValidationContext {
1035 used_nodes_names,
1036 ..Default::default()
1037 }));
1038 let node_config =
1039 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1040 .with_name("mynode")
1041 .build()
1042 .unwrap();
1043
1044 assert_eq!(node_config.name, "mynode-1");
1045 }
1046
1047 #[test]
1048 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1049 let (node_name, errors) =
1050 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1051 .with_name("node")
1052 .with_command("invalid command")
1053 .build()
1054 .unwrap_err();
1055
1056 assert_eq!(node_name, "node");
1057 assert_eq!(errors.len(), 1);
1058 assert_eq!(
1059 errors.first().unwrap().to_string(),
1060 "command: 'invalid command' shouldn't contains whitespace"
1061 );
1062 }
1063
1064 #[test]
1065 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1066 let (node_name, errors) =
1067 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1068 .with_name("node")
1069 .with_image("myinvalid.image")
1070 .build()
1071 .unwrap_err();
1072
1073 assert_eq!(node_name, "node");
1074 assert_eq!(errors.len(), 1);
1075 assert_eq!(
1076 errors.first().unwrap().to_string(),
1077 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1078 );
1079 }
1080
1081 #[test]
1082 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1083 ) {
1084 let (node_name, errors) =
1085 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1086 .with_name("node")
1087 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1088 .build()
1089 .unwrap_err();
1090
1091 assert_eq!(node_name, "node");
1092 assert_eq!(errors.len(), 1);
1093 assert_eq!(
1094 errors.first().unwrap().to_string(),
1095 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1096 );
1097 }
1098
1099 #[test]
1100 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1101 ) {
1102 let (node_name, errors) =
1103 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1104 .with_name("node")
1105 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1106 .build()
1107 .unwrap_err();
1108
1109 assert_eq!(node_name, "node");
1110 assert_eq!(errors.len(), 2);
1111 assert_eq!(
1112 errors.first().unwrap().to_string(),
1113 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1114 );
1115 assert_eq!(
1116 errors.get(1).unwrap().to_string(),
1117 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1118 );
1119 }
1120
1121 #[test]
1122 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1123 ) {
1124 let (node_name, errors) =
1125 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1126 .with_name("node")
1127 .with_resources(|resources| resources.with_limit_cpu("invalid"))
1128 .build()
1129 .unwrap_err();
1130
1131 assert_eq!(node_name, "node");
1132 assert_eq!(errors.len(), 1);
1133 assert_eq!(
1134 errors.first().unwrap().to_string(),
1135 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1136 );
1137 }
1138
1139 #[test]
1140 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1141 ) {
1142 let (node_name, errors) =
1143 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1144 .with_name("node")
1145 .with_resources(|resources| {
1146 resources
1147 .with_limit_cpu("invalid")
1148 .with_request_memory("invalid")
1149 })
1150 .build()
1151 .unwrap_err();
1152
1153 assert_eq!(node_name, "node");
1154 assert_eq!(errors.len(), 2);
1155 assert_eq!(
1156 errors.first().unwrap().to_string(),
1157 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1158 );
1159 assert_eq!(
1160 errors.get(1).unwrap().to_string(),
1161 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1162 );
1163 }
1164
1165 #[test]
1166 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1167 ) {
1168 let (node_name, errors) =
1169 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1170 .with_name("node")
1171 .with_command("invalid command")
1172 .with_image("myinvalid.image")
1173 .with_resources(|resources| {
1174 resources
1175 .with_limit_cpu("invalid")
1176 .with_request_memory("invalid")
1177 })
1178 .build()
1179 .unwrap_err();
1180
1181 assert_eq!(node_name, "node");
1182 assert_eq!(errors.len(), 4);
1183 assert_eq!(
1184 errors.first().unwrap().to_string(),
1185 "command: 'invalid command' shouldn't contains whitespace"
1186 );
1187 assert_eq!(
1188 errors.get(1).unwrap().to_string(),
1189 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1190 );
1191 assert_eq!(
1192 errors.get(2).unwrap().to_string(),
1193 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1194 );
1195 assert_eq!(
1196 errors.get(3).unwrap().to_string(),
1197 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1198 );
1199 }
1200
1201 #[test]
1202 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1203 ) {
1204 let validation_context = Rc::new(RefCell::new(ValidationContext {
1205 used_ports: vec![30333],
1206 ..Default::default()
1207 }));
1208 let (node_name, errors) =
1209 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1210 .with_name("node")
1211 .with_ws_port(30333)
1212 .build()
1213 .unwrap_err();
1214
1215 assert_eq!(node_name, "node");
1216 assert_eq!(errors.len(), 1);
1217 assert_eq!(
1218 errors.first().unwrap().to_string(),
1219 "ws_port: '30333' is already used across config"
1220 );
1221 }
1222
1223 #[test]
1224 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1225 ) {
1226 let validation_context = Rc::new(RefCell::new(ValidationContext {
1227 used_ports: vec![4444],
1228 ..Default::default()
1229 }));
1230 let (node_name, errors) =
1231 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1232 .with_name("node")
1233 .with_rpc_port(4444)
1234 .build()
1235 .unwrap_err();
1236
1237 assert_eq!(node_name, "node");
1238 assert_eq!(errors.len(), 1);
1239 assert_eq!(
1240 errors.first().unwrap().to_string(),
1241 "rpc_port: '4444' is already used across config"
1242 );
1243 }
1244
1245 #[test]
1246 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1247 ) {
1248 let validation_context = Rc::new(RefCell::new(ValidationContext {
1249 used_ports: vec![9089],
1250 ..Default::default()
1251 }));
1252 let (node_name, errors) =
1253 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1254 .with_name("node")
1255 .with_prometheus_port(9089)
1256 .build()
1257 .unwrap_err();
1258
1259 assert_eq!(node_name, "node");
1260 assert_eq!(errors.len(), 1);
1261 assert_eq!(
1262 errors.first().unwrap().to_string(),
1263 "prometheus_port: '9089' is already used across config"
1264 );
1265 }
1266
1267 #[test]
1268 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1269 ) {
1270 let validation_context = Rc::new(RefCell::new(ValidationContext {
1271 used_ports: vec![45093],
1272 ..Default::default()
1273 }));
1274 let (node_name, errors) =
1275 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1276 .with_name("node")
1277 .with_p2p_port(45093)
1278 .build()
1279 .unwrap_err();
1280
1281 assert_eq!(node_name, "node");
1282 assert_eq!(errors.len(), 1);
1283 assert_eq!(
1284 errors.first().unwrap().to_string(),
1285 "p2p_port: '45093' is already used across config"
1286 );
1287 }
1288
1289 #[test]
1290 fn node_config_builder_should_fails_if_node_name_is_empty() {
1291 let validation_context = Rc::new(RefCell::new(ValidationContext {
1292 ..Default::default()
1293 }));
1294
1295 let (_, errors) =
1296 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1297 .with_name("")
1298 .build()
1299 .unwrap_err();
1300
1301 assert_eq!(errors.len(), 1);
1302 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1303 }
1304
1305 #[test]
1306 fn group_default_base_node() {
1307 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1308
1309 let group_config =
1310 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1311 .with_base_node(|node| node.with_name("validator"))
1312 .build()
1313 .unwrap();
1314
1315 assert_eq!(group_config.count, 1);
1317 assert_eq!(group_config.base_config.name(), "validator");
1318 }
1319
1320 #[test]
1321 fn group_custom_base_node() {
1322 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1323 let node_config =
1324 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1325 .with_name("node")
1326 .with_command("some_command")
1327 .with_image("repo:image")
1328 .validator(true)
1329 .invulnerable(true)
1330 .bootnode(true);
1331
1332 let group_config =
1333 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1334 .with_count(5)
1335 .with_base_node(|_node| node_config)
1336 .build()
1337 .unwrap();
1338
1339 assert_eq!(group_config.count, 5);
1341
1342 assert_eq!(group_config.base_config.name(), "node");
1343 assert_eq!(
1344 group_config.base_config.command().unwrap().as_str(),
1345 "some_command"
1346 );
1347 assert_eq!(
1348 group_config.base_config.image().unwrap().as_str(),
1349 "repo:image"
1350 );
1351 assert!(group_config.base_config.is_validator());
1352 assert!(group_config.base_config.is_invulnerable());
1353 assert!(group_config.base_config.is_bootnode());
1354 }
1355}