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