zombienet_configuration/
relaychain.rs

1use std::{cell::RefCell, error::Error, fmt::Debug, marker::PhantomData, rc::Rc};
2
3use serde::{Deserialize, Serialize};
4use support::constants::{DEFAULT_TYPESTATE, THIS_IS_A_BUG};
5
6use crate::{
7    shared::{
8        errors::{ConfigError, FieldError},
9        helpers::{merge_errors, merge_errors_vecs},
10        macros::states,
11        node::{self, GroupNodeConfig, GroupNodeConfigBuilder, NodeConfig, NodeConfigBuilder},
12        resources::{Resources, ResourcesBuilder},
13        types::{
14            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext,
15        },
16    },
17    types::{ChainSpecRuntime, JsonOverrides},
18    utils::{default_command_polkadot, default_relaychain_chain, is_false},
19};
20
21/// A relay chain configuration, composed of nodes and fine-grained configuration options.
22#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23pub struct RelaychainConfig {
24    #[serde(default = "default_relaychain_chain")]
25    chain: Chain,
26    #[serde(default = "default_command_polkadot")]
27    default_command: Option<Command>,
28    default_image: Option<Image>,
29    default_resources: Option<Resources>,
30    default_db_snapshot: Option<AssetLocation>,
31    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
32    default_args: Vec<Arg>,
33    /// chain-spec to use (location can be url or file path)
34    chain_spec_path: Option<AssetLocation>,
35    /// Full _template_ command, will be rendered (using custom token replacements)
36    /// and executed for generate the chain-spec.
37    /// available tokens {{chainName}} / {{disableBootnodes}}
38    chain_spec_command: Option<String>,
39    /// runtime to use for generating the chain-spec.
40    /// Location can be url or file path and an optional preset
41    chain_spec_runtime: Option<ChainSpecRuntime>,
42    #[serde(skip_serializing_if = "is_false", default)]
43    chain_spec_command_is_local: bool,
44    chain_spec_command_output_path: Option<String>,
45    random_nominators_count: Option<u32>,
46    max_nominations: Option<u8>,
47    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
48    nodes: Vec<NodeConfig>,
49    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
50    node_groups: Vec<GroupNodeConfig>,
51    #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
52    runtime_genesis_patch: Option<serde_json::Value>,
53    // Path or url to override the runtime (:code) in the chain-spec
54    wasm_override: Option<AssetLocation>,
55    command: Option<Command>,
56    // Inline json or asset location to override raw chainspec
57    raw_spec_override: Option<JsonOverrides>,
58}
59
60impl RelaychainConfig {
61    /// The chain name.
62    pub fn chain(&self) -> &Chain {
63        &self.chain
64    }
65
66    /// The default command used for nodes.
67    pub fn default_command(&self) -> Option<&Command> {
68        self.default_command.as_ref()
69    }
70
71    /// The default container image used for nodes.
72    pub fn default_image(&self) -> Option<&Image> {
73        self.default_image.as_ref()
74    }
75
76    /// The default resources limits used for nodes.
77    pub fn default_resources(&self) -> Option<&Resources> {
78        self.default_resources.as_ref()
79    }
80
81    /// The default database snapshot location that will be used for state.
82    pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
83        self.default_db_snapshot.as_ref()
84    }
85
86    /// The default arguments that will be used to launch the node command.
87    pub fn default_args(&self) -> Vec<&Arg> {
88        self.default_args.iter().collect::<Vec<&Arg>>()
89    }
90
91    /// The location of an pre-existing chain specification for the relay chain.
92    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
93        self.chain_spec_path.as_ref()
94    }
95
96    /// The location of a wasm runtime to override in the chain-spec.
97    pub fn wasm_override(&self) -> Option<&AssetLocation> {
98        self.wasm_override.as_ref()
99    }
100
101    /// The full _template_ command to genera the chain-spec
102    pub fn chain_spec_command(&self) -> Option<&str> {
103        self.chain_spec_command.as_deref()
104    }
105
106    /// Does the chain_spec_command needs to be run locally
107    pub fn chain_spec_command_is_local(&self) -> bool {
108        self.chain_spec_command_is_local
109    }
110
111    /// The file where the `chain_spec_command` will write the chain-spec into.
112    /// Defaults to /dev/stdout.
113    pub fn chain_spec_command_output_path(&self) -> Option<&str> {
114        self.chain_spec_command_output_path.as_deref()
115    }
116
117    /// The non-default command used for nodes.
118    pub fn command(&self) -> Option<&Command> {
119        self.command.as_ref()
120    }
121
122    /// The number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
123    pub fn random_nominators_count(&self) -> Option<u32> {
124        self.random_nominators_count
125    }
126
127    /// The maximum number of nominations to create per nominator.
128    pub fn max_nominations(&self) -> Option<u8> {
129        self.max_nominations
130    }
131
132    /// The genesis overrides as a JSON value.
133    pub fn runtime_genesis_patch(&self) -> Option<&serde_json::Value> {
134        self.runtime_genesis_patch.as_ref()
135    }
136
137    /// The nodes of the relay chain.
138    pub fn nodes(&self) -> Vec<&NodeConfig> {
139        self.nodes.iter().collect::<Vec<&NodeConfig>>()
140    }
141
142    /// The group nodes of the relay chain.
143    pub fn group_node_configs(&self) -> Vec<&GroupNodeConfig> {
144        self.node_groups.iter().collect::<Vec<&GroupNodeConfig>>()
145    }
146
147    /// The location of a file or inline json to override raw chain-spec.
148    pub fn raw_spec_override(&self) -> Option<&JsonOverrides> {
149        self.raw_spec_override.as_ref()
150    }
151
152    /// Set the nodes to build
153    pub(crate) fn set_nodes(&mut self, nodes: Vec<NodeConfig>) {
154        self.nodes = nodes;
155    }
156
157    /// The location of runtime to use by chain-spec builder lib (from `sc-chain-spec` crate)
158    pub fn chain_spec_runtime(&self) -> Option<&ChainSpecRuntime> {
159        self.chain_spec_runtime.as_ref()
160    }
161}
162
163states! {
164    Initial,
165    WithChain,
166    WithAtLeastOneNode
167}
168
169/// A relay chain configuration builder, used to build a [`RelaychainConfig`] declaratively with fields validation.
170pub struct RelaychainConfigBuilder<State> {
171    config: RelaychainConfig,
172    validation_context: Rc<RefCell<ValidationContext>>,
173    errors: Vec<anyhow::Error>,
174    _state: PhantomData<State>,
175}
176
177impl Default for RelaychainConfigBuilder<Initial> {
178    fn default() -> Self {
179        Self {
180            config: RelaychainConfig {
181                chain: "default"
182                    .try_into()
183                    .expect(&format!("{DEFAULT_TYPESTATE} {THIS_IS_A_BUG}")),
184                default_command: None,
185                default_image: None,
186                default_resources: None,
187                default_db_snapshot: None,
188                default_args: vec![],
189                chain_spec_path: None,
190                chain_spec_command: None,
191                chain_spec_command_output_path: None,
192                chain_spec_runtime: None,
193                wasm_override: None,
194                chain_spec_command_is_local: false, // remote cmd by default
195                command: None,
196                random_nominators_count: None,
197                max_nominations: None,
198                runtime_genesis_patch: None,
199                nodes: vec![],
200                node_groups: vec![],
201                raw_spec_override: None,
202            },
203            validation_context: Default::default(),
204            errors: vec![],
205            _state: PhantomData,
206        }
207    }
208}
209
210impl<A> RelaychainConfigBuilder<A> {
211    fn transition<B>(
212        config: RelaychainConfig,
213        validation_context: Rc<RefCell<ValidationContext>>,
214        errors: Vec<anyhow::Error>,
215    ) -> RelaychainConfigBuilder<B> {
216        RelaychainConfigBuilder {
217            config,
218            validation_context,
219            errors,
220            _state: PhantomData,
221        }
222    }
223
224    fn default_chain_context(&self) -> ChainDefaultContext {
225        ChainDefaultContext {
226            default_command: self.config.default_command.clone(),
227            default_image: self.config.default_image.clone(),
228            default_resources: self.config.default_resources.clone(),
229            default_db_snapshot: self.config.default_db_snapshot.clone(),
230            default_args: self.config.default_args.clone(),
231        }
232    }
233
234    fn create_node_builder<F>(&self, f: F) -> NodeConfigBuilder<node::Buildable>
235    where
236        F: FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
237    {
238        f(NodeConfigBuilder::new(
239            self.default_chain_context(),
240            self.validation_context.clone(),
241        ))
242    }
243}
244
245impl RelaychainConfigBuilder<Initial> {
246    pub fn new(
247        validation_context: Rc<RefCell<ValidationContext>>,
248    ) -> RelaychainConfigBuilder<Initial> {
249        Self {
250            validation_context,
251            ..Self::default()
252        }
253    }
254
255    /// Set the chain name (e.g. rococo-local).
256    pub fn with_chain<T>(self, chain: T) -> RelaychainConfigBuilder<WithChain>
257    where
258        T: TryInto<Chain>,
259        T::Error: Error + Send + Sync + 'static,
260    {
261        match chain.try_into() {
262            Ok(chain) => Self::transition(
263                RelaychainConfig {
264                    chain,
265                    ..self.config
266                },
267                self.validation_context,
268                self.errors,
269            ),
270            Err(error) => Self::transition(
271                self.config,
272                self.validation_context,
273                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
274            ),
275        }
276    }
277}
278
279impl RelaychainConfigBuilder<WithChain> {
280    /// Set the default command used for nodes. Can be overridden.
281    pub fn with_default_command<T>(self, command: T) -> Self
282    where
283        T: TryInto<Command>,
284        T::Error: Error + Send + Sync + 'static,
285    {
286        match command.try_into() {
287            Ok(command) => Self::transition(
288                RelaychainConfig {
289                    default_command: Some(command),
290                    ..self.config
291                },
292                self.validation_context,
293                self.errors,
294            ),
295            Err(error) => Self::transition(
296                self.config,
297                self.validation_context,
298                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
299            ),
300        }
301    }
302
303    /// Set the default container image used for nodes. Can be overridden.
304    pub fn with_default_image<T>(self, image: T) -> Self
305    where
306        T: TryInto<Image>,
307        T::Error: Error + Send + Sync + 'static,
308    {
309        match image.try_into() {
310            Ok(image) => Self::transition(
311                RelaychainConfig {
312                    default_image: Some(image),
313                    ..self.config
314                },
315                self.validation_context,
316                self.errors,
317            ),
318            Err(error) => Self::transition(
319                self.config,
320                self.validation_context,
321                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
322            ),
323        }
324    }
325
326    /// Set the default resources limits used for nodes. Can be overridden.
327    pub fn with_default_resources(
328        self,
329        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
330    ) -> Self {
331        match f(ResourcesBuilder::new()).build() {
332            Ok(default_resources) => Self::transition(
333                RelaychainConfig {
334                    default_resources: Some(default_resources),
335                    ..self.config
336                },
337                self.validation_context,
338                self.errors,
339            ),
340            Err(errors) => Self::transition(
341                self.config,
342                self.validation_context,
343                merge_errors_vecs(
344                    self.errors,
345                    errors
346                        .into_iter()
347                        .map(|error| FieldError::DefaultResources(error).into())
348                        .collect::<Vec<_>>(),
349                ),
350            ),
351        }
352    }
353
354    /// Set the default database snapshot location that will be used for state. Can be overridden.
355    pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
356        Self::transition(
357            RelaychainConfig {
358                default_db_snapshot: Some(location.into()),
359                ..self.config
360            },
361            self.validation_context,
362            self.errors,
363        )
364    }
365
366    /// Set the default arguments that will be used to execute the node command. Can be overridden.
367    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
368        Self::transition(
369            RelaychainConfig {
370                default_args: args,
371                ..self.config
372            },
373            self.validation_context,
374            self.errors,
375        )
376    }
377
378    /// Set the location of a pre-existing chain specification for the relay chain.
379    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
380        Self::transition(
381            RelaychainConfig {
382                chain_spec_path: Some(location.into()),
383                ..self.config
384            },
385            self.validation_context,
386            self.errors,
387        )
388    }
389
390    /// Set the location of a wasm to override the chain-spec.
391    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
392        Self::transition(
393            RelaychainConfig {
394                wasm_override: Some(location.into()),
395                ..self.config
396            },
397            self.validation_context,
398            self.errors,
399        )
400    }
401
402    /// Set the chain-spec command _template_ for the relay chain.
403    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
404        Self::transition(
405            RelaychainConfig {
406                chain_spec_command: Some(cmd_template.into()),
407                ..self.config
408            },
409            self.validation_context,
410            self.errors,
411        )
412    }
413
414    /// Set the runtime path to use for generating the chain-spec and an optiona preset.
415    /// If the preset is not set, we will try to match [`local_testnet`, `development`, `dev`]
416    /// with the available ones and fallback to the default configuration as last option.
417    pub fn with_chain_spec_runtime(
418        self,
419        location: impl Into<AssetLocation>,
420        preset: Option<&str>,
421    ) -> Self {
422        let chain_spec_runtime = if let Some(preset) = preset {
423            ChainSpecRuntime::with_preset(location.into(), preset.to_string())
424        } else {
425            ChainSpecRuntime::new(location.into())
426        };
427        Self::transition(
428            RelaychainConfig {
429                chain_spec_runtime: Some(chain_spec_runtime),
430                ..self.config
431            },
432            self.validation_context,
433            self.errors,
434        )
435    }
436
437    /// Set if the chain-spec command needs to be run locally or not (false by default)
438    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
439        Self::transition(
440            RelaychainConfig {
441                chain_spec_command_is_local: choice,
442                ..self.config
443            },
444            self.validation_context,
445            self.errors,
446        )
447    }
448
449    /// Set the output path for the chain-spec command.
450    pub fn with_chain_spec_command_output_path(self, output_path: &str) -> Self {
451        Self::transition(
452            RelaychainConfig {
453                chain_spec_command_output_path: Some(output_path.to_string()),
454                ..self.config
455            },
456            self.validation_context,
457            self.errors,
458        )
459    }
460
461    /// Set the number of `random nominators` to create for chains using staking, this is used in tandem with `max_nominations` to simulate the amount of nominators and nominations.
462    pub fn with_random_nominators_count(self, random_nominators_count: u32) -> Self {
463        Self::transition(
464            RelaychainConfig {
465                random_nominators_count: Some(random_nominators_count),
466                ..self.config
467            },
468            self.validation_context,
469            self.errors,
470        )
471    }
472
473    /// Set the maximum number of nominations to create per nominator.
474    pub fn with_max_nominations(self, max_nominations: u8) -> Self {
475        Self::transition(
476            RelaychainConfig {
477                max_nominations: Some(max_nominations),
478                ..self.config
479            },
480            self.validation_context,
481            self.errors,
482        )
483    }
484
485    /// Set the genesis overrides as a JSON object.
486    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
487        Self::transition(
488            RelaychainConfig {
489                runtime_genesis_patch: Some(genesis_overrides.into()),
490                ..self.config
491            },
492            self.validation_context,
493            self.errors,
494        )
495    }
496
497    /// Add a new validator node using a nested [`NodeConfigBuilder`].
498    /// The node will be configured as a validator (authority) with the --validator flag.
499    pub fn with_validator(
500        self,
501        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
502    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
503        match self.create_node_builder(f).validator(true).build() {
504            Ok(node) => Self::transition(
505                RelaychainConfig {
506                    nodes: [self.config.nodes, vec![node]].concat(),
507                    ..self.config
508                },
509                self.validation_context,
510                self.errors,
511            ),
512            Err((name, errors)) => Self::transition(
513                self.config,
514                self.validation_context,
515                merge_errors_vecs(
516                    self.errors,
517                    errors
518                        .into_iter()
519                        .map(|error| ConfigError::Node(name.clone(), error).into())
520                        .collect::<Vec<_>>(),
521                ),
522            ),
523        }
524    }
525
526    /// Add a new full node using a nested [`NodeConfigBuilder`].
527    /// The node will be configured as a full node (non-validator).
528    pub fn with_fullnode(
529        self,
530        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
531    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
532        match self.create_node_builder(f).validator(false).build() {
533            Ok(node) => Self::transition(
534                RelaychainConfig {
535                    nodes: [self.config.nodes, vec![node]].concat(),
536                    ..self.config
537                },
538                self.validation_context,
539                self.errors,
540            ),
541            Err((name, errors)) => Self::transition(
542                self.config,
543                self.validation_context,
544                merge_errors_vecs(
545                    self.errors,
546                    errors
547                        .into_iter()
548                        .map(|error| ConfigError::Node(name.clone(), error).into())
549                        .collect::<Vec<_>>(),
550                ),
551            ),
552        }
553    }
554
555    /// Add a new node using a nested [`NodeConfigBuilder`].
556    ///
557    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
558    #[deprecated(
559        since = "0.4.0",
560        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
561    )]
562    pub fn with_node(
563        self,
564        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
565    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
566        match self.create_node_builder(f).build() {
567            Ok(node) => Self::transition(
568                RelaychainConfig {
569                    nodes: vec![node],
570                    ..self.config
571                },
572                self.validation_context,
573                self.errors,
574            ),
575            Err((name, errors)) => Self::transition(
576                self.config,
577                self.validation_context,
578                merge_errors_vecs(
579                    self.errors,
580                    errors
581                        .into_iter()
582                        .map(|error| ConfigError::Node(name.clone(), error).into())
583                        .collect::<Vec<_>>(),
584                ),
585            ),
586        }
587    }
588
589    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
590    pub fn with_node_group(
591        self,
592        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
593    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
594        match f(GroupNodeConfigBuilder::new(
595            self.default_chain_context(),
596            self.validation_context.clone(),
597        ))
598        .build()
599        {
600            Ok(group_node) => Self::transition(
601                RelaychainConfig {
602                    node_groups: vec![group_node],
603                    ..self.config
604                },
605                self.validation_context,
606                self.errors,
607            ),
608            Err((name, errors)) => Self::transition(
609                self.config,
610                self.validation_context,
611                merge_errors_vecs(
612                    self.errors,
613                    errors
614                        .into_iter()
615                        .map(|error| ConfigError::Node(name.clone(), error).into())
616                        .collect::<Vec<_>>(),
617                ),
618            ),
619        }
620    }
621
622    /// Set the location or inline value of a json to override the raw chain-spec.
623    pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
624        Self::transition(
625            RelaychainConfig {
626                raw_spec_override: Some(overrides.into()),
627                ..self.config
628            },
629            self.validation_context,
630            self.errors,
631        )
632    }
633}
634
635impl RelaychainConfigBuilder<WithAtLeastOneNode> {
636    /// Add a new validator node using a nested [`NodeConfigBuilder`].
637    /// The node will be configured as a validator (authority) with the --validator flag.
638    pub fn with_validator(
639        self,
640        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
641    ) -> RelaychainConfigBuilder<WithAtLeastOneNode> {
642        match self.create_node_builder(f).validator(true).build() {
643            Ok(node) => Self::transition(
644                RelaychainConfig {
645                    nodes: [self.config.nodes, vec![node]].concat(),
646                    ..self.config
647                },
648                self.validation_context,
649                self.errors,
650            ),
651            Err((name, errors)) => Self::transition(
652                self.config,
653                self.validation_context,
654                merge_errors_vecs(
655                    self.errors,
656                    errors
657                        .into_iter()
658                        .map(|error| ConfigError::Node(name.clone(), error).into())
659                        .collect::<Vec<_>>(),
660                ),
661            ),
662        }
663    }
664
665    /// Add a new full node using a nested [`NodeConfigBuilder`].
666    /// The node will be configured as a full node (non-validator).
667    pub fn with_fullnode(
668        self,
669        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
670    ) -> Self {
671        match self.create_node_builder(f).validator(false).build() {
672            Ok(node) => Self::transition(
673                RelaychainConfig {
674                    nodes: [self.config.nodes, vec![node]].concat(),
675                    ..self.config
676                },
677                self.validation_context,
678                self.errors,
679            ),
680            Err((name, errors)) => Self::transition(
681                self.config,
682                self.validation_context,
683                merge_errors_vecs(
684                    self.errors,
685                    errors
686                        .into_iter()
687                        .map(|error| ConfigError::Node(name.clone(), error).into())
688                        .collect::<Vec<_>>(),
689                ),
690            ),
691        }
692    }
693
694    /// Add a new node using a nested [`NodeConfigBuilder`].
695    ///
696    /// **Deprecated**: Use [`with_validator`] for validator nodes or [`with_fullnode`] for full nodes instead.
697    #[deprecated(
698        since = "0.4.0",
699        note = "Use `with_validator()` for validator nodes or `with_fullnode()` for full nodes instead"
700    )]
701    pub fn with_node(
702        self,
703        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
704    ) -> Self {
705        match self.create_node_builder(f).build() {
706            Ok(node) => Self::transition(
707                RelaychainConfig {
708                    nodes: [self.config.nodes, vec![node]].concat(),
709                    ..self.config
710                },
711                self.validation_context,
712                self.errors,
713            ),
714            Err((name, errors)) => Self::transition(
715                self.config,
716                self.validation_context,
717                merge_errors_vecs(
718                    self.errors,
719                    errors
720                        .into_iter()
721                        .map(|error| ConfigError::Node(name.clone(), error).into())
722                        .collect::<Vec<_>>(),
723                ),
724            ),
725        }
726    }
727
728    /// Add a new group node using a nested [`GroupNodeConfigBuilder`].
729    pub fn with_node_group(
730        self,
731        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
732    ) -> Self {
733        match f(GroupNodeConfigBuilder::new(
734            self.default_chain_context(),
735            self.validation_context.clone(),
736        ))
737        .build()
738        {
739            Ok(group_node) => Self::transition(
740                RelaychainConfig {
741                    node_groups: [self.config.node_groups, vec![group_node]].concat(),
742                    ..self.config
743                },
744                self.validation_context,
745                self.errors,
746            ),
747            Err((name, errors)) => Self::transition(
748                self.config,
749                self.validation_context,
750                merge_errors_vecs(
751                    self.errors,
752                    errors
753                        .into_iter()
754                        .map(|error| ConfigError::Node(name.clone(), error).into())
755                        .collect::<Vec<_>>(),
756                ),
757            ),
758        }
759    }
760
761    /// Seals the builder and returns a [`RelaychainConfig`] if there are no validation errors, else returns errors.
762    pub fn build(self) -> Result<RelaychainConfig, Vec<anyhow::Error>> {
763        if !self.errors.is_empty() {
764            return Err(self
765                .errors
766                .into_iter()
767                .map(|error| ConfigError::Relaychain(error).into())
768                .collect::<Vec<_>>());
769        }
770
771        Ok(self.config)
772    }
773}
774
775#[cfg(test)]
776mod tests {
777    use super::*;
778
779    #[test]
780    fn relaychain_config_builder_should_succeeds_and_returns_a_relaychain_config() {
781        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
782            .with_chain("polkadot")
783            .with_default_image("myrepo:myimage")
784            .with_default_command("default_command")
785            .with_default_resources(|resources| {
786                resources
787                    .with_limit_cpu("500M")
788                    .with_limit_memory("1G")
789                    .with_request_cpu("250M")
790            })
791            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
792            .with_chain_spec_path("./path/to/chain/spec.json")
793            .with_chain_spec_runtime("./path/to/runtime.wasm", Some("local_testnet"))
794            .with_wasm_override("./path/to/override/runtime.wasm")
795            .with_raw_spec_override(serde_json::json!({"some_override_key": "some_override_val"}))
796            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
797            .with_random_nominators_count(42)
798            .with_max_nominations(5)
799            .with_fullnode(|node| node.with_name("node1").bootnode(true))
800            .with_validator(|node| node.with_name("node2").with_command("command2"))
801            .build()
802            .unwrap();
803
804        assert_eq!(relaychain_config.chain().as_str(), "polkadot");
805        assert_eq!(relaychain_config.nodes().len(), 2);
806        let &node1 = relaychain_config.nodes().first().unwrap();
807        assert_eq!(node1.name(), "node1");
808        assert_eq!(node1.command().unwrap().as_str(), "default_command");
809        assert!(node1.is_bootnode());
810        let &node2 = relaychain_config.nodes().last().unwrap();
811        assert_eq!(node2.name(), "node2");
812        assert_eq!(node2.command().unwrap().as_str(), "command2");
813        assert!(node2.is_validator());
814        assert_eq!(
815            relaychain_config.default_command().unwrap().as_str(),
816            "default_command"
817        );
818        assert_eq!(
819            relaychain_config.default_image().unwrap().as_str(),
820            "myrepo:myimage"
821        );
822        let default_resources = relaychain_config.default_resources().unwrap();
823        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
824        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
825        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
826        assert!(matches!(
827            relaychain_config.default_db_snapshot().unwrap(),
828            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
829        ));
830        assert!(matches!(
831            relaychain_config.chain_spec_path().unwrap(),
832            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
833        ));
834        assert!(matches!(
835            &relaychain_config.chain_spec_runtime().unwrap().location,
836            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/runtime.wasm"
837        ));
838        assert_eq!(
839            relaychain_config
840                .chain_spec_runtime()
841                .unwrap()
842                .preset
843                .as_deref(),
844            Some("local_testnet")
845        );
846        assert!(matches!(
847            relaychain_config.wasm_override().unwrap(),
848            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
849        ));
850        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
851        assert_eq!(
852            relaychain_config.default_args(),
853            args.iter().collect::<Vec<_>>()
854        );
855        assert_eq!(relaychain_config.random_nominators_count().unwrap(), 42);
856        assert_eq!(relaychain_config.max_nominations().unwrap(), 5);
857
858        assert!(matches!(
859            relaychain_config.raw_spec_override().unwrap(),
860            JsonOverrides::Json(value) if *value == serde_json::json!({"some_override_key": "some_override_val"})
861        ));
862    }
863
864    #[test]
865    fn relaychain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
866        let errors = RelaychainConfigBuilder::new(Default::default())
867            .with_chain("invalid chain")
868            .with_validator(|node| node.with_name("node").with_command("command"))
869            .build()
870            .unwrap_err();
871
872        assert_eq!(errors.len(), 1);
873        assert_eq!(
874            errors.first().unwrap().to_string(),
875            "relaychain.chain: 'invalid chain' shouldn't contains whitespace"
876        );
877    }
878
879    #[test]
880    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
881        let errors = RelaychainConfigBuilder::new(Default::default())
882            .with_chain("chain")
883            .with_default_command("invalid command")
884            .with_validator(|node| node.with_name("node").with_command("command"))
885            .build()
886            .unwrap_err();
887
888        assert_eq!(errors.len(), 1);
889        assert_eq!(
890            errors.first().unwrap().to_string(),
891            "relaychain.default_command: 'invalid command' shouldn't contains whitespace"
892        );
893    }
894
895    #[test]
896    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
897        let errors = RelaychainConfigBuilder::new(Default::default())
898            .with_chain("chain")
899            .with_default_image("invalid image")
900            .with_validator(|node| node.with_name("node").with_command("command"))
901            .build()
902            .unwrap_err();
903
904        assert_eq!(errors.len(), 1);
905        assert_eq!(
906            errors.first().unwrap().to_string(),
907            r"relaychain.default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
908        );
909    }
910
911    #[test]
912    fn relaychain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid(
913    ) {
914        let errors = RelaychainConfigBuilder::new(Default::default())
915            .with_chain("chain")
916            .with_default_resources(|default_resources| {
917                default_resources
918                    .with_limit_memory("100m")
919                    .with_request_cpu("invalid")
920            })
921            .with_validator(|node| node.with_name("node").with_command("command"))
922            .build()
923            .unwrap_err();
924
925        assert_eq!(errors.len(), 1);
926        assert_eq!(
927            errors.first().unwrap().to_string(),
928            r"relaychain.default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
929        );
930    }
931
932    #[test]
933    fn relaychain_config_builder_should_fails_and_returns_an_error_if_first_node_is_invalid() {
934        let errors = RelaychainConfigBuilder::new(Default::default())
935            .with_chain("chain")
936            .with_validator(|node| node.with_name("node").with_command("invalid command"))
937            .build()
938            .unwrap_err();
939
940        assert_eq!(errors.len(), 1);
941        assert_eq!(
942            errors.first().unwrap().to_string(),
943            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
944        );
945    }
946
947    #[test]
948    fn relaychain_config_builder_with_at_least_one_node_should_fails_and_returns_an_error_if_second_node_is_invalid(
949    ) {
950        let errors = RelaychainConfigBuilder::new(Default::default())
951            .with_chain("chain")
952            .with_validator(|node| node.with_name("node1").with_command("command1"))
953            .with_validator(|node| node.with_name("node2").with_command("invalid command"))
954            .build()
955            .unwrap_err();
956
957        assert_eq!(errors.len(), 1);
958        assert_eq!(
959            errors.first().unwrap().to_string(),
960            "relaychain.nodes['node2'].command: 'invalid command' shouldn't contains whitespace"
961        );
962    }
963
964    #[test]
965    fn relaychain_config_builder_should_fails_returns_multiple_errors_if_a_node_and_default_resources_are_invalid(
966    ) {
967        let errors = RelaychainConfigBuilder::new(Default::default())
968            .with_chain("chain")
969            .with_default_resources(|resources| {
970                resources
971                    .with_request_cpu("100Mi")
972                    .with_limit_memory("1Gi")
973                    .with_limit_cpu("invalid")
974            })
975            .with_validator(|node| node.with_name("node").with_image("invalid image"))
976            .build()
977            .unwrap_err();
978
979        assert_eq!(errors.len(), 2);
980        assert_eq!(
981            errors.first().unwrap().to_string(),
982            "relaychain.default_resources.limit_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
983        );
984        assert_eq!(
985            errors.get(1).unwrap().to_string(),
986            "relaychain.nodes['node'].image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
987        );
988    }
989
990    #[test]
991    fn relaychain_config_builder_should_works_with_chain_spec_command() {
992        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
993        let config = RelaychainConfigBuilder::new(Default::default())
994            .with_chain("polkadot")
995            .with_default_image("myrepo:myimage")
996            .with_default_command("default_command")
997            .with_chain_spec_command(CMD_TPL)
998            .with_fullnode(|node| node.with_name("node1").bootnode(true))
999            .build()
1000            .unwrap();
1001
1002        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1003        assert!(!config.chain_spec_command_is_local());
1004    }
1005
1006    #[test]
1007    fn relaychain_config_builder_should_works_with_chain_spec_command_locally() {
1008        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1009        let config = RelaychainConfigBuilder::new(Default::default())
1010            .with_chain("polkadot")
1011            .with_default_image("myrepo:myimage")
1012            .with_default_command("default_command")
1013            .with_chain_spec_command(CMD_TPL)
1014            .chain_spec_command_is_local(true)
1015            .with_fullnode(|node| node.with_name("node1").bootnode(true))
1016            .build()
1017            .unwrap();
1018
1019        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1020        assert!(config.chain_spec_command_is_local());
1021    }
1022
1023    #[test]
1024    fn relaychain_with_group_config_should_succeeds_and_returns_a_relaychain_config() {
1025        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1026            .with_chain("chain")
1027            .with_default_command("command")
1028            .with_validator(|node| node.with_name("node").with_command("node_command"))
1029            .with_node_group(|group| {
1030                group.with_count(2).with_base_node(|base| {
1031                    base.with_name("group_node")
1032                        .with_command("some_command")
1033                        .with_image("repo:image")
1034                        .validator(true)
1035                })
1036            })
1037            .build()
1038            .unwrap();
1039
1040        assert_eq!(relaychain_config.chain().as_str(), "chain");
1041        assert_eq!(relaychain_config.nodes().len(), 1);
1042        assert_eq!(relaychain_config.group_node_configs().len(), 1);
1043        assert_eq!(
1044            relaychain_config
1045                .group_node_configs()
1046                .first()
1047                .unwrap()
1048                .count,
1049            2
1050        );
1051        let &node = relaychain_config.nodes().first().unwrap();
1052        assert_eq!(node.name(), "node");
1053        assert_eq!(node.command().unwrap().as_str(), "node_command");
1054
1055        let group_nodes = relaychain_config.group_node_configs();
1056        let group_base_node = group_nodes.first().unwrap();
1057        assert_eq!(group_base_node.base_config.name(), "group_node");
1058        assert_eq!(
1059            group_base_node.base_config.command().unwrap().as_str(),
1060            "some_command"
1061        );
1062        assert_eq!(
1063            group_base_node.base_config.image().unwrap().as_str(),
1064            "repo:image"
1065        );
1066        assert!(group_base_node.base_config.is_validator());
1067    }
1068
1069    #[test]
1070    fn relaychain_with_group_count_0_config_should_fail() {
1071        let relaychain_config = RelaychainConfigBuilder::new(Default::default())
1072            .with_chain("chain")
1073            .with_default_command("command")
1074            .with_validator(|node| node.with_name("node").with_command("node_command"))
1075            .with_node_group(|group| {
1076                group.with_count(0).with_base_node(|base| {
1077                    base.with_name("group_node")
1078                        .with_command("some_command")
1079                        .with_image("repo:image")
1080                        .validator(true)
1081                })
1082            })
1083            .build();
1084
1085        let errors: Vec<anyhow::Error> = match relaychain_config {
1086            Ok(_) => vec![],
1087            Err(errs) => errs,
1088        };
1089
1090        assert_eq!(errors.len(), 1);
1091        assert_eq!(
1092            errors.first().unwrap().to_string(),
1093            "relaychain.nodes['group_node'].Count cannot be zero"
1094        );
1095    }
1096}