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