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