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