use std::{cell::RefCell, error::Error, fmt::Display, marker::PhantomData, rc::Rc};
use anyhow::anyhow;
use multiaddr::Multiaddr;
use serde::{
de::{self, Visitor},
ser::SerializeStruct,
Deserialize, Serialize,
};
use crate::{
shared::{
errors::{ConfigError, FieldError},
helpers::{merge_errors, merge_errors_vecs},
node::{self, NodeConfig, NodeConfigBuilder},
resources::{Resources, ResourcesBuilder},
types::{
Arg, AssetLocation, Chain, ChainDefaultContext, Command, Image, ValidationContext, U128,
},
},
utils::{default_as_false, default_as_true, default_initial_balance, is_false},
};
#[derive(Debug, Clone, PartialEq)]
pub enum RegistrationStrategy {
InGenesis,
UsingExtrinsic,
Manual,
}
impl Serialize for RegistrationStrategy {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut state = serializer.serialize_struct("RegistrationStrategy", 1)?;
match self {
Self::InGenesis => state.serialize_field("add_to_genesis", &true)?,
Self::UsingExtrinsic => state.serialize_field("register_para", &true)?,
Self::Manual => {
state.serialize_field("add_to_genesis", &false)?;
state.serialize_field("register_para", &false)?;
},
}
state.end()
}
}
struct RegistrationStrategyVisitor;
impl<'de> Visitor<'de> for RegistrationStrategyVisitor {
type Value = RegistrationStrategy;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("struct RegistrationStrategy")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut add_to_genesis = false;
let mut register_para = false;
while let Some(key) = map.next_key::<String>()? {
match key.as_str() {
"addToGenesis" | "add_to_genesis" => add_to_genesis = map.next_value()?,
"registerPara" | "register_para" => register_para = map.next_value()?,
_ => {
return Err(de::Error::unknown_field(
&key,
&["add_to_genesis", "register_para"],
))
},
}
}
match (add_to_genesis, register_para) {
(true, false) => Ok(RegistrationStrategy::InGenesis),
(false, true) => Ok(RegistrationStrategy::UsingExtrinsic),
_ => Err(de::Error::missing_field("add_to_genesis or register_para")),
}
}
}
impl<'de> Deserialize<'de> for RegistrationStrategy {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_struct(
"RegistrationStrategy",
&["add_to_genesis", "register_para"],
RegistrationStrategyVisitor,
)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ParachainConfig {
id: u32,
chain: Option<Chain>,
#[serde(flatten)]
registration_strategy: Option<RegistrationStrategy>,
#[serde(
skip_serializing_if = "super::utils::is_true",
default = "default_as_true"
)]
onboard_as_parachain: bool,
#[serde(rename = "balance", default = "default_initial_balance")]
initial_balance: U128,
default_command: Option<Command>,
default_image: Option<Image>,
default_resources: Option<Resources>,
default_db_snapshot: Option<AssetLocation>,
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
default_args: Vec<Arg>,
genesis_wasm_path: Option<AssetLocation>,
genesis_wasm_generator: Option<Command>,
genesis_state_path: Option<AssetLocation>,
genesis_state_generator: Option<Command>,
chain_spec_path: Option<AssetLocation>,
chain_spec_command: Option<String>,
#[serde(skip_serializing_if = "is_false", default)]
chain_spec_command_is_local: bool,
#[serde(rename = "cumulus_based", default = "default_as_true")]
is_cumulus_based: bool,
#[serde(rename = "evm_based", default = "default_as_false")]
is_evm_based: bool,
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
bootnodes_addresses: Vec<Multiaddr>,
genesis_overrides: Option<serde_json::Value>,
#[serde(skip_serializing_if = "std::vec::Vec::is_empty", default)]
pub(crate) collators: Vec<NodeConfig>,
collator: Option<NodeConfig>,
}
impl ParachainConfig {
pub fn id(&self) -> u32 {
self.id
}
pub fn chain(&self) -> Option<&Chain> {
self.chain.as_ref()
}
pub fn registration_strategy(&self) -> Option<&RegistrationStrategy> {
self.registration_strategy.as_ref()
}
pub fn onboard_as_parachain(&self) -> bool {
self.onboard_as_parachain
}
pub fn initial_balance(&self) -> u128 {
self.initial_balance.0
}
pub fn default_command(&self) -> Option<&Command> {
self.default_command.as_ref()
}
pub fn default_image(&self) -> Option<&Image> {
self.default_image.as_ref()
}
pub fn default_resources(&self) -> Option<&Resources> {
self.default_resources.as_ref()
}
pub fn default_db_snapshot(&self) -> Option<&AssetLocation> {
self.default_db_snapshot.as_ref()
}
pub fn default_args(&self) -> Vec<&Arg> {
self.default_args.iter().collect::<Vec<&Arg>>()
}
pub fn genesis_wasm_path(&self) -> Option<&AssetLocation> {
self.genesis_wasm_path.as_ref()
}
pub fn genesis_wasm_generator(&self) -> Option<&Command> {
self.genesis_wasm_generator.as_ref()
}
pub fn genesis_state_path(&self) -> Option<&AssetLocation> {
self.genesis_state_path.as_ref()
}
pub fn genesis_state_generator(&self) -> Option<&Command> {
self.genesis_state_generator.as_ref()
}
pub fn genesis_overrides(&self) -> Option<&serde_json::Value> {
self.genesis_overrides.as_ref()
}
pub fn chain_spec_path(&self) -> Option<&AssetLocation> {
self.chain_spec_path.as_ref()
}
pub fn chain_spec_command(&self) -> Option<&str> {
self.chain_spec_command.as_deref()
}
pub fn chain_spec_command_is_local(&self) -> bool {
self.chain_spec_command_is_local
}
pub fn is_cumulus_based(&self) -> bool {
self.is_cumulus_based
}
pub fn is_evm_based(&self) -> bool {
self.is_evm_based
}
pub fn bootnodes_addresses(&self) -> Vec<&Multiaddr> {
self.bootnodes_addresses.iter().collect::<Vec<_>>()
}
pub fn collators(&self) -> Vec<&NodeConfig> {
let mut cols = self.collators.iter().collect::<Vec<_>>();
if let Some(col) = self.collator.as_ref() {
cols.push(col);
}
cols
}
}
pub mod states {
use crate::shared::macros::states;
states! {
Initial,
WithId,
WithAtLeastOneCollator
}
states! {
Bootstrap,
Running
}
pub trait Context {}
impl Context for Bootstrap {}
impl Context for Running {}
}
use states::{Bootstrap, Context, Initial, Running, WithAtLeastOneCollator, WithId};
pub struct ParachainConfigBuilder<S, C> {
config: ParachainConfig,
validation_context: Rc<RefCell<ValidationContext>>,
errors: Vec<anyhow::Error>,
_state: PhantomData<S>,
_context: PhantomData<C>,
}
impl<C: Context> Default for ParachainConfigBuilder<Initial, C> {
fn default() -> Self {
Self {
config: ParachainConfig {
id: 100,
chain: None,
registration_strategy: Some(RegistrationStrategy::InGenesis),
onboard_as_parachain: true,
initial_balance: 2_000_000_000_000.into(),
default_command: None,
default_image: None,
default_resources: None,
default_db_snapshot: None,
default_args: vec![],
genesis_wasm_path: None,
genesis_wasm_generator: None,
genesis_state_path: None,
genesis_state_generator: None,
genesis_overrides: None,
chain_spec_path: None,
chain_spec_command: None,
chain_spec_command_is_local: false, is_cumulus_based: true,
is_evm_based: false,
bootnodes_addresses: vec![],
collators: vec![],
collator: None,
},
validation_context: Default::default(),
errors: vec![],
_state: PhantomData,
_context: PhantomData,
}
}
}
impl<A, C> ParachainConfigBuilder<A, C> {
fn transition<B>(
config: ParachainConfig,
validation_context: Rc<RefCell<ValidationContext>>,
errors: Vec<anyhow::Error>,
) -> ParachainConfigBuilder<B, C> {
ParachainConfigBuilder {
config,
validation_context,
errors,
_state: PhantomData,
_context: PhantomData,
}
}
fn default_chain_context(&self) -> ChainDefaultContext {
ChainDefaultContext {
default_command: self.config.default_command.clone(),
default_image: self.config.default_image.clone(),
default_resources: self.config.default_resources.clone(),
default_db_snapshot: self.config.default_db_snapshot.clone(),
default_args: self.config.default_args.clone(),
}
}
}
impl ParachainConfigBuilder<Initial, Bootstrap> {
pub fn new(
validation_context: Rc<RefCell<ValidationContext>>,
) -> ParachainConfigBuilder<Initial, Bootstrap> {
Self {
validation_context,
..Self::default()
}
}
}
impl ParachainConfigBuilder<WithId, Bootstrap> {
pub fn with_registration_strategy(self, strategy: RegistrationStrategy) -> Self {
Self::transition(
ParachainConfig {
registration_strategy: Some(strategy),
..self.config
},
self.validation_context,
self.errors,
)
}
}
impl ParachainConfigBuilder<WithId, Running> {
pub fn with_registration_strategy(self, strategy: RegistrationStrategy) -> Self {
match strategy {
RegistrationStrategy::InGenesis => Self::transition(
self.config,
self.validation_context,
merge_errors(
self.errors,
FieldError::RegistrationStrategy(anyhow!(
"Can be set to InGenesis in Running context"
))
.into(),
),
),
RegistrationStrategy::Manual | RegistrationStrategy::UsingExtrinsic => {
Self::transition(
ParachainConfig {
registration_strategy: Some(strategy),
..self.config
},
self.validation_context,
self.errors,
)
},
}
}
}
impl ParachainConfigBuilder<Initial, Running> {
pub fn new_with_running(
validation_context: Rc<RefCell<ValidationContext>>,
) -> ParachainConfigBuilder<Initial, Running> {
let mut builder = Self {
validation_context,
..Self::default()
};
builder.config.registration_strategy = Some(RegistrationStrategy::UsingExtrinsic);
builder
}
}
impl<C: Context> ParachainConfigBuilder<Initial, C> {
pub fn with_id(self, id: u32) -> ParachainConfigBuilder<WithId, C> {
Self::transition(
ParachainConfig { id, ..self.config },
self.validation_context,
self.errors,
)
}
}
impl<C: Context> ParachainConfigBuilder<WithId, C> {
pub fn with_chain<T>(self, chain: T) -> Self
where
T: TryInto<Chain>,
T::Error: Error + Send + Sync + 'static,
{
match chain.try_into() {
Ok(chain) => Self::transition(
ParachainConfig {
chain: Some(chain),
..self.config
},
self.validation_context,
self.errors,
),
Err(error) => Self::transition(
self.config,
self.validation_context,
merge_errors(self.errors, FieldError::Chain(error.into()).into()),
),
}
}
pub fn onboard_as_parachain(self, choice: bool) -> Self {
Self::transition(
ParachainConfig {
onboard_as_parachain: choice,
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_initial_balance(self, initial_balance: u128) -> Self {
Self::transition(
ParachainConfig {
initial_balance: initial_balance.into(),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_default_command<T>(self, command: T) -> Self
where
T: TryInto<Command>,
T::Error: Error + Send + Sync + 'static,
{
match command.try_into() {
Ok(command) => Self::transition(
ParachainConfig {
default_command: Some(command),
..self.config
},
self.validation_context,
self.errors,
),
Err(error) => Self::transition(
self.config,
self.validation_context,
merge_errors(self.errors, FieldError::DefaultCommand(error.into()).into()),
),
}
}
pub fn with_default_image<T>(self, image: T) -> Self
where
T: TryInto<Image>,
T::Error: Error + Send + Sync + 'static,
{
match image.try_into() {
Ok(image) => Self::transition(
ParachainConfig {
default_image: Some(image),
..self.config
},
self.validation_context,
self.errors,
),
Err(error) => Self::transition(
self.config,
self.validation_context,
merge_errors(self.errors, FieldError::DefaultImage(error.into()).into()),
),
}
}
pub fn with_default_resources(
self,
f: impl FnOnce(ResourcesBuilder) -> ResourcesBuilder,
) -> Self {
match f(ResourcesBuilder::new()).build() {
Ok(default_resources) => Self::transition(
ParachainConfig {
default_resources: Some(default_resources),
..self.config
},
self.validation_context,
self.errors,
),
Err(errors) => Self::transition(
self.config,
self.validation_context,
merge_errors_vecs(
self.errors,
errors
.into_iter()
.map(|error| FieldError::DefaultResources(error).into())
.collect::<Vec<_>>(),
),
),
}
}
pub fn with_default_db_snapshot(self, location: impl Into<AssetLocation>) -> Self {
Self::transition(
ParachainConfig {
default_db_snapshot: Some(location.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_default_args(self, args: Vec<Arg>) -> Self {
Self::transition(
ParachainConfig {
default_args: args,
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_genesis_wasm_path(self, location: impl Into<AssetLocation>) -> Self {
Self::transition(
ParachainConfig {
genesis_wasm_path: Some(location.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_genesis_wasm_generator<T>(self, command: T) -> Self
where
T: TryInto<Command>,
T::Error: Error + Send + Sync + 'static,
{
match command.try_into() {
Ok(command) => Self::transition(
ParachainConfig {
genesis_wasm_generator: Some(command),
..self.config
},
self.validation_context,
self.errors,
),
Err(error) => Self::transition(
self.config,
self.validation_context,
merge_errors(
self.errors,
FieldError::GenesisWasmGenerator(error.into()).into(),
),
),
}
}
pub fn with_genesis_state_path(self, location: impl Into<AssetLocation>) -> Self {
Self::transition(
ParachainConfig {
genesis_state_path: Some(location.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_genesis_state_generator<T>(self, command: T) -> Self
where
T: TryInto<Command>,
T::Error: Error + Send + Sync + 'static,
{
match command.try_into() {
Ok(command) => Self::transition(
ParachainConfig {
genesis_state_generator: Some(command),
..self.config
},
self.validation_context,
self.errors,
),
Err(error) => Self::transition(
self.config,
self.validation_context,
merge_errors(
self.errors,
FieldError::GenesisStateGenerator(error.into()).into(),
),
),
}
}
pub fn with_genesis_overrides(self, genesis_overrides: impl Into<serde_json::Value>) -> Self {
Self::transition(
ParachainConfig {
genesis_overrides: Some(genesis_overrides.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_chain_spec_path(self, location: impl Into<AssetLocation>) -> Self {
Self::transition(
ParachainConfig {
chain_spec_path: Some(location.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_chain_spec_command(self, cmd_template: impl Into<String>) -> Self {
Self::transition(
ParachainConfig {
chain_spec_command: Some(cmd_template.into()),
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn chain_spec_command_is_local(self, choice: bool) -> Self {
Self::transition(
ParachainConfig {
chain_spec_command_is_local: choice,
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn cumulus_based(self, choice: bool) -> Self {
Self::transition(
ParachainConfig {
is_cumulus_based: choice,
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn evm_based(self, choice: bool) -> Self {
Self::transition(
ParachainConfig {
is_evm_based: choice,
..self.config
},
self.validation_context,
self.errors,
)
}
pub fn with_bootnodes_addresses<T>(self, bootnodes_addresses: Vec<T>) -> Self
where
T: TryInto<Multiaddr> + Display + Copy,
T::Error: Error + Send + Sync + 'static,
{
let mut addrs = vec![];
let mut errors = vec![];
for (index, addr) in bootnodes_addresses.into_iter().enumerate() {
match addr.try_into() {
Ok(addr) => addrs.push(addr),
Err(error) => errors.push(
FieldError::BootnodesAddress(index, addr.to_string(), error.into()).into(),
),
}
}
Self::transition(
ParachainConfig {
bootnodes_addresses: addrs,
..self.config
},
self.validation_context,
merge_errors_vecs(self.errors, errors),
)
}
pub fn with_collator(
self,
f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
) -> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
match f(NodeConfigBuilder::new(
self.default_chain_context(),
self.validation_context.clone(),
))
.build()
{
Ok(collator) => Self::transition(
ParachainConfig {
collators: vec![collator],
..self.config
},
self.validation_context,
self.errors,
),
Err((name, errors)) => Self::transition(
self.config,
self.validation_context,
merge_errors_vecs(
self.errors,
errors
.into_iter()
.map(|error| ConfigError::Collator(name.clone(), error).into())
.collect::<Vec<_>>(),
),
),
}
}
}
impl<C: Context> ParachainConfigBuilder<WithAtLeastOneCollator, C> {
pub fn with_collator(
self,
f: impl FnOnce(NodeConfigBuilder<node::Initial>) -> NodeConfigBuilder<node::Buildable>,
) -> Self {
match f(NodeConfigBuilder::new(
ChainDefaultContext::default(),
self.validation_context.clone(),
))
.build()
{
Ok(collator) => Self::transition(
ParachainConfig {
collators: [self.config.collators, vec![collator]].concat(),
..self.config
},
self.validation_context,
self.errors,
),
Err((name, errors)) => Self::transition(
self.config,
self.validation_context,
merge_errors_vecs(
self.errors,
errors
.into_iter()
.map(|error| ConfigError::Collator(name.clone(), error).into())
.collect::<Vec<_>>(),
),
),
}
}
pub fn build(self) -> Result<ParachainConfig, Vec<anyhow::Error>> {
if !self.errors.is_empty() {
return Err(self
.errors
.into_iter()
.map(|error| ConfigError::Parachain(self.config.id, error).into())
.collect::<Vec<_>>());
}
Ok(self.config)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::NetworkConfig;
#[test]
fn parachain_config_builder_should_succeeds_and_returns_a_new_parachain_config() {
let parachain_config = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("mychainname")
.with_registration_strategy(RegistrationStrategy::UsingExtrinsic)
.onboard_as_parachain(false)
.with_initial_balance(100_000_042)
.with_default_image("myrepo:myimage")
.with_default_command("default_command")
.with_default_resources(|resources| {
resources
.with_limit_cpu("500M")
.with_limit_memory("1G")
.with_request_cpu("250M")
})
.with_default_db_snapshot("https://www.urltomysnapshot.com/file.tgz")
.with_default_args(vec![("--arg1", "value1").into(), "--option2".into()])
.with_genesis_wasm_path("https://www.backupsite.com/my/wasm/file.tgz")
.with_genesis_wasm_generator("generator_wasm")
.with_genesis_state_path("./path/to/genesis/state")
.with_genesis_state_generator("generator_state")
.with_chain_spec_path("./path/to/chain/spec.json")
.cumulus_based(false)
.evm_based(false)
.with_bootnodes_addresses(vec![
"/ip4/10.41.122.55/tcp/45421",
"/ip4/51.144.222.10/tcp/2333",
])
.with_collator(|collator| {
collator
.with_name("collator1")
.with_command("command1")
.bootnode(true)
})
.with_collator(|collator| {
collator
.with_name("collator2")
.with_command("command2")
.validator(true)
})
.build()
.unwrap();
assert_eq!(parachain_config.id(), 1000);
assert_eq!(parachain_config.collators().len(), 2);
let &collator1 = parachain_config.collators().first().unwrap();
assert_eq!(collator1.name(), "collator1");
assert_eq!(collator1.command().unwrap().as_str(), "command1");
assert!(collator1.is_bootnode());
let &collator2 = parachain_config.collators().last().unwrap();
assert_eq!(collator2.name(), "collator2");
assert_eq!(collator2.command().unwrap().as_str(), "command2");
assert!(collator2.is_validator());
assert_eq!(parachain_config.chain().unwrap().as_str(), "mychainname");
assert_eq!(
parachain_config.registration_strategy().unwrap(),
&RegistrationStrategy::UsingExtrinsic
);
assert!(!parachain_config.onboard_as_parachain());
assert_eq!(parachain_config.initial_balance(), 100_000_042);
assert_eq!(
parachain_config.default_command().unwrap().as_str(),
"default_command"
);
assert_eq!(
parachain_config.default_image().unwrap().as_str(),
"myrepo:myimage"
);
let default_resources = parachain_config.default_resources().unwrap();
assert_eq!(default_resources.limit_cpu().unwrap().as_str(), "500M");
assert_eq!(default_resources.limit_memory().unwrap().as_str(), "1G");
assert_eq!(default_resources.request_cpu().unwrap().as_str(), "250M");
assert!(matches!(
parachain_config.default_db_snapshot().unwrap(),
AssetLocation::Url(value) if value.as_str() == "https://www.urltomysnapshot.com/file.tgz",
));
assert!(matches!(
parachain_config.chain_spec_path().unwrap(),
AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
));
let args: Vec<Arg> = vec![("--arg1", "value1").into(), "--option2".into()];
assert_eq!(
parachain_config.default_args(),
args.iter().collect::<Vec<_>>()
);
assert!(matches!(
parachain_config.genesis_wasm_path().unwrap(),
AssetLocation::Url(value) if value.as_str() == "https://www.backupsite.com/my/wasm/file.tgz"
));
assert_eq!(
parachain_config.genesis_wasm_generator().unwrap().as_str(),
"generator_wasm"
);
assert!(matches!(
parachain_config.genesis_state_path().unwrap(),
AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/genesis/state"
));
assert_eq!(
parachain_config.genesis_state_generator().unwrap().as_str(),
"generator_state"
);
assert!(matches!(
parachain_config.chain_spec_path().unwrap(),
AssetLocation::FilePath(value) if value.to_str().unwrap() == "./path/to/chain/spec.json"
));
assert!(!parachain_config.is_cumulus_based());
let bootnodes_addresses: Vec<Multiaddr> = vec![
"/ip4/10.41.122.55/tcp/45421".try_into().unwrap(),
"/ip4/51.144.222.10/tcp/2333".try_into().unwrap(),
];
assert_eq!(
parachain_config.bootnodes_addresses(),
bootnodes_addresses.iter().collect::<Vec<_>>()
);
assert!(!parachain_config.is_evm_based());
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_chain_is_invalid() {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("invalid chain")
.with_collator(|collator| {
collator
.with_name("collator")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[1000].chain: 'invalid chain' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_default_command_is_invalid() {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("chain")
.with_default_command("invalid command")
.with_collator(|collator| {
collator
.with_name("node")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[1000].default_command: 'invalid command' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_default_image_is_invalid() {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("chain")
.with_default_image("invalid image")
.with_collator(|collator| {
collator
.with_name("node")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
r"parachain[1000].default_image: 'invalid image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_default_resources_are_invalid()
{
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("chain")
.with_default_resources(|default_resources| {
default_resources
.with_limit_memory("100m")
.with_request_cpu("invalid")
})
.with_collator(|collator| {
collator
.with_name("node")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
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)?$'"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_genesis_wasm_generator_is_invalid(
) {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_genesis_wasm_generator("invalid command")
.with_collator(|collator| {
collator
.with_name("collator")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[2000].genesis_wasm_generator: 'invalid command' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_genesis_state_generator_is_invalid(
) {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("myparachain")
.with_genesis_state_generator("invalid command")
.with_collator(|collator| {
collator
.with_name("collator")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[1000].genesis_state_generator: 'invalid command' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_bootnodes_addresses_are_invalid(
) {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.with_collator(|collator| {
collator
.with_name("collator")
.with_command("command")
.validator(true)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 2);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
);
assert_eq!(
errors.get(1).unwrap().to_string(),
"parachain[2000].bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_an_error_if_first_collator_is_invalid() {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(1000)
.with_chain("myparachain")
.with_collator(|collator| {
collator
.with_name("collator")
.with_command("invalid command")
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[1000].collators['collator'].command: 'invalid command' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_with_at_least_one_collator_should_fails_and_returns_an_error_if_second_collator_is_invalid(
) {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_collator(|collator| {
collator
.with_name("collator1")
.with_command("command1")
.invulnerable(true)
.bootnode(true)
})
.with_collator(|collator| {
collator
.with_name("collator2")
.with_command("invalid command")
.with_initial_balance(20000000)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 1);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[2000].collators['collator2'].command: 'invalid command' shouldn't contains whitespace"
);
}
#[test]
fn parachain_config_builder_should_fails_and_returns_multiple_errors_if_multiple_fields_are_invalid(
) {
let errors = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_bootnodes_addresses(vec!["/ip4//tcp/45421", "//10.42.153.10/tcp/43111"])
.with_collator(|collator| {
collator
.with_name("collator1")
.with_command("invalid command")
.invulnerable(true)
.bootnode(true)
.with_resources(|resources| {
resources
.with_limit_cpu("invalid")
.with_request_memory("1G")
})
})
.with_collator(|collator| {
collator
.with_name("collator2")
.with_command("command2")
.with_image("invalid.image")
.with_initial_balance(20000000)
})
.build()
.unwrap_err();
assert_eq!(errors.len(), 5);
assert_eq!(
errors.first().unwrap().to_string(),
"parachain[2000].bootnodes_addresses[0]: '/ip4//tcp/45421' failed to parse: invalid IPv4 address syntax"
);
assert_eq!(
errors.get(1).unwrap().to_string(),
"parachain[2000].bootnodes_addresses[1]: '//10.42.153.10/tcp/43111' unknown protocol string: "
);
assert_eq!(
errors.get(2).unwrap().to_string(),
"parachain[2000].collators['collator1'].command: 'invalid command' shouldn't contains whitespace"
);
assert_eq!(
errors.get(3).unwrap().to_string(),
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)?$'",
);
assert_eq!(
errors.get(4).unwrap().to_string(),
"parachain[2000].collators['collator2'].image: 'invalid.image' doesn't match regex '^([ip]|[hostname]/)?[tag_name]:[tag_version]?$'"
);
}
#[test]
fn import_toml_registration_strategy_should_deserialize() {
let load_from_toml =
NetworkConfig::load_from_toml("./testing/snapshots/0001-big-network.toml").unwrap();
for parachain in load_from_toml.parachains().iter() {
if parachain.id() == 1000 {
assert_eq!(
parachain.registration_strategy(),
Some(&RegistrationStrategy::UsingExtrinsic)
);
}
if parachain.id() == 2000 {
assert_eq!(
parachain.registration_strategy(),
Some(&RegistrationStrategy::InGenesis)
);
}
}
let load_from_toml_small = NetworkConfig::load_from_toml(
"./testing/snapshots/0003-small-network_w_parachain.toml",
)
.unwrap();
let parachain = load_from_toml_small.parachains()[0];
let parachain_evm = load_from_toml_small.parachains()[1];
assert_eq!(parachain.registration_strategy(), None);
assert!(!parachain.is_evm_based());
assert_eq!(parachain.collators().len(), 1);
assert!(parachain_evm.is_evm_based());
}
#[test]
fn onboard_as_parachain_should_default_to_true() {
let config = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert!(config.onboard_as_parachain());
}
#[test]
fn evm_based_default_to_false() {
let config = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert!(!config.is_evm_based());
}
#[test]
fn evm_based() {
let config = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("myparachain")
.evm_based(true)
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert!(config.is_evm_based());
}
#[test]
fn build_config_in_running_context() {
let config = ParachainConfigBuilder::new_with_running(Default::default())
.with_id(2000)
.with_chain("myparachain")
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert_eq!(
config.registration_strategy(),
Some(&RegistrationStrategy::UsingExtrinsic)
);
}
#[test]
fn parachain_config_builder_should_works_with_chain_spec_command() {
const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
let config = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("some-chain")
.with_default_image("myrepo:myimage")
.with_default_command("default_command")
.with_chain_spec_command(CMD_TPL)
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
assert!(!config.chain_spec_command_is_local());
}
#[test]
fn parachain_config_builder_should_works_with_chain_spec_command_and_local() {
const CMD_TPL: &str = "./bin/chain-spec-generator {% raw %} {{chainName}} {% endraw %}";
let config = ParachainConfigBuilder::new(Default::default())
.with_id(2000)
.with_chain("some-chain")
.with_default_image("myrepo:myimage")
.with_default_command("default_command")
.with_chain_spec_command(CMD_TPL)
.chain_spec_command_is_local(true)
.with_collator(|collator| collator.with_name("collator"))
.build()
.unwrap();
assert_eq!(config.chain_spec_command(), Some(CMD_TPL));
assert!(config.chain_spec_command_is_local());
}
}