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(|i| {
230 let mut node = self.base_config.clone();
231 let node_name = format!("{}-{i}", node.name);
233
234 let unique_name = generate_unique_node_name_from_names(node_name, &mut used_names);
235 node.name = unique_name;
236
237 if let Some(ref base_log_path) = node.node_log_path {
239 let unique_log_path = if let Some(parent) = base_log_path.parent() {
240 parent.join(format!("{}.log", node.name))
241 } else {
242 PathBuf::from(format!("{}.log", node.name))
243 };
244 node.node_log_path = Some(unique_log_path);
245 }
246
247 node
248 })
249 .collect()
250 }
251}
252
253impl Serialize for GroupNodeConfig {
254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
255 where
256 S: serde::Serializer,
257 {
258 let mut state = serializer.serialize_struct("GroupNodeConfig", 18)?;
259 state.serialize_field("NodeConfig", &self.base_config)?;
260 state.serialize_field("count", &self.count)?;
261 state.end()
262 }
263}
264
265impl NodeConfig {
266 pub fn name(&self) -> &str {
268 &self.name
269 }
270
271 pub fn image(&self) -> Option<&Image> {
273 self.image.as_ref()
274 }
275
276 pub fn command(&self) -> Option<&Command> {
278 self.command.as_ref()
279 }
280
281 pub fn subcommand(&self) -> Option<&Command> {
283 self.subcommand.as_ref()
284 }
285
286 pub fn args(&self) -> Vec<&Arg> {
288 self.args.iter().collect()
289 }
290
291 pub(crate) fn set_args(&mut self, args: Vec<Arg>) {
293 self.args = args;
294 }
295
296 pub fn is_validator(&self) -> bool {
298 self.is_validator
299 }
300
301 pub fn is_invulnerable(&self) -> bool {
303 self.is_invulnerable
304 }
305
306 pub fn is_bootnode(&self) -> bool {
308 self.is_bootnode
309 }
310
311 pub fn initial_balance(&self) -> u128 {
313 self.initial_balance.0
314 }
315
316 pub fn env(&self) -> Vec<&EnvVar> {
318 self.env.iter().collect()
319 }
320
321 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
323 self.bootnodes_addresses.iter().collect()
324 }
325
326 pub fn resources(&self) -> Option<&Resources> {
328 self.resources.as_ref()
329 }
330
331 pub fn ws_port(&self) -> Option<u16> {
333 self.ws_port
334 }
335
336 pub fn rpc_port(&self) -> Option<u16> {
338 self.rpc_port
339 }
340
341 pub fn prometheus_port(&self) -> Option<u16> {
343 self.prometheus_port
344 }
345
346 pub fn p2p_port(&self) -> Option<u16> {
348 self.p2p_port
349 }
350
351 pub fn p2p_cert_hash(&self) -> Option<&str> {
353 self.p2p_cert_hash.as_deref()
354 }
355
356 pub fn db_snapshot(&self) -> Option<&AssetLocation> {
358 self.db_snapshot.as_ref()
359 }
360
361 pub fn node_log_path(&self) -> Option<&PathBuf> {
363 self.node_log_path.as_ref()
364 }
365
366 pub fn keystore_path(&self) -> Option<&PathBuf> {
368 self.keystore_path.as_ref()
369 }
370
371 pub fn override_eth_key(&self) -> Option<&str> {
373 self.override_eth_key.as_deref()
374 }
375
376 pub fn keystore_key_types(&self) -> Vec<&str> {
379 self.keystore_key_types.iter().map(String::as_str).collect()
380 }
381
382 pub fn chain_spec_key_types(&self) -> Vec<&str> {
385 self.chain_spec_key_types
386 .iter()
387 .map(String::as_str)
388 .collect()
389 }
390}
391
392pub struct NodeConfigBuilder<S> {
394 config: NodeConfig,
395 validation_context: Rc<RefCell<ValidationContext>>,
396 errors: Vec<anyhow::Error>,
397 _state: PhantomData<S>,
398}
399
400impl Default for NodeConfigBuilder<Initial> {
401 fn default() -> Self {
402 Self {
403 config: NodeConfig {
404 name: "".into(),
405 image: None,
406 command: None,
407 subcommand: None,
408 args: vec![],
409 is_validator: true,
410 is_invulnerable: true,
411 is_bootnode: false,
412 initial_balance: 2_000_000_000_000.into(),
413 env: vec![],
414 bootnodes_addresses: vec![],
415 resources: None,
416 ws_port: None,
417 rpc_port: None,
418 prometheus_port: None,
419 p2p_port: None,
420 p2p_cert_hash: None,
421 db_snapshot: None,
422 override_eth_key: None,
423 chain_context: Default::default(),
424 node_log_path: None,
425 keystore_path: None,
426 keystore_key_types: vec![],
427 chain_spec_key_types: vec![],
428 },
429 validation_context: Default::default(),
430 errors: vec![],
431 _state: PhantomData,
432 }
433 }
434}
435
436impl<A> NodeConfigBuilder<A> {
437 fn transition<B>(
438 config: NodeConfig,
439 validation_context: Rc<RefCell<ValidationContext>>,
440 errors: Vec<anyhow::Error>,
441 ) -> NodeConfigBuilder<B> {
442 NodeConfigBuilder {
443 config,
444 validation_context,
445 errors,
446 _state: PhantomData,
447 }
448 }
449}
450
451impl NodeConfigBuilder<Initial> {
452 pub fn new(
453 chain_context: ChainDefaultContext,
454 validation_context: Rc<RefCell<ValidationContext>>,
455 ) -> Self {
456 Self::transition(
457 NodeConfig {
458 command: chain_context.default_command.clone(),
459 image: chain_context.default_image.clone(),
460 resources: chain_context.default_resources.clone(),
461 db_snapshot: chain_context.default_db_snapshot.clone(),
462 args: chain_context.default_args.clone(),
463 chain_context,
464 ..Self::default().config
465 },
466 validation_context,
467 vec![],
468 )
469 }
470
471 pub fn with_name<T: Into<String> + Copy>(self, name: T) -> NodeConfigBuilder<Buildable> {
473 let name: String = generate_unique_node_name(name, self.validation_context.clone());
474
475 match ensure_value_is_not_empty(&name) {
476 Ok(_) => Self::transition(
477 NodeConfig {
478 name,
479 ..self.config
480 },
481 self.validation_context,
482 self.errors,
483 ),
484 Err(e) => Self::transition(
485 NodeConfig {
486 name,
488 ..self.config
489 },
490 self.validation_context,
491 merge_errors(self.errors, FieldError::Name(e).into()),
492 ),
493 }
494 }
495}
496
497impl NodeConfigBuilder<Buildable> {
498 pub fn with_command<T>(self, command: T) -> Self
500 where
501 T: TryInto<Command>,
502 T::Error: Error + Send + Sync + 'static,
503 {
504 match command.try_into() {
505 Ok(command) => Self::transition(
506 NodeConfig {
507 command: Some(command),
508 ..self.config
509 },
510 self.validation_context,
511 self.errors,
512 ),
513 Err(error) => Self::transition(
514 self.config,
515 self.validation_context,
516 merge_errors(self.errors, FieldError::Command(error.into()).into()),
517 ),
518 }
519 }
520
521 pub fn with_subcommand<T>(self, subcommand: T) -> Self
523 where
524 T: TryInto<Command>,
525 T::Error: Error + Send + Sync + 'static,
526 {
527 match subcommand.try_into() {
528 Ok(subcommand) => Self::transition(
529 NodeConfig {
530 subcommand: Some(subcommand),
531 ..self.config
532 },
533 self.validation_context,
534 self.errors,
535 ),
536 Err(error) => Self::transition(
537 self.config,
538 self.validation_context,
539 merge_errors(self.errors, FieldError::Command(error.into()).into()),
540 ),
541 }
542 }
543
544 pub fn with_image<T>(self, image: T) -> Self
546 where
547 T: TryInto<Image>,
548 T::Error: Error + Send + Sync + 'static,
549 {
550 match image.try_into() {
551 Ok(image) => Self::transition(
552 NodeConfig {
553 image: Some(image),
554 ..self.config
555 },
556 self.validation_context,
557 self.errors,
558 ),
559 Err(error) => Self::transition(
560 self.config,
561 self.validation_context,
562 merge_errors(self.errors, FieldError::Image(error.into()).into()),
563 ),
564 }
565 }
566
567 pub fn with_args(self, args: Vec<Arg>) -> Self {
569 Self::transition(
570 NodeConfig {
571 args,
572 ..self.config
573 },
574 self.validation_context,
575 self.errors,
576 )
577 }
578
579 pub fn validator(self, choice: bool) -> Self {
581 Self::transition(
582 NodeConfig {
583 is_validator: choice,
584 ..self.config
585 },
586 self.validation_context,
587 self.errors,
588 )
589 }
590
591 pub fn invulnerable(self, choice: bool) -> Self {
593 Self::transition(
594 NodeConfig {
595 is_invulnerable: choice,
596 ..self.config
597 },
598 self.validation_context,
599 self.errors,
600 )
601 }
602
603 pub fn bootnode(self, choice: bool) -> Self {
605 Self::transition(
606 NodeConfig {
607 is_bootnode: choice,
608 ..self.config
609 },
610 self.validation_context,
611 self.errors,
612 )
613 }
614
615 pub fn with_override_eth_key(self, session_key: impl Into<String>) -> Self {
617 Self::transition(
618 NodeConfig {
619 override_eth_key: Some(session_key.into()),
620 ..self.config
621 },
622 self.validation_context,
623 self.errors,
624 )
625 }
626
627 pub fn with_initial_balance(self, initial_balance: u128) -> Self {
629 Self::transition(
630 NodeConfig {
631 initial_balance: initial_balance.into(),
632 ..self.config
633 },
634 self.validation_context,
635 self.errors,
636 )
637 }
638
639 pub fn with_env(self, env: Vec<impl Into<EnvVar>>) -> Self {
641 let env = env.into_iter().map(|var| var.into()).collect::<Vec<_>>();
642
643 Self::transition(
644 NodeConfig { env, ..self.config },
645 self.validation_context,
646 self.errors,
647 )
648 }
649
650 pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
655 where
656 T: TryInto<Multiaddr> + Display + Copy,
657 T::Error: Error + Send + Sync + 'static,
658 {
659 let mut addrs = vec![];
660 let mut errors = vec![];
661
662 for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
663 match addr.try_into() {
664 Ok(addr) => addrs.push(addr),
665 Err(error) => errors.push(
666 FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
667 ),
668 }
669 }
670
671 Self::transition(
672 NodeConfig {
673 bootnodes_addresses: addrs,
674 ..self.config
675 },
676 self.validation_context,
677 merge_errors_vecs(self.errors, errors),
678 )
679 }
680
681 pub fn with_resources(self, f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder) -> Self {
683 match f(ResourcesBuilder::new()).build() {
684 Ok(resources) => Self::transition(
685 NodeConfig {
686 resources: Some(resources),
687 ..self.config
688 },
689 self.validation_context,
690 self.errors,
691 ),
692 Err(errors) => Self::transition(
693 self.config,
694 self.validation_context,
695 merge_errors_vecs(
696 self.errors,
697 errors
698 .into_iter()
699 .map(|error| FieldError::Resources(error).into())
700 .collect::<Vec<_>>(),
701 ),
702 ),
703 }
704 }
705
706 pub fn with_ws_port(self, ws_port: Port) -> Self {
708 match ensure_port_unique(ws_port, self.validation_context.clone()) {
709 Ok(_) => Self::transition(
710 NodeConfig {
711 ws_port: Some(ws_port),
712 ..self.config
713 },
714 self.validation_context,
715 self.errors,
716 ),
717 Err(error) => Self::transition(
718 self.config,
719 self.validation_context,
720 merge_errors(self.errors, FieldError::WsPort(error).into()),
721 ),
722 }
723 }
724
725 pub fn with_rpc_port(self, rpc_port: Port) -> Self {
727 match ensure_port_unique(rpc_port, self.validation_context.clone()) {
728 Ok(_) => Self::transition(
729 NodeConfig {
730 rpc_port: Some(rpc_port),
731 ..self.config
732 },
733 self.validation_context,
734 self.errors,
735 ),
736 Err(error) => Self::transition(
737 self.config,
738 self.validation_context,
739 merge_errors(self.errors, FieldError::RpcPort(error).into()),
740 ),
741 }
742 }
743
744 pub fn with_prometheus_port(self, prometheus_port: Port) -> Self {
746 match ensure_port_unique(prometheus_port, self.validation_context.clone()) {
747 Ok(_) => Self::transition(
748 NodeConfig {
749 prometheus_port: Some(prometheus_port),
750 ..self.config
751 },
752 self.validation_context,
753 self.errors,
754 ),
755 Err(error) => Self::transition(
756 self.config,
757 self.validation_context,
758 merge_errors(self.errors, FieldError::PrometheusPort(error).into()),
759 ),
760 }
761 }
762
763 pub fn with_p2p_port(self, p2p_port: Port) -> Self {
765 match ensure_port_unique(p2p_port, self.validation_context.clone()) {
766 Ok(_) => Self::transition(
767 NodeConfig {
768 p2p_port: Some(p2p_port),
769 ..self.config
770 },
771 self.validation_context,
772 self.errors,
773 ),
774 Err(error) => Self::transition(
775 self.config,
776 self.validation_context,
777 merge_errors(self.errors, FieldError::P2pPort(error).into()),
778 ),
779 }
780 }
781
782 pub fn with_p2p_cert_hash(self, p2p_cert_hash: impl Into<String>) -> Self {
785 Self::transition(
786 NodeConfig {
787 p2p_cert_hash: Some(p2p_cert_hash.into()),
788 ..self.config
789 },
790 self.validation_context,
791 self.errors,
792 )
793 }
794
795 pub fn with_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
797 Self::transition(
798 NodeConfig {
799 db_snapshot: Some(location.into()),
800 ..self.config
801 },
802 self.validation_context,
803 self.errors,
804 )
805 }
806
807 pub fn with_optional_db_snapshot(self, location: Option<impl Into<AssetLocation>>) -> Self {
811 match location {
812 Some(location) => self.with_db_snapshot(location),
813 None => self,
814 }
815 }
816
817 pub fn with_log_path(self, log_path: impl Into<PathBuf>) -> Self {
819 Self::transition(
820 NodeConfig {
821 node_log_path: Some(log_path.into()),
822 ..self.config
823 },
824 self.validation_context,
825 self.errors,
826 )
827 }
828
829 pub fn with_keystore_path(self, keystore_path: impl Into<PathBuf>) -> Self {
831 Self::transition(
832 NodeConfig {
833 keystore_path: Some(keystore_path.into()),
834 ..self.config
835 },
836 self.validation_context,
837 self.errors,
838 )
839 }
840
841 pub fn with_keystore_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
861 Self::transition(
862 NodeConfig {
863 keystore_key_types: key_types.into_iter().map(|k| k.into()).collect(),
864 ..self.config
865 },
866 self.validation_context,
867 self.errors,
868 )
869 }
870
871 pub fn with_chain_spec_key_types(self, key_types: Vec<impl Into<String>>) -> Self {
897 Self::transition(
898 NodeConfig {
899 chain_spec_key_types: key_types.into_iter().map(|k| k.into()).collect(),
900 ..self.config
901 },
902 self.validation_context,
903 self.errors,
904 )
905 }
906
907 pub fn build(self) -> Result<NodeConfig, (String, Vec<anyhow::Error>)> {
909 if !self.errors.is_empty() {
910 return Err((self.config.name.clone(), self.errors));
911 }
912
913 Ok(self.config)
914 }
915}
916
917pub struct GroupNodeConfigBuilder<S> {
919 base_config: NodeConfig,
920 count: usize,
921 validation_context: Rc<RefCell<ValidationContext>>,
922 errors: Vec<anyhow::Error>,
923 _state: PhantomData<S>,
924}
925
926impl GroupNodeConfigBuilder<Initial> {
927 pub fn new(
928 chain_context: ChainDefaultContext,
929 validation_context: Rc<RefCell<ValidationContext>>,
930 ) -> Self {
931 let (errors, base_config) = match NodeConfigBuilder::new(
932 chain_context.clone(),
933 validation_context.clone(),
934 )
935 .with_name(" ") .build()
937 {
938 Ok(base_config) => (vec![], base_config),
939 Err((_name, errors)) => (errors, NodeConfig::default()),
940 };
941
942 Self {
943 base_config,
944 count: 1,
945 validation_context,
946 errors,
947 _state: PhantomData,
948 }
949 }
950
951 pub fn with_base_node(
953 mut self,
954 f: impl FnOnce(NodeConfigBuilder<Initial>) -> NodeConfigBuilder<Buildable>,
955 ) -> GroupNodeConfigBuilder<Buildable> {
956 match f(NodeConfigBuilder::new(
957 ChainDefaultContext::default(),
958 self.validation_context.clone(),
959 ))
960 .build()
961 {
962 Ok(node) => {
963 self.base_config = node;
964 GroupNodeConfigBuilder {
965 base_config: self.base_config,
966 count: self.count,
967 validation_context: self.validation_context,
968 errors: self.errors,
969 _state: PhantomData,
970 }
971 },
972 Err((_name, errors)) => {
973 self.errors.extend(errors);
974 GroupNodeConfigBuilder {
975 base_config: self.base_config,
976 count: self.count,
977 validation_context: self.validation_context,
978 errors: self.errors,
979 _state: PhantomData,
980 }
981 },
982 }
983 }
984
985 pub fn with_count(mut self, count: usize) -> Self {
987 self.count = count;
988 self
989 }
990}
991
992impl GroupNodeConfigBuilder<Buildable> {
993 pub fn with_count(mut self, count: usize) -> Self {
995 self.count = count;
996 self
997 }
998
999 pub fn build(self) -> Result<GroupNodeConfig, (String, Vec<anyhow::Error>)> {
1000 if self.count == 0 {
1001 return Err((
1002 self.base_config.name().to_string(),
1003 vec![anyhow::anyhow!("Count cannot be zero")],
1004 ));
1005 }
1006
1007 if !self.errors.is_empty() {
1008 return Err((self.base_config.name().to_string(), self.errors));
1009 }
1010
1011 Ok(GroupNodeConfig {
1012 base_config: self.base_config,
1013 count: self.count,
1014 })
1015 }
1016}
1017
1018#[cfg(test)]
1019mod tests {
1020 use std::collections::HashSet;
1021
1022 use super::*;
1023
1024 #[test]
1025 fn node_config_builder_should_succeeds_and_returns_a_node_config() {
1026 let node_config =
1027 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1028 .with_name("node")
1029 .with_command("mycommand")
1030 .with_image("myrepo:myimage")
1031 .with_args(vec![("--arg1", "value1").into(), "--option2".into()])
1032 .validator(true)
1033 .invulnerable(true)
1034 .bootnode(true)
1035 .with_override_eth_key("0x0123456789abcdef0123456789abcdef01234567")
1036 .with_initial_balance(100_000_042)
1037 .with_env(vec![("VAR1", "VALUE1"), ("VAR2", "VALUE2")])
1038 .with_raw_bootnodes_addresses(vec![
1039 "/ip4/10.41.122.55/tcp/45421",
1040 "/ip4/51.144.222.10/tcp/2333",
1041 ])
1042 .with_resources(|resources| {
1043 resources
1044 .with_request_cpu("200M")
1045 .with_request_memory("500M")
1046 .with_limit_cpu("1G")
1047 .with_limit_memory("2G")
1048 })
1049 .with_ws_port(5000)
1050 .with_rpc_port(6000)
1051 .with_prometheus_port(7000)
1052 .with_p2p_port(8000)
1053 .with_p2p_cert_hash(
1054 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f",
1055 )
1056 .with_db_snapshot("/tmp/mysnapshot")
1057 .with_keystore_path("/tmp/mykeystore")
1058 .build()
1059 .unwrap();
1060
1061 assert_eq!(node_config.name(), "node");
1062 assert_eq!(node_config.command().unwrap().as_str(), "mycommand");
1063 assert_eq!(node_config.image().unwrap().as_str(), "myrepo:myimage");
1064 let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1065 assert_eq!(node_config.args(), args.iter().collect::<Vec<_>>());
1066 assert!(node_config.is_validator());
1067 assert!(node_config.is_invulnerable());
1068 assert!(node_config.is_bootnode());
1069 assert_eq!(
1070 node_config.override_eth_key(),
1071 Some("0x0123456789abcdef0123456789abcdef01234567")
1072 );
1073 assert_eq!(node_config.initial_balance(), 100_000_042);
1074 let env: Vec<EnvVar> = vec![("VAR1", "VALUE1").into(), ("VAR2", "VALUE2").into()];
1075 assert_eq!(node_config.env(), env.iter().collect::<Vec<_>>());
1076 let bootnodes_addresses: Vec<Multiaddr> = vec![
1077 "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1078 "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1079 ];
1080 assert_eq!(
1081 node_config.bootnodes_addresses(),
1082 bootnodes_addresses.iter().collect::<Vec<_>>()
1083 );
1084 let resources = node_config.resources().unwrap();
1085 assert_eq!(resources.request_cpu().unwrap().as_str(), "200M");
1086 assert_eq!(resources.request_memory().unwrap().as_str(), "500M");
1087 assert_eq!(resources.limit_cpu().unwrap().as_str(), "1G");
1088 assert_eq!(resources.limit_memory().unwrap().as_str(), "2G");
1089 assert_eq!(node_config.ws_port().unwrap(), 5000);
1090 assert_eq!(node_config.rpc_port().unwrap(), 6000);
1091 assert_eq!(node_config.prometheus_port().unwrap(), 7000);
1092 assert_eq!(node_config.p2p_port().unwrap(), 8000);
1093 assert_eq!(
1094 node_config.p2p_cert_hash().unwrap(),
1095 "ec8d6467180a4b72a52b24c53aa1e53b76c05602fa96f5d0961bf720edda267f"
1096 );
1097 assert!(matches!(
1098 node_config.db_snapshot().unwrap(), AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1099 ));
1100 assert!(matches!(
1101 node_config.keystore_path().unwrap().to_str().unwrap(),
1102 "/tmp/mykeystore"
1103 ));
1104 }
1105
1106 #[test]
1107 fn with_optional_db_snapshot_applies_when_some() {
1108 let node_config =
1109 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1110 .with_name("node")
1111 .with_command("mycommand")
1112 .with_optional_db_snapshot(Some("/tmp/mysnapshot"))
1113 .build()
1114 .unwrap();
1115 assert!(matches!(
1116 node_config.db_snapshot().unwrap(),
1117 AssetLocation::FilePath(value) if value.to_str().unwrap() == "/tmp/mysnapshot"
1118 ));
1119 }
1120
1121 #[test]
1122 fn with_optional_db_snapshot_is_noop_when_none() {
1123 let node_config =
1124 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1125 .with_name("node")
1126 .with_command("mycommand")
1127 .with_optional_db_snapshot(None::<&str>)
1128 .build()
1129 .unwrap();
1130 assert!(node_config.db_snapshot().is_none());
1131 }
1132
1133 #[test]
1134 fn node_config_builder_should_use_unique_name_if_node_name_already_used() {
1135 let mut used_nodes_names = HashSet::new();
1136 used_nodes_names.insert("mynode".into());
1137 let validation_context = Rc::new(RefCell::new(ValidationContext {
1138 used_nodes_names,
1139 ..Default::default()
1140 }));
1141 let node_config =
1142 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1143 .with_name("mynode")
1144 .build()
1145 .unwrap();
1146
1147 assert_eq!(node_config.name, "mynode-1");
1148 }
1149
1150 #[test]
1151 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_command_is_invalid() {
1152 let (node_name, errors) =
1153 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1154 .with_name("node")
1155 .with_command("invalid command")
1156 .build()
1157 .unwrap_err();
1158
1159 assert_eq!(node_name, "node");
1160 assert_eq!(errors.len(), 1);
1161 assert_eq!(
1162 errors.first().unwrap().to_string(),
1163 "command: 'invalid command' shouldn't contains whitespace"
1164 );
1165 }
1166
1167 #[test]
1168 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_image_is_invalid() {
1169 let (node_name, errors) =
1170 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1171 .with_name("node")
1172 .with_image("myinvalid.image")
1173 .build()
1174 .unwrap_err();
1175
1176 assert_eq!(node_name, "node");
1177 assert_eq!(errors.len(), 1);
1178 assert_eq!(
1179 errors.first().unwrap().to_string(),
1180 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1181 );
1182 }
1183
1184 #[test]
1185 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_one_bootnode_address_is_invalid(
1186 ) {
1187 let (node_name, errors) =
1188 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1189 .with_name("node")
1190 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1191 .build()
1192 .unwrap_err();
1193
1194 assert_eq!(node_name, "node");
1195 assert_eq!(errors.len(), 1);
1196 assert_eq!(
1197 errors.first().unwrap().to_string(),
1198 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1199 );
1200 }
1201
1202 #[test]
1203 fn node_config_builder_should_fails_and_returns_mulitle_errors_and_node_name_if_multiple_bootnode_address_are_invalid(
1204 ) {
1205 let (node_name, errors) =
1206 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1207 .with_name("node")
1208 .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1209 .build()
1210 .unwrap_err();
1211
1212 assert_eq!(node_name, "node");
1213 assert_eq!(errors.len(), 2);
1214 assert_eq!(
1215 errors.first().unwrap().to_string(),
1216 "bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1217 );
1218 assert_eq!(
1219 errors.get(1).unwrap().to_string(),
1220 "bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1221 );
1222 }
1223
1224 #[test]
1225 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_resources_has_an_error(
1226 ) {
1227 let (node_name, errors) =
1228 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1229 .with_name("node")
1230 .with_resources(|resources| resources.with_limit_cpu("invalid"))
1231 .build()
1232 .unwrap_err();
1233
1234 assert_eq!(node_name, "node");
1235 assert_eq!(errors.len(), 1);
1236 assert_eq!(
1237 errors.first().unwrap().to_string(),
1238 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1239 );
1240 }
1241
1242 #[test]
1243 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_resources_has_multiple_errors(
1244 ) {
1245 let (node_name, errors) =
1246 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1247 .with_name("node")
1248 .with_resources(|resources| {
1249 resources
1250 .with_limit_cpu("invalid")
1251 .with_request_memory("invalid")
1252 })
1253 .build()
1254 .unwrap_err();
1255
1256 assert_eq!(node_name, "node");
1257 assert_eq!(errors.len(), 2);
1258 assert_eq!(
1259 errors.first().unwrap().to_string(),
1260 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1261 );
1262 assert_eq!(
1263 errors.get(1).unwrap().to_string(),
1264 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1265 );
1266 }
1267
1268 #[test]
1269 fn node_config_builder_should_fails_and_returns_multiple_errors_and_node_name_if_multiple_fields_have_errors(
1270 ) {
1271 let (node_name, errors) =
1272 NodeConfigBuilder::new(ChainDefaultContext::default(), Default::default())
1273 .with_name("node")
1274 .with_command("invalid command")
1275 .with_image("myinvalid.image")
1276 .with_resources(|resources| {
1277 resources
1278 .with_limit_cpu("invalid")
1279 .with_request_memory("invalid")
1280 })
1281 .build()
1282 .unwrap_err();
1283
1284 assert_eq!(node_name, "node");
1285 assert_eq!(errors.len(), 4);
1286 assert_eq!(
1287 errors.first().unwrap().to_string(),
1288 "command: 'invalid command' shouldn't contains whitespace"
1289 );
1290 assert_eq!(
1291 errors.get(1).unwrap().to_string(),
1292 "image: 'myinvalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1293 );
1294 assert_eq!(
1295 errors.get(2).unwrap().to_string(),
1296 r"resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1297 );
1298 assert_eq!(
1299 errors.get(3).unwrap().to_string(),
1300 r"resources.request_memory: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1301 );
1302 }
1303
1304 #[test]
1305 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_ws_port_is_already_used(
1306 ) {
1307 let validation_context = Rc::new(RefCell::new(ValidationContext {
1308 used_ports: vec![30333],
1309 ..Default::default()
1310 }));
1311 let (node_name, errors) =
1312 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1313 .with_name("node")
1314 .with_ws_port(30333)
1315 .build()
1316 .unwrap_err();
1317
1318 assert_eq!(node_name, "node");
1319 assert_eq!(errors.len(), 1);
1320 assert_eq!(
1321 errors.first().unwrap().to_string(),
1322 "ws_port: '30333' is already used across config"
1323 );
1324 }
1325
1326 #[test]
1327 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_rpc_port_is_already_used(
1328 ) {
1329 let validation_context = Rc::new(RefCell::new(ValidationContext {
1330 used_ports: vec![4444],
1331 ..Default::default()
1332 }));
1333 let (node_name, errors) =
1334 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1335 .with_name("node")
1336 .with_rpc_port(4444)
1337 .build()
1338 .unwrap_err();
1339
1340 assert_eq!(node_name, "node");
1341 assert_eq!(errors.len(), 1);
1342 assert_eq!(
1343 errors.first().unwrap().to_string(),
1344 "rpc_port: '4444' is already used across config"
1345 );
1346 }
1347
1348 #[test]
1349 fn node_config_builder_should_fails_and_returns_an_error_and_node_name_if_prometheus_port_is_already_used(
1350 ) {
1351 let validation_context = Rc::new(RefCell::new(ValidationContext {
1352 used_ports: vec![9089],
1353 ..Default::default()
1354 }));
1355 let (node_name, errors) =
1356 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1357 .with_name("node")
1358 .with_prometheus_port(9089)
1359 .build()
1360 .unwrap_err();
1361
1362 assert_eq!(node_name, "node");
1363 assert_eq!(errors.len(), 1);
1364 assert_eq!(
1365 errors.first().unwrap().to_string(),
1366 "prometheus_port: '9089' is already used across config"
1367 );
1368 }
1369
1370 #[test]
1371 fn node_config_builder_should_fails_and_returns_and_error_and_node_name_if_p2p_port_is_already_used(
1372 ) {
1373 let validation_context = Rc::new(RefCell::new(ValidationContext {
1374 used_ports: vec![45093],
1375 ..Default::default()
1376 }));
1377 let (node_name, errors) =
1378 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1379 .with_name("node")
1380 .with_p2p_port(45093)
1381 .build()
1382 .unwrap_err();
1383
1384 assert_eq!(node_name, "node");
1385 assert_eq!(errors.len(), 1);
1386 assert_eq!(
1387 errors.first().unwrap().to_string(),
1388 "p2p_port: '45093' is already used across config"
1389 );
1390 }
1391
1392 #[test]
1393 fn node_config_builder_should_fails_if_node_name_is_empty() {
1394 let validation_context = Rc::new(RefCell::new(ValidationContext {
1395 ..Default::default()
1396 }));
1397
1398 let (_, errors) =
1399 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context)
1400 .with_name("")
1401 .build()
1402 .unwrap_err();
1403
1404 assert_eq!(errors.len(), 1);
1405 assert_eq!(errors.first().unwrap().to_string(), "name: can't be empty");
1406 }
1407
1408 #[test]
1409 fn group_default_base_node() {
1410 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1411
1412 let group_config =
1413 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1414 .with_base_node(|node| node.with_name("validator"))
1415 .build()
1416 .unwrap();
1417
1418 assert_eq!(group_config.count, 1);
1420 assert_eq!(group_config.base_config.name(), "validator");
1421 }
1422
1423 #[test]
1424 fn group_custom_base_node() {
1425 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1426 let node_config =
1427 NodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1428 .with_name("node")
1429 .with_command("some_command")
1430 .with_image("repo:image")
1431 .validator(true)
1432 .invulnerable(true)
1433 .bootnode(true);
1434
1435 let group_config =
1436 GroupNodeConfigBuilder::new(ChainDefaultContext::default(), validation_context.clone())
1437 .with_count(5)
1438 .with_base_node(|_node| node_config)
1439 .build()
1440 .unwrap();
1441
1442 assert_eq!(group_config.count, 5);
1444
1445 assert_eq!(group_config.base_config.name(), "node");
1446 assert_eq!(
1447 group_config.base_config.command().unwrap().as_str(),
1448 "some_command"
1449 );
1450 assert_eq!(
1451 group_config.base_config.image().unwrap().as_str(),
1452 "repo:image"
1453 );
1454 assert!(group_config.base_config.is_validator());
1455 assert!(group_config.base_config.is_invulnerable());
1456 assert!(group_config.base_config.is_bootnode());
1457 }
1458
1459 #[test]
1460 fn ensure_default_args_are_overrided() {
1461 let validation_context = Rc::new(RefCell::new(ValidationContext::default()));
1462 let chain_context = ChainDefaultContext {
1463 default_args: vec!["-lruntime=trace".into()],
1464 ..Default::default()
1465 };
1466 let node_config = NodeConfigBuilder::new(chain_context, validation_context)
1467 .with_name("node")
1468 .with_args(vec!["-lruntime=info".into()])
1469 .build()
1470 .unwrap();
1471
1472 assert_eq!(node_config.args, vec!["-lruntime=info".into()]);
1473 }
1474}