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