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#[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 #[serde(skip)]
111 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 wasm_override: Option<AssetLocation>,
137 chain_spec_command: Option<String>,
141 #[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 pub(crate) collator: Option<NodeConfig>,
163}
164
165impl ParachainConfig {
166 pub fn id(&self) -> u32 {
168 self.id
169 }
170
171 pub fn unique_id(&self) -> &str {
173 &self.unique_id
174 }
175
176 pub fn chain(&self) -> Option<&Chain> {
178 self.chain.as_ref()
179 }
180
181 pub fn registration_strategy(&self) -> Option<&RegistrationStrategy> {
183 self.registration_strategy.as_ref()
184 }
185
186 pub fn onboard_as_parachain(&self) -> bool {
188 self.onboard_as_parachain
189 }
190
191 pub fn initial_balance(&self) -> u128 {
193 self.initial_balance.0
194 }
195
196 pub fn default_command(&self) -> Option<&Command> {
198 self.default_command.as_ref()
199 }
200
201 pub fn default_image(&self) -> Option<&Image> {
203 self.default_image.as_ref()
204 }
205
206 pub fn default_resources(&self) -> Option<&Resources> {
208 self.default_resources.as_ref()
209 }
210
211 pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
213 self.default_db_snapshot.as_ref()
214 }
215
216 pub fn default_args(&self) -> Vec<&Arg> {
218 self.default_args.iter().collect::<Vec<&Arg>>()
219 }
220
221 pub fn genesis_wasm_path(&self) -> Option<&AssetLocation> {
223 self.genesis_wasm_path.as_ref()
224 }
225
226 pub fn genesis_wasm_generator(&self) -> Option<&Command> {
228 self.genesis_wasm_generator.as_ref()
229 }
230
231 pub fn genesis_state_path(&self) -> Option<&AssetLocation> {
233 self.genesis_state_path.as_ref()
234 }
235
236 pub fn genesis_state_generator(&self) -> Option<&CommandWithCustomArgs> {
238 self.genesis_state_generator.as_ref()
239 }
240
241 pub fn genesis_overrides(&self) -> Option<&serde_json::Value> {
243 self.genesis_overrides.as_ref()
244 }
245
246 pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
248 self.chain_spec_path.as_ref()
249 }
250
251 pub fn chain_spec_command(&self) -> Option<&str> {
253 self.chain_spec_command.as_deref()
254 }
255
256 pub fn chain_spec_command_is_local(&self) -> bool {
258 self.chain_spec_command_is_local
259 }
260
261 pub fn is_cumulus_based(&self) -> bool {
263 self.is_cumulus_based
264 }
265
266 pub fn is_evm_based(&self) -> bool {
268 self.is_evm_based
269 }
270
271 pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
273 self.bootnodes_addresses.iter().collect::<Vec<_>>()
274 }
275
276 pub fn no_default_bootnodes(&self) -> bool {
279 self.no_default_bootnodes
280 }
281
282 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 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};
317pub 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, 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 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 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 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 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 builder.config.registration_strategy = Some(RegistrationStrategy::UsingExtrinsic);
460 builder
461 }
462}
463
464impl<C: Context> ParachainConfigBuilder<Initial, C> {
465 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}