Skip to main content

zombienet_configuration/
network.rs

1use std::{
2    cell::RefCell,
3    collections::HashSet,
4    fs,
5    marker::PhantomData,
6    path::{Path, PathBuf},
7    rc::Rc,
8};
9
10use anyhow::anyhow;
11use regex::{Captures, Regex};
12use serde::{Deserialize, Serialize};
13use support::{
14    constants::{NO_ERR_DEF_BUILDER, RELAY_NOT_NONE, THIS_IS_A_BUG, VALID_REGEX},
15    replacer::apply_env_replacements,
16};
17use tracing::trace;
18
19use crate::{
20    custom_process,
21    global_settings::{GlobalSettings, GlobalSettingsBuilder},
22    hrmp_channel::{self, HrmpChannelConfig, HrmpChannelConfigBuilder},
23    jamchain::{self, JamchainConfig, JamchainConfigBuilder},
24    parachain::{self, ParachainConfig, ParachainConfigBuilder},
25    relaychain::{self, RelaychainConfig, RelaychainConfigBuilder},
26    shared::{
27        errors::{ConfigError, ValidationError},
28        helpers::{generate_unique_node_name_from_names, merge_errors, merge_errors_vecs},
29        macros::states,
30        node::{GroupNodeConfig, NodeConfig},
31        types::{Arg, AssetLocation, Chain, Command, Image, ValidationContext},
32    },
33    types::ParaId,
34    CustomProcess, CustomProcessBuilder, RegistrationStrategy,
35};
36
37/// A network configuration, composed of a relaychain (or a JAM chain), parachains and HRMP
38/// channels.
39///
40/// The JAM chain takes the place of the relaychain, so a network has one or the other, never
41/// both.
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
43pub struct NetworkConfig {
44    #[serde(rename = "settings", default = "GlobalSettings::default")]
45    global_settings: GlobalSettings,
46    relaychain: Option<RelaychainConfig>,
47    jamchain: Option<JamchainConfig>,
48    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
49    parachains: Vec<ParachainConfig>,
50    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
51    hrmp_channels: Vec<HrmpChannelConfig>,
52    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
53    custom_processes: Vec<CustomProcess>,
54}
55
56impl NetworkConfig {
57    /// The global settings of the network.
58    pub fn global_settings(&self) -> &GlobalSettings {
59        &self.global_settings
60    }
61
62    /// The relay chain of the network.
63    pub fn relaychain(&self) -> &RelaychainConfig {
64        self.relaychain
65            .as_ref()
66            .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
67    }
68
69    /// The relay chain of the network, `None` for a JAM network.
70    pub fn try_relaychain(&self) -> Option<&RelaychainConfig> {
71        self.relaychain.as_ref()
72    }
73
74    /// The jam chain of the network.
75    pub fn jamchain(&self) -> Option<&JamchainConfig> {
76        if let Some(jamconfig) = &self.jamchain {
77            Some(jamconfig)
78        } else {
79            None
80        }
81    }
82
83    /// The parachains of the network.
84    pub fn parachains(&self) -> Vec<&ParachainConfig> {
85        self.parachains.iter().collect::<Vec<_>>()
86    }
87
88    /// The HRMP channels of the network.
89    pub fn hrmp_channels(&self) -> Vec<&HrmpChannelConfig> {
90        self.hrmp_channels.iter().collect::<Vec<_>>()
91    }
92
93    /// The Custom process to spawn
94    pub fn custom_processes(&self) -> Vec<&CustomProcess> {
95        self.custom_processes.iter().collect::<Vec<_>>()
96    }
97
98    /// Set the parachain.
99    fn set_parachains(&mut self, parachains: Vec<ParachainConfig>) {
100        self.parachains = parachains;
101    }
102
103    /// A helper function to dump the network configuration to a TOML string.
104    pub fn dump_to_toml(&self) -> Result<String, toml::ser::Error> {
105        // This regex is used to replace the "" enclosed u128 value to a raw u128 because u128 is not supported for TOML serialization/deserialization.
106        let re = Regex::new(r#""U128%(?<u128_value>\d+)""#)
107            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
108        let toml_string = toml::to_string_pretty(&self)?;
109
110        Ok(re.replace_all(&toml_string, "$u128_value").to_string())
111    }
112
113    /// A helper function to load a network configuration from a TOML file.
114    pub fn load_from_toml_with_settings(
115        path: &str,
116        settings: &GlobalSettings,
117    ) -> Result<NetworkConfig, anyhow::Error> {
118        let mut network_config = NetworkConfig::load_from_toml(path)?;
119        network_config.global_settings = settings.clone();
120        Ok(network_config)
121    }
122
123    /// A helper function to load a network configuration from a TOML file.
124    pub fn load_from_toml(path: &str) -> Result<NetworkConfig, anyhow::Error> {
125        let file_path = PathBuf::from(path).canonicalize()?;
126        let network_definition_dir = file_path
127            .parent()
128            .ok_or(anyhow!("should have a base dir"))?;
129        trace!(
130            "file_dir for replacer: {:?}",
131            network_definition_dir.as_os_str()
132        );
133        let file_str = fs::read_to_string(path)
134            .map_err(|e| anyhow!("Failed to read configuration file '{}': {}", path, e))?;
135
136        Self::load_from_toml_string_with_base_dir(&file_str, network_definition_dir)
137    }
138
139    /// A helper function to load a network configuration from a TOML string.
140    ///
141    /// Relative paths (e.g. `command = "./bin/polkadot"`) are resolved against the current
142    /// working directory, since there is no configuration file to use as base. Use
143    /// [`NetworkConfig::load_from_toml_string_with_base_dir`] to resolve them against a
144    /// specific directory instead.
145    pub fn load_from_toml_string(toml_data: &str) -> Result<NetworkConfig, anyhow::Error> {
146        let base_dir = std::env::current_dir()?;
147
148        Self::load_from_toml_string_with_base_dir(toml_data, &base_dir)
149    }
150
151    /// A helper function to load a network configuration from a TOML string, resolving
152    /// relative paths (e.g. `command = "./bin/polkadot"`) against `base_dir`.
153    pub fn load_from_toml_string_with_base_dir(
154        toml_text: &str,
155        base_dir: &Path,
156    ) -> Result<NetworkConfig, anyhow::Error> {
157        let re: Regex = Regex::new(r"(?<field_name>(initial_)?balance)\s+=\s+(?<u128_value>\d+)")
158            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
159
160        let re_paths: Regex = Regex::new(r#"(?<field_name>(default_)?(command|db_snapshot|chain_spec_path|genesis_wasm_path|genesis_state_path))\s+=\s+\"(?<value_string>[a-zA-Z0-9\.\/]+)\""#)
161            .expect(&format!("{VALID_REGEX} {THIS_IS_A_BUG}"));
162
163        let path_replacer = |caps: &Captures| {
164            trace!("cmd replacer captures: {:?}", caps);
165            let cmd = maybe_absolute(&caps["value_string"], base_dir);
166            let line = format!("{} = \"{cmd}\"", &caps["field_name"]);
167            trace!("line after replacer: {line}");
168            line
169        };
170
171        let toml_text = re.replace_all(toml_text, "$field_name = \"$u128_value\"");
172        let toml_text = re_paths.replace_all(&toml_text, &path_replacer);
173
174        trace!("toml text to parse: {}", toml_text);
175        // apply replacements from env in toml
176        let toml_text = apply_env_replacements(&toml_text);
177        trace!("toml text after replacements: {}", toml_text);
178        let mut network_config: NetworkConfig = toml::from_str(&toml_text)?;
179        trace!("parsed config {network_config:#?}");
180
181        // A network is rooted either in a relaychain or in a JAM chain.
182        if network_config.relaychain.is_none() && network_config.jamchain.is_none() {
183            Err(anyhow!("Relay chain or Jam definition does not exist."))?
184        }
185
186        // Keep track of node names to ensure uniqueness, shared between the nodes of the
187        // root chain and the collators.
188        let mut names = HashSet::new();
189
190        if let Some(relaychain) = network_config.relaychain.as_mut() {
191            // retrieve the defaults relaychain for assigning to nodes if needed
192            let mut relaychain_default_command: Option<Command> =
193                relaychain.default_command().cloned();
194
195            if relaychain_default_command.is_none() {
196                relaychain_default_command = relaychain.command().cloned();
197            }
198
199            let relaychain_default_image: Option<Image> = relaychain.default_image().cloned();
200
201            let relaychain_default_db_snapshot: Option<AssetLocation> =
202                relaychain.default_db_snapshot().cloned();
203
204            let default_args: Vec<Arg> = relaychain.default_args().into_iter().cloned().collect();
205
206            let mut nodes: Vec<NodeConfig> = relaychain.nodes().into_iter().cloned().collect();
207
208            let group_nodes: Vec<GroupNodeConfig> = relaychain
209                .group_node_configs()
210                .into_iter()
211                .cloned()
212                .collect();
213
214            if let Some(group) = group_nodes.iter().find(|n| n.count == 0) {
215                return Err(anyhow!(
216                    "Group node '{}' must have a count greater than 0.",
217                    group.base_config.name()
218                ));
219            }
220
221            // Validation checks for relay
222            TryInto::<Chain>::try_into(relaychain.chain().as_str())?;
223            if let Some(default_image) = &relaychain_default_image {
224                TryInto::<Image>::try_into(default_image.clone())?;
225            }
226            if let Some(default_command) = &relaychain_default_command {
227                TryInto::<Command>::try_into(default_command.clone())?;
228            }
229
230            for node in nodes.iter_mut() {
231                if relaychain_default_command.is_some() {
232                    // we modify only nodes which don't already have a command
233                    if node.command.is_none() {
234                        node.command.clone_from(&relaychain_default_command);
235                    }
236                }
237
238                if relaychain_default_image.is_some() && node.image.is_none() {
239                    node.image.clone_from(&relaychain_default_image);
240                }
241
242                if relaychain_default_db_snapshot.is_some() && node.db_snapshot.is_none() {
243                    node.db_snapshot.clone_from(&relaychain_default_db_snapshot);
244                }
245
246                if !default_args.is_empty() && node.args().is_empty() {
247                    node.set_args(default_args.clone());
248                }
249
250                let unique_name = generate_unique_node_name_from_names(node.name(), &mut names);
251                node.name = unique_name;
252            }
253
254            relaychain.set_nodes(nodes);
255        }
256
257        let mut parachains: Vec<ParachainConfig> =
258            network_config.parachains().into_iter().cloned().collect();
259
260        for para in parachains.iter_mut() {
261            // retrieve the defaults parachain for assigning to collators if needed
262            let parachain_default_command: Option<Command> = para.default_command().cloned();
263
264            let parachain_default_image: Option<Image> = para.default_image().cloned();
265
266            let parachain_default_db_snapshot: Option<AssetLocation> =
267                para.default_db_snapshot().cloned();
268
269            let default_args: Vec<Arg> = para.default_args().into_iter().cloned().collect();
270
271            let group_collators: Vec<GroupNodeConfig> = para
272                .group_collators_configs()
273                .into_iter()
274                .cloned()
275                .collect();
276
277            if let Some(group) = group_collators.iter().find(|n| n.count == 0) {
278                return Err(anyhow!(
279                    "Group node '{}' must have a count greater than 0.",
280                    group.base_config.name()
281                ));
282            }
283
284            let mut collators: Vec<NodeConfig> = para.collators.clone();
285
286            for collator in collators.iter_mut() {
287                populate_collator_with_defaults(
288                    collator,
289                    &parachain_default_command,
290                    &parachain_default_image,
291                    &parachain_default_db_snapshot,
292                    &default_args,
293                );
294                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
295                collator.name = unique_name;
296            }
297
298            para.collators = collators;
299
300            if let Some(mut collator) = para.collator.clone() {
301                populate_collator_with_defaults(
302                    &mut collator,
303                    &parachain_default_command,
304                    &parachain_default_image,
305                    &parachain_default_db_snapshot,
306                    &default_args,
307                );
308                let unique_name = generate_unique_node_name_from_names(collator.name(), &mut names);
309                collator.name = unique_name;
310                para.collator = Some(collator);
311            }
312        }
313
314        network_config.set_parachains(parachains);
315
316        // Validation checks for parachains
317        network_config.parachains().iter().for_each(|parachain| {
318            if let Some(default_image) = parachain.default_image() {
319                let _ = TryInto::<Image>::try_into(default_image.as_str());
320            }
321            if let Some(default_command) = parachain.default_command() {
322                let _ = TryInto::<Command>::try_into(default_command.as_str());
323            }
324        });
325        Ok(network_config)
326    }
327}
328
329fn maybe_absolute(cmd: &str, base_dir: &Path) -> String {
330    if cmd.starts_with(".") {
331        let mut full = PathBuf::from(base_dir);
332        full.push(cmd);
333        trace!("full_path: {:?}", full);
334        std::path::absolute(&full)
335            .expect(&format!(
336                "absolute path shoud be valid: {:?}",
337                full.as_os_str()
338            ))
339            .to_string_lossy()
340            .to_string()
341    } else {
342        cmd.to_string()
343    }
344}
345
346fn populate_collator_with_defaults(
347    collator: &mut NodeConfig,
348    parachain_default_command: &Option<Command>,
349    parachain_default_image: &Option<Image>,
350    parachain_default_db_snapshot: &Option<AssetLocation>,
351    default_args: &[Arg],
352) {
353    if parachain_default_command.is_some() {
354        // we modify only nodes which don't already have a command
355        if collator.command.is_none() {
356            collator.command.clone_from(parachain_default_command);
357        }
358    }
359
360    if parachain_default_image.is_some() && collator.image.is_none() {
361        collator.image.clone_from(parachain_default_image);
362    }
363
364    if parachain_default_db_snapshot.is_some() && collator.db_snapshot.is_none() {
365        collator
366            .db_snapshot
367            .clone_from(parachain_default_db_snapshot);
368    }
369
370    if !default_args.is_empty() && collator.args().is_empty() {
371        collator.set_args(default_args.to_owned());
372    }
373}
374
375states! {
376    Initial,
377    Buildable
378}
379
380/// A network configuration builder, used to build a [`NetworkConfig`] declaratively with fields validation.
381///
382/// # Example:
383///
384/// ```
385/// use zombienet_configuration::NetworkConfigBuilder;
386///
387/// let network_config = NetworkConfigBuilder::new()
388///     .with_relaychain(|relaychain| {
389///         relaychain
390///             .with_chain("polkadot")
391///             .with_random_nominators_count(10)
392///             .with_default_resources(|resources| {
393///                 resources
394///                     .with_limit_cpu("1000m")
395///                     .with_request_memory("1Gi")
396///                     .with_request_cpu(100_000)
397///             })
398///             .with_validator(|node| node.with_name("node").with_command("command"))
399///             .with_validator(|node| node.with_name("node1").with_command("command"))
400///     })
401///     .with_parachain(|parachain| {
402///         parachain
403///             .with_id(1000)
404///             .with_chain("myparachain1")
405///             .with_initial_balance(100_000)
406///             .with_default_image("myimage:version")
407///             .with_collator(|collator| {
408///                 collator
409///                     .with_name("collator1")
410///                     .with_command("command1")
411///                     .validator(true)
412///             })
413///     })
414///     .with_parachain(|parachain| {
415///         parachain
416///             .with_id(2000)
417///             .with_chain("myparachain2")
418///             .with_initial_balance(50_0000)
419///             .with_collator(|collator| {
420///                 collator
421///                     .with_name("collator2")
422///                     .with_command("command2")
423///                     .validator(true)
424///             })
425///     })
426///     .with_hrmp_channel(|hrmp_channel1| {
427///         hrmp_channel1
428///             .with_sender(1)
429///             .with_recipient(2)
430///             .with_max_capacity(200)
431///             .with_max_message_size(500)
432///     })
433///     .with_hrmp_channel(|hrmp_channel2| {
434///         hrmp_channel2
435///             .with_sender(2)
436///             .with_recipient(1)
437///             .with_max_capacity(100)
438///             .with_max_message_size(250)
439///     })
440///     .with_global_settings(|global_settings| {
441///         global_settings
442///             .with_network_spawn_timeout(1200)
443///             .with_node_spawn_timeout(240)
444///     })
445///     .build();
446///
447/// assert!(network_config.is_ok())
448/// ```
449pub struct NetworkConfigBuilder<State> {
450    config: NetworkConfig,
451    validation_context: Rc<RefCell<ValidationContext>>,
452    errors: Vec<anyhow::Error>,
453    _state: PhantomData<State>,
454}
455
456impl Default for NetworkConfigBuilder<Initial> {
457    fn default() -> Self {
458        Self {
459            config: NetworkConfig {
460                global_settings: GlobalSettingsBuilder::new()
461                    .build()
462                    .expect(&format!("{NO_ERR_DEF_BUILDER}, {THIS_IS_A_BUG}")),
463                relaychain: None,
464                jamchain: None,
465                parachains: vec![],
466                hrmp_channels: vec![],
467                custom_processes: vec![],
468            },
469            validation_context: Default::default(),
470            errors: vec![],
471            _state: PhantomData,
472        }
473    }
474}
475
476impl<A> NetworkConfigBuilder<A> {
477    fn transition<B>(
478        config: NetworkConfig,
479        validation_context: Rc<RefCell<ValidationContext>>,
480        errors: Vec<anyhow::Error>,
481    ) -> NetworkConfigBuilder<B> {
482        NetworkConfigBuilder {
483            config,
484            errors,
485            validation_context,
486            _state: PhantomData,
487        }
488    }
489
490    /// Set the default tiny configuration for JAM
491    /// id: dev
492    /// 6 validators with names jam0..jam5
493    /// 1 ordinary with name jam-or
494    /// NO corevm builder
495    /// NO corevm monitor
496    fn setup_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
497        let mut builder = JamchainConfigBuilder::new(self.validation_context.clone())
498            .with_id("dev")
499            .with_validator(|n| n.with_name("jam0"));
500
501        for i in 1..6 {
502            builder = builder.with_validator(|n| n.with_name(&format!("jam{i}")));
503        }
504
505        let builder = builder.with_ordinary(|n| n.with_name("jam-or"));
506
507        match builder.build() {
508            Ok(jamchain) => Self::transition(
509                NetworkConfig {
510                    jamchain: Some(jamchain),
511                    ..self.config
512                },
513                self.validation_context,
514                self.errors,
515            ),
516            Err(errors) => Self::transition(self.config, self.validation_context, errors),
517        }
518    }
519}
520
521impl NetworkConfigBuilder<Initial> {
522    pub fn new() -> NetworkConfigBuilder<Initial> {
523        Self::default()
524    }
525
526    /// uses the default options for both the relay chain and the validator nodes
527    /// the only required fields are the name of the validator nodes,
528    /// and the name of the relay chain ("rococo-local", "polkadot", etc.)
529    pub fn with_chain_and_nodes(
530        relay_name: &str,
531        node_names: Vec<String>,
532    ) -> NetworkConfigBuilder<Buildable> {
533        let network_config = NetworkConfigBuilder::new().with_relaychain(|relaychain| {
534            let mut relaychain_with_node =
535                relaychain.with_chain(relay_name).with_validator(|node| {
536                    node.with_name(node_names.first().unwrap_or(&"".to_string()))
537                });
538
539            for node_name in node_names.iter().skip(1) {
540                relaychain_with_node = relaychain_with_node
541                    .with_validator(|node_builder| node_builder.with_name(node_name));
542            }
543            relaychain_with_node
544        });
545
546        Self::transition(
547            network_config.config,
548            network_config.validation_context,
549            network_config.errors,
550        )
551    }
552
553    /// Set the relay chain using a nested [`RelaychainConfigBuilder`].
554    pub fn with_relaychain(
555        self,
556        f: impl FnOnce(
557            RelaychainConfigBuilder<relaychain::Initial>,
558        ) -> RelaychainConfigBuilder<relaychain::WithAtLeastOneNode>,
559    ) -> NetworkConfigBuilder<Buildable> {
560        match f(RelaychainConfigBuilder::new(
561            self.validation_context.clone(),
562        ))
563        .build()
564        {
565            Ok(relaychain) => Self::transition(
566                NetworkConfig {
567                    relaychain: Some(relaychain),
568                    ..self.config
569                },
570                self.validation_context,
571                self.errors,
572            ),
573            Err(errors) => Self::transition(self.config, self.validation_context, errors),
574        }
575    }
576
577    /// Set the jam chain using a nested [`JamchainConfigBuilder`].
578    pub fn with_jamchain(
579        self,
580        f: impl FnOnce(
581            JamchainConfigBuilder<jamchain::Initial>,
582        ) -> JamchainConfigBuilder<jamchain::WithAtLeastOneNode>,
583    ) -> NetworkConfigBuilder<Buildable> {
584        match f(JamchainConfigBuilder::new(self.validation_context.clone())).build() {
585            Ok(jamchain) => Self::transition(
586                NetworkConfig {
587                    jamchain: Some(jamchain),
588                    ..self.config
589                },
590                self.validation_context,
591                self.errors,
592            ),
593            Err(errors) => Self::transition(self.config, self.validation_context, errors),
594        }
595    }
596
597    /// Set the default tiny configuration for JAM
598    /// see [`setup_tiny_jamchain`] fn.
599    pub fn with_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
600        self.setup_tiny_jamchain()
601    }
602}
603
604impl NetworkConfigBuilder<Buildable> {
605    // Allow to set jam/relay in buildable state (override if already set)
606
607    /// Set the relay chain using a nested [`RelaychainConfigBuilder`].
608    pub fn with_relaychain(
609        self,
610        f: impl FnOnce(
611            RelaychainConfigBuilder<relaychain::Initial>,
612        ) -> RelaychainConfigBuilder<relaychain::WithAtLeastOneNode>,
613    ) -> NetworkConfigBuilder<Buildable> {
614        match f(RelaychainConfigBuilder::new(
615            self.validation_context.clone(),
616        ))
617        .build()
618        {
619            Ok(relaychain) => Self::transition(
620                NetworkConfig {
621                    relaychain: Some(relaychain),
622                    ..self.config
623                },
624                self.validation_context,
625                self.errors,
626            ),
627            Err(errors) => Self::transition(self.config, self.validation_context, errors),
628        }
629    }
630
631    /// Set the jam chain using a nested [`JamchainConfigBuilder`].
632    pub fn with_jamchain(
633        self,
634        f: impl FnOnce(
635            JamchainConfigBuilder<jamchain::Initial>,
636        ) -> JamchainConfigBuilder<jamchain::WithAtLeastOneNode>,
637    ) -> NetworkConfigBuilder<Buildable> {
638        match f(JamchainConfigBuilder::new(self.validation_context.clone())).build() {
639            Ok(jamchain) => Self::transition(
640                NetworkConfig {
641                    jamchain: Some(jamchain),
642                    ..self.config
643                },
644                self.validation_context,
645                self.errors,
646            ),
647            Err(errors) => Self::transition(self.config, self.validation_context, errors),
648        }
649    }
650
651    /// Set the default tiny configuration for JAM
652    /// see [`setup_tiny_jamchain`] fn.
653    pub fn with_tiny_jamchain(self) -> NetworkConfigBuilder<Buildable> {
654        self.setup_tiny_jamchain()
655    }
656
657    /// Set the global settings using a nested [`GlobalSettingsBuilder`].
658    pub fn with_global_settings(
659        self,
660        f: impl FnOnce(GlobalSettingsBuilder) -> GlobalSettingsBuilder,
661    ) -> Self {
662        match f(GlobalSettingsBuilder::new()).build() {
663            Ok(global_settings) => Self::transition(
664                NetworkConfig {
665                    global_settings,
666                    ..self.config
667                },
668                self.validation_context,
669                self.errors,
670            ),
671            Err(errors) => Self::transition(
672                self.config,
673                self.validation_context,
674                merge_errors_vecs(self.errors, errors),
675            ),
676        }
677    }
678
679    /// Add a parachain using a nested [`ParachainConfigBuilder`].
680    pub fn with_parachain(
681        self,
682        f: impl FnOnce(
683            ParachainConfigBuilder<parachain::states::Initial, parachain::states::Bootstrap>,
684        ) -> ParachainConfigBuilder<
685            parachain::states::WithAtLeastOneCollator,
686            parachain::states::Bootstrap,
687        >,
688    ) -> Self {
689        match f(ParachainConfigBuilder::new(self.validation_context.clone())).build() {
690            Ok(parachain) => Self::transition(
691                NetworkConfig {
692                    parachains: [self.config.parachains, vec![parachain]].concat(),
693                    ..self.config
694                },
695                self.validation_context,
696                self.errors,
697            ),
698            Err(errors) => Self::transition(
699                self.config,
700                self.validation_context,
701                merge_errors_vecs(self.errors, errors),
702            ),
703        }
704    }
705
706    /// uses default settings for setting for:
707    /// - the parachain,
708    /// - the global settings
709    /// - the hrmp channels
710    ///
711    /// the only required parameters are the names of the collators as a vector,
712    /// and the id of the parachain
713    pub fn with_parachain_id_and_collators(self, id: u32, collator_names: Vec<String>) -> Self {
714        if collator_names.is_empty() {
715            return Self::transition(
716                self.config,
717                self.validation_context,
718                merge_errors(
719                    self.errors,
720                    ConfigError::Parachain(id, ValidationError::CantBeEmpty().into()).into(),
721                ),
722            );
723        }
724
725        self.with_parachain(|parachain| {
726            let mut parachain_config = parachain.with_id(id).with_collator(|collator| {
727                collator
728                    .with_name(collator_names.first().unwrap_or(&"".to_string()))
729                    .validator(true)
730            });
731
732            for collator_name in collator_names.iter().skip(1) {
733                parachain_config = parachain_config
734                    .with_collator(|collator| collator.with_name(collator_name).validator(true));
735            }
736            parachain_config
737        })
738
739        // TODO: if need to set global settings and hrmp channels
740        // we can also do in here
741    }
742
743    /// Add an HRMP channel using a nested [`HrmpChannelConfigBuilder`].
744    pub fn with_hrmp_channel(
745        self,
746        f: impl FnOnce(
747            HrmpChannelConfigBuilder<hrmp_channel::Initial>,
748        ) -> HrmpChannelConfigBuilder<hrmp_channel::WithRecipient>,
749    ) -> Self {
750        let new_hrmp_channel = f(HrmpChannelConfigBuilder::new()).build();
751
752        Self::transition(
753            NetworkConfig {
754                hrmp_channels: [self.config.hrmp_channels, vec![new_hrmp_channel]].concat(),
755                ..self.config
756            },
757            self.validation_context,
758            self.errors,
759        )
760    }
761
762    /// Add a custom process using a nested [`CustomProcessBuilder`].
763    pub fn with_custom_process(
764        self,
765        f: impl FnOnce(
766            CustomProcessBuilder<custom_process::WithOutName, custom_process::WithOutCmd>,
767        )
768            -> CustomProcessBuilder<custom_process::WithName, custom_process::WithCmd>,
769    ) -> Self {
770        match f(CustomProcessBuilder::new()).build() {
771            Ok(custom_process) => Self::transition(
772                NetworkConfig {
773                    custom_processes: [self.config.custom_processes, vec![custom_process]].concat(),
774                    ..self.config
775                },
776                self.validation_context,
777                self.errors,
778            ),
779            Err((name, errors)) => Self::transition(
780                self.config,
781                self.validation_context,
782                merge_errors_vecs(
783                    self.errors,
784                    errors
785                        .into_iter()
786                        .map(|error| ConfigError::Node(name.clone(), error).into())
787                        .collect::<Vec<_>>(),
788                ),
789            ),
790        }
791    }
792
793    /// Seals the builder and returns a [`NetworkConfig`] if there are no validation errors, else returns errors.
794    pub fn build(self) -> Result<NetworkConfig, Vec<anyhow::Error>> {
795        let mut paras_to_register: HashSet<ParaId> = Default::default();
796        let mut num_of_requested_cores = 0;
797
798        let mut errs: Vec<anyhow::Error> = self
799            .config
800            .parachains
801            .iter()
802            .filter_map(|para| {
803                if let Some(cores) = para.num_cores() {
804                    num_of_requested_cores += cores;
805                } else if let Some(RegistrationStrategy::InGenesis) = para.registration_strategy() {
806                    // add 1 by default
807                    num_of_requested_cores += 1;
808                }
809
810                if let Some(RegistrationStrategy::Manual) = para.registration_strategy() {
811                    return None;
812                };
813
814                if paras_to_register.insert(para.id()) {
815                    None
816                } else {
817                    // already in the set
818                    Some(anyhow!(
819                        "ParaId {} already set to be registered, only one should be.",
820                        para.id()
821                    ))
822                }
823            })
824            .collect();
825
826        // ensure we can make this check
827        if let Some(rc_config) = self.config.relaychain.as_ref() {
828            // check we have num_validators >= num_requested_cores
829            let mut num_validators = rc_config.nodes().iter().fold(0u32, |mut acc, node| {
830                if node.is_validator {
831                    acc += 1;
832                }
833
834                acc
835            });
836
837            let node_groups = rc_config.group_node_configs();
838            for ng in &node_groups {
839                if ng.base_config.is_validator {
840                    num_validators += ng.count as u32;
841                }
842            }
843
844            if num_validators < num_of_requested_cores {
845                errs.push(anyhow!("Number of assigned cores at genesis {num_of_requested_cores} is greater than validators ({num_validators})"));
846            }
847        }
848
849        if !self.errors.is_empty() || !errs.is_empty() {
850            let mut ret_errs = self.errors;
851            ret_errs.append(&mut errs);
852            return Err(ret_errs);
853        }
854
855        Ok(self.config)
856    }
857}
858
859#[cfg(test)]
860mod tests {
861    use std::path::PathBuf;
862
863    use super::*;
864    use crate::{parachain::RegistrationStrategy, types::JamNodeMode};
865
866    #[test]
867    fn network_config_builder_should_succeeds_and_returns_a_network_config() {
868        let network_config = NetworkConfigBuilder::new()
869            .with_relaychain(|relaychain| {
870                relaychain
871                    .with_chain("polkadot")
872                    .with_random_nominators_count(10)
873                    .with_validator(|node| node.with_name("node").with_command("command"))
874                    .with_validator(|node| node.with_name("node-1").with_command("command"))
875            })
876            .with_parachain(|parachain| {
877                parachain
878                    .with_id(1)
879                    .with_chain("myparachain1")
880                    .with_initial_balance(100_000)
881                    .with_collator(|collator| {
882                        collator
883                            .with_name("collator1")
884                            .with_command("command1")
885                            .validator(true)
886                    })
887            })
888            .with_parachain(|parachain| {
889                parachain
890                    .with_id(2)
891                    .with_chain("myparachain2")
892                    .with_initial_balance(0)
893                    .with_collator(|collator| {
894                        collator
895                            .with_name("collator2")
896                            .with_command("command2")
897                            .validator(true)
898                    })
899            })
900            .with_hrmp_channel(|hrmp_channel1| {
901                hrmp_channel1
902                    .with_sender(1)
903                    .with_recipient(2)
904                    .with_max_capacity(200)
905                    .with_max_message_size(500)
906            })
907            .with_hrmp_channel(|hrmp_channel2| {
908                hrmp_channel2
909                    .with_sender(2)
910                    .with_recipient(1)
911                    .with_max_capacity(100)
912                    .with_max_message_size(250)
913            })
914            .with_global_settings(|global_settings| {
915                global_settings
916                    .with_network_spawn_timeout(1200)
917                    .with_node_spawn_timeout(240)
918            })
919            .build()
920            .unwrap();
921
922        // relaychain
923        assert_eq!(network_config.relaychain().chain().as_str(), "polkadot");
924        assert_eq!(network_config.relaychain().nodes().len(), 2);
925        let &node = network_config.relaychain().nodes().first().unwrap();
926        assert_eq!(node.name(), "node");
927        assert_eq!(node.command().unwrap().as_str(), "command");
928        assert!(node.is_validator());
929        assert_eq!(
930            network_config
931                .relaychain()
932                .random_nominators_count()
933                .unwrap(),
934            10
935        );
936
937        // parachains
938        assert_eq!(network_config.parachains().len(), 2);
939
940        // parachain1
941        let &parachain1 = network_config.parachains().first().unwrap();
942        assert_eq!(parachain1.id(), 1);
943        assert_eq!(parachain1.collators().len(), 1);
944        let &collator = parachain1.collators().first().unwrap();
945        assert_eq!(collator.name(), "collator1");
946        assert_eq!(collator.command().unwrap().as_str(), "command1");
947        assert!(collator.is_validator());
948        assert_eq!(parachain1.initial_balance(), 100_000);
949        assert_eq!(parachain1.unique_id(), "1");
950
951        // parachain2
952        let &parachain2 = network_config.parachains().last().unwrap();
953        assert_eq!(parachain2.id(), 2);
954        assert_eq!(parachain2.collators().len(), 1);
955        let &collator = parachain2.collators().first().unwrap();
956        assert_eq!(collator.name(), "collator2");
957        assert_eq!(collator.command().unwrap().as_str(), "command2");
958        assert!(collator.is_validator());
959        assert_eq!(parachain2.initial_balance(), 0);
960
961        // hrmp_channels
962        assert_eq!(network_config.hrmp_channels().len(), 2);
963
964        // hrmp_channel1
965        let &hrmp_channel1 = network_config.hrmp_channels().first().unwrap();
966        assert_eq!(hrmp_channel1.sender(), 1);
967        assert_eq!(hrmp_channel1.recipient(), 2);
968        assert_eq!(hrmp_channel1.max_capacity(), 200);
969        assert_eq!(hrmp_channel1.max_message_size(), 500);
970
971        // hrmp_channel2
972        let &hrmp_channel2 = network_config.hrmp_channels().last().unwrap();
973        assert_eq!(hrmp_channel2.sender(), 2);
974        assert_eq!(hrmp_channel2.recipient(), 1);
975        assert_eq!(hrmp_channel2.max_capacity(), 100);
976        assert_eq!(hrmp_channel2.max_message_size(), 250);
977
978        // global settings
979        assert_eq!(
980            network_config.global_settings().network_spawn_timeout(),
981            1200
982        );
983        assert_eq!(network_config.global_settings().node_spawn_timeout(), 240);
984        assert!(network_config.global_settings().tear_down_on_failure());
985    }
986
987    #[test]
988    fn network_config_builder_should_fails_and_returns_multiple_errors_if_relaychain_is_invalid() {
989        let errors = NetworkConfigBuilder::new()
990            .with_relaychain(|relaychain| {
991                relaychain
992                    .with_chain("polkadot")
993                    .with_random_nominators_count(10)
994                    .with_default_image("invalid.image")
995                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
996            })
997            .with_parachain(|parachain| {
998                parachain
999                    .with_id(1)
1000                    .with_chain("myparachain")
1001                    .with_initial_balance(100_000)
1002                    .with_collator(|collator| {
1003                        collator
1004                            .with_name("collator1")
1005                            .with_command("command1")
1006                            .validator(true)
1007                    })
1008            })
1009            .build()
1010            .unwrap_err();
1011
1012        assert_eq!(errors.len(), 2);
1013        assert_eq!(
1014            errors.first().unwrap().to_string(),
1015            "relaychain.default_image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1016        );
1017        assert_eq!(
1018            errors.get(1).unwrap().to_string(),
1019            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1020        );
1021    }
1022
1023    #[test]
1024    fn network_config_builder_should_fails_and_returns_multiple_errors_if_parachain_is_invalid() {
1025        let errors = NetworkConfigBuilder::new()
1026            .with_relaychain(|relaychain| {
1027                relaychain
1028                    .with_chain("polkadot")
1029                    .with_random_nominators_count(10)
1030                    .with_validator(|node| node.with_name("node").with_command("command"))
1031            })
1032            .with_parachain(|parachain| {
1033                parachain
1034                    .with_id(1000)
1035                    .with_chain("myparachain")
1036                    .with_initial_balance(100_000)
1037                    .with_collator(|collator| {
1038                        collator
1039                            .with_name("collator1")
1040                            .with_command("invalid command")
1041                            .with_image("invalid.image")
1042                            .validator(true)
1043                    })
1044            })
1045            .build()
1046            .unwrap_err();
1047
1048        assert_eq!(errors.len(), 2);
1049        assert_eq!(
1050            errors.first().unwrap().to_string(),
1051            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
1052        );
1053        assert_eq!(
1054            errors.get(1).unwrap().to_string(),
1055            "parachain[1000].collators['collator1'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1056        );
1057    }
1058
1059    #[test]
1060    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_parachains_are_invalid(
1061    ) {
1062        let errors = NetworkConfigBuilder::new()
1063            .with_relaychain(|relaychain| {
1064                relaychain
1065                    .with_chain("polkadot")
1066                    .with_random_nominators_count(10)
1067                    .with_validator(|node| node.with_name("node").with_command("command"))
1068            })
1069            .with_parachain(|parachain| {
1070                parachain
1071                    .with_id(1000)
1072                    .with_chain("myparachain1")
1073                    .with_initial_balance(100_000)
1074                    .with_collator(|collator| {
1075                        collator
1076                            .with_name("collator1")
1077                            .with_command("invalid command")
1078                    })
1079            })
1080            .with_parachain(|parachain| {
1081                parachain
1082                    .with_id(2000)
1083                    .with_chain("myparachain2")
1084                    .with_initial_balance(100_000)
1085                    .with_collator(|collator| {
1086                        collator
1087                            .with_name("collator2")
1088                            .validator(true)
1089                            .with_resources(|resources| {
1090                                resources
1091                                    .with_limit_cpu("1000m")
1092                                    .with_request_memory("1Gi")
1093                                    .with_request_cpu("invalid")
1094                            })
1095                    })
1096            })
1097            .build()
1098            .unwrap_err();
1099
1100        assert_eq!(errors.len(), 2);
1101        assert_eq!(
1102            errors.first().unwrap().to_string(),
1103            "parachain[1000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
1104        );
1105        assert_eq!(
1106            errors.get(1).unwrap().to_string(),
1107            "parachain[2000].collators['collator2'].resources.request_cpu: 'invalid' doesn't match regex '^\\d+(.\\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1108        );
1109    }
1110
1111    #[test]
1112    fn network_config_builder_should_fails_and_returns_multiple_errors_if_global_settings_is_invalid(
1113    ) {
1114        let errors = NetworkConfigBuilder::new()
1115            .with_relaychain(|relaychain| {
1116                relaychain
1117                    .with_chain("polkadot")
1118                    .with_random_nominators_count(10)
1119                    .with_validator(|node| node.with_name("node").with_command("command"))
1120            })
1121            .with_parachain(|parachain| {
1122                parachain
1123                    .with_id(1000)
1124                    .with_chain("myparachain")
1125                    .with_initial_balance(100_000)
1126                    .with_collator(|collator| {
1127                        collator
1128                            .with_name("collator")
1129                            .with_command("command")
1130                            .validator(true)
1131                    })
1132            })
1133            .with_global_settings(|global_settings| {
1134                global_settings
1135                    .with_local_ip("127.0.0000.1")
1136                    .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421"])
1137            })
1138            .build()
1139            .unwrap_err();
1140
1141        assert_eq!(errors.len(), 2);
1142        assert_eq!(
1143            errors.first().unwrap().to_string(),
1144            "global_settings.local_ip: invalid IP address syntax"
1145        );
1146        assert_eq!(
1147            errors.get(1).unwrap().to_string(),
1148            "global_settings.bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1149        );
1150    }
1151
1152    #[test]
1153    fn network_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
1154    ) {
1155        let errors = NetworkConfigBuilder::new()
1156            .with_relaychain(|relaychain| {
1157                relaychain
1158                    .with_chain("polkadot")
1159                    .with_random_nominators_count(10)
1160                    .with_validator(|node| node.with_name("node").with_command("invalid command"))
1161            })
1162            .with_parachain(|parachain| {
1163                parachain
1164                    .with_id(1000)
1165                    .with_chain("myparachain")
1166                    .with_initial_balance(100_000)
1167                    .with_collator(|collator| {
1168                        collator
1169                            .with_name("collator")
1170                            .with_command("command")
1171                            .with_image("invalid.image")
1172                    })
1173            })
1174            .with_global_settings(|global_settings| global_settings.with_local_ip("127.0.0000.1"))
1175            .build()
1176            .unwrap_err();
1177
1178        assert_eq!(errors.len(), 3);
1179        assert_eq!(
1180            errors.first().unwrap().to_string(),
1181            "relaychain.nodes['node'].command: 'invalid command' shouldn't contains whitespace"
1182        );
1183        assert_eq!(
1184            errors.get(1).unwrap().to_string(),
1185            "parachain[1000].collators['collator'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1186        );
1187        assert_eq!(
1188            errors.get(2).unwrap().to_string(),
1189            "global_settings.local_ip: invalid IP address syntax"
1190        );
1191    }
1192
1193    #[test]
1194    fn network_config_should_be_dumpable_to_a_toml_config_for_a_small_network() {
1195        let network_config = NetworkConfigBuilder::new()
1196            .with_relaychain(|relaychain| {
1197                relaychain
1198                    .with_chain("rococo-local")
1199                    .with_default_command("polkadot")
1200                    .with_default_image("docker.io/parity/polkadot:latest")
1201                    .with_default_args(vec![("-lparachain", "debug").into()])
1202                    .with_validator(|node| node.with_name("alice"))
1203                    .with_validator(|node| {
1204                        node.with_name("bob")
1205                            .invulnerable(false)
1206                            .bootnode(true)
1207                            .with_args(vec![("--database", "paritydb-experimental").into()])
1208                    })
1209            })
1210            .build()
1211            .unwrap();
1212
1213        let got = network_config.dump_to_toml().unwrap();
1214        let expected = fs::read_to_string("./testing/snapshots/0000-small-network.toml").unwrap();
1215        assert_eq!(got, expected);
1216    }
1217
1218    #[test]
1219    fn network_config_should_be_dumpable_to_a_toml_config_for_a_big_network() {
1220        let network_config = NetworkConfigBuilder::new()
1221            .with_relaychain(|relaychain| {
1222                relaychain
1223                    .with_chain("polkadot")
1224                    .with_default_command("polkadot")
1225                    .with_default_image("docker.io/parity/polkadot:latest")
1226                    .with_default_resources(|resources| {
1227                        resources
1228                            .with_request_cpu(100000)
1229                            .with_request_memory("500M")
1230                            .with_limit_cpu("10Gi")
1231                            .with_limit_memory("4000M")
1232                    })
1233                    .with_validator(|node| {
1234                        node.with_name("alice")
1235                            .with_initial_balance(1_000_000_000)
1236                            .bootnode(true)
1237                            .invulnerable(true)
1238                    })
1239                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1240            })
1241            .with_parachain(|parachain| {
1242                parachain
1243                    .with_id(1000)
1244                    .with_chain("myparachain")
1245                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1246                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1247                    .onboard_as_parachain(false)
1248                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1249                    .with_collator(|collator| {
1250                        collator
1251                            .with_name("john")
1252                            .bootnode(true)
1253                            .invulnerable(true)
1254                            .with_initial_balance(5_000_000_000)
1255                    })
1256                    .with_fullnode(|collator| {
1257                        collator
1258                            .with_name("charles")
1259                            .bootnode(true)
1260                            .invulnerable(true)
1261                            .with_initial_balance(0)
1262                    })
1263                    .with_collator(|collator| {
1264                        collator
1265                            .with_name("frank")
1266                            .invulnerable(false)
1267                            .bootnode(true)
1268                            .with_initial_balance(1_000_000_000)
1269                    })
1270            })
1271            .with_parachain(|parachain| {
1272                parachain
1273                    .with_id(2000)
1274                    .with_chain("myotherparachain")
1275                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1276                    .with_collator(|collator| {
1277                        collator
1278                            .with_name("mike")
1279                            .bootnode(true)
1280                            .invulnerable(true)
1281                            .with_initial_balance(5_000_000_000)
1282                    })
1283                    .with_fullnode(|collator| {
1284                        collator
1285                            .with_name("georges")
1286                            .bootnode(true)
1287                            .invulnerable(true)
1288                            .with_initial_balance(0)
1289                    })
1290                    .with_collator(|collator| {
1291                        collator
1292                            .with_name("victor")
1293                            .invulnerable(false)
1294                            .bootnode(true)
1295                            .with_initial_balance(1_000_000_000)
1296                    })
1297            })
1298            .with_hrmp_channel(|hrmp_channel| {
1299                hrmp_channel
1300                    .with_sender(1000)
1301                    .with_recipient(2000)
1302                    .with_max_capacity(150)
1303                    .with_max_message_size(5000)
1304            })
1305            .with_hrmp_channel(|hrmp_channel| {
1306                hrmp_channel
1307                    .with_sender(2000)
1308                    .with_recipient(1000)
1309                    .with_max_capacity(200)
1310                    .with_max_message_size(8000)
1311            })
1312            .with_custom_process(|c| c.with_name("eth-rpc").with_command("eth-rpc"))
1313            .build()
1314            .unwrap();
1315
1316        let got = network_config.dump_to_toml().unwrap();
1317        let expected = fs::read_to_string("./testing/snapshots/0001-big-network.toml").unwrap();
1318        assert_eq!(got, expected);
1319    }
1320
1321    #[test]
1322    fn network_config_builder_should_be_dumplable_to_a_toml_config_a_overrides_default_correctly() {
1323        let network_config = NetworkConfigBuilder::new()
1324            .with_relaychain(|relaychain| {
1325                relaychain
1326                    .with_chain("polkadot")
1327                    .with_default_command("polkadot")
1328                    .with_default_image("docker.io/parity/polkadot:latest")
1329                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1330                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1331                    .with_default_resources(|resources| {
1332                        resources
1333                            .with_request_cpu(100000)
1334                            .with_request_memory("500M")
1335                            .with_limit_cpu("10Gi")
1336                            .with_limit_memory("4000M")
1337                    })
1338                    .with_validator(|node| {
1339                        node.with_name("alice")
1340                            .with_initial_balance(1_000_000_000)
1341                            .bootnode(true)
1342                            .invulnerable(true)
1343                    })
1344                    .with_validator(|node| {
1345                        node.with_name("bob")
1346                            .invulnerable(true)
1347                            .bootnode(true)
1348                            .with_image("mycustomimage:latest")
1349                            .with_command("my-custom-command")
1350                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1351                            .with_resources(|resources| {
1352                                resources
1353                                    .with_request_cpu(1000)
1354                                    .with_request_memory("250Mi")
1355                                    .with_limit_cpu("5Gi")
1356                                    .with_limit_memory("2Gi")
1357                            })
1358                            .with_args(vec![("-myothername", "value").into()])
1359                    })
1360            })
1361            .with_parachain(|parachain| {
1362                parachain
1363                    .with_id(1000)
1364                    .with_chain("myparachain")
1365                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1366                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1367                    .with_default_command("my-default-command")
1368                    .with_default_image("mydefaultimage:latest")
1369                    .with_collator(|collator| {
1370                        collator
1371                            .with_name("john")
1372                            .bootnode(true)
1373                            .invulnerable(true)
1374                            .with_initial_balance(5_000_000_000)
1375                            .with_command("my-non-default-command")
1376                            .with_image("anotherimage:latest")
1377                    })
1378                    .with_fullnode(|collator| {
1379                        collator
1380                            .with_name("charles")
1381                            .bootnode(true)
1382                            .invulnerable(true)
1383                            .with_initial_balance(0)
1384                    })
1385            })
1386            .build()
1387            .unwrap();
1388
1389        let got = network_config.dump_to_toml().unwrap();
1390        let expected =
1391            fs::read_to_string("./testing/snapshots/0002-overridden-defaults.toml").unwrap();
1392        assert_eq!(got, expected);
1393    }
1394
1395    #[test]
1396    fn the_toml_config_with_custom_settings() {
1397        let settings = GlobalSettingsBuilder::new()
1398            .with_base_dir("/tmp/test-demo")
1399            .build()
1400            .unwrap();
1401
1402        let load_from_toml = NetworkConfig::load_from_toml_with_settings(
1403            "./testing/snapshots/0000-small-network.toml",
1404            &settings,
1405        )
1406        .unwrap();
1407
1408        assert_eq!(
1409            Some(PathBuf::from("/tmp/test-demo").as_path()),
1410            load_from_toml.global_settings.base_dir()
1411        );
1412    }
1413
1414    #[test]
1415    fn the_toml_config_should_be_imported_and_match_a_network() {
1416        let load_from_toml =
1417            NetworkConfig::load_from_toml("./testing/snapshots/0000-small-network.toml").unwrap();
1418
1419        let expected = NetworkConfigBuilder::new()
1420            .with_relaychain(|relaychain| {
1421                relaychain
1422                    .with_chain("rococo-local")
1423                    .with_default_command("polkadot")
1424                    .with_default_image("docker.io/parity/polkadot:latest")
1425                    .with_default_args(vec![("-lparachain=debug").into()])
1426                    .with_validator(|node| {
1427                        node.with_name("alice")
1428                            .validator(true)
1429                            .invulnerable(true)
1430                            .bootnode(false)
1431                            .with_initial_balance(2000000000000)
1432                    })
1433                    .with_validator(|node| {
1434                        node.with_name("bob")
1435                            .with_args(vec![("--database", "paritydb-experimental").into()])
1436                            .invulnerable(false)
1437                            .bootnode(true)
1438                            .with_initial_balance(2000000000000)
1439                    })
1440            })
1441            .build()
1442            .unwrap();
1443
1444        // We need to assert parts of the network config separately because the expected one contains the chain default context which
1445        // is used for dumbing to tomp while the
1446        // while loaded
1447        assert_eq!(
1448            expected.relaychain().chain(),
1449            load_from_toml.relaychain().chain()
1450        );
1451        assert_eq!(
1452            expected.relaychain().default_args(),
1453            load_from_toml.relaychain().default_args()
1454        );
1455        assert_eq!(
1456            expected.relaychain().default_command(),
1457            load_from_toml.relaychain().default_command()
1458        );
1459        assert_eq!(
1460            expected.relaychain().default_image(),
1461            load_from_toml.relaychain().default_image()
1462        );
1463
1464        // Check the nodes without the Chain Default Context
1465        expected
1466            .relaychain()
1467            .nodes()
1468            .iter()
1469            .zip(load_from_toml.relaychain().nodes().iter())
1470            .for_each(|(expected_node, loaded_node)| {
1471                assert_eq!(expected_node.name(), loaded_node.name());
1472                assert_eq!(expected_node.command(), loaded_node.command());
1473                assert_eq!(expected_node.args(), loaded_node.args());
1474                assert_eq!(
1475                    expected_node.is_invulnerable(),
1476                    loaded_node.is_invulnerable()
1477                );
1478                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1479                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1480                assert_eq!(
1481                    expected_node.initial_balance(),
1482                    loaded_node.initial_balance()
1483                );
1484            });
1485    }
1486
1487    #[test]
1488    fn the_toml_config_without_settings_should_be_imported_and_match_a_network() {
1489        let load_from_toml = NetworkConfig::load_from_toml(
1490            "./testing/snapshots/0004-small-network-without-settings.toml",
1491        )
1492        .unwrap();
1493
1494        let expected = NetworkConfigBuilder::new()
1495            .with_relaychain(|relaychain| {
1496                relaychain
1497                    .with_chain("rococo-local")
1498                    .with_default_command("polkadot")
1499                    .with_validator(|node| node.with_name("alice"))
1500                    .with_validator(|node| node.with_name("bob"))
1501            })
1502            .build()
1503            .unwrap();
1504
1505        assert_eq!(
1506            load_from_toml.global_settings().network_spawn_timeout(),
1507            expected.global_settings().network_spawn_timeout()
1508        )
1509    }
1510
1511    #[test]
1512    fn the_toml_config_should_be_imported_and_match_a_network_with_parachains() {
1513        let load_from_toml =
1514            NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
1515
1516        let expected = NetworkConfigBuilder::new()
1517            .with_relaychain(|relaychain| {
1518                relaychain
1519                    .with_chain("polkadot")
1520                    .with_default_command("polkadot")
1521                    .with_default_image("docker.io/parity/polkadot:latest")
1522                    .with_default_resources(|resources| {
1523                        resources
1524                            .with_request_cpu(100000)
1525                            .with_request_memory("500M")
1526                            .with_limit_cpu("10Gi")
1527                            .with_limit_memory("4000M")
1528                    })
1529                    .with_validator(|node| {
1530                        node.with_name("alice")
1531                            .with_initial_balance(1_000_000_000)
1532                            .bootnode(true)
1533                            .invulnerable(true)
1534                    })
1535                    .with_validator(|node| node.with_name("bob").invulnerable(true).bootnode(true))
1536            })
1537            .with_parachain(|parachain| {
1538                parachain
1539                    .with_id(1000)
1540                    .with_chain("myparachain")
1541                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1542                    .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1543                    .onboard_as_parachain(false)
1544                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1545                    .with_collator(|collator| {
1546                        collator
1547                            .with_name("john")
1548                            .bootnode(true)
1549                            .invulnerable(true)
1550                            .with_initial_balance(5_000_000_000)
1551                    })
1552                    .with_fullnode(|collator| {
1553                        collator
1554                            .with_name("charles")
1555                            .bootnode(true)
1556                            .invulnerable(true)
1557                            .with_initial_balance(0)
1558                    })
1559                    .with_collator(|collator| {
1560                        collator
1561                            .with_name("frank")
1562                            .invulnerable(false)
1563                            .bootnode(true)
1564                            .with_initial_balance(1_000_000_000)
1565                    })
1566            })
1567            .with_parachain(|parachain| {
1568                parachain
1569                    .with_id(2000)
1570                    .with_chain("myotherparachain")
1571                    .with_chain_spec_path("/path/to/my/other/chain/spec.json")
1572                    .with_collator(|collator| {
1573                        collator
1574                            .with_name("mike")
1575                            .bootnode(true)
1576                            .invulnerable(true)
1577                            .with_initial_balance(5_000_000_000)
1578                    })
1579                    .with_fullnode(|collator| {
1580                        collator
1581                            .with_name("georges")
1582                            .bootnode(true)
1583                            .invulnerable(true)
1584                            .with_initial_balance(0)
1585                    })
1586                    .with_collator(|collator| {
1587                        collator
1588                            .with_name("victor")
1589                            .invulnerable(false)
1590                            .bootnode(true)
1591                            .with_initial_balance(1_000_000_000)
1592                    })
1593            })
1594            .with_hrmp_channel(|hrmp_channel| {
1595                hrmp_channel
1596                    .with_sender(1000)
1597                    .with_recipient(2000)
1598                    .with_max_capacity(150)
1599                    .with_max_message_size(5000)
1600            })
1601            .with_hrmp_channel(|hrmp_channel| {
1602                hrmp_channel
1603                    .with_sender(2000)
1604                    .with_recipient(1000)
1605                    .with_max_capacity(200)
1606                    .with_max_message_size(8000)
1607            })
1608            .build()
1609            .unwrap();
1610
1611        // Check the relay chain
1612        assert_eq!(
1613            expected.relaychain().default_resources(),
1614            load_from_toml.relaychain().default_resources()
1615        );
1616
1617        // Check the nodes without the Chain Default Context
1618        expected
1619            .relaychain()
1620            .nodes()
1621            .iter()
1622            .zip(load_from_toml.relaychain().nodes().iter())
1623            .for_each(|(expected_node, loaded_node)| {
1624                assert_eq!(expected_node.name(), loaded_node.name());
1625                assert_eq!(expected_node.command(), loaded_node.command());
1626                assert_eq!(expected_node.args(), loaded_node.args());
1627                assert_eq!(expected_node.is_validator(), loaded_node.is_validator());
1628                assert_eq!(expected_node.is_bootnode(), loaded_node.is_bootnode());
1629                assert_eq!(
1630                    expected_node.initial_balance(),
1631                    loaded_node.initial_balance()
1632                );
1633                assert_eq!(
1634                    expected_node.is_invulnerable(),
1635                    loaded_node.is_invulnerable()
1636                );
1637            });
1638
1639        expected
1640            .parachains()
1641            .iter()
1642            .zip(load_from_toml.parachains().iter())
1643            .for_each(|(expected_parachain, loaded_parachain)| {
1644                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1645                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1646                assert_eq!(
1647                    expected_parachain.chain_spec_path(),
1648                    loaded_parachain.chain_spec_path()
1649                );
1650                assert_eq!(
1651                    expected_parachain.registration_strategy(),
1652                    loaded_parachain.registration_strategy()
1653                );
1654                assert_eq!(
1655                    expected_parachain.onboard_as_parachain(),
1656                    loaded_parachain.onboard_as_parachain()
1657                );
1658                assert_eq!(
1659                    expected_parachain.default_db_snapshot(),
1660                    loaded_parachain.default_db_snapshot()
1661                );
1662                assert_eq!(
1663                    expected_parachain.default_command(),
1664                    loaded_parachain.default_command()
1665                );
1666                assert_eq!(
1667                    expected_parachain.default_image(),
1668                    loaded_parachain.default_image()
1669                );
1670                assert_eq!(
1671                    expected_parachain.collators().len(),
1672                    loaded_parachain.collators().len()
1673                );
1674                expected_parachain
1675                    .collators()
1676                    .iter()
1677                    .zip(loaded_parachain.collators().iter())
1678                    .for_each(|(expected_collator, loaded_collator)| {
1679                        assert_eq!(expected_collator.name(), loaded_collator.name());
1680                        assert_eq!(expected_collator.command(), loaded_collator.command());
1681                        assert_eq!(expected_collator.image(), loaded_collator.image());
1682                        assert_eq!(
1683                            expected_collator.is_validator(),
1684                            loaded_collator.is_validator()
1685                        );
1686                        assert_eq!(
1687                            expected_collator.is_bootnode(),
1688                            loaded_collator.is_bootnode()
1689                        );
1690                        assert_eq!(
1691                            expected_collator.is_invulnerable(),
1692                            loaded_collator.is_invulnerable()
1693                        );
1694                        assert_eq!(
1695                            expected_collator.initial_balance(),
1696                            loaded_collator.initial_balance()
1697                        );
1698                    });
1699            });
1700
1701        expected
1702            .hrmp_channels()
1703            .iter()
1704            .zip(load_from_toml.hrmp_channels().iter())
1705            .for_each(|(expected_hrmp_channel, loaded_hrmp_channel)| {
1706                assert_eq!(expected_hrmp_channel.sender(), loaded_hrmp_channel.sender());
1707                assert_eq!(
1708                    expected_hrmp_channel.recipient(),
1709                    loaded_hrmp_channel.recipient()
1710                );
1711                assert_eq!(
1712                    expected_hrmp_channel.max_capacity(),
1713                    loaded_hrmp_channel.max_capacity()
1714                );
1715                assert_eq!(
1716                    expected_hrmp_channel.max_message_size(),
1717                    loaded_hrmp_channel.max_message_size()
1718                );
1719            });
1720    }
1721
1722    #[test]
1723    fn the_toml_config_should_be_imported_and_match_a_network_with_overriden_defaults() {
1724        let load_from_toml =
1725            NetworkConfig::load_from_toml("./testing/snapshots/0002-overridden-defaults.toml")
1726                .unwrap();
1727
1728        let expected = NetworkConfigBuilder::new()
1729            .with_relaychain(|relaychain| {
1730                relaychain
1731                    .with_chain("polkadot")
1732                    .with_default_command("polkadot")
1733                    .with_default_image("docker.io/parity/polkadot:latest")
1734                    .with_default_args(vec![("-name", "value").into(), "--flag".into()])
1735                    .with_default_db_snapshot("https://storage.com/path/to/db_snapshot.tgz")
1736                    .with_default_resources(|resources| {
1737                        resources
1738                            .with_request_cpu(100000)
1739                            .with_request_memory("500M")
1740                            .with_limit_cpu("10Gi")
1741                            .with_limit_memory("4000M")
1742                    })
1743                    .with_validator(|node| {
1744                        node.with_name("alice")
1745                            .with_initial_balance(1_000_000_000)
1746                            .bootnode(true)
1747                            .invulnerable(true)
1748                    })
1749                    .with_validator(|node| {
1750                        node.with_name("bob")
1751                            .invulnerable(true)
1752                            .bootnode(true)
1753                            .with_image("mycustomimage:latest")
1754                            .with_command("my-custom-command")
1755                            .with_db_snapshot("https://storage.com/path/to/other/db_snapshot.tgz")
1756                            .with_resources(|resources| {
1757                                resources
1758                                    .with_request_cpu(1000)
1759                                    .with_request_memory("250Mi")
1760                                    .with_limit_cpu("5Gi")
1761                                    .with_limit_memory("2Gi")
1762                            })
1763                            .with_args(vec![("-myothername", "value").into()])
1764                    })
1765            })
1766            .with_parachain(|parachain| {
1767                parachain
1768                    .with_id(1000)
1769                    .with_chain("myparachain")
1770                    .with_chain_spec_path("/path/to/my/chain/spec.json")
1771                    .with_default_db_snapshot("https://storage.com/path/to/other_snapshot.tgz")
1772                    .with_default_command("my-default-command")
1773                    .with_default_image("mydefaultimage:latest")
1774                    .with_collator(|collator| {
1775                        collator
1776                            .with_name("john")
1777                            .bootnode(true)
1778                            .validator(true)
1779                            .invulnerable(true)
1780                            .with_initial_balance(5_000_000_000)
1781                            .with_command("my-non-default-command")
1782                            .with_image("anotherimage:latest")
1783                    })
1784                    .with_fullnode(|collator| {
1785                        collator
1786                            .with_name("charles")
1787                            .bootnode(true)
1788                            .invulnerable(true)
1789                            .with_initial_balance(0)
1790                    })
1791            })
1792            .build()
1793            .unwrap();
1794
1795        expected
1796            .parachains()
1797            .iter()
1798            .zip(load_from_toml.parachains().iter())
1799            .for_each(|(expected_parachain, loaded_parachain)| {
1800                assert_eq!(expected_parachain.id(), loaded_parachain.id());
1801                assert_eq!(expected_parachain.chain(), loaded_parachain.chain());
1802                assert_eq!(
1803                    expected_parachain.chain_spec_path(),
1804                    loaded_parachain.chain_spec_path()
1805                );
1806                assert_eq!(
1807                    expected_parachain.registration_strategy(),
1808                    loaded_parachain.registration_strategy()
1809                );
1810                assert_eq!(
1811                    expected_parachain.onboard_as_parachain(),
1812                    loaded_parachain.onboard_as_parachain()
1813                );
1814                assert_eq!(
1815                    expected_parachain.default_db_snapshot(),
1816                    loaded_parachain.default_db_snapshot()
1817                );
1818                assert_eq!(
1819                    expected_parachain.default_command(),
1820                    loaded_parachain.default_command()
1821                );
1822                assert_eq!(
1823                    expected_parachain.default_image(),
1824                    loaded_parachain.default_image()
1825                );
1826                assert_eq!(
1827                    expected_parachain.collators().len(),
1828                    loaded_parachain.collators().len()
1829                );
1830                expected_parachain
1831                    .collators()
1832                    .iter()
1833                    .zip(loaded_parachain.collators().iter())
1834                    .for_each(|(expected_collator, loaded_collator)| {
1835                        assert_eq!(expected_collator.name(), loaded_collator.name());
1836                        assert_eq!(expected_collator.command(), loaded_collator.command());
1837                        assert_eq!(expected_collator.image(), loaded_collator.image());
1838                        assert_eq!(
1839                            expected_collator.is_validator(),
1840                            loaded_collator.is_validator()
1841                        );
1842                        assert_eq!(
1843                            expected_collator.is_bootnode(),
1844                            loaded_collator.is_bootnode()
1845                        );
1846                        assert_eq!(
1847                            expected_collator.is_invulnerable(),
1848                            loaded_collator.is_invulnerable()
1849                        );
1850                        assert_eq!(
1851                            expected_collator.initial_balance(),
1852                            loaded_collator.initial_balance()
1853                        );
1854                    });
1855            });
1856    }
1857
1858    #[test]
1859    fn with_chain_and_nodes_works() {
1860        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1861            "rococo-local",
1862            vec!["alice".to_string(), "bob".to_string()],
1863        )
1864        .build()
1865        .unwrap();
1866
1867        // relaychain
1868        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
1869        assert_eq!(network_config.relaychain().nodes().len(), 2);
1870        let mut node_names = network_config.relaychain().nodes().into_iter();
1871        let node1 = node_names.next().unwrap().name();
1872        assert_eq!(node1, "alice");
1873        let node2 = node_names.next().unwrap().name();
1874        assert_eq!(node2, "bob");
1875
1876        // parachains
1877        assert_eq!(network_config.parachains().len(), 0);
1878    }
1879
1880    #[test]
1881    fn with_chain_and_nodes_should_fail_with_empty_relay_name() {
1882        let errors = NetworkConfigBuilder::with_chain_and_nodes("", vec!["alice".to_string()])
1883            .build()
1884            .unwrap_err();
1885
1886        assert_eq!(
1887            errors.first().unwrap().to_string(),
1888            "relaychain.chain: can't be empty"
1889        );
1890    }
1891
1892    #[test]
1893    fn with_chain_and_nodes_should_fail_with_empty_node_list() {
1894        let errors = NetworkConfigBuilder::with_chain_and_nodes("rococo-local", vec![])
1895            .build()
1896            .unwrap_err();
1897
1898        assert_eq!(
1899            errors.first().unwrap().to_string(),
1900            "relaychain.nodes[''].name: can't be empty"
1901        );
1902    }
1903
1904    #[test]
1905    fn with_chain_and_nodes_should_fail_with_empty_node_name() {
1906        let errors = NetworkConfigBuilder::with_chain_and_nodes(
1907            "rococo-local",
1908            vec!["alice".to_string(), "".to_string()],
1909        )
1910        .build()
1911        .unwrap_err();
1912
1913        assert_eq!(
1914            errors.first().unwrap().to_string(),
1915            "relaychain.nodes[''].name: can't be empty"
1916        );
1917    }
1918
1919    #[test]
1920    fn with_parachain_id_and_collators_works() {
1921        let network_config = NetworkConfigBuilder::with_chain_and_nodes(
1922            "rococo-local",
1923            vec!["alice".to_string(), "bob".to_string()],
1924        )
1925        .with_parachain_id_and_collators(
1926            100,
1927            vec!["collator1".to_string(), "collator2".to_string()],
1928        )
1929        .build()
1930        .unwrap();
1931
1932        // relaychain
1933        assert_eq!(network_config.relaychain().chain().as_str(), "rococo-local");
1934        assert_eq!(network_config.relaychain().nodes().len(), 2);
1935        let mut node_names = network_config.relaychain().nodes().into_iter();
1936        let node1 = node_names.next().unwrap().name();
1937        assert_eq!(node1, "alice");
1938        let node2 = node_names.next().unwrap().name();
1939        assert_eq!(node2, "bob");
1940
1941        // parachains
1942        assert_eq!(network_config.parachains().len(), 1);
1943        let &parachain1 = network_config.parachains().first().unwrap();
1944        assert_eq!(parachain1.id(), 100);
1945        assert_eq!(parachain1.collators().len(), 2);
1946        let mut collator_names = parachain1.collators().into_iter();
1947        let collator1 = collator_names.next().unwrap().name();
1948        assert_eq!(collator1, "collator1");
1949        let collator2 = collator_names.next().unwrap().name();
1950        assert_eq!(collator2, "collator2");
1951
1952        assert_eq!(parachain1.initial_balance(), 2_000_000_000_000);
1953    }
1954
1955    #[test]
1956    fn with_parachain_id_and_collators_should_fail_with_empty_collator_list() {
1957        let errors =
1958            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
1959                .with_parachain_id_and_collators(1, vec![])
1960                .build()
1961                .unwrap_err();
1962
1963        assert_eq!(
1964            errors.first().unwrap().to_string(),
1965            "parachain[1].can't be empty"
1966        );
1967    }
1968
1969    #[test]
1970    fn with_parachain_id_and_collators_should_fail_with_empty_collator_name() {
1971        let errors =
1972            NetworkConfigBuilder::with_chain_and_nodes("polkadot", vec!["alice".to_string()])
1973                .with_parachain_id_and_collators(1, vec!["collator1".to_string(), "".to_string()])
1974                .build()
1975                .unwrap_err();
1976
1977        assert_eq!(
1978            errors.first().unwrap().to_string(),
1979            "parachain[1].collators[''].name: can't be empty"
1980        );
1981    }
1982
1983    #[test]
1984    fn wasm_override_in_toml_should_work() {
1985        let load_from_toml = NetworkConfig::load_from_toml(
1986            "./testing/snapshots/0005-small-networl-with-wasm-override.toml",
1987        )
1988        .unwrap();
1989
1990        let expected = NetworkConfigBuilder::new()
1991            .with_relaychain(|relaychain| {
1992                relaychain
1993                    .with_chain("rococo-local")
1994                    .with_default_command("polkadot")
1995                    .with_wasm_override("/some/path/runtime.wasm")
1996                    .with_validator(|node| node.with_name("alice"))
1997                    .with_validator(|node| node.with_name("bob"))
1998            })
1999            .with_parachain(|p| {
2000                p.with_id(1000)
2001                    .with_wasm_override("https://some.com/runtime.wasm")
2002                    .with_collator(|c| c.with_name("john"))
2003            })
2004            .build()
2005            .unwrap();
2006
2007        assert_eq!(
2008            load_from_toml.relaychain().wasm_override(),
2009            expected.relaychain().wasm_override()
2010        );
2011        assert_eq!(
2012            load_from_toml.parachains()[0].wasm_override(),
2013            expected.parachains()[0].wasm_override()
2014        );
2015    }
2016
2017    #[test]
2018    fn multiple_paras_with_same_id_should_work() {
2019        let network_config = NetworkConfigBuilder::new()
2020            .with_relaychain(|relaychain| {
2021                relaychain
2022                    .with_chain("polkadot")
2023                    .with_fullnode(|node| node.with_name("node").with_command("command"))
2024                    .with_validator(|n| n.with_name("v"))
2025                    .with_validator(|n| n.with_name("v1"))
2026            })
2027            .with_parachain(|parachain| {
2028                parachain
2029                    .with_id(1)
2030                    .with_chain("myparachain1")
2031                    .with_collator(|collator| {
2032                        collator.with_name("collator1").with_command("command1")
2033                    })
2034            })
2035            .with_parachain(|parachain| {
2036                parachain
2037                    .with_id(1)
2038                    .with_chain("myparachain1")
2039                    .with_registration_strategy(RegistrationStrategy::Manual)
2040                    .with_collator(|collator| {
2041                        collator.with_name("collator2").with_command("command1")
2042                    })
2043            })
2044            .build()
2045            .unwrap();
2046
2047        let &parachain2 = network_config.parachains().last().unwrap();
2048        assert_eq!(parachain2.unique_id(), "1-1");
2049    }
2050
2051    #[test]
2052    fn multiple_paras_with_same_id_both_for_register_should_fail() {
2053        let errors = NetworkConfigBuilder::new()
2054            .with_relaychain(|relaychain| {
2055                relaychain
2056                    .with_chain("polkadot")
2057                    .with_fullnode(|node| node.with_name("node").with_command("command"))
2058            })
2059            .with_parachain(|parachain| {
2060                parachain
2061                    .with_id(1)
2062                    .with_chain("myparachain1")
2063                    .with_collator(|collator| {
2064                        collator.with_name("collator1").with_command("command1")
2065                    })
2066            })
2067            .with_parachain(|parachain| {
2068                parachain
2069                    .with_id(1)
2070                    .with_chain("myparachain1")
2071                    // .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
2072                    .with_collator(|collator| {
2073                        collator
2074                            .with_name("collator2")
2075                            .with_command("command1")
2076                    })
2077            })
2078            .build()
2079            .unwrap_err();
2080
2081        assert_eq!(
2082            errors.first().unwrap().to_string(),
2083            "ParaId 1 already set to be registered, only one should be."
2084        );
2085    }
2086
2087    #[test]
2088    fn network_config_should_work_from_toml_without_chain_name() {
2089        let loaded_from_toml =
2090            NetworkConfig::load_from_toml("./testing/snapshots/0006-without-rc-chain-name.toml")
2091                .unwrap();
2092
2093        assert_eq!(
2094            "rococo-local",
2095            loaded_from_toml.relaychain().chain().as_str()
2096        );
2097    }
2098
2099    #[test]
2100    fn network_config_should_work_from_toml_with_duplicate_name_between_collator_and_relay_node() {
2101        let loaded_from_toml = NetworkConfig::load_from_toml(
2102            "./testing/snapshots/0007-small-network_w_parachain_w_duplicate_node_names.toml",
2103        )
2104        .unwrap();
2105
2106        assert_eq!(
2107            loaded_from_toml
2108                .relaychain()
2109                .nodes()
2110                .iter()
2111                .filter(|n| n.name() == "alice")
2112                .count(),
2113            1
2114        );
2115        assert_eq!(
2116            loaded_from_toml
2117                .parachains()
2118                .iter()
2119                .flat_map(|para| para.collators())
2120                .filter(|n| n.name() == "alice-1")
2121                .count(),
2122            1
2123        );
2124    }
2125
2126    #[test]
2127    fn raw_spec_override_in_toml_should_work() {
2128        let load_from_toml = NetworkConfig::load_from_toml(
2129            "./testing/snapshots/0008-small-network-with-raw-spec-override.toml",
2130        )
2131        .unwrap();
2132
2133        let expected = NetworkConfigBuilder::new()
2134            .with_relaychain(|relaychain| {
2135                relaychain
2136                    .with_chain("rococo-local")
2137                    .with_default_command("polkadot")
2138                    .with_raw_spec_override("/some/path/raw_spec_override.json")
2139                    .with_validator(|node| node.with_name("alice"))
2140                    .with_validator(|node| node.with_name("bob"))
2141            })
2142            .with_parachain(|p| {
2143                p.with_id(1000)
2144                    .with_raw_spec_override("https://some.com/raw_spec_override.json")
2145                    .with_collator(|c| c.with_name("john"))
2146            })
2147            .build()
2148            .unwrap();
2149
2150        assert_eq!(
2151            load_from_toml.relaychain().raw_spec_override(),
2152            expected.relaychain().raw_spec_override()
2153        );
2154        assert_eq!(
2155            load_from_toml.parachains()[0].raw_spec_override(),
2156            expected.parachains()[0].raw_spec_override()
2157        );
2158    }
2159
2160    const TOML_WITH_RELATIVE_PATHS: &str = r#"
2161[relaychain]
2162chain = "rococo-local"
2163default_command = "./bin/polkadot"
2164chain_spec_path = "./rc.json"
2165
2166[[relaychain.nodes]]
2167name = "alice"
2168validator = true
2169command = "polkadot"
2170"#;
2171
2172    #[test]
2173    fn toml_string_should_resolve_relative_paths_against_the_given_base_dir() {
2174        let config = NetworkConfig::load_from_toml_string_with_base_dir(
2175            TOML_WITH_RELATIVE_PATHS,
2176            Path::new("/tmp/some-base-dir"),
2177        )
2178        .unwrap();
2179
2180        assert_eq!(
2181            config.relaychain().default_command().unwrap().as_str(),
2182            "/tmp/some-base-dir/bin/polkadot"
2183        );
2184        assert_eq!(
2185            config.relaychain().chain_spec_path().unwrap().to_string(),
2186            "/tmp/some-base-dir/rc.json"
2187        );
2188        // non-relative values are left untouched
2189        assert_eq!(
2190            config.relaychain().nodes()[0].command().unwrap().as_str(),
2191            "polkadot"
2192        );
2193    }
2194
2195    #[test]
2196    fn toml_string_should_resolve_relative_paths_against_the_cwd_by_default() {
2197        let from_cwd = NetworkConfig::load_from_toml_string(TOML_WITH_RELATIVE_PATHS).unwrap();
2198        let expected = NetworkConfig::load_from_toml_string_with_base_dir(
2199            TOML_WITH_RELATIVE_PATHS,
2200            &std::env::current_dir().unwrap(),
2201        )
2202        .unwrap();
2203
2204        assert_eq!(from_cwd, expected);
2205    }
2206
2207    #[test]
2208    fn toml_string_and_toml_file_should_produce_the_same_config() {
2209        let path = "./testing/snapshots/0006-without-rc-chain-name.toml";
2210        let from_file = NetworkConfig::load_from_toml(path).unwrap();
2211
2212        let file_path = PathBuf::from(path).canonicalize().unwrap();
2213        let base_dir = file_path.parent().unwrap();
2214        let from_string = NetworkConfig::load_from_toml_string_with_base_dir(
2215            &fs::read_to_string(path).unwrap(),
2216            base_dir,
2217        )
2218        .unwrap();
2219
2220        assert_eq!(from_file, from_string);
2221
2222        // and the relative paths were actually rewritten, not just rewritten identically
2223        assert_eq!(
2224            from_string
2225                .relaychain()
2226                .chain_spec_path()
2227                .unwrap()
2228                .to_string(),
2229            base_dir.join("rc.json").to_string_lossy()
2230        );
2231        assert_eq!(
2232            from_string.parachains()[0]
2233                .chain_spec_path()
2234                .unwrap()
2235                .to_string(),
2236            base_dir.join("parachain.json").to_string_lossy()
2237        );
2238    }
2239
2240    #[test]
2241    fn with_jamchain() {
2242        let network_config = NetworkConfigBuilder::new()
2243            .with_relaychain(|relaychain| {
2244                relaychain
2245                    .with_chain("westend")
2246                    .with_default_command("polkadot")
2247                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2248                    .with_validator(|n| n.with_name("validator-0"))
2249                    .with_validator(|n| n.with_name("validator-1"))
2250            })
2251            .with_parachain(|parachain| {
2252                parachain
2253                    .with_id(1004)
2254                    .with_chain_spec_path("https://raw.githubusercontent.com/paritytech/polkadot-sdk/104e66f7114ea3322187b4b93255bb9f3f4d5005/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/statement_store/people-westend-local-spec.json")
2255                    .with_collator_group(|g| {
2256                        g.with_count(100)
2257                        .with_base_node(|n|{
2258                            n.with_name("collator")
2259                            .with_command("polkadot-parachain")
2260                            .with_args(vec!["--enable-statement-store".into()])
2261                            .with_image("docker.io/parity/polkadot-parachain:stable2603-3")
2262                        })
2263                })
2264            })
2265            .with_jamchain(|jamchain| {
2266                jamchain
2267                .with_id("dev")
2268                .with_validator(|jamnode|{
2269                    jamnode.with_name("jam1")
2270                })
2271            })
2272            .build()
2273            .unwrap();
2274
2275        let toml_string = network_config.dump_to_toml().unwrap();
2276        println!("{}", toml_string);
2277    }
2278
2279    /// A network file is the other way to describe a jamchain, and the override is plain data,
2280    /// so a `[jamchain.genesis_overrides]` table has to come back as the same JSON the builder
2281    /// would have been given — and survive being dumped and loaded again.
2282    #[test]
2283    fn jamchain_genesis_overrides_round_trip_through_toml() {
2284        let toml = r#"
2285[relaychain]
2286chain = "rococo-local"
2287default_command = "polkadot"
2288
2289[[relaychain.nodes]]
2290name = "alice"
2291
2292[jamchain]
2293id = "dev"
2294
2295[jamchain.genesis_overrides]
2296auth_queues = { "0" = "2bbda8cb" }
2297assigners = { "0" = 5 }
2298
2299[[jamchain.genesis_overrides.services]]
2300id = 5
2301code = "/blobs/parasim-service.jam"
2302preimages = ["/blobs/parachain-authorizer-sr25519.jam"]
2303
2304[[jamchain.nodes]]
2305name = "jam0"
2306mode = "validator"
2307"#;
2308        let expected = serde_json::json!({
2309            "auth_queues": { "0": "2bbda8cb" },
2310            "assigners": { "0": 5 },
2311            "services": [{
2312                "id": 5,
2313                "code": "/blobs/parasim-service.jam",
2314                "preimages": ["/blobs/parachain-authorizer-sr25519.jam"],
2315            }],
2316        });
2317
2318        let loaded = NetworkConfig::load_from_toml_string(toml).unwrap();
2319        let jamchain = loaded.jamchain().expect("the file has a jamchain");
2320        assert_eq!(jamchain.genesis_overrides(), Some(&expected));
2321
2322        let reloaded =
2323            NetworkConfig::load_from_toml_string(&loaded.dump_to_toml().unwrap()).unwrap();
2324        assert_eq!(reloaded.jamchain(), Some(jamchain));
2325    }
2326
2327    #[test]
2328    fn jamchain_toml_without_relaychain_works() {
2329        let config = NetworkConfig::load_from_toml_string(
2330            r#"
2331[jamchain]
2332id = "dev"
2333default_command = "polkajam"
2334
2335[[jamchain.nodes]]
2336name = "jam0"
2337mode = "validator"
2338
2339[[jamchain.nodes]]
2340name = "jam-or"
2341mode = "ordinary"
2342"#,
2343        )
2344        .unwrap();
2345
2346        assert!(config.try_relaychain().is_none());
2347        let jamchain = config.jamchain().unwrap();
2348        assert_eq!(jamchain.id().as_str(), "dev");
2349        assert_eq!(jamchain.nodes().len(), 2);
2350        assert_eq!(jamchain.nodes()[1].mode(), &JamNodeMode::Ordinary);
2351    }
2352
2353    #[test]
2354    fn jamchain_toml_with_parachains_works() {
2355        let config = NetworkConfig::load_from_toml_string(
2356            r#"
2357[jamchain]
2358id = "dev"
2359default_command = "polkajam"
2360
2361[[jamchain.nodes]]
2362name = "jam-or"
2363mode = "ordinary"
2364
2365[[parachains]]
2366id = 1000
2367default_command = "polkadot-omni-node"
2368default_args = ["--jam-rpc-url http://{{ZOMBIE:jam-or:rpc_uri}}"]
2369
2370[[parachains.collators]]
2371name = "collator"
2372
2373[[parachains.collators]]
2374name = "collator"
2375"#,
2376        )
2377        .unwrap();
2378
2379        assert!(config.try_relaychain().is_none());
2380        assert!(config.jamchain().is_some());
2381
2382        let para = config.parachains()[0];
2383        assert_eq!(para.id(), 1000);
2384
2385        // the parachain defaults are applied to the collators, and their names deduplicated
2386        let collators = para.collators();
2387        assert_eq!(collators.len(), 2);
2388        assert_eq!(collators[0].name(), "collator");
2389        assert_eq!(collators[1].name(), "collator-1");
2390        for collator in collators {
2391            assert_eq!(collator.command().unwrap().as_str(), "polkadot-omni-node");
2392            assert_eq!(
2393                collator.args(),
2394                vec![&Arg::Option(
2395                    "--jam-rpc-url".into(),
2396                    "http://{{ZOMBIE:jam-or:rpc_uri}}".into()
2397                )]
2398            );
2399        }
2400    }
2401
2402    #[test]
2403    fn toml_without_relaychain_nor_jamchain_fails() {
2404        let err = NetworkConfig::load_from_toml_string(
2405            r#"
2406[[parachains]]
2407id = 1000
2408"#,
2409        )
2410        .unwrap_err();
2411
2412        assert_eq!(
2413            err.to_string(),
2414            "Relay chain or Jam definition does not exist."
2415        );
2416    }
2417
2418    #[test]
2419    fn with_tiny_jamchain() {
2420        let network_config = NetworkConfigBuilder::new()
2421            .with_relaychain(|relaychain| {
2422                relaychain
2423                    .with_chain("westend")
2424                    .with_default_command("polkadot")
2425                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2426                    .with_validator(|n| n.with_name("validator-0"))
2427                    .with_validator(|n| n.with_name("validator-1"))
2428            })
2429            .with_tiny_jamchain()
2430            .build()
2431            .unwrap();
2432
2433        let toml_string = network_config.dump_to_toml().unwrap();
2434        println!("{}", toml_string);
2435    }
2436    #[test]
2437    fn demo_denis() {
2438        let network_config = NetworkConfigBuilder::new()
2439            .with_relaychain(|relaychain| {
2440                relaychain
2441                    .with_chain("westend")
2442                    .with_default_command("polkadot")
2443                    .with_default_image("docker.io/parity/polkadot:stable2603-3")
2444                    .with_validator(|n| n.with_name("validator-0"))
2445                    .with_validator(|n| n.with_name("validator-1"))
2446            })
2447            .with_parachain(|parachain| {
2448                parachain
2449                    .with_id(1004)
2450                    .with_chain_spec_path("https://raw.githubusercontent.com/paritytech/polkadot-sdk/104e66f7114ea3322187b4b93255bb9f3f4d5005/cumulus/zombienet/zombienet-sdk/tests/zombie_ci/statement_store/people-westend-local-spec.json")
2451                    .with_collator_group(|g| {
2452                        g.with_count(100)
2453                        .with_base_node(|n|{
2454                            n.with_name("collator")
2455                            .with_command("polkadot-parachain")
2456                            .with_args(vec!["--enable-statement-store".into()])
2457                            .with_image("docker.io/parity/polkadot-parachain:stable2603-3")
2458                        })
2459                })
2460            })
2461            .build()
2462            .unwrap();
2463
2464        let toml_string = network_config.dump_to_toml().unwrap();
2465        println!("{}", toml_string);
2466    }
2467}