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