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