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