zombienet_configuration/
parachain.rs

1use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, rc::Rc};
2
3use anyhow::anyhow;
4use multiaddr::Multiaddr;
5use serde::{
6    de::{self, Visitor},
7    ser::SerializeStruct,
8    Deserialize, Serialize,
9};
10
11use crate::{
12    shared::{
13        errors::{ConfigError, FieldError},
14        helpers::{generate_unique_para_id, merge_errors, merge_errors_vecs},
15        node::{self, GroupNodeConfig, GroupNodeConfigBuilder, NodeConfig, NodeConfigBuilder},
16        resources::{Resources, ResourcesBuilder},
17        types::{
18            Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext, U128,
19        },
20    },
21    types::{ChainSpecRuntime, CommandWithCustomArgs, JsonOverrides},
22    utils::{default_as_false, default_as_true, default_initial_balance, is_false},
23};
24
25/// The registration strategy that will be used for the parachain.
26#[derive(Debug, Clone, PartialEq)]
27pub enum RegistrationStrategy {
28    /// The parachain will be added to the genesis before spawning.
29    InGenesis,
30    /// The parachain will be registered using an extrinsic after spawning.
31    UsingExtrinsic,
32    /// The parachaing will not be registered and the user can doit after spawning manually.
33    Manual,
34}
35
36impl Serialize for RegistrationStrategy {
37    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
38    where
39        S: serde::Serializer,
40    {
41        let mut state = serializer.serialize_struct("RegistrationStrategy", 1)?;
42
43        match self {
44            Self::InGenesis => state.serialize_field("add_to_genesis", &true)?,
45            Self::UsingExtrinsic => state.serialize_field("register_para", &true)?,
46            Self::Manual => {
47                state.serialize_field("add_to_genesis", &false)?;
48                state.serialize_field("register_para", &false)?;
49            },
50        }
51
52        state.end()
53    }
54}
55
56struct RegistrationStrategyVisitor;
57
58impl<'de> Visitor<'de> for RegistrationStrategyVisitor {
59    type Value = RegistrationStrategy;
60
61    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
62        formatter.write_str("struct RegistrationStrategy")
63    }
64
65    fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
66    where
67        A: serde::de::MapAccess<'de>,
68    {
69        let mut add_to_genesis = false;
70        let mut register_para = false;
71
72        while let Some(key) = map.next_key::<String>()? {
73            match key.as_str() {
74                "addToGenesis" | "add_to_genesis" => add_to_genesis = map.next_value()?,
75                "registerPara" | "register_para" => register_para = map.next_value()?,
76                _ => {
77                    return Err(de::Error::unknown_field(
78                        &key,
79                        &["add_to_genesis", "register_para"],
80                    ))
81                },
82            }
83        }
84
85        match (add_to_genesis, register_para) {
86            (true, false) => Ok(RegistrationStrategy::InGenesis),
87            (false, true) => Ok(RegistrationStrategy::UsingExtrinsic),
88            _ => Err(de::Error::missing_field("add_to_genesis or register_para")),
89        }
90    }
91}
92
93impl<'de> Deserialize<'de> for RegistrationStrategy {
94    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
95    where
96        D: serde::Deserializer<'de>,
97    {
98        deserializer.deserialize_struct(
99            "RegistrationStrategy",
100            &["add_to_genesis", "register_para"],
101            RegistrationStrategyVisitor,
102        )
103    }
104}
105
106/// A parachain configuration, composed of collators and fine-grained configuration options.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108pub struct ParachainConfig {
109    id: u32,
110    #[serde(skip)]
111    // unique_id is internally used to allow multiple parachains with the same id
112    // BUT, only one of them could be register automatically at spawn
113    unique_id: String,
114    chain: Option<Chain>,
115    #[serde(flatten)]
116    registration_strategy: Option<RegistrationStrategy>,
117    #[serde(
118        skip_serializing_if = "super::utils::is_true",
119        default = "default_as_true"
120    )]
121    onboard_as_parachain: bool,
122    /// Number of cores to assign
123    /// Only valid for Paras registered in genesis.
124    num_cores: Option<u32>,
125    #[serde(rename = "balance", default = "default_initial_balance")]
126    initial_balance: U128,
127    default_command: Option<Command>,
128    default_image: Option<Image>,
129    default_resources: Option<Resources>,
130    default_db_snapshot: Option<AssetLocation>,
131    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
132    default_args: Vec<Arg>,
133    genesis_wasm_path: Option<AssetLocation>,
134    genesis_wasm_generator: Option<Command>,
135    genesis_state_path: Option<AssetLocation>,
136    genesis_state_generator: Option<CommandWithCustomArgs>,
137    /// chain-spec to use (location can be url or file path)
138    chain_spec_path: Option<AssetLocation>,
139    /// runtime to use for generating the chain-spec.
140    /// Location can be url or file path and an optional preset
141    chain_spec_runtime: Option<ChainSpecRuntime>,
142    // Path or url to override the runtime (:code) in the chain-spec
143    wasm_override: Option<AssetLocation>,
144    // Full _template_ command, will be rendered using [tera]
145    // and executed for generate the chain-spec.
146    // available tokens {{chainName}} / {{disableBootnodes}}
147    chain_spec_command: Option<String>,
148    /// Optional post-process script to run after chain-spec generation for this parachain.
149    #[serde(skip_serializing_if = "Option::is_none")]
150    post_process_script: Option<String>,
151    // Does the chain_spec_command needs to be run locally
152    #[serde(skip_serializing_if = "is_false", default)]
153    chain_spec_command_is_local: bool,
154    // Path to the file where the `chain_spec_command` will write the chain-spec into.
155    // Defaults to /dev/stdout.
156    chain_spec_command_output_path: Option<String>,
157    #[serde(rename = "cumulus_based", default = "default_as_true")]
158    is_cumulus_based: bool,
159    #[serde(rename = "evm_based", default = "default_as_false")]
160    is_evm_based: bool,
161    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
162    bootnodes_addresses: Vec<Multiaddr>,
163    #[serde(skip_serializing_if = "is_false", default)]
164    no_default_bootnodes: bool,
165    #[serde(rename = "genesis", skip_serializing_if = "Option::is_none")]
166    genesis_overrides: Option<serde_json::Value>,
167    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
168    pub(crate) collators: Vec<NodeConfig>,
169    // Single collator config, added for backward compatibility
170    // with `toml` networks definitions from v1.
171    // This field can only be set loading an old `toml` definition
172    // with `[parachain.collator]` key.
173    // NOTE: if the file also contains multiple collators defined in
174    // `[[parachain.collators]], the single configuration will be added to the bottom.
175    pub(crate) collator: Option<NodeConfig>,
176    #[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
177    pub(crate) collator_groups: Vec<GroupNodeConfig>,
178    // Inline json or asset location to override raw chainspec
179    raw_spec_override: Option<JsonOverrides>,
180}
181
182impl ParachainConfig {
183    /// The parachain ID.
184    pub fn id(&self) -> u32 {
185        self.id
186    }
187
188    /// The parachain unique ID.
189    pub fn unique_id(&self) -> &str {
190        &self.unique_id
191    }
192
193    /// The chain name.
194    pub fn chain(&self) -> Option<&Chain> {
195        self.chain.as_ref()
196    }
197
198    /// The registration strategy for the parachain.
199    pub fn registration_strategy(&self) -> Option<&RegistrationStrategy> {
200        self.registration_strategy.as_ref()
201    }
202
203    /// Whether the parachain should be onboarded or stay a parathread
204    pub fn onboard_as_parachain(&self) -> bool {
205        self.onboard_as_parachain
206    }
207
208    /// Number of cores to assign (1 assigned autatically at genesis)
209    pub fn num_cores(&self) -> Option<u32> {
210        self.num_cores
211    }
212
213    /// The initial balance of the parachain account.
214    pub fn initial_balance(&self) -> u128 {
215        self.initial_balance.0
216    }
217
218    /// The default command used for collators.
219    pub fn default_command(&self) -> Option<&Command> {
220        self.default_command.as_ref()
221    }
222
223    /// The default container image used for collators.
224    pub fn default_image(&self) -> Option<&Image> {
225        self.default_image.as_ref()
226    }
227
228    /// The default resources limits used for collators.
229    pub fn default_resources(&self) -> Option<&Resources> {
230        self.default_resources.as_ref()
231    }
232
233    /// The default database snapshot location that will be used for state.
234    pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
235        self.default_db_snapshot.as_ref()
236    }
237
238    /// The default arguments that will be used to execute the collator command.
239    pub fn default_args(&self) -> Vec<&Arg> {
240        self.default_args.iter().collect::<Vec<&Arg>>()
241    }
242
243    /// The location of a pre-existing genesis WASM runtime blob of the parachain.
244    pub fn genesis_wasm_path(&self) -> Option<&AssetLocation> {
245        self.genesis_wasm_path.as_ref()
246    }
247
248    /// The generator command used to create the genesis WASM runtime blob of the parachain.
249    pub fn genesis_wasm_generator(&self) -> Option<&Command> {
250        self.genesis_wasm_generator.as_ref()
251    }
252
253    /// The location of a pre-existing genesis state of the parachain.
254    pub fn genesis_state_path(&self) -> Option<&AssetLocation> {
255        self.genesis_state_path.as_ref()
256    }
257
258    /// The generator command used to create the genesis state of the parachain.
259    pub fn genesis_state_generator(&self) -> Option<&CommandWithCustomArgs> {
260        self.genesis_state_generator.as_ref()
261    }
262
263    /// The genesis overrides as a JSON value.
264    pub fn genesis_overrides(&self) -> Option<&serde_json::Value> {
265        self.genesis_overrides.as_ref()
266    }
267
268    /// The location of a pre-existing chain specification for the parachain.
269    pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
270        self.chain_spec_path.as_ref()
271    }
272
273    /// The full _template_ command to genera the chain-spec
274    pub fn chain_spec_command(&self) -> Option<&str> {
275        self.chain_spec_command.as_deref()
276    }
277
278    /// Does the chain_spec_command needs to be run locally
279    pub fn chain_spec_command_is_local(&self) -> bool {
280        self.chain_spec_command_is_local
281    }
282
283    /// The file where the `chain_spec_command` will write the chain-spec into.
284    /// Defaults to /dev/stdout.
285    pub fn chain_spec_command_output_path(&self) -> Option<&str> {
286        self.chain_spec_command_output_path.as_deref()
287    }
288
289    /// Optional post-process script to run after chain-spec generation for this parachain.
290    pub fn post_process_script(&self) -> Option<&str> {
291        self.post_process_script.as_deref()
292    }
293
294    /// Whether the parachain is based on cumulus.
295    pub fn is_cumulus_based(&self) -> bool {
296        self.is_cumulus_based
297    }
298
299    /// Whether the parachain is evm based (e.g frontier).
300    pub fn is_evm_based(&self) -> bool {
301        self.is_evm_based
302    }
303
304    /// The bootnodes addresses the collators will connect to.
305    pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
306        self.bootnodes_addresses.iter().collect::<Vec<_>>()
307    }
308
309    /// Whether to not automatically assign a bootnode role if none of the nodes are marked
310    /// as bootnodes.
311    pub fn no_default_bootnodes(&self) -> bool {
312        self.no_default_bootnodes
313    }
314
315    /// The collators of the parachain.
316    pub fn collators(&self) -> Vec<&NodeConfig> {
317        let mut cols = self.collators.iter().collect::<Vec<_>>();
318        if let Some(col) = self.collator.as_ref() {
319            cols.push(col);
320        }
321        cols
322    }
323
324    /// The grouped collators of the parachain.
325    pub fn group_collators_configs(&self) -> Vec<&GroupNodeConfig> {
326        self.collator_groups.iter().collect::<Vec<_>>()
327    }
328
329    /// The location of a wasm runtime to override in the chain-spec.
330    pub fn wasm_override(&self) -> Option<&AssetLocation> {
331        self.wasm_override.as_ref()
332    }
333
334    /// The location of a file or inline json to override raw chain-spec.
335    pub fn raw_spec_override(&self) -> Option<&JsonOverrides> {
336        self.raw_spec_override.as_ref()
337    }
338
339    /// The location of runtime to use by chain-spec builder lib (from `sc-chain-spec` crate)
340    pub fn chain_spec_runtime(&self) -> Option<&ChainSpecRuntime> {
341        self.chain_spec_runtime.as_ref()
342    }
343}
344
345pub mod states {
346    use crate::shared::macros::states;
347
348    states! {
349        Initial,
350        WithId,
351        WithAtLeastOneCollator
352    }
353
354    states! {
355        Bootstrap,
356        Running
357    }
358
359    pub trait Context {}
360    impl Context for Bootstrap {}
361    impl Context for Running {}
362}
363
364use states::{Bootstrap, Context, Initial, Running, WithAtLeastOneCollator, WithId};
365/// A parachain configuration builder, used to build a [`ParachainConfig`] declaratively with fields validation.
366pub struct ParachainConfigBuilder<S, C> {
367    config: ParachainConfig,
368    validation_context: Rc<RefCell<ValidationContext>>,
369    errors: Vec<anyhow::Error>,
370    _state: PhantomData<S>,
371    _context: PhantomData<C>,
372}
373
374impl<C: Context> Default for ParachainConfigBuilder<Initial, C> {
375    fn default() -> Self {
376        Self {
377            config: ParachainConfig {
378                id: 100,
379                unique_id: String::from("100"),
380                chain: None,
381                registration_strategy: Some(RegistrationStrategy::InGenesis),
382                onboard_as_parachain: true,
383                num_cores: None,
384                initial_balance: 2_000_000_000_000.into(),
385                default_command: None,
386                default_image: None,
387                default_resources: None,
388                default_db_snapshot: None,
389                default_args: vec![],
390                genesis_wasm_path: None,
391                genesis_wasm_generator: None,
392                genesis_state_path: None,
393                genesis_state_generator: None,
394                genesis_overrides: None,
395                chain_spec_path: None,
396                chain_spec_runtime: None,
397                chain_spec_command: None,
398                post_process_script: None,
399                chain_spec_command_output_path: None,
400                wasm_override: None,
401                chain_spec_command_is_local: false, // remote by default
402                is_cumulus_based: true,
403                is_evm_based: false,
404                bootnodes_addresses: vec![],
405                no_default_bootnodes: false,
406                collators: vec![],
407                collator: None,
408                collator_groups: vec![],
409                raw_spec_override: None,
410            },
411            validation_context: Default::default(),
412            errors: vec![],
413            _state: PhantomData,
414            _context: PhantomData,
415        }
416    }
417}
418
419impl<A, C> ParachainConfigBuilder<A, C> {
420    fn transition<B>(
421        config: ParachainConfig,
422        validation_context: Rc<RefCell<ValidationContext>>,
423        errors: Vec<anyhow::Error>,
424    ) -> ParachainConfigBuilder<B, C> {
425        ParachainConfigBuilder {
426            config,
427            validation_context,
428            errors,
429            _state: PhantomData,
430            _context: PhantomData,
431        }
432    }
433
434    fn default_chain_context(&self) -> ChainDefaultContext {
435        ChainDefaultContext {
436            default_command: self.config.default_command.clone(),
437            default_image: self.config.default_image.clone(),
438            default_resources: self.config.default_resources.clone(),
439            default_db_snapshot: self.config.default_db_snapshot.clone(),
440            default_args: self.config.default_args.clone(),
441        }
442    }
443
444    fn create_node_builder<F>(&self, f: F) -> NodeConfigBuilder<node::Buildable>
445    where
446        F: FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
447    {
448        f(NodeConfigBuilder::new(
449            self.default_chain_context(),
450            self.validation_context.clone(),
451        ))
452    }
453
454    /// Set an optional post-process script to run after chain-spec generation for this parachain.
455    pub fn with_post_process_script(mut self, script: impl Into<String>) -> Self {
456        self.config.post_process_script = Some(script.into());
457        self
458    }
459}
460
461impl ParachainConfigBuilder<Initial, Bootstrap> {
462    /// Instantiate a new builder that can be used to build a [`ParachainConfig`] during the bootstrap phase.
463    pub fn new(
464        validation_context: Rc<RefCell<ValidationContext>>,
465    ) -> ParachainConfigBuilder<Initial, Bootstrap> {
466        Self {
467            validation_context,
468            ..Self::default()
469        }
470    }
471}
472
473impl ParachainConfigBuilder<WithId, Bootstrap> {
474    /// Set the registration strategy for the parachain, could be Manual (no registered by zombienet) or automatic
475    /// using an extrinsic or in genesis.
476    pub fn with_registration_strategy(self, strategy: RegistrationStrategy) -> Self {
477        Self::transition(
478            ParachainConfig {
479                registration_strategy: Some(strategy),
480                ..self.config
481            },
482            self.validation_context,
483            self.errors,
484        )
485    }
486}
487
488impl ParachainConfigBuilder<WithId, Running> {
489    /// Set the registration strategy for the parachain, could be Manual (no registered by zombienet) or automatic
490    /// Using an extrinsic. Genesis option is not allowed in `Running` context.
491    pub fn with_registration_strategy(self, strategy: RegistrationStrategy) -> Self {
492        match strategy {
493            RegistrationStrategy::InGenesis => Self::transition(
494                self.config,
495                self.validation_context,
496                merge_errors(
497                    self.errors,
498                    FieldError::RegistrationStrategy(anyhow!(
499                        "Can be set to InGenesis in Running context"
500                    ))
501                    .into(),
502                ),
503            ),
504            RegistrationStrategy::Manual | RegistrationStrategy::UsingExtrinsic => {
505                Self::transition(
506                    ParachainConfig {
507                        registration_strategy: Some(strategy),
508                        ..self.config
509                    },
510                    self.validation_context,
511                    self.errors,
512                )
513            },
514        }
515    }
516}
517
518impl ParachainConfigBuilder<Initial, Running> {
519    /// Start a new builder in the context of a running network
520    pub fn new_with_running(
521        validation_context: Rc<RefCell<ValidationContext>>,
522    ) -> ParachainConfigBuilder<Initial, Running> {
523        let mut builder = Self {
524            validation_context,
525            ..Self::default()
526        };
527
528        // override the registration strategy
529        builder.config.registration_strategy = Some(RegistrationStrategy::UsingExtrinsic);
530        builder
531    }
532}
533
534impl<C: Context> ParachainConfigBuilder<Initial, C> {
535    /// Set the parachain ID and the unique_id (with the suffix `<para_id>-x` if the id is already used)
536    pub fn with_id(self, id: u32) -> ParachainConfigBuilder<WithId, C> {
537        let unique_id = generate_unique_para_id(id, self.validation_context.clone());
538        Self::transition(
539            ParachainConfig {
540                id,
541                unique_id,
542                ..self.config
543            },
544            self.validation_context,
545            self.errors,
546        )
547    }
548}
549
550impl<C: Context> ParachainConfigBuilder<WithId, C> {
551    /// Set the chain name (e.g. rococo-local).
552    /// Use [`None`], if you are running adder-collator or undying-collator).
553    pub fn with_chain<T>(self, chain: T) -> Self
554    where
555        T: TryInto<Chain>,
556        T::Error: Error + Send + Sync + 'static,
557    {
558        match chain.try_into() {
559            Ok(chain) => Self::transition(
560                ParachainConfig {
561                    chain: Some(chain),
562                    ..self.config
563                },
564                self.validation_context,
565                self.errors,
566            ),
567            Err(error) => Self::transition(
568                self.config,
569                self.validation_context,
570                merge_errors(self.errors, FieldError::Chain(error.into()).into()),
571            ),
572        }
573    }
574
575    /// Set whether the parachain should be onboarded or stay a parathread. Default is ```true```.
576    pub fn onboard_as_parachain(self, choice: bool) -> Self {
577        Self::transition(
578            ParachainConfig {
579                onboard_as_parachain: choice,
580                ..self.config
581            },
582            self.validation_context,
583            self.errors,
584        )
585    }
586
587    /// Set the number of cores to assign at genesis
588    /// NOTE: 1 is the default
589    pub fn with_num_cores(self, cores: u32) -> Self {
590        Self::transition(
591            ParachainConfig {
592                num_cores: Some(cores),
593                ..self.config
594            },
595            self.validation_context,
596            self.errors,
597        )
598    }
599
600    /// Set the initial balance of the parachain account.
601    pub fn with_initial_balance(self, initial_balance: u128) -> Self {
602        Self::transition(
603            ParachainConfig {
604                initial_balance: initial_balance.into(),
605                ..self.config
606            },
607            self.validation_context,
608            self.errors,
609        )
610    }
611
612    /// Set the default command used for collators. Can be overridden.
613    pub fn with_default_command<T>(self, command: T) -> Self
614    where
615        T: TryInto<Command>,
616        T::Error: Error + Send + Sync + 'static,
617    {
618        match command.try_into() {
619            Ok(command) => Self::transition(
620                ParachainConfig {
621                    default_command: Some(command),
622                    ..self.config
623                },
624                self.validation_context,
625                self.errors,
626            ),
627            Err(error) => Self::transition(
628                self.config,
629                self.validation_context,
630                merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
631            ),
632        }
633    }
634
635    /// Set the default container image used for collators. Can be overridden.
636    pub fn with_default_image<T>(self, image: T) -> Self
637    where
638        T: TryInto<Image>,
639        T::Error: Error + Send + Sync + 'static,
640    {
641        match image.try_into() {
642            Ok(image) => Self::transition(
643                ParachainConfig {
644                    default_image: Some(image),
645                    ..self.config
646                },
647                self.validation_context,
648                self.errors,
649            ),
650            Err(error) => Self::transition(
651                self.config,
652                self.validation_context,
653                merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
654            ),
655        }
656    }
657
658    /// Set the default resources limits used for collators. Can be overridden.
659    pub fn with_default_resources(
660        self,
661        f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
662    ) -> Self {
663        match f(ResourcesBuilder::new()).build() {
664            Ok(default_resources) => Self::transition(
665                ParachainConfig {
666                    default_resources: Some(default_resources),
667                    ..self.config
668                },
669                self.validation_context,
670                self.errors,
671            ),
672            Err(errors) => Self::transition(
673                self.config,
674                self.validation_context,
675                merge_errors_vecs(
676                    self.errors,
677                    errors
678                        .into_iter()
679                        .map(|error| FieldError::DefaultResources(error).into())
680                        .collect::<Vec<_>>(),
681                ),
682            ),
683        }
684    }
685
686    /// Set the default database snapshot location that will be used for state. Can be overridden.
687    pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
688        Self::transition(
689            ParachainConfig {
690                default_db_snapshot: Some(location.into()),
691                ..self.config
692            },
693            self.validation_context,
694            self.errors,
695        )
696    }
697
698    /// Set the default arguments that will be used to execute the collator command. Can be overridden.
699    pub fn with_default_args(self, args: Vec<Arg>) -> Self {
700        Self::transition(
701            ParachainConfig {
702                default_args: args,
703                ..self.config
704            },
705            self.validation_context,
706            self.errors,
707        )
708    }
709
710    /// Set the location of a pre-existing genesis WASM runtime blob of the parachain.
711    pub fn with_genesis_wasm_path(self, location: impl Into<AssetLocation>) -> Self {
712        Self::transition(
713            ParachainConfig {
714                genesis_wasm_path: Some(location.into()),
715                ..self.config
716            },
717            self.validation_context,
718            self.errors,
719        )
720    }
721
722    /// Set the generator command used to create the genesis WASM runtime blob of the parachain.
723    pub fn with_genesis_wasm_generator<T>(self, command: T) -> Self
724    where
725        T: TryInto<Command>,
726        T::Error: Error + Send + Sync + 'static,
727    {
728        match command.try_into() {
729            Ok(command) => Self::transition(
730                ParachainConfig {
731                    genesis_wasm_generator: Some(command),
732                    ..self.config
733                },
734                self.validation_context,
735                self.errors,
736            ),
737            Err(error) => Self::transition(
738                self.config,
739                self.validation_context,
740                merge_errors(
741                    self.errors,
742                    FieldError::GenesisWasmGenerator(error.into()).into(),
743                ),
744            ),
745        }
746    }
747
748    /// Set the location of a pre-existing genesis state of the parachain.
749    pub fn with_genesis_state_path(self, location: impl Into<AssetLocation>) -> Self {
750        Self::transition(
751            ParachainConfig {
752                genesis_state_path: Some(location.into()),
753                ..self.config
754            },
755            self.validation_context,
756            self.errors,
757        )
758    }
759
760    /// Set the generator command used to create the genesis state of the parachain.
761    pub fn with_genesis_state_generator<T>(self, command: T) -> Self
762    where
763        T: TryInto<CommandWithCustomArgs>,
764        T::Error: Error + Send + Sync + 'static,
765    {
766        match command.try_into() {
767            Ok(command) => Self::transition(
768                ParachainConfig {
769                    genesis_state_generator: Some(command),
770                    ..self.config
771                },
772                self.validation_context,
773                self.errors,
774            ),
775            Err(error) => Self::transition(
776                self.config,
777                self.validation_context,
778                merge_errors(
779                    self.errors,
780                    FieldError::GenesisStateGenerator(error.into()).into(),
781                ),
782            ),
783        }
784    }
785
786    /// Set the genesis overrides as a JSON object.
787    pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
788        Self::transition(
789            ParachainConfig {
790                genesis_overrides: Some(genesis_overrides.into()),
791                ..self.config
792            },
793            self.validation_context,
794            self.errors,
795        )
796    }
797
798    /// Set the location of a pre-existing chain specification for the parachain.
799    pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
800        Self::transition(
801            ParachainConfig {
802                chain_spec_path: Some(location.into()),
803                ..self.config
804            },
805            self.validation_context,
806            self.errors,
807        )
808    }
809
810    /// Set the chain-spec command _template_ for the relay chain.
811    pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
812        Self::transition(
813            ParachainConfig {
814                chain_spec_command: Some(cmd_template.into()),
815                ..self.config
816            },
817            self.validation_context,
818            self.errors,
819        )
820    }
821
822    /// Set the runtime path to use for generating the chain-spec and an optiona preset.
823    /// If the preset is not set, we will try to match [`local_testnet`, `development`, `dev`]
824    /// with the available ones and fallback to the default configuration as last option.
825    pub fn with_chain_spec_runtime(
826        self,
827        location: impl Into<AssetLocation>,
828        preset: Option<&str>,
829    ) -> Self {
830        let chain_spec_runtime = if let Some(preset) = preset {
831            ChainSpecRuntime::with_preset(location.into(), preset.to_string())
832        } else {
833            ChainSpecRuntime::new(location.into())
834        };
835        Self::transition(
836            ParachainConfig {
837                chain_spec_runtime: Some(chain_spec_runtime),
838                ..self.config
839            },
840            self.validation_context,
841            self.errors,
842        )
843    }
844
845    /// Set the location of a wasm to override the chain-spec.
846    pub fn with_wasm_override(self, location: impl Into<AssetLocation>) -> Self {
847        Self::transition(
848            ParachainConfig {
849                wasm_override: Some(location.into()),
850                ..self.config
851            },
852            self.validation_context,
853            self.errors,
854        )
855    }
856
857    /// Set if the chain-spec command needs to be run locally or not (false by default)
858    pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
859        Self::transition(
860            ParachainConfig {
861                chain_spec_command_is_local: choice,
862                ..self.config
863            },
864            self.validation_context,
865            self.errors,
866        )
867    }
868
869    /// Set the output path for the chain-spec command.
870    pub fn with_chain_spec_command_output_path(self, output_path: &str) -> Self {
871        Self::transition(
872            ParachainConfig {
873                chain_spec_command_output_path: Some(output_path.to_string()),
874                ..self.config
875            },
876            self.validation_context,
877            self.errors,
878        )
879    }
880
881    /// Set whether the parachain is based on cumulus (true in a majority of case, except adder or undying collators).
882    pub fn cumulus_based(self, choice: bool) -> Self {
883        Self::transition(
884            ParachainConfig {
885                is_cumulus_based: choice,
886                ..self.config
887            },
888            self.validation_context,
889            self.errors,
890        )
891    }
892
893    /// Set whether the parachain is evm based (e.g frontier /evm template)
894    pub fn evm_based(self, choice: bool) -> Self {
895        Self::transition(
896            ParachainConfig {
897                is_evm_based: choice,
898                ..self.config
899            },
900            self.validation_context,
901            self.errors,
902        )
903    }
904
905    /// Set the bootnodes addresses the collators will connect to.
906    ///
907    /// Note: Bootnode address replacements are NOT supported here.
908    /// Only arguments (`args`) support dynamic replacements. Bootnode addresses must be a valid address.
909    pub fn with_raw_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
910    where
911        T: TryInto<Multiaddr> + Display + Copy,
912        T::Error: Error + Send + Sync + 'static,
913    {
914        let mut addrs = vec![];
915        let mut errors = vec![];
916
917        for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
918            match addr.try_into() {
919                Ok(addr) => addrs.push(addr),
920                Err(error) => errors.push(
921                    FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
922                ),
923            }
924        }
925
926        Self::transition(
927            ParachainConfig {
928                bootnodes_addresses: addrs,
929                ..self.config
930            },
931            self.validation_context,
932            merge_errors_vecs(self.errors, errors),
933        )
934    }
935
936    /// Do not assign a bootnode role automatically if no nodes are marked as bootnodes.
937    pub fn without_default_bootnodes(self) -> Self {
938        Self::transition(
939            ParachainConfig {
940                no_default_bootnodes: true,
941                ..self.config
942            },
943            self.validation_context,
944            self.errors,
945        )
946    }
947
948    /// Add a new collator using a nested [`NodeConfigBuilder`].
949    pub fn with_collator(
950        self,
951        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
952    ) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
953        match self.create_node_builder(f).validator(true).build() {
954            Ok(collator) => Self::transition(
955                ParachainConfig {
956                    collators: vec![collator],
957                    ..self.config
958                },
959                self.validation_context,
960                self.errors,
961            ),
962            Err((name, errors)) => Self::transition(
963                self.config,
964                self.validation_context,
965                merge_errors_vecs(
966                    self.errors,
967                    errors
968                        .into_iter()
969                        .map(|error| ConfigError::Collator(name.clone(), error).into())
970                        .collect::<Vec<_>>(),
971                ),
972            ),
973        }
974    }
975
976    /// Add a new full node using a nested [`NodeConfigBuilder`].
977    /// The node will be configured as a full node (non-validator).
978    pub fn with_fullnode(
979        self,
980        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
981    ) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
982        match self.create_node_builder(f).validator(false).build() {
983            Ok(node) => Self::transition(
984                ParachainConfig {
985                    collators: vec![node],
986                    ..self.config
987                },
988                self.validation_context,
989                self.errors,
990            ),
991            Err((name, errors)) => Self::transition(
992                self.config,
993                self.validation_context,
994                merge_errors_vecs(
995                    self.errors,
996                    errors
997                        .into_iter()
998                        .map(|error| ConfigError::Collator(name.clone(), error).into())
999                        .collect::<Vec<_>>(),
1000                ),
1001            ),
1002        }
1003    }
1004
1005    /// Add a new node using a nested [`NodeConfigBuilder`].
1006    ///
1007    /// **Deprecated**: Use [`with_collator`] for collator nodes or [`with_fullnode`] for full nodes instead.
1008    #[deprecated(
1009        since = "0.4.0",
1010        note = "Use `with_collator()` for collator nodes or `with_fullnode()` for full nodes instead"
1011    )]
1012    pub fn with_node(
1013        self,
1014        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
1015    ) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
1016        match self.create_node_builder(f).build() {
1017            Ok(node) => Self::transition(
1018                ParachainConfig {
1019                    collators: vec![node],
1020                    ..self.config
1021                },
1022                self.validation_context,
1023                self.errors,
1024            ),
1025            Err((name, errors)) => Self::transition(
1026                self.config,
1027                self.validation_context,
1028                merge_errors_vecs(
1029                    self.errors,
1030                    errors
1031                        .into_iter()
1032                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1033                        .collect::<Vec<_>>(),
1034                ),
1035            ),
1036        }
1037    }
1038
1039    /// Add a new collator group using a nested [`GroupNodeConfigBuilder`].
1040    pub fn with_collator_group(
1041        self,
1042        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
1043    ) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
1044        match f(GroupNodeConfigBuilder::new(
1045            self.default_chain_context(),
1046            self.validation_context.clone(),
1047        ))
1048        .build()
1049        {
1050            Ok(group) => Self::transition(
1051                ParachainConfig {
1052                    collator_groups: [self.config.collator_groups, vec![group]].concat(),
1053                    ..self.config
1054                },
1055                self.validation_context,
1056                self.errors,
1057            ),
1058            Err((name, errors)) => Self::transition(
1059                self.config,
1060                self.validation_context,
1061                merge_errors_vecs(
1062                    self.errors,
1063                    errors
1064                        .into_iter()
1065                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1066                        .collect::<Vec<_>>(),
1067                ),
1068            ),
1069        }
1070    }
1071
1072    /// Set the location or inline value of json to override the raw chain-spec.
1073    pub fn with_raw_spec_override(self, overrides: impl Into<JsonOverrides>) -> Self {
1074        Self::transition(
1075            ParachainConfig {
1076                raw_spec_override: Some(overrides.into()),
1077                ..self.config
1078            },
1079            self.validation_context,
1080            self.errors,
1081        )
1082    }
1083}
1084
1085impl<C: Context> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
1086    /// Add a new collator using a nested [`NodeConfigBuilder`].
1087    pub fn with_collator(
1088        self,
1089        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
1090    ) -> Self {
1091        match self.create_node_builder(f).validator(true).build() {
1092            Ok(collator) => Self::transition(
1093                ParachainConfig {
1094                    collators: [self.config.collators, vec![collator]].concat(),
1095                    ..self.config
1096                },
1097                self.validation_context,
1098                self.errors,
1099            ),
1100            Err((name, errors)) => Self::transition(
1101                self.config,
1102                self.validation_context,
1103                merge_errors_vecs(
1104                    self.errors,
1105                    errors
1106                        .into_iter()
1107                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1108                        .collect::<Vec<_>>(),
1109                ),
1110            ),
1111        }
1112    }
1113
1114    /// Add a new full node using a nested [`NodeConfigBuilder`].
1115    /// The node will be configured as a full node (non-validator).
1116    pub fn with_fullnode(
1117        self,
1118        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
1119    ) -> Self {
1120        match self.create_node_builder(f).validator(false).build() {
1121            Ok(node) => Self::transition(
1122                ParachainConfig {
1123                    collators: [self.config.collators, vec![node]].concat(),
1124                    ..self.config
1125                },
1126                self.validation_context,
1127                self.errors,
1128            ),
1129            Err((name, errors)) => Self::transition(
1130                self.config,
1131                self.validation_context,
1132                merge_errors_vecs(
1133                    self.errors,
1134                    errors
1135                        .into_iter()
1136                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1137                        .collect::<Vec<_>>(),
1138                ),
1139            ),
1140        }
1141    }
1142
1143    /// Add a new node using a nested [`NodeConfigBuilder`].
1144    ///
1145    /// **Deprecated**: Use [`with_collator`] for collator nodes or [`with_fullnode`] for full nodes instead.
1146    #[deprecated(
1147        since = "0.4.0",
1148        note = "Use `with_collator()` for collator nodes or `with_fullnode()` for full nodes instead"
1149    )]
1150    pub fn with_node(
1151        self,
1152        f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
1153    ) -> Self {
1154        match self.create_node_builder(f).build() {
1155            Ok(node) => Self::transition(
1156                ParachainConfig {
1157                    collators: [self.config.collators, vec![node]].concat(),
1158                    ..self.config
1159                },
1160                self.validation_context,
1161                self.errors,
1162            ),
1163            Err((name, errors)) => Self::transition(
1164                self.config,
1165                self.validation_context,
1166                merge_errors_vecs(
1167                    self.errors,
1168                    errors
1169                        .into_iter()
1170                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1171                        .collect::<Vec<_>>(),
1172                ),
1173            ),
1174        }
1175    }
1176
1177    /// Add a new collator group using a nested [`GroupNodeConfigBuilder`].
1178    pub fn with_collator_group(
1179        self,
1180        f: impl FnOnce(GroupNodeConfigBuilder<node::Initial>) -> GroupNodeConfigBuilder<node::Buildable>,
1181    ) -> Self {
1182        match f(GroupNodeConfigBuilder::new(
1183            self.default_chain_context(),
1184            self.validation_context.clone(),
1185        ))
1186        .build()
1187        {
1188            Ok(group) => Self::transition(
1189                ParachainConfig {
1190                    collator_groups: [self.config.collator_groups, vec![group]].concat(),
1191                    ..self.config
1192                },
1193                self.validation_context,
1194                self.errors,
1195            ),
1196            Err((name, errors)) => Self::transition(
1197                self.config,
1198                self.validation_context,
1199                merge_errors_vecs(
1200                    self.errors,
1201                    errors
1202                        .into_iter()
1203                        .map(|error| ConfigError::Collator(name.clone(), error).into())
1204                        .collect::<Vec<_>>(),
1205                ),
1206            ),
1207        }
1208    }
1209
1210    /// Seals the builder and returns a [`ParachainConfig`] if there are no validation errors, else returns errors.
1211    pub fn build(self) -> Result<ParachainConfig, Vec<anyhow::Error>> {
1212        if !self.errors.is_empty() {
1213            return Err(self
1214                .errors
1215                .into_iter()
1216                .map(|error| ConfigError::Parachain(self.config.id, error).into())
1217                .collect::<Vec<_>>());
1218        }
1219
1220        Ok(self.config)
1221    }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227    use crate::NetworkConfig;
1228
1229    #[test]
1230    fn parachain_config_builder_should_succeeds_and_returns_a_new_parachain_config() {
1231        let parachain_config = ParachainConfigBuilder::new(Default::default())
1232            .with_id(1000)
1233            .with_chain("mychainname")
1234            .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1235            .with_num_cores(3)
1236            .onboard_as_parachain(false)
1237            .with_initial_balance(100_000_042)
1238            .with_default_image("myrepo:myimage")
1239            .with_default_command("default_command")
1240            .with_default_resources(|resources| {
1241                resources
1242                    .with_limit_cpu("500M")
1243                    .with_limit_memory("1G")
1244                    .with_request_cpu("250M")
1245            })
1246            .with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
1247            .with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
1248            .with_genesis_wasm_path("https://www.backupsite.com/my/wasm/file.tgz")
1249            .with_genesis_wasm_generator("generator_wasm")
1250            .with_genesis_state_path("./path/to/genesis/state")
1251            .with_genesis_state_generator(
1252                "undying-collator export-genesis-state --pov-size=10000 --pvf-complexity=1",
1253            )
1254            .with_chain_spec_path("./path/to/chain/spec.json")
1255            .with_chain_spec_runtime("./path/to/runtime.wasm", Some("dev"))
1256            .with_wasm_override("./path/to/override/runtime.wasm")
1257            .with_raw_spec_override("./path/to/override/rawspec.json")
1258            .cumulus_based(false)
1259            .evm_based(false)
1260            .with_raw_bootnodes_addresses(vec![
1261                "/ip4/10.41.122.55/tcp/45421",
1262                "/ip4/51.144.222.10/tcp/2333",
1263            ])
1264            .without_default_bootnodes()
1265            .with_collator(|collator| {
1266                collator
1267                    .with_name("collator1")
1268                    .with_command("command1")
1269                    .bootnode(true)
1270            })
1271            .with_collator(|collator| {
1272                collator
1273                    .with_name("collator2")
1274                    .with_command("command2")
1275                    .validator(true)
1276            })
1277            .build()
1278            .unwrap();
1279
1280        assert_eq!(parachain_config.id(), 1000);
1281        assert_eq!(parachain_config.num_cores(), Some(3));
1282        assert_eq!(parachain_config.collators().len(), 2);
1283        let &collator1 = parachain_config.collators().first().unwrap();
1284        assert_eq!(collator1.name(), "collator1");
1285        assert_eq!(collator1.command().unwrap().as_str(), "command1");
1286        assert!(collator1.is_bootnode());
1287        let &collator2 = parachain_config.collators().last().unwrap();
1288        assert_eq!(collator2.name(), "collator2");
1289        assert_eq!(collator2.command().unwrap().as_str(), "command2");
1290        assert!(collator2.is_validator());
1291        assert_eq!(parachain_config.chain().unwrap().as_str(), "mychainname");
1292
1293        assert_eq!(
1294            parachain_config.registration_strategy().unwrap(),
1295            &RegistrationStrategy::UsingExtrinsic
1296        );
1297        assert!(!parachain_config.onboard_as_parachain());
1298        assert_eq!(parachain_config.initial_balance(), 100_000_042);
1299        assert_eq!(
1300            parachain_config.default_command().unwrap().as_str(),
1301            "default_command"
1302        );
1303        assert_eq!(
1304            parachain_config.default_image().unwrap().as_str(),
1305            "myrepo:myimage"
1306        );
1307        let default_resources = parachain_config.default_resources().unwrap();
1308        assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
1309        assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
1310        assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
1311        assert!(matches!(
1312            parachain_config.default_db_snapshot().unwrap(),
1313            AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
1314        ));
1315        assert!(matches!(
1316            parachain_config.chain_spec_path().unwrap(),
1317            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
1318        ));
1319        assert!(matches!(
1320            parachain_config.wasm_override().unwrap(),
1321            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/override/runtime.wasm"
1322        ));
1323        assert!(matches!(
1324            &parachain_config.chain_spec_runtime().unwrap().location,
1325            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/runtime.wasm"
1326        ));
1327        assert_eq!(
1328            parachain_config
1329                .chain_spec_runtime()
1330                .unwrap()
1331                .preset
1332                .as_deref(),
1333            Some("dev")
1334        );
1335
1336        let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
1337        assert_eq!(
1338            parachain_config.default_args(),
1339            args.iter().collect::<Vec<_>>()
1340        );
1341        assert!(matches!(
1342            parachain_config.genesis_wasm_path().unwrap(),
1343            AssetLocation::Url(value) if value.as_str() == "https://www.backupsite.com/my/wasm/file.tgz"
1344        ));
1345        assert_eq!(
1346            parachain_config.genesis_wasm_generator().unwrap().as_str(),
1347            "generator_wasm"
1348        );
1349        assert!(matches!(
1350            parachain_config.genesis_state_path().unwrap(),
1351            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/genesis/state"
1352        ));
1353        assert_eq!(
1354            parachain_config
1355                .genesis_state_generator()
1356                .unwrap()
1357                .cmd()
1358                .as_str(),
1359            "undying-collator"
1360        );
1361
1362        assert_eq!(
1363            parachain_config.genesis_state_generator().unwrap().args(),
1364            &vec![
1365                "export-genesis-state".into(),
1366                ("--pov-size", "10000").into(),
1367                ("--pvf-complexity", "1").into()
1368            ]
1369        );
1370
1371        assert!(matches!(
1372            parachain_config.chain_spec_path().unwrap(),
1373            AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
1374        ));
1375        assert!(!parachain_config.is_cumulus_based());
1376        let bootnodes_addresses: Vec<Multiaddr> = vec![
1377            "/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
1378            "/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
1379        ];
1380        assert!(parachain_config.no_default_bootnodes());
1381        assert_eq!(
1382            parachain_config.bootnodes_addresses(),
1383            bootnodes_addresses.iter().collect::<Vec<_>>()
1384        );
1385        assert!(!parachain_config.is_evm_based());
1386        assert!(matches!(
1387            parachain_config.raw_spec_override().unwrap(),
1388            JsonOverrides::Location(AssetLocation::FilePath(value)) if value.to_str().unwrap() == "./path/to/override/rawspec.json"
1389        ));
1390    }
1391
1392    #[test]
1393    fn parachain_config_builder_should_works_when_genesis_state_generator_contains_args() {
1394        let parachain_config = ParachainConfigBuilder::new(Default::default())
1395            .with_id(1000)
1396            .with_chain("myparachain")
1397            .with_genesis_state_generator("generator_state --simple-flag --flag=value")
1398            .with_collator(|collator| {
1399                collator
1400                    .with_name("collator")
1401                    .with_command("command")
1402                    .validator(true)
1403            })
1404            .build()
1405            .unwrap();
1406
1407        assert_eq!(
1408            parachain_config
1409                .genesis_state_generator()
1410                .unwrap()
1411                .cmd()
1412                .as_str(),
1413            "generator_state"
1414        );
1415
1416        assert_eq!(
1417            parachain_config
1418                .genesis_state_generator()
1419                .unwrap()
1420                .args()
1421                .len(),
1422            2
1423        );
1424
1425        let args = parachain_config.genesis_state_generator().unwrap().args();
1426
1427        assert_eq!(
1428            args,
1429            &vec![
1430                Arg::Flag("--simple-flag".into()),
1431                Arg::Option("--flag".into(), "value".into())
1432            ]
1433        );
1434    }
1435
1436    #[test]
1437    fn parachain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
1438        let errors = ParachainConfigBuilder::new(Default::default())
1439            .with_id(1000)
1440            .with_chain("invalid chain")
1441            .with_collator(|collator| {
1442                collator
1443                    .with_name("collator")
1444                    .with_command("command")
1445                    .validator(true)
1446            })
1447            .build()
1448            .unwrap_err();
1449
1450        assert_eq!(errors.len(), 1);
1451        assert_eq!(
1452            errors.first().unwrap().to_string(),
1453            "parachain[1000].chain: 'invalid chain' shouldn't contains whitespace"
1454        );
1455    }
1456
1457    #[test]
1458    fn parachain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
1459        let errors = ParachainConfigBuilder::new(Default::default())
1460            .with_id(1000)
1461            .with_chain("chain")
1462            .with_default_command("invalid command")
1463            .with_collator(|collator| {
1464                collator
1465                    .with_name("node")
1466                    .with_command("command")
1467                    .validator(true)
1468            })
1469            .build()
1470            .unwrap_err();
1471
1472        assert_eq!(errors.len(), 1);
1473        assert_eq!(
1474            errors.first().unwrap().to_string(),
1475            "parachain[1000].default_command: 'invalid command' shouldn't contains whitespace"
1476        );
1477    }
1478
1479    #[test]
1480    fn parachain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
1481        let errors = ParachainConfigBuilder::new(Default::default())
1482            .with_id(1000)
1483            .with_chain("chain")
1484            .with_default_image("invalid image")
1485            .with_collator(|collator| {
1486                collator
1487                    .with_name("node")
1488                    .with_command("command")
1489                    .validator(true)
1490            })
1491            .build()
1492            .unwrap_err();
1493
1494        assert_eq!(errors.len(), 1);
1495        assert_eq!(
1496            errors.first().unwrap().to_string(),
1497            r"parachain[1000].default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1498        );
1499    }
1500
1501    #[test]
1502    fn parachain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid()
1503    {
1504        let errors = ParachainConfigBuilder::new(Default::default())
1505            .with_id(1000)
1506            .with_chain("chain")
1507            .with_default_resources(|default_resources| {
1508                default_resources
1509                    .with_limit_memory("100m")
1510                    .with_request_cpu("invalid")
1511            })
1512            .with_collator(|collator| {
1513                collator
1514                    .with_name("node")
1515                    .with_command("command")
1516                    .validator(true)
1517            })
1518            .build()
1519            .unwrap_err();
1520
1521        assert_eq!(errors.len(), 1);
1522        assert_eq!(
1523            errors.first().unwrap().to_string(),
1524            r"parachain[1000].default_resources.request_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'"
1525        );
1526    }
1527
1528    #[test]
1529    fn parachain_config_builder_should_fails_and_returns_an_error_if_genesis_wasm_generator_is_invalid(
1530    ) {
1531        let errors = ParachainConfigBuilder::new(Default::default())
1532            .with_id(2000)
1533            .with_chain("myparachain")
1534            .with_genesis_wasm_generator("invalid command")
1535            .with_collator(|collator| {
1536                collator
1537                    .with_name("collator")
1538                    .with_command("command")
1539                    .validator(true)
1540            })
1541            .build()
1542            .unwrap_err();
1543
1544        assert_eq!(errors.len(), 1);
1545        assert_eq!(
1546            errors.first().unwrap().to_string(),
1547            "parachain[2000].genesis_wasm_generator: 'invalid command' shouldn't contains whitespace"
1548        );
1549    }
1550
1551    #[test]
1552    fn parachain_config_builder_should_fails_and_returns_an_error_if_bootnodes_addresses_are_invalid(
1553    ) {
1554        let errors = ParachainConfigBuilder::new(Default::default())
1555            .with_id(2000)
1556            .with_chain("myparachain")
1557            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1558            .with_collator(|collator| {
1559                collator
1560                    .with_name("collator")
1561                    .with_command("command")
1562                    .validator(true)
1563            })
1564            .build()
1565            .unwrap_err();
1566
1567        assert_eq!(errors.len(), 2);
1568        assert_eq!(
1569            errors.first().unwrap().to_string(),
1570            "parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1571        );
1572        assert_eq!(
1573            errors.get(1).unwrap().to_string(),
1574            "parachain[2000].bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1575        );
1576    }
1577
1578    #[test]
1579    fn parachain_config_builder_should_fails_and_returns_an_error_if_first_collator_is_invalid() {
1580        let errors = ParachainConfigBuilder::new(Default::default())
1581            .with_id(1000)
1582            .with_chain("myparachain")
1583            .with_collator(|collator| {
1584                collator
1585                    .with_name("collator")
1586                    .with_command("invalid command")
1587            })
1588            .build()
1589            .unwrap_err();
1590
1591        assert_eq!(errors.len(), 1);
1592        assert_eq!(
1593            errors.first().unwrap().to_string(),
1594            "parachain[1000].collators['collator'].command: 'invalid command' shouldn't contains whitespace"
1595        );
1596    }
1597
1598    #[test]
1599    fn parachain_config_builder_with_at_least_one_collator_should_fails_and_returns_an_error_if_second_collator_is_invalid(
1600    ) {
1601        let errors = ParachainConfigBuilder::new(Default::default())
1602            .with_id(2000)
1603            .with_chain("myparachain")
1604            .with_collator(|collator| {
1605                collator
1606                    .with_name("collator1")
1607                    .with_command("command1")
1608                    .invulnerable(true)
1609                    .bootnode(true)
1610            })
1611            .with_collator(|collator| {
1612                collator
1613                    .with_name("collator2")
1614                    .with_command("invalid command")
1615                    .with_initial_balance(20000000)
1616            })
1617            .build()
1618            .unwrap_err();
1619
1620        assert_eq!(errors.len(), 1);
1621        assert_eq!(
1622            errors.first().unwrap().to_string(),
1623            "parachain[2000].collators['collator2'].command: 'invalid command' shouldn't contains whitespace"
1624        );
1625    }
1626
1627    #[test]
1628    fn parachain_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
1629    ) {
1630        let errors = ParachainConfigBuilder::new(Default::default())
1631            .with_id(2000)
1632            .with_chain("myparachain")
1633            .with_raw_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
1634            .with_collator(|collator| {
1635                collator
1636                    .with_name("collator1")
1637                    .with_command("invalid command")
1638                    .invulnerable(true)
1639                    .bootnode(true)
1640                    .with_resources(|resources| {
1641                        resources
1642                            .with_limit_cpu("invalid")
1643                            .with_request_memory("1G")
1644                    })
1645            })
1646            .with_collator(|collator| {
1647                collator
1648                    .with_name("collator2")
1649                    .with_command("command2")
1650                    .with_image("invalid.image")
1651                    .with_initial_balance(20000000)
1652            })
1653            .build()
1654            .unwrap_err();
1655
1656        assert_eq!(errors.len(), 5);
1657        assert_eq!(
1658            errors.first().unwrap().to_string(),
1659            "parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
1660        );
1661        assert_eq!(
1662            errors.get(1).unwrap().to_string(),
1663            "parachain[2000].bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
1664        );
1665        assert_eq!(
1666            errors.get(2).unwrap().to_string(),
1667            "parachain[2000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
1668        );
1669        assert_eq!(
1670            errors.get(3).unwrap().to_string(),
1671            r"parachain[2000].collators['collator1'].resources.limit_cpu: 'invalid' doesn't match regex '^\d+(.\d+)?(m|K|M|G|T|P|E|Ki|Mi|Gi|Ti|Pi|Ei)?$'",
1672        );
1673        assert_eq!(
1674            errors.get(4).unwrap().to_string(),
1675            "parachain[2000].collators['collator2'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
1676        );
1677    }
1678
1679    #[test]
1680    fn import_toml_registration_strategy_should_deserialize() {
1681        let load_from_toml =
1682            NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
1683
1684        for parachain in load_from_toml.parachains().iter() {
1685            if parachain.id() == 1000 {
1686                assert_eq!(
1687                    parachain.registration_strategy(),
1688                    Some(&RegistrationStrategy::UsingExtrinsic)
1689                );
1690            }
1691            if parachain.id() == 2000 {
1692                assert_eq!(
1693                    parachain.registration_strategy(),
1694                    Some(&RegistrationStrategy::InGenesis)
1695                );
1696            }
1697        }
1698
1699        let load_from_toml_small = NetworkConfig::load_from_toml(
1700            "./testing/snapshots/0003-small-network_w_parachain.toml",
1701        )
1702        .unwrap();
1703
1704        let parachain = load_from_toml_small.parachains()[0];
1705        let parachain_evm = load_from_toml_small.parachains()[1];
1706
1707        assert_eq!(parachain.registration_strategy(), None);
1708        assert!(!parachain.is_evm_based());
1709        assert_eq!(parachain.collators().len(), 1);
1710        assert!(parachain_evm.is_evm_based());
1711    }
1712
1713    #[test]
1714    fn onboard_as_parachain_should_default_to_true() {
1715        let config = ParachainConfigBuilder::new(Default::default())
1716            .with_id(2000)
1717            .with_chain("myparachain")
1718            .with_collator(|collator| collator.with_name("collator"))
1719            .build()
1720            .unwrap();
1721
1722        assert!(config.onboard_as_parachain());
1723    }
1724
1725    #[test]
1726    fn evm_based_default_to_false() {
1727        let config = ParachainConfigBuilder::new(Default::default())
1728            .with_id(2000)
1729            .with_chain("myparachain")
1730            .with_collator(|collator| collator.with_name("collator"))
1731            .build()
1732            .unwrap();
1733
1734        assert!(!config.is_evm_based());
1735    }
1736
1737    #[test]
1738    fn evm_based() {
1739        let config = ParachainConfigBuilder::new(Default::default())
1740            .with_id(2000)
1741            .with_chain("myparachain")
1742            .evm_based(true)
1743            .with_collator(|collator| collator.with_name("collator"))
1744            .build()
1745            .unwrap();
1746
1747        assert!(config.is_evm_based());
1748    }
1749
1750    #[test]
1751    fn build_config_in_running_context() {
1752        let config = ParachainConfigBuilder::new_with_running(Default::default())
1753            .with_id(2000)
1754            .with_chain("myparachain")
1755            .with_collator(|collator| collator.with_name("collator"))
1756            .build()
1757            .unwrap();
1758
1759        assert_eq!(
1760            config.registration_strategy(),
1761            Some(&RegistrationStrategy::UsingExtrinsic)
1762        );
1763    }
1764
1765    #[test]
1766    fn parachain_config_builder_should_works_with_chain_spec_command() {
1767        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1768        let config = ParachainConfigBuilder::new(Default::default())
1769            .with_id(2000)
1770            .with_chain("some-chain")
1771            .with_default_image("myrepo:myimage")
1772            .with_default_command("default_command")
1773            .with_chain_spec_command(CMD_TPL)
1774            .with_collator(|collator| collator.with_name("collator"))
1775            .build()
1776            .unwrap();
1777
1778        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1779        assert!(!config.chain_spec_command_is_local());
1780    }
1781
1782    #[test]
1783    fn parachain_config_builder_should_works_with_chain_spec_command_and_local() {
1784        const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
1785        let config = ParachainConfigBuilder::new(Default::default())
1786            .with_id(2000)
1787            .with_chain("some-chain")
1788            .with_default_image("myrepo:myimage")
1789            .with_default_command("default_command")
1790            .with_chain_spec_command(CMD_TPL)
1791            .chain_spec_command_is_local(true)
1792            .with_collator(|collator| collator.with_name("collator"))
1793            .build()
1794            .unwrap();
1795
1796        assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
1797        assert!(config.chain_spec_command_is_local());
1798    }
1799
1800    #[test]
1801    fn parachain_with_group_config_builder_should_succeeds_and_returns_a_new_parachain_config() {
1802        let parachain_config = ParachainConfigBuilder::new(Default::default())
1803            .with_id(1000)
1804            .with_chain("mychainname")
1805            .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1806            .onboard_as_parachain(false)
1807            .with_initial_balance(100_000_042)
1808            .with_default_image("myrepo:myimage")
1809            .with_default_command("default_command")
1810            .without_default_bootnodes()
1811            .with_collator(|collator| {
1812                collator
1813                    .with_name("collator1")
1814                    .with_command("command1")
1815                    .bootnode(true)
1816            })
1817            .with_collator_group(|group| {
1818                group.with_count(2).with_base_node(|base| {
1819                    base.with_name("collator_group1")
1820                        .with_command("group_command1")
1821                        .bootnode(true)
1822                })
1823            })
1824            .with_collator_group(|group| {
1825                group.with_count(3).with_base_node(|base| {
1826                    base.with_name("collator_group2")
1827                        .with_command("group_command2")
1828                        .bootnode(false)
1829                })
1830            })
1831            .build()
1832            .unwrap();
1833
1834        assert_eq!(parachain_config.id(), 1000);
1835        assert_eq!(parachain_config.collators().len(), 1);
1836        assert_eq!(parachain_config.group_collators_configs().len(), 2);
1837
1838        let group_collator1 = parachain_config.group_collators_configs()[0].clone();
1839        assert_eq!(group_collator1.count, 2);
1840        let base_config1 = group_collator1.base_config;
1841        assert_eq!(base_config1.name(), "collator_group1");
1842        assert_eq!(base_config1.command().unwrap().as_str(), "group_command1");
1843        assert!(base_config1.is_bootnode());
1844
1845        let group_collator2 = parachain_config.group_collators_configs()[1].clone();
1846        assert_eq!(group_collator2.count, 3);
1847        let base_config2 = group_collator2.base_config;
1848        assert_eq!(base_config2.name(), "collator_group2");
1849        assert_eq!(base_config2.command().unwrap().as_str(), "group_command2");
1850        assert!(!base_config2.is_bootnode());
1851    }
1852
1853    #[test]
1854    fn parachain_with_group_count_0_config_builder_should_fail() {
1855        let parachain_config = ParachainConfigBuilder::new(Default::default())
1856            .with_id(1000)
1857            .with_chain("mychainname")
1858            .with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
1859            .onboard_as_parachain(false)
1860            .with_initial_balance(100_000_042)
1861            .with_default_image("myrepo:myimage")
1862            .with_default_command("default_command")
1863            .without_default_bootnodes()
1864            .with_collator(|collator| {
1865                collator
1866                    .with_name("collator1")
1867                    .with_command("command1")
1868                    .bootnode(true)
1869            })
1870            .with_collator_group(|group| {
1871                group.with_count(2).with_base_node(|base| {
1872                    base.with_name("collator_group1")
1873                        .with_command("group_command1")
1874                        .bootnode(true)
1875                })
1876            })
1877            .with_collator_group(|group| {
1878                group.with_count(0).with_base_node(|base| {
1879                    base.with_name("collator_group2")
1880                        .with_command("group_command2")
1881                        .bootnode(false)
1882                })
1883            })
1884            .build();
1885
1886        let errors: Vec<anyhow::Error> = match parachain_config {
1887            Ok(_) => vec![],
1888            Err(errs) => errs,
1889        };
1890
1891        assert_eq!(errors.len(), 1);
1892        assert_eq!(
1893            errors.first().unwrap().to_string(),
1894            "parachain[1000].collators['collator_group2'].Count cannot be zero"
1895        );
1896    }
1897}