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#[derive(Debug, Clone, PartialEq)]
27pub enum RegistrationStrategy {
28 InGenesis,
30 UsingExtrinsic,
32 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#[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 wasm_override: Option<AssetLocation>,
133 chain_spec_command: Option<String>,
137 #[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 collator: Option<NodeConfig>,
159}
160
161impl ParachainConfig {
162 pub fn id(&self) -> u32 {
164 self.id
165 }
166
167 pub fn chain(&self) -> Option<&Chain> {
169 self.chain.as_ref()
170 }
171
172 pub fn registration_strategy(&self) -> Option<&RegistrationStrategy> {
174 self.registration_strategy.as_ref()
175 }
176
177 pub fn onboard_as_parachain(&self) -> bool {
179 self.onboard_as_parachain
180 }
181
182 pub fn initial_balance(&self) -> u128 {
184 self.initial_balance.0
185 }
186
187 pub fn default_command(&self) -> Option<&Command> {
189 self.default_command.as_ref()
190 }
191
192 pub fn default_image(&self) -> Option<&Image> {
194 self.default_image.as_ref()
195 }
196
197 pub fn default_resources(&self) -> Option<&Resources> {
199 self.default_resources.as_ref()
200 }
201
202 pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
204 self.default_db_snapshot.as_ref()
205 }
206
207 pub fn default_args(&self) -> Vec<&Arg> {
209 self.default_args.iter().collect::<Vec<&Arg>>()
210 }
211
212 pub fn genesis_wasm_path(&self) -> Option<&AssetLocation> {
214 self.genesis_wasm_path.as_ref()
215 }
216
217 pub fn genesis_wasm_generator(&self) -> Option<&Command> {
219 self.genesis_wasm_generator.as_ref()
220 }
221
222 pub fn genesis_state_path(&self) -> Option<&AssetLocation> {
224 self.genesis_state_path.as_ref()
225 }
226
227 pub fn genesis_state_generator(&self) -> Option<&CommandWithCustomArgs> {
229 self.genesis_state_generator.as_ref()
230 }
231
232 pub fn genesis_overrides(&self) -> Option<&serde_json::Value> {
234 self.genesis_overrides.as_ref()
235 }
236
237 pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
239 self.chain_spec_path.as_ref()
240 }
241
242 pub fn chain_spec_command(&self) -> Option<&str> {
244 self.chain_spec_command.as_deref()
245 }
246
247 pub fn chain_spec_command_is_local(&self) -> bool {
249 self.chain_spec_command_is_local
250 }
251
252 pub fn is_cumulus_based(&self) -> bool {
254 self.is_cumulus_based
255 }
256
257 pub fn is_evm_based(&self) -> bool {
259 self.is_evm_based
260 }
261
262 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
264 self.bootnodes_addresses.iter().collect::<Vec<_>>()
265 }
266
267 pub fn no_default_bootnodes(&self) -> bool {
270 self.no_default_bootnodes
271 }
272
273 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 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};
308pub 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, 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 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 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 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 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 builder.config.registration_strategy = Some(RegistrationStrategy::UsingExtrinsic);
450 builder
451 }
452}
453
454impl<C: Context> ParachainConfigBuilder<Initial, C> {
455 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}