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