From 8af198dfb9257cc5b9e6d666e434635fdbb76cb5 Mon Sep 17 00:00:00 2001 From: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Date: Fri, 26 Jul 2024 22:22:09 +0200 Subject: [PATCH] feat: pop build spec (#257) * feat: pop build spec * fix: ammend test * feat: better use of strum * feat(spec): better ux * chore(spec): small nits * chore(spec): silent exit if not supported * chore(spec): update cargo lock * feat(spec): simple progress spinner * fix(spec): outro message if not supported * fix(spec): amend pop-parachains README * fix(spec): integration test * chore: improve pop-parachains/build test coverage * style: clean code * fix: integration tests * remove unnecesary tests * Update crates/pop-cli/src/commands/build/mod.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * Update crates/pop-cli/tests/parachain.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * Update crates/pop-cli/tests/parachain.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * fix: address feedback * Update crates/pop-cli/src/commands/build/spec.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * fix: more consistent ui * style: More concise prompt * fix: relay chain selection based on chain type * style: default name for spec * fix: remove para_id magic num * Update crates/pop-cli/src/commands/build/spec.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * Update crates/pop-parachains/src/build.rs Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> * fix: default_para_id_prompt * refactor(chain-spec): json replacement * refactor: fix typos and remove unnecessary formats * feat: default values from existing chain spec * fix: merge conflicts * fix: add missing intro for ux consistency * refactor: remove unnecessary info * refactor: improve ux * docs: remove para_id from generate_plain_chain_spec * fix: Promt user to check profile * refactor: handle errors and fix docs (#265) * docs: update README docs * refactor: handle errors for replace_para_id * refactor: handle errors for rest of functions * test: fix integration test --------- Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Co-authored-by: Frank Bell Co-authored-by: Alex Bean --- Cargo.lock | 6 +- Cargo.toml | 2 +- crates/pop-cli/Cargo.toml | 13 +- crates/pop-cli/src/commands/build/mod.rs | 10 +- .../pop-cli/src/commands/build/parachain.rs | 40 +- crates/pop-cli/src/commands/build/spec.rs | 457 ++++++++++++++++++ crates/pop-cli/src/commands/mod.rs | 2 + crates/pop-cli/tests/parachain.rs | 45 +- crates/pop-parachains/README.md | 44 +- crates/pop-parachains/src/build.rs | 420 +++++++++++++--- crates/pop-parachains/src/errors.rs | 2 + crates/pop-parachains/src/lib.rs | 4 +- 12 files changed, 905 insertions(+), 140 deletions(-) create mode 100644 crates/pop-cli/src/commands/build/spec.rs diff --git a/Cargo.lock b/Cargo.lock index 39e11e77..b0cd5f55 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4492,6 +4492,7 @@ dependencies = [ "sp-core", "sp-weights", "strum 0.26.2", + "strum_macros 0.26.4", "tempfile", "tokio", "url", @@ -5698,6 +5699,7 @@ version = "1.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" dependencies = [ + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -6519,9 +6521,9 @@ dependencies = [ [[package]] name = "subtle" -version = "2.6.0" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0208408ba0c3df17ed26eb06992cb1a1268d41b2c0e12e65203fbe3972cee5" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "subxt" diff --git a/Cargo.toml b/Cargo.toml index 044374fb..81bff7c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ walkdir = "2.5" indexmap = "2.2" toml_edit = { version = "0.22", features = ["serde"] } symlink = "0.1" -serde_json = "1.0" +serde_json = { version = "1.0", features = ["preserve_order"] } serde = { version = "1.0", features = ["derive"] } zombienet-sdk = "0.2.5" zombienet-support = "0.2.5" diff --git a/crates/pop-cli/Cargo.toml b/crates/pop-cli/Cargo.toml index 1fdcfeed..5ada9032 100644 --- a/crates/pop-cli/Cargo.toml +++ b/crates/pop-cli/Cargo.toml @@ -28,6 +28,7 @@ clap.workspace = true cliclack.workspace = true console.workspace = true strum.workspace = true +strum_macros.workspace = true # contracts pop-contracts = { path = "../pop-contracts", version = "0.2.0", optional = true } @@ -51,14 +52,6 @@ predicates.workspace = true [features] default = ["contract", "parachain", "telemetry"] -contract = [ - "dep:pop-contracts", - "dep:sp-core", - "dep:sp-weights", - "dep:dirs", -] -parachain = [ - "dep:pop-parachains", - "dep:dirs", -] +contract = ["dep:pop-contracts", "dep:sp-core", "dep:sp-weights", "dep:dirs"] +parachain = ["dep:pop-parachains", "dep:dirs"] telemetry = ["dep:pop-telemetry"] diff --git a/crates/pop-cli/src/commands/build/mod.rs b/crates/pop-cli/src/commands/build/mod.rs index bb3dc7fc..32871d27 100644 --- a/crates/pop-cli/src/commands/build/mod.rs +++ b/crates/pop-cli/src/commands/build/mod.rs @@ -5,14 +5,16 @@ use clap::{Args, Subcommand}; #[cfg(feature = "contract")] use contract::BuildContractCommand; use duct::cmd; -#[cfg(feature = "parachain")] -use parachain::BuildParachainCommand; use std::path::PathBuf; +#[cfg(feature = "parachain")] +use {parachain::BuildParachainCommand, spec::BuildSpecCommand}; #[cfg(feature = "contract")] pub(crate) mod contract; #[cfg(feature = "parachain")] pub(crate) mod parachain; +#[cfg(feature = "parachain")] +pub(crate) mod spec; /// Arguments for building a project. #[derive(Args)] @@ -46,6 +48,10 @@ pub(crate) enum Command { #[cfg(feature = "contract")] #[clap(alias = "c")] Contract(BuildContractCommand), + /// Build a chain specification and its genesis artifacts. + #[cfg(feature = "parachain")] + #[clap(alias = "s")] + Spec(BuildSpecCommand), } impl Command { diff --git a/crates/pop-cli/src/commands/build/parachain.rs b/crates/pop-cli/src/commands/build/parachain.rs index 89390748..5debc0e4 100644 --- a/crates/pop-cli/src/commands/build/parachain.rs +++ b/crates/pop-cli/src/commands/build/parachain.rs @@ -3,17 +3,11 @@ use crate::{cli, style::style}; use clap::Args; use pop_common::Profile; -use pop_parachains::{ - build_parachain, export_wasm_file, generate_genesis_state_file, generate_plain_chain_spec, - generate_raw_chain_spec, -}; +use pop_parachains::build_parachain; use std::path::PathBuf; #[cfg(not(test))] use std::{thread::sleep, time::Duration}; -const PLAIN_CHAIN_SPEC_FILE_NAME: &str = "plain-parachain-chainspec.json"; -const RAW_CHAIN_SPEC_FILE_NAME: &str = "raw-parachain-chainspec.json"; - #[derive(Args)] pub struct BuildParachainCommand { /// Directory path for your project [default: current directory]. @@ -67,37 +61,7 @@ impl BuildParachainCommand { let binary = build_parachain(&project_path, self.package, &mode, None)?; cli.info(format!("The {project} was built in {mode} mode."))?; cli.outro("Build completed successfully!")?; - let mut generated_files = vec![format!("Binary generated at: {}", binary.display())]; - - // If `para_id` is provided, generate the chain spec - if let Some(para_id) = self.id { - let plain_chain_spec = project_path.join(PLAIN_CHAIN_SPEC_FILE_NAME); - generate_plain_chain_spec(&binary, &plain_chain_spec, para_id)?; - generated_files.push(format!( - "Plain text chain specification file generated at: {}", - plain_chain_spec.display() - )); - let raw_chain_spec = - generate_raw_chain_spec(&binary, &plain_chain_spec, RAW_CHAIN_SPEC_FILE_NAME)?; - generated_files.push(format!( - "New raw chain specification file generated at: {}", - raw_chain_spec.display() - )); - let wasm_file_name = format!("para-{}-wasm.wasm", para_id); - let wasm_file = export_wasm_file(&binary, &raw_chain_spec, &wasm_file_name)?; - generated_files.push(format!( - "WebAssembly runtime file exported at: {}", - wasm_file.display().to_string() - )); - let genesis_file_name = format!("para-{}-genesis-state", para_id); - let genesis_state_file = - generate_genesis_state_file(&binary, &raw_chain_spec, &genesis_file_name)?; - generated_files.push(format!( - "Genesis State exported at {} file", - genesis_state_file.display().to_string() - )); - console::Term::stderr().clear_last_lines(5)?; - } + let generated_files = vec![format!("Binary generated at: {}", binary.display())]; let generated_files: Vec<_> = generated_files .iter() .map(|s| style(format!("{} {s}", console::Emoji("●", ">"))).dim().to_string()) diff --git a/crates/pop-cli/src/commands/build/spec.rs b/crates/pop-cli/src/commands/build/spec.rs new file mode 100644 index 00000000..0a1c8745 --- /dev/null +++ b/crates/pop-cli/src/commands/build/spec.rs @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{cli, cli::traits::Cli as _, cli::Cli, style::style}; +use clap::{Args, ValueEnum}; +use cliclack::{confirm, input}; +use pop_common::Profile; +use pop_parachains::{ + binary_path, build_parachain, export_wasm_file, generate_genesis_state_file, + generate_plain_chain_spec, generate_raw_chain_spec, is_supported, ChainSpec, +}; +use std::{ + env::current_dir, + fs::create_dir_all, + path::{Path, PathBuf}, +}; +#[cfg(not(test))] +use std::{thread::sleep, time::Duration}; +use strum::{EnumMessage, VariantArray}; +use strum_macros::{AsRefStr, Display, EnumString}; + +const DEFAULT_PARA_ID: u32 = 2000; +const DEFAULT_PROTOCOL_ID: &str = "my-protocol"; +const DEFAULT_SPEC_NAME: &str = "chain-spec.json"; + +#[derive( + AsRefStr, + Clone, + Default, + Debug, + Display, + EnumString, + EnumMessage, + ValueEnum, + VariantArray, + Eq, + PartialEq, +)] +/// Supported chain types for the resulting chain spec. +pub(crate) enum ChainType { + // A development chain that runs mainly on one node. + #[default] + #[strum( + serialize = "Development", + message = "Development", + detailed_message = "Meant for a development chain that runs mainly on one node." + )] + Development, + // A local chain that runs locally on multiple nodes for testing purposes. + #[strum( + serialize = "Local", + message = "Local", + detailed_message = "Meant for a local chain that runs locally on multiple nodes for testing purposes." + )] + Local, + // A live chain. + #[strum(serialize = "Live", message = "Live", detailed_message = "Meant for a live chain.")] + Live, +} + +#[derive( + AsRefStr, + Clone, + Default, + Debug, + Display, + EnumString, + EnumMessage, + ValueEnum, + VariantArray, + Eq, + PartialEq, +)] +/// Supported relay chains that can be included in the resulting chain spec. +pub(crate) enum RelayChain { + #[strum( + serialize = "paseo", + message = "Paseo", + detailed_message = "Polkadot's community testnet." + )] + Paseo, + #[default] + #[strum( + serialize = "paseo-local", + message = "Paseo Local", + detailed_message = "Local configuration for Paseo network." + )] + PaseoLocal, + #[strum( + serialize = "westend", + message = "Westend", + detailed_message = "Parity's test network for protocol testing." + )] + Westend, + #[strum( + serialize = "westend-local", + message = "Westend Local", + detailed_message = "Local configuration for Westend network." + )] + WestendLocal, + #[strum( + serialize = "kusama", + message = "Kusama", + detailed_message = "Polkadot's canary network." + )] + Kusama, + #[strum( + serialize = "kusama-local", + message = "Kusama Local", + detailed_message = "Local configuration for Kusama network." + )] + KusamaLocal, + #[strum( + serialize = "polkadot", + message = "Polkadot", + detailed_message = "Polkadot live network." + )] + Polkadot, + #[strum( + serialize = "polkadot-local", + message = "Polkadot Local", + detailed_message = "Local configuration for Polkadot network." + )] + PolkadotLocal, +} + +#[derive(Args)] +pub struct BuildSpecCommand { + /// File name for the resulting spec. If a path is given, + /// the necessary directories will be created + /// [default: ./chain-spec.json]. + #[arg(short = 'o', long = "output")] + pub(crate) output_file: Option, + /// For production, always build in release mode to exclude debug features. + #[clap(short = 'r', long, default_value = "true")] + pub(crate) release: bool, + /// Parachain ID to be used when generating the chain spec files. + #[arg(short = 'i', long = "id")] + pub(crate) id: Option, + /// Whether to keep localhost as a bootnode. + #[clap(long, default_value = "true")] + pub(crate) default_bootnode: bool, + /// Type of the chain [default: development]. + #[arg(short = 't', long = "type", value_enum)] + pub(crate) chain_type: Option, + /// Relay chain this parachain will connect to [default: paseo-local]. + #[arg(long, value_enum)] + pub(crate) relay: Option, + /// Protocol-id to use in the specification. + #[arg(long = "protocol-id")] + pub(crate) protocol_id: Option, + /// Whether the genesis state file should be generated [default: true]. + #[clap(long = "genesis-state", default_value = "true")] + pub(crate) genesis_state: bool, + /// Whether the genesis code file should be generated [default: true]. + #[clap(long = "genesis-code", default_value = "true")] + pub(crate) genesis_code: bool, +} + +impl BuildSpecCommand { + /// Executes the command. + pub(crate) async fn execute(self) -> anyhow::Result<&'static str> { + // Checks for appchain project in `./`. + if is_supported(None)? { + // If para id has been provided we can build the spec + // otherwise, we need to guide the user. + let _ = match self.id { + Some(_) => self.build(&mut Cli), + None => { + let config = guide_user_to_generate_spec(self).await?; + config.build(&mut Cli) + }, + }; + return Ok("spec"); + } else { + Cli.intro("Building your chain spec")?; + Cli.outro_cancel( + "🚫 Can't build a specification for target. Maybe not a chain project ?", + )?; + Ok("spec") + } + } + + /// Builds a parachain spec. + /// + /// # Arguments + /// * `cli` - The CLI implementation to be used. + fn build(self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<&'static str> { + cli.intro("Building your chain spec")?; + + // Either a para id was already provided or user has been guided to provide one. + let para_id = self.id.unwrap_or(DEFAULT_PARA_ID); + // Notify user in case we need to build the parachain project. + if !self.release { + cli.warning("NOTE: this command defaults to DEBUG builds for development chain types. Please use `--release` (or simply `-r` for a release build...)")?; + #[cfg(not(test))] + sleep(Duration::from_secs(3)) + } + + let spinner = cliclack::spinner(); + spinner.start("Generating chain specification..."); + + // Create output path if needed + let mut output_path = self.output_file.unwrap_or_else(|| PathBuf::from("./")); + let mut plain_chain_spec; + if output_path.is_dir() { + if !output_path.exists() { + // Generate the output path if needed + create_dir_all(&output_path)?; + } + plain_chain_spec = output_path.join(DEFAULT_SPEC_NAME); + } else { + plain_chain_spec = output_path.clone(); + output_path.pop(); + if !output_path.exists() { + // Generate the output path if needed + create_dir_all(&output_path)?; + } + } + plain_chain_spec.set_extension("json"); + + // Locate binary, if it doesn't exist trigger build. + let mode: Profile = self.release.into(); + let cwd = current_dir().unwrap_or(PathBuf::from("./")); + let binary_path = match binary_path(&mode.target_folder(&cwd), &cwd.join("node")) { + Ok(binary_path) => binary_path, + _ => { + cli.info("Node was not found. The project will be built locally.".to_string())?; + cli.warning("NOTE: this may take some time...")?; + build_parachain(&cwd, None, &mode, None)? + }, + }; + + // Generate plain spec. + spinner.set_message("Generating plain chain specification..."); + let mut generated_files = vec![]; + generate_plain_chain_spec(&binary_path, &plain_chain_spec, self.default_bootnode)?; + generated_files.push(format!( + "Plain text chain specification file generated at: {}", + plain_chain_spec.display() + )); + + // Customize spec based on input. + let mut chain_spec = ChainSpec::from(&plain_chain_spec)?; + chain_spec.replace_para_id(para_id)?; + let relay = self.relay.unwrap_or(RelayChain::PaseoLocal).to_string(); + chain_spec.replace_relay_chain(&relay)?; + let chain_type = self.chain_type.unwrap_or(ChainType::Development).to_string(); + chain_spec.replace_chain_type(&chain_type)?; + if self.protocol_id.is_some() { + let protocol_id = self.protocol_id.unwrap_or(DEFAULT_PROTOCOL_ID.to_string()); + chain_spec.replace_protocol_id(&protocol_id)?; + } + chain_spec.to_file(&plain_chain_spec)?; + + // Generate raw spec. + spinner.set_message("Generating raw chain specification..."); + let spec_name = plain_chain_spec + .file_name() + .and_then(|s| s.to_str()) + .unwrap_or(DEFAULT_SPEC_NAME) + .trim_end_matches(".json"); + let raw_spec_name = format!("{spec_name}-raw.json"); + let raw_chain_spec = + generate_raw_chain_spec(&binary_path, &plain_chain_spec, &raw_spec_name)?; + generated_files.push(format!( + "Raw chain specification file generated at: {}", + raw_chain_spec.display() + )); + + // Generate genesis artifacts. + if self.genesis_code { + spinner.set_message("Generating genesis code..."); + let wasm_file_name = format!("para-{}.wasm", para_id); + let wasm_file = export_wasm_file(&binary_path, &raw_chain_spec, &wasm_file_name)?; + generated_files.push(format!( + "WebAssembly runtime file exported at: {}", + wasm_file.display().to_string() + )); + } + + if self.genesis_state { + spinner.set_message("Generating genesis state..."); + let genesis_file_name = format!("para-{}-genesis-state", para_id); + let genesis_state_file = + generate_genesis_state_file(&binary_path, &raw_chain_spec, &genesis_file_name)?; + generated_files.push(format!( + "Genesis State file exported at: {}", + genesis_state_file.display().to_string() + )); + } + + cli.intro("Building your chain spec".to_string())?; + let generated_files: Vec<_> = generated_files + .iter() + .map(|s| style(format!("{} {s}", console::Emoji("●", ">"))).dim().to_string()) + .collect(); + cli.success(format!("Generated files:\n{}", generated_files.join("\n")))?; + cli.outro(format!( + "Need help? Learn more at {}\n", + style("https://learn.onpop.io").magenta().underlined() + ))?; + + Ok("spec") + } +} + +/// Guide the user to generate their chain specification. +async fn guide_user_to_generate_spec(args: BuildSpecCommand) -> anyhow::Result { + Cli.intro("Generate your chain spec")?; + + // Confirm output path + let default_output = format!("./{DEFAULT_SPEC_NAME}"); + let output_file: String = input("Name of the plain chain spec file. If a path is given, the necessary directories will be created:") + .placeholder(&default_output) + .default_input(&default_output) + .interact()?; + + // Check if specified chain spec already exists, allowing us to default values for prompts + let path = Path::new(&output_file); + let chain_spec = + (path.is_file() && path.exists()).then(|| ChainSpec::from(path).ok()).flatten(); + + // Prompt for chain id. + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_parachain_id()) + .unwrap_or(DEFAULT_PARA_ID as u64) + .to_string(); + let para_id: u32 = input("What parachain ID should be used?") + .placeholder(&default) + .default_input(&default) + .interact()?; + + // Prompt for chain type. + // If relay is Kusama or Polkadot, then Live type is used and user is not prompted. + let chain_type: ChainType; + let mut prompt = cliclack::select("Choose the chain type: ".to_string()); + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_chain_type()) + .and_then(|r| ChainType::from_str(r, true).ok()); + if let Some(chain_type) = default.as_ref() { + prompt = prompt.initial_value(chain_type); + } + for (i, chain_type) in ChainType::VARIANTS.iter().enumerate() { + if default.is_none() && i == 0 { + prompt = prompt.initial_value(chain_type); + } + prompt = prompt.item( + chain_type, + chain_type.get_message().unwrap_or(chain_type.as_ref()), + chain_type.get_detailed_message().unwrap_or_default(), + ); + } + chain_type = prompt.interact()?.clone(); + + // Prompt for relay chain. + let mut prompt = + cliclack::select("Choose the relay chain your chain will be connecting to: ".to_string()); + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_relay_chain()) + .and_then(|r| RelayChain::from_str(r, true).ok()); + if let Some(relay) = default.as_ref() { + prompt = prompt.initial_value(relay); + } + // Prompt relays chains based on the chain type + match chain_type { + ChainType::Live => { + for relay in RelayChain::VARIANTS { + if !matches!( + relay, + RelayChain::Westend + | RelayChain::Paseo | RelayChain::Kusama + | RelayChain::Polkadot + ) { + continue; + } else { + prompt = prompt.item( + relay, + relay.get_message().unwrap_or(relay.as_ref()), + relay.get_detailed_message().unwrap_or_default(), + ); + } + } + }, + _ => { + for relay in RelayChain::VARIANTS { + if matches!( + relay, + RelayChain::Westend + | RelayChain::Paseo | RelayChain::Kusama + | RelayChain::Polkadot + ) { + continue; + } else { + prompt = prompt.item( + relay, + relay.get_message().unwrap_or(relay.as_ref()), + relay.get_detailed_message().unwrap_or_default(), + ); + } + } + }, + } + + let relay_chain = prompt.interact()?.clone(); + + // Prompt for default bootnode if chain type is Local or Live. + let default_bootnode = match chain_type { + ChainType::Development => true, + _ => confirm("Would you like to use local host as a bootnode ?".to_string()).interact()?, + }; + + // Prompt for protocol-id. + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_protocol_id()) + .unwrap_or(DEFAULT_PROTOCOL_ID) + .to_string(); + let protocol_id: String = input("Enter the protocol ID that will identify your network:") + .placeholder(&default) + .default_input(&default) + .interact()?; + + // Prompt for genesis state + let genesis_state = confirm("Should the genesis state file be generated ?".to_string()) + .initial_value(true) + .interact()?; + + // Prompt for genesis code + let genesis_code = confirm("Should the genesis code file be generated ?".to_string()) + .initial_value(true) + .interact()?; + + // Only check user to check their profile selection if a live spec is being built on debug mode. + let profile = + if !args.release && matches!(chain_type, ChainType::Live) { + confirm("Using Debug profile to build a Live specification. Should Release be used instead ?") + .initial_value(true) + .interact()? + } else { + args.release + }; + + Ok(BuildSpecCommand { + output_file: Some(PathBuf::from(output_file)), + release: profile, + id: Some(para_id), + default_bootnode, + chain_type: Some(chain_type), + relay: Some(relay_chain), + protocol_id: Some(protocol_id), + genesis_state, + genesis_code, + }) +} diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 21d489fb..77c77db3 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -90,6 +90,8 @@ impl Command { build::Command::Parachain(cmd) => cmd.execute().map(|_| Value::Null), #[cfg(feature = "contract")] build::Command::Contract(cmd) => cmd.execute().map(|_| Value::Null), + #[cfg(feature = "parachain")] + build::Command::Spec(cmd) => cmd.execute().await.map(|_| Value::Null), }, }, #[cfg(feature = "contract")] diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 2d5ab2df..a261fd89 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -39,26 +39,55 @@ async fn parachain_lifecycle() -> Result<()> { Command::cargo_bin("pop") .unwrap() .current_dir(&temp_dir) - .args(&["build", "--path", "./test_parachain", "--id", "2000"]) + .args(&["build", "--path", "./test_parachain"]) .assert() .success(); assert!(temp_dir.join("test_parachain/target").exists()); - // Assert build files has been generated - assert!(temp_dir.join("test_parachain/raw-parachain-chainspec.json").exists()); - assert!(temp_dir.join("test_parachain/para-2000-wasm.wasm").exists()); - assert!(temp_dir.join("test_parachain/para-2000-genesis-state").exists()); - let content = fs::read_to_string(temp_dir.join("test_parachain/raw-parachain-chainspec.json")) + let temp_parachain_dir = temp_dir.join("test_parachain"); + // pop build spec --output ./target/pop/test-spec.json --id 2222 --type development --relay paseo-local --protocol-id pop-protocol" + Command::cargo_bin("pop") + .unwrap() + .current_dir(&temp_parachain_dir) + .args(&[ + "build", + "spec", + "--output", + "./target/pop/test-spec.json", + "--id", + "2222", + "--type", + "development", + "--relay", + "paseo-local", + "--genesis-state", + "--genesis-code", + "--protocol-id", + "pop-protocol", + ]) + .assert() + .success(); + + // Assert build files have been generated + assert!(temp_parachain_dir.join("target").exists()); + assert!(temp_parachain_dir.join("target/pop/test-spec.json").exists()); + assert!(temp_parachain_dir.join("target/pop/test-spec-raw.json").exists()); + assert!(temp_parachain_dir.join("target/pop/para-2222.wasm").exists()); + assert!(temp_parachain_dir.join("target/pop/para-2222-genesis-state").exists()); + + let content = fs::read_to_string(temp_parachain_dir.join("target/pop/test-spec-raw.json")) .expect("Could not read file"); // Assert custom values has been set propertly - assert!(content.contains("\"para_id\": 2000")); + assert!(content.contains("\"para_id\": 2222")); assert!(content.contains("\"tokenDecimals\": 6")); assert!(content.contains("\"tokenSymbol\": \"POP\"")); + assert!(content.contains("\"relay_chain\": \"paseo-local\"")); + assert!(content.contains("\"protocolId\": \"pop-protocol\"")); // pop up contract -p "./test_parachain" let mut cmd = Cmd::new(cargo_bin("pop")) - .current_dir(&temp_dir.join("test_parachain")) + .current_dir(&temp_parachain_dir) .args(&["up", "parachain", "-f", "./network.toml", "--skip-confirm"]) .spawn() .unwrap(); diff --git a/crates/pop-parachains/README.md b/crates/pop-parachains/README.md index 23169bf1..de941e4d 100644 --- a/crates/pop-parachains/README.md +++ b/crates/pop-parachains/README.md @@ -6,6 +6,7 @@ A crate for generating, building and running parachains and pallets. Used by ## Usage Generate a new parachain: + ```rust,no_run use pop_parachains::{instantiate_template_dir, Config, Parachain}; use std::path::Path; @@ -21,6 +22,7 @@ let tag = instantiate_template_dir(&Parachain::Standard, &destination_path, tag_ ``` Build a Parachain: + ```rust,no_run use pop_common::Profile; use pop_parachains::build_parachain; @@ -31,7 +33,32 @@ let package = None; // The optional package to be built. let binary_path = build_parachain(&path, package, &Profile::Release, None).unwrap(); ``` +Generate a plain chain specification file and customize it with your specific parachain values: + +```rust,no_run +use pop_common::Profile; +use pop_parachains::{build_parachain, export_wasm_file, generate_plain_chain_spec, generate_raw_chain_spec, generate_genesis_state_file, ChainSpec}; +use std::path::Path; + +let path = Path::new("./"); // Location of the parachain project. +let package = None; // The optional package to be built. +// The path to the node binary executable. +let binary_path = build_parachain(&path, package, &Profile::Release, None).unwrap();; +// Generate a plain chain specification file of a parachain +let plain_chain_spec_path = path.join("plain-parachain-chainspec.json"); +generate_plain_chain_spec(&binary_path, &plain_chain_spec_path, true); +// Customize your chain specification +let mut chain_spec = ChainSpec::from(&plain_chain_spec_path).unwrap(); +chain_spec.replace_para_id(2002); +chain_spec.replace_relay_chain("paseo-local"); +chain_spec.replace_chain_type("Development"); +chain_spec.replace_protocol_id("my-protocol"); +// Writes the chain specification to a file +chain_spec.to_file(&plain_chain_spec_path).unwrap(); +``` + Generate a raw chain specification file and export the WASM and genesis state files: + ```rust,no_run use pop_common::Profile; use pop_parachains::{build_parachain, export_wasm_file, generate_plain_chain_spec, generate_raw_chain_spec, generate_genesis_state_file}; @@ -39,21 +66,21 @@ use std::path::Path; let path = Path::new("./"); // Location of the parachain project. let package = None; // The optional package to be built. -let para_id = 2000; // The path to the node binary executable. let binary_path = build_parachain(&path, package, &Profile::Release, None).unwrap();; // Generate a plain chain specification file of a parachain let plain_chain_spec_path = path.join("plain-parachain-chainspec.json"); -generate_plain_chain_spec(&binary_path, &plain_chain_spec_path, para_id); +generate_plain_chain_spec(&binary_path, &plain_chain_spec_path, true); // Generate a raw chain specification file of a parachain let chain_spec = generate_raw_chain_spec(&binary_path, &plain_chain_spec_path, "raw-parachain-chainspec.json").unwrap(); -// Export the WebAssembly runtime for the parachain. -let wasm_file = export_wasm_file(&binary_path, &chain_spec, "para-2000-wasm").unwrap(); +// Export the WebAssembly runtime for the parachain. +let wasm_file = export_wasm_file(&binary_path, &chain_spec, "para-2000-wasm").unwrap(); // Generate the parachain genesis state. -let genesis_state_file = generate_genesis_state_file(&binary_path, &chain_spec, "para-2000-genesis-state").unwrap(); +let genesis_state_file = generate_genesis_state_file(&binary_path, &chain_spec, "para-2000-genesis-state").unwrap(); ``` Run a Parachain: + ```rust,no_run use pop_parachains::Zombienet; use std::path::Path; @@ -86,12 +113,13 @@ tokio_test::block_on(async { let verbose = false; // Whether verbose output is required let missing = zombienet.binaries(); for binary in missing { - binary.source(release, &status, verbose).await; + binary.source(release, &status, verbose).await; } }) ``` Generate a new Pallet: + ```rust,no_run use pop_parachains::{create_pallet_template, TemplatePalletConfig}; @@ -106,4 +134,6 @@ create_pallet_template(Some(path),pallet_config); ``` ## Acknowledgements -`pop-parachains` would not be possible without the awesome crate: [zombienet-sdk](https://github.com/paritytech/zombienet-sdk). + +`pop-parachains` would not be possible without the awesome +crate: [zombienet-sdk](https://github.com/paritytech/zombienet-sdk). diff --git a/crates/pop-parachains/src/build.rs b/crates/pop-parachains/src/build.rs index 6962fc53..dc899af8 100644 --- a/crates/pop-parachains/src/build.rs +++ b/crates/pop-parachains/src/build.rs @@ -3,12 +3,12 @@ use crate::Error; use anyhow::Result; use duct::cmd; -use pop_common::{manifest::from_path, replace_in_file, Profile}; -use serde_json::Value; +use pop_common::{manifest::from_path, Profile}; +use serde_json::{json, Value}; use std::{ - collections::HashMap, fs, path::{Path, PathBuf}, + str::FromStr, }; /// Build the parachain and returns the path to the binary. @@ -41,7 +41,7 @@ pub fn build_parachain( /// # Arguments /// * `path` - The optional path to the manifest, defaulting to the current directory if not specified. pub fn is_supported(path: Option<&Path>) -> Result { - let manifest = pop_common::manifest::from_path(path)?; + let manifest = from_path(path)?; // Simply check for a parachain dependency const DEPENDENCIES: [&str; 4] = ["cumulus-client-collator", "cumulus-primitives-core", "parachains-common", "polkadot-sdk"]; @@ -56,7 +56,7 @@ pub fn is_supported(path: Option<&Path>) -> Result { /// # Arguments /// * `target_path` - The path where the binaries are expected to be found. /// * `node_path` - The path to the node from which the node name will be parsed. -fn binary_path(target_path: &Path, node_path: &Path) -> Result { +pub fn binary_path(target_path: &Path, node_path: &Path) -> Result { let manifest = from_path(Some(node_path))?; let node_name = manifest.package().name(); let release = target_path.join(node_name); @@ -71,18 +71,18 @@ fn binary_path(target_path: &Path, node_path: &Path) -> Result { /// # Arguments /// * `binary_path` - The path to the node binary executable that contains the `build-spec` command. /// * `plain_chain_spec` - Location of the plain_parachain_spec file to be generated. -/// * `para_id` - The parachain ID to be replaced in the specification. +/// * `default_bootnode` - Whether to include localhost as a bootnode. pub fn generate_plain_chain_spec( binary_path: &Path, plain_chain_spec: &Path, - para_id: u32, + default_bootnode: bool, ) -> Result<(), Error> { check_command_exists(&binary_path, "build-spec")?; - cmd(binary_path, vec!["build-spec", "--disable-default-bootnode"]) - .stdout_path(plain_chain_spec) - .run()?; - let generated_para_id = get_parachain_id(plain_chain_spec)?.unwrap_or(para_id.into()) as u32; - replace_para_id(plain_chain_spec.to_path_buf(), para_id, generated_para_id)?; + let mut args = vec!["build-spec"]; + if !default_bootnode { + args.push("--disable-default-bootnode"); + } + cmd(binary_path, args).stdout_path(plain_chain_spec).stderr_null().run()?; Ok(()) } @@ -101,8 +101,7 @@ pub fn generate_raw_chain_spec( return Err(Error::MissingChainSpec(plain_chain_spec.display().to_string())); } check_command_exists(&binary_path, "build-spec")?; - let raw_chain_spec = - plain_chain_spec.parent().unwrap_or(Path::new("./")).join(chain_spec_file_name); + let raw_chain_spec = plain_chain_spec.with_file_name(chain_spec_file_name); cmd( binary_path, vec![ @@ -113,6 +112,7 @@ pub fn generate_raw_chain_spec( "--raw", ], ) + .stderr_null() .stdout_path(&raw_chain_spec) .run()?; Ok(raw_chain_spec) @@ -143,6 +143,8 @@ pub fn export_wasm_file( &wasm_file.display().to_string(), ], ) + .stdout_null() + .stderr_null() .run()?; Ok(wasm_file) } @@ -172,30 +174,12 @@ pub fn generate_genesis_state_file( &genesis_file.display().to_string(), ], ) + .stdout_null() + .stderr_null() .run()?; Ok(genesis_file) } -/// Get the parachain id from the chain specification file. -fn get_parachain_id(chain_spec: &Path) -> Result> { - let data = fs::read_to_string(chain_spec)?; - let value = serde_json::from_str::(&data)?; - Ok(value.get("para_id").and_then(Value::as_u64)) -} - -/// Replaces the generated parachain id in the chain specification file with the provided para_id. -fn replace_para_id(chain_spec: PathBuf, para_id: u32, generated_para_id: u32) -> Result<()> { - let mut replacements_in_cargo: HashMap<&str, &str> = HashMap::new(); - let old_para_id = format!("\"para_id\": {generated_para_id}"); - let new_para_id = format!("\"para_id\": {para_id}"); - replacements_in_cargo.insert(&old_para_id, &new_para_id); - let old_parachain_id = format!("\"parachainId\": {generated_para_id}"); - let new_parachain_id = format!("\"parachainId\": {para_id}"); - replacements_in_cargo.insert(&old_parachain_id, &new_parachain_id); - replace_in_file(chain_spec, replacements_in_cargo)?; - Ok(()) -} - /// Checks if a given command exists and can be executed by running it with the "--help" argument. fn check_command_exists(binary_path: &Path, command: &str) -> Result<(), Error> { cmd(binary_path, vec![command, "--help"]).stdout_null().run().map_err(|_err| { @@ -207,14 +191,132 @@ fn check_command_exists(binary_path: &Path, command: &str) -> Result<(), Error> Ok(()) } +/// A chain specification. +pub struct ChainSpec(Value); +impl ChainSpec { + /// Parses a chain specification from a path. + /// + /// # Arguments + /// * `path` - The path to a chain specification file. + pub fn from(path: &Path) -> Result { + Ok(ChainSpec(Value::from_str(&std::fs::read_to_string(path)?)?)) + } + + /// Get the chain type from the chain specification. + pub fn get_chain_type(&self) -> Option<&str> { + self.0.get("chainType").and_then(|v| v.as_str()) + } + + /// Get the parachain ID from the chain specification. + pub fn get_parachain_id(&self) -> Option { + self.0.get("para_id").and_then(|v| v.as_u64()) + } + + /// Get the protocol ID from the chain specification. + pub fn get_protocol_id(&self) -> Option<&str> { + self.0.get("protocolId").and_then(|v| v.as_str()) + } + + /// Get the relay chain from the chain specification. + pub fn get_relay_chain(&self) -> Option<&str> { + self.0.get("relay_chain").and_then(|v| v.as_str()) + } + + /// Replaces the parachain id with the provided `para_id`. + /// + /// # Arguments + /// * `para_id` - The new value for the para_id. + pub fn replace_para_id(&mut self, para_id: u32) -> Result<(), Error> { + // Replace para_id + let replace = self + .0 + .get_mut("para_id") + .ok_or_else(|| Error::Config("expected `para_id`".into()))?; + *replace = json!(para_id); + + // Replace genesis.runtimeGenesis.patch.parachainInfo.parachainId + let replace = self + .0 + .get_mut("genesis") + .ok_or_else(|| Error::Config("expected `genesis`".into()))? + .get_mut("runtimeGenesis") + .ok_or_else(|| Error::Config("expected `runtimeGenesis`".into()))? + .get_mut("patch") + .ok_or_else(|| Error::Config("expected `patch`".into()))? + .get_mut("parachainInfo") + .ok_or_else(|| Error::Config("expected `parachainInfo`".into()))? + .get_mut("parachainId") + .ok_or_else(|| Error::Config("expected `parachainInfo.parachainId`".into()))?; + *replace = json!(para_id); + Ok(()) + } + + /// Replaces the relay chain name with the given one. + /// + /// # Arguments + /// * `relay_name` - The new value for the relay chain field in the specification. + pub fn replace_relay_chain(&mut self, relay_name: &str) -> Result<(), Error> { + // Replace relay_chain + let replace = self + .0 + .get_mut("relay_chain") + .ok_or_else(|| Error::Config("expected `relay_chain`".into()))?; + *replace = json!(relay_name); + Ok(()) + } + + /// Replaces the chain type with the given one. + /// + /// # Arguments + /// * `chain_type` - The new value for the chain type. + pub fn replace_chain_type(&mut self, chain_type: &str) -> Result<(), Error> { + // Replace chainType + let replace = self + .0 + .get_mut("chainType") + .ok_or_else(|| Error::Config("expected `chainType`".into()))?; + *replace = json!(chain_type); + Ok(()) + } + + /// Replaces the protocol ID with the given one. + /// + /// # Arguments + /// * `protocol_id` - The new value for the protocolId of the given specification. + pub fn replace_protocol_id(&mut self, protocol_id: &str) -> Result<(), Error> { + // Replace protocolId + let replace = self + .0 + .get_mut("protocolId") + .ok_or_else(|| Error::Config("expected `protocolId`".into()))?; + *replace = json!(protocol_id); + Ok(()) + } + + /// Converts the chain specification to a string. + pub fn to_string(&self) -> Result { + Ok(serde_json::to_string_pretty(&self.0)?) + } + + /// Writes the chain specification to a file. + /// + /// # Arguments + /// * `path` - The path to the chain specification file. + pub fn to_file(&self, path: &Path) -> Result<()> { + fs::write(path, self.to_string()?)?; + Ok(()) + } +} + #[cfg(test)] mod tests { use super::*; use crate::{ - new_parachain::instantiate_standard_template, templates::Parachain, Config, Zombienet, + new_parachain::instantiate_standard_template, templates::Parachain, Config, Error, + Zombienet, }; use anyhow::Result; - use pop_common::manifest::{self, Dependency}; + use pop_common::manifest::Dependency; use std::{ fs, fs::{metadata, write}, @@ -225,7 +327,7 @@ mod tests { use tempfile::{tempdir, Builder}; fn setup_template_and_instantiate() -> Result { - let temp_dir = tempfile::tempdir().expect("Failed to create temp dir"); + let temp_dir = tempdir().expect("Failed to create temp dir"); let config = Config { symbol: "DOT".to_string(), decimals: 18, @@ -296,12 +398,12 @@ default_command = "pop-node" fn replace_mock_with_binary(temp_dir: &Path, binary_name: String) -> Result { let binary_path = temp_dir.join(binary_name); let content = fs::read(&binary_path)?; - fs::write(temp_dir.join("target/release/parachain-template-node"), content)?; + write(temp_dir.join("target/release/parachain-template-node"), content)?; // Make executable let mut perms = metadata(temp_dir.join("target/release/parachain-template-node"))?.permissions(); perms.set_mode(0o755); - std::fs::set_permissions(temp_dir.join("target/release/parachain-template-node"), perms)?; + fs::set_permissions(temp_dir.join("target/release/parachain-template-node"), perms)?; Ok(binary_path) } @@ -348,7 +450,7 @@ default_command = "pop-node" } #[tokio::test] - async fn generate_files_works() -> anyhow::Result<()> { + async fn generate_files_works() -> Result<()> { let temp_dir = setup_template_and_instantiate().expect("Failed to setup template and instantiate"); mock_build_process(temp_dir.path())?; @@ -359,9 +461,14 @@ default_command = "pop-node" generate_plain_chain_spec( &binary_path, &temp_dir.path().join("plain-parachain-chainspec.json"), - 2001, + true, )?; assert!(plain_chain_spec.exists()); + { + let mut chain_spec = ChainSpec::from(&plain_chain_spec)?; + chain_spec.replace_para_id(2001)?; + chain_spec.to_file(&plain_chain_spec)?; + } let raw_chain_spec = generate_raw_chain_spec( &binary_path, &plain_chain_spec, @@ -419,42 +526,215 @@ default_command = "pop-node" Ok(()) } + #[test] + fn get_chain_type_works() -> Result<()> { + let chain_spec = ChainSpec(json!({ + "chainType": "test", + })); + assert_eq!(chain_spec.get_chain_type(), Some("test")); + Ok(()) + } + #[test] fn get_parachain_id_works() -> Result<()> { - let mut file = tempfile::NamedTempFile::new()?; - writeln!(file, r#"{{ "name": "Local Testnet", "para_id": 2002 }}"#)?; - let get_parachain_id = get_parachain_id(&file.path())?; - assert_eq!(get_parachain_id, Some(2002)); + let chain_spec = ChainSpec(json!({ + "para_id": 2002, + })); + assert_eq!(chain_spec.get_parachain_id(), Some(2002)); + Ok(()) + } + + #[test] + fn get_protocol_id_works() -> Result<()> { + let chain_spec = ChainSpec(json!({ + "protocolId": "test", + })); + assert_eq!(chain_spec.get_protocol_id(), Some("test")); + Ok(()) + } + + #[test] + fn get_relay_chain_works() -> Result<()> { + let chain_spec = ChainSpec(json!({ + "relay_chain": "test", + })); + assert_eq!(chain_spec.get_relay_chain(), Some("test")); Ok(()) } #[test] fn replace_para_id_works() -> Result<()> { - let temp_dir = tempfile::tempdir()?; - let file_path = temp_dir.path().join("chain-spec.json"); - let mut file = fs::File::create(temp_dir.path().join("chain-spec.json"))?; - writeln!( - file, - r#" - "name": "Local Testnet", - "para_id": 1000, - "parachainInfo": {{ - "parachainId": 1000 - }}, - "# - )?; - replace_para_id(file_path.clone(), 2001, 1000)?; - let content = fs::read_to_string(file_path).expect("Could not read file"); + let mut chain_spec = ChainSpec(json!({ + "para_id": 1000, + "genesis": { + "runtimeGenesis": { + "patch": { + "parachainInfo": { + "parachainId": 1000 + } + } + } + }, + })); + chain_spec.replace_para_id(2001)?; assert_eq!( - content.trim(), - r#" - "name": "Local Testnet", + chain_spec.0, + json!({ "para_id": 2001, - "parachainInfo": { - "parachainId": 2001 + "genesis": { + "runtimeGenesis": { + "patch": { + "parachainInfo": { + "parachainId": 2001 + } + } + } }, - "# - .trim() + }) + ); + Ok(()) + } + + #[test] + fn replace_para_id_fails() -> Result<()> { + let mut chain_spec = ChainSpec(json!({ + "genesis": { + "runtimeGenesis": { + "patch": { + "parachainInfo": { + "parachainId": 1000 + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `para_id`") + ); + chain_spec = ChainSpec(json!({ + "para_id": 2001, + "": { + "runtimeGenesis": { + "patch": { + "parachainInfo": { + "parachainId": 1000 + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `genesis`") + ); + chain_spec = ChainSpec(json!({ + "para_id": 2001, + "genesis": { + "": { + "patch": { + "parachainInfo": { + "parachainId": 1000 + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `runtimeGenesis`") + ); + chain_spec = ChainSpec(json!({ + "para_id": 2001, + "genesis": { + "runtimeGenesis": { + "": { + "parachainInfo": { + "parachainId": 1000 + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `patch`") + ); + chain_spec = ChainSpec(json!({ + "para_id": 2001, + "genesis": { + "runtimeGenesis": { + "patch": { + "": { + "parachainId": 1000 + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `parachainInfo`") + ); + chain_spec = ChainSpec(json!({ + "para_id": 2001, + "genesis": { + "runtimeGenesis": { + "patch": { + "parachainInfo": { + } + } + } + }, + })); + assert!( + matches!(chain_spec.replace_para_id(2001), Err(Error::Config(error)) if error == "expected `parachainInfo.parachainId`") + ); + Ok(()) + } + + #[test] + fn replace_relay_chain_works() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"relay_chain": "old-relay"})); + chain_spec.replace_relay_chain("new-relay")?; + assert_eq!(chain_spec.0, json!({"relay_chain": "new-relay"})); + Ok(()) + } + + #[test] + fn replace_relay_chain_fails() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"": "old-relay"})); + assert!( + matches!(chain_spec.replace_relay_chain("new-relay"), Err(Error::Config(error)) if error == "expected `relay_chain`") + ); + Ok(()) + } + + #[test] + fn replace_chain_type_works() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"chainType": "old-chainType"})); + chain_spec.replace_chain_type("new-chainType")?; + assert_eq!(chain_spec.0, json!({"chainType": "new-chainType"})); + Ok(()) + } + + #[test] + fn replace_chain_type_fails() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"": "old-chainType"})); + assert!( + matches!(chain_spec.replace_chain_type("new-chainType"), Err(Error::Config(error)) if error == "expected `chainType`") + ); + Ok(()) + } + + #[test] + fn replace_protocol_id_works() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"protocolId": "old-protocolId"})); + chain_spec.replace_protocol_id("new-protocolId")?; + assert_eq!(chain_spec.0, json!({"protocolId": "new-protocolId"})); + Ok(()) + } + + #[test] + fn replace_protocol_id_fails() -> Result<()> { + let mut chain_spec = ChainSpec(json!({"": "old-protocolId"})); + assert!( + matches!(chain_spec.replace_protocol_id("new-protocolId"), Err(Error::Config(error)) if error == "expected `protocolId`") ); Ok(()) } @@ -472,8 +752,8 @@ default_command = "pop-node" } #[test] - fn is_supported_works() -> anyhow::Result<()> { - let temp_dir = tempfile::tempdir()?; + fn is_supported_works() -> Result<()> { + let temp_dir = tempdir()?; let path = temp_dir.path(); // Standard rust project @@ -482,7 +762,7 @@ default_command = "pop-node" assert!(!is_supported(Some(&path.join(name)))?); // Parachain - let mut manifest = manifest::from_path(Some(&path.join(name)))?; + let mut manifest = from_path(Some(&path.join(name)))?; manifest .dependencies .insert("cumulus-client-collator".into(), Dependency::Simple("^0.14.0".into())); diff --git a/crates/pop-parachains/src/errors.rs b/crates/pop-parachains/src/errors.rs index 1d6ff960..a97c5eff 100644 --- a/crates/pop-parachains/src/errors.rs +++ b/crates/pop-parachains/src/errors.rs @@ -19,6 +19,8 @@ pub enum Error { EndowmentError, #[error("IO error: {0}")] IO(#[from] std::io::Error), + #[error("JSON error: {0}")] + JsonError(#[from] serde_json::Error), #[error("Missing binary: {0}")] MissingBinary(String), #[error("Missing chain spec file at: {0}")] diff --git a/crates/pop-parachains/src/lib.rs b/crates/pop-parachains/src/lib.rs index 9874dabb..86192db5 100644 --- a/crates/pop-parachains/src/lib.rs +++ b/crates/pop-parachains/src/lib.rs @@ -11,8 +11,8 @@ mod up; mod utils; pub use build::{ - build_parachain, export_wasm_file, generate_genesis_state_file, generate_plain_chain_spec, - generate_raw_chain_spec, is_supported, + binary_path, build_parachain, export_wasm_file, generate_genesis_state_file, + generate_plain_chain_spec, generate_raw_chain_spec, is_supported, ChainSpec, }; pub use errors::Error; pub use indexmap::IndexSet;