From 1bc52dc9ccf6ed88bfa358fcb692e41d442b5621 Mon Sep 17 00:00:00 2001 From: Alex Bean Date: Thu, 5 Dec 2024 20:38:41 +0100 Subject: [PATCH] fix: build spec experience (#331) * fix: add chain to specify the chain specification * fix: default_bootnode by default to true * chore: fmt * chore: deprecate flag --release in build specs * fix: clean output (#334) * fix: undo deprecation of --release flag * refactor: small fix * style: remove extra space * fix(spec): better handling of spinner * style: use spinner instead of multispinner * docs: help message to include build * feat: reuse existing chain spec * refactor: remove clone * refactor: opt in to edit provided chain spec * docs: improve * refactor: flow flag input * fix: prepare_output_path * refactor: resolve small improvements * fix: protocol id prompt * fix: spinner * fix: docs * test: test cli * chore: refactor * chore: amend test * feat: production profile * refactor: improve profile experience * chore: feedback and rebase * chore: add profile tests * fix(test): parachain_lifecycle * style: fmt * fix: clippy * fix: cli required changes introduced by PR * fix: test * fix: clippy * docs: deprecation message --------- Co-authored-by: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> Co-authored-by: Daanvdplas --- Cargo.lock | 10 + Cargo.toml | 1 + crates/pop-cli/src/cli.rs | 67 +- crates/pop-cli/src/commands/build/mod.rs | 80 +- .../pop-cli/src/commands/build/parachain.rs | 31 +- crates/pop-cli/src/commands/build/spec.rs | 903 +++++++++++++----- crates/pop-cli/src/commands/call/contract.rs | 67 +- crates/pop-cli/src/commands/mod.rs | 4 +- crates/pop-cli/tests/parachain.rs | 7 +- crates/pop-common/Cargo.toml | 3 +- crates/pop-common/src/build.rs | 97 +- crates/pop-common/src/manifest.rs | 79 +- crates/pop-parachains/README.md | 4 +- crates/pop-parachains/src/build.rs | 98 +- crates/pop-parachains/src/utils/onboard.rs | 0 15 files changed, 1088 insertions(+), 363 deletions(-) create mode 100644 crates/pop-parachains/src/utils/onboard.rs diff --git a/Cargo.lock b/Cargo.lock index cd6ef4ab..3472f50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4782,6 +4782,7 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "toml 0.5.11", "toml_edit 0.22.20", "url", ] @@ -7393,6 +7394,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + [[package]] name = "toml" version = "0.7.8" diff --git a/Cargo.toml b/Cargo.toml index 49ca3e68..f9304fca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ tar = "0.4.40" tempfile = "3.10" thiserror = "1.0.58" tokio-test = "0.4.4" +toml = "0.5.0" # networking reqwest = { version = "0.12", features = ["json"] } diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 968c1a4f..6c6bb3bc 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -68,6 +68,8 @@ pub(crate) mod traits { /// A select prompt. pub trait Select { + /// Sets the initially selected value. + fn initial_value(self, initial_value: T) -> Self; /// Starts the prompt interaction. fn interact(&mut self) -> Result; /// Adds an item to the selection prompt. @@ -133,20 +135,22 @@ impl traits::Cli for Cli { /// A confirmation prompt using cliclack. struct Confirm(cliclack::Confirm); + impl traits::Confirm for Confirm { - /// Starts the prompt interaction. - fn interact(&mut self) -> Result { - self.0.interact() - } /// Sets the initially selected value. fn initial_value(mut self, initial_value: bool) -> Self { self.0 = self.0.initial_value(initial_value); self } + /// Starts the prompt interaction. + fn interact(&mut self) -> Result { + self.0.interact() + } } /// A input prompt using cliclack. struct Input(cliclack::Input); + impl traits::Input for Input { /// Sets the default value for the input. fn default_input(mut self, value: &str) -> Self { @@ -203,6 +207,12 @@ impl traits::MultiSelect for MultiSelect { struct Select(cliclack::Select); impl traits::Select for Select { + /// Sets the initially selected value. + fn initial_value(mut self, initial_value: T) -> Self { + self.0 = self.0.initial_value(initial_value); + self + } + /// Starts the prompt interaction. fn interact(&mut self) -> Result { self.0.interact() @@ -231,8 +241,7 @@ pub(crate) mod tests { multiselect_expectation: Option<(String, Option, bool, Option>)>, outro_cancel_expectation: Option, - select_expectation: - Option<(String, Option, bool, Option>, usize)>, + select_expectation: Vec<(String, Option, bool, Option>, usize)>, success_expectations: Vec, warning_expectations: Vec, } @@ -243,17 +252,17 @@ pub(crate) mod tests { } pub(crate) fn expect_confirm(mut self, prompt: impl Display, confirm: bool) -> Self { - self.confirm_expectation.push((prompt.to_string(), confirm)); + self.confirm_expectation.insert(0, (prompt.to_string(), confirm)); self } pub(crate) fn expect_input(mut self, prompt: impl Display, input: String) -> Self { - self.input_expectations.push((prompt.to_string(), input)); + self.input_expectations.insert(0, (prompt.to_string(), input)); self } pub(crate) fn expect_info(mut self, message: impl Display) -> Self { - self.info_expectations.push(message.to_string()); + self.info_expectations.insert(0, message.to_string()); self } @@ -283,7 +292,7 @@ pub(crate) mod tests { self } - pub(crate) fn expect_select( + pub(crate) fn expect_select( mut self, prompt: impl Display, required: Option, @@ -291,7 +300,8 @@ pub(crate) mod tests { items: Option>, item: usize, ) -> Self { - self.select_expectation = Some((prompt.to_string(), required, collect, items, item)); + self.select_expectation + .insert(0, (prompt.to_string(), required, collect, items, item)); self } @@ -327,8 +337,15 @@ pub(crate) mod tests { if let Some(expectation) = self.outro_cancel_expectation { panic!("`{expectation}` outro cancel expectation not satisfied") } - if let Some((prompt, _, _, _, _)) = self.select_expectation { - panic!("`{prompt}` select prompt expectation not satisfied") + if !self.select_expectation.is_empty() { + panic!( + "`{}` select prompt expectation not satisfied", + self.select_expectation + .iter() + .map(|(s, _, _, _, _)| s.clone()) // Extract the `String` part + .collect::>() + .join(", ") + ); } if !self.success_expectations.is_empty() { panic!( @@ -425,10 +442,16 @@ pub(crate) mod tests { fn select(&mut self, prompt: impl Display) -> impl Select { let prompt = prompt.to_string(); if let Some((expectation, _, collect, items_expectation, item)) = - self.select_expectation.take() + self.select_expectation.pop() { assert_eq!(expectation, prompt, "prompt does not satisfy expectation"); - return MockSelect { items_expectation, collect, items: vec![], item }; + return MockSelect { + items_expectation, + collect, + items: vec![], + item, + initial_value: None, + }; } MockSelect::default() @@ -553,15 +576,27 @@ pub(crate) mod tests { collect: bool, items: Vec, item: usize, + initial_value: Option, } impl MockSelect { pub(crate) fn default() -> Self { - Self { items_expectation: None, collect: false, items: vec![], item: 0 } + Self { + items_expectation: None, + collect: false, + items: vec![], + item: 0, + initial_value: None, + } } } impl Select for MockSelect { + fn initial_value(mut self, initial_value: T) -> Self { + self.initial_value = Some(initial_value); + self + } + fn interact(&mut self) -> Result { Ok(self.items[self.item].clone()) } diff --git a/crates/pop-cli/src/commands/build/mod.rs b/crates/pop-cli/src/commands/build/mod.rs index 08af079b..f1a69c9c 100644 --- a/crates/pop-cli/src/commands/build/mod.rs +++ b/crates/pop-cli/src/commands/build/mod.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; #[cfg(feature = "contract")] use contract::BuildContractCommand; use duct::cmd; +use pop_common::Profile; use std::path::PathBuf; #[cfg(feature = "parachain")] use {parachain::BuildParachainCommand, spec::BuildSpecCommand}; @@ -29,8 +30,11 @@ pub(crate) struct BuildArgs { #[arg(short = 'p', long)] pub(crate) package: Option, /// For production, always build in release mode to exclude debug features. - #[clap(short, long)] + #[clap(short, long, conflicts_with = "profile")] pub(crate) release: bool, + /// Build profile [default: debug]. + #[clap(long, value_enum)] + pub(crate) profile: Option, /// Parachain ID to be used when generating the chain spec files. #[arg(short = 'i', long = "id")] #[cfg(feature = "parachain")] @@ -62,19 +66,26 @@ impl Command { #[cfg(feature = "contract")] if pop_contracts::is_supported(args.path.as_deref())? { // All commands originating from root command are valid - BuildContractCommand { path: args.path, release: args.release, valid: true } - .execute()?; + let release = match args.profile { + Some(profile) => profile.into(), + None => args.release, + }; + BuildContractCommand { path: args.path, release, valid: true }.execute()?; return Ok("contract"); } // If only parachain feature enabled, build as parachain #[cfg(feature = "parachain")] if pop_parachains::is_supported(args.path.as_deref())? { + let profile = match args.profile { + Some(profile) => profile, + None => args.release.into(), + }; // All commands originating from root command are valid BuildParachainCommand { path: args.path, package: args.package, - release: args.release, + profile: Some(profile), id: args.id, valid: true, } @@ -101,13 +112,15 @@ impl Command { _args.push("--package"); _args.push(package) } - if args.release { + let profile = args.profile.unwrap_or(Profile::Debug); + if profile == Profile::Release { _args.push("--release"); + } else if profile == Profile::Production { + _args.push("--profile=production"); } cmd("cargo", _args).dir(args.path.unwrap_or_else(|| "./".into())).run()?; - let mode = if args.release { "RELEASE" } else { "DEBUG" }; - cli.info(format!("The {project} was built in {mode} mode."))?; + cli.info(format!("The {project} was built in {} mode.", profile))?; cli.outro("Build completed successfully!")?; Ok(project) } @@ -117,41 +130,46 @@ impl Command { mod tests { use super::*; use cli::MockCli; + use pop_common::manifest::add_production_profile; + use strum::VariantArray; #[test] fn build_works() -> anyhow::Result<()> { let name = "hello_world"; let temp_dir = tempfile::tempdir()?; let path = temp_dir.path(); + let project_path = path.join(name); cmd("cargo", ["new", name, "--bin"]).dir(&path).run()?; + add_production_profile(&project_path)?; for package in [None, Some(name.to_string())] { - for release in [false, true] { - let project = if package.is_some() { "package" } else { "project" }; - let mode = if release { "RELEASE" } else { "DEBUG" }; - let mut cli = MockCli::new() - .expect_intro(format!("Building your {project}")) - .expect_info(format!("The {project} was built in {mode} mode.")) - .expect_outro("Build completed successfully!"); - - assert_eq!( - Command::build( - BuildArgs { - command: None, - path: Some(path.join(name)), - package: package.clone(), - release, - id: None, - }, - &mut cli, - )?, - project - ); - - cli.verify()?; + for release in [true, false] { + for profile in Profile::VARIANTS { + let profile = if release { Profile::Release } else { profile.clone() }; + let project = if package.is_some() { "package" } else { "project" }; + let mut cli = MockCli::new() + .expect_intro(format!("Building your {project}")) + .expect_info(format!("The {project} was built in {profile} mode.")) + .expect_outro("Build completed successfully!"); + + assert_eq!( + Command::build( + BuildArgs { + command: None, + path: Some(project_path.clone()), + package: package.clone(), + release, + profile: Some(profile.clone()), + id: None, + }, + &mut cli, + )?, + project + ); + cli.verify()?; + } } } - Ok(()) } } diff --git a/crates/pop-cli/src/commands/build/parachain.rs b/crates/pop-cli/src/commands/build/parachain.rs index a6e47c67..67000e62 100644 --- a/crates/pop-cli/src/commands/build/parachain.rs +++ b/crates/pop-cli/src/commands/build/parachain.rs @@ -16,9 +16,9 @@ pub struct BuildParachainCommand { /// The package to be built. #[arg(short = 'p', long)] pub(crate) package: Option, - /// For production, always build in release mode to exclude debug features. - #[clap(short, long, default_value = "true")] - pub(crate) release: bool, + /// Build profile [default: debug]. + #[clap(long, value_enum)] + pub(crate) profile: Option, /// Parachain ID to be used when generating the chain spec files. #[arg(short = 'i', long = "id")] pub(crate) id: Option, @@ -41,12 +41,13 @@ impl BuildParachainCommand { let project = if self.package.is_some() { "package" } else { "parachain" }; cli.intro(format!("Building your {project}"))?; + let profile = self.profile.unwrap_or(Profile::Debug); // Show warning if specified as deprecated. if !self.valid { cli.warning("NOTE: this command is deprecated. Please use `pop build` (or simply `pop b`) in future...")?; #[cfg(not(test))] sleep(Duration::from_secs(3)) - } else if !self.release { + } else if profile == Profile::Debug { cli.warning("NOTE: this command now defaults to DEBUG builds. Please use `--release` (or simply `-r`) for a release build...")?; #[cfg(not(test))] sleep(Duration::from_secs(3)) @@ -55,9 +56,8 @@ impl BuildParachainCommand { // Build parachain. cli.warning("NOTE: this may take some time...")?; let project_path = self.path.unwrap_or_else(|| PathBuf::from("./")); - let mode: Profile = self.release.into(); - let binary = build_parachain(&project_path, self.package, &mode, None)?; - cli.info(format!("The {project} was built in {mode} mode."))?; + let binary = build_parachain(&project_path, self.package, &profile, None)?; + cli.info(format!("The {project} was built in {} mode.", profile))?; cli.outro("Build completed successfully!")?; let generated_files = [format!("Binary generated at: {}", binary.display())]; let generated_files: Vec<_> = generated_files @@ -79,7 +79,9 @@ mod tests { use super::*; use cli::MockCli; use duct::cmd; + use pop_common::manifest::add_production_profile; use std::{fs, io::Write, path::Path}; + use strum::VariantArray; // Function that generates a Cargo.toml inside node directory for testing. fn generate_mock_node(temp_dir: &Path) -> anyhow::Result<()> { @@ -106,33 +108,34 @@ mod tests { let name = "hello_world"; let temp_dir = tempfile::tempdir()?; let path = temp_dir.path(); + let project_path = path.join(name); cmd("cargo", ["new", name, "--bin"]).dir(&path).run()?; - generate_mock_node(&temp_dir.path().join(name))?; + add_production_profile(&project_path)?; + generate_mock_node(&project_path)?; for package in [None, Some(name.to_string())] { - for release in [false, true] { + for profile in Profile::VARIANTS { for valid in [false, true] { let project = if package.is_some() { "package" } else { "parachain" }; - let mode = if release { Profile::Release } else { Profile::Debug }; let mut cli = MockCli::new() .expect_intro(format!("Building your {project}")) .expect_warning("NOTE: this may take some time...") - .expect_info(format!("The {project} was built in {mode} mode.")) + .expect_info(format!("The {project} was built in {profile} mode.")) .expect_outro("Build completed successfully!"); if !valid { cli = cli.expect_warning("NOTE: this command is deprecated. Please use `pop build` (or simply `pop b`) in future..."); } else { - if !release { + if profile == &Profile::Debug { cli = cli.expect_warning("NOTE: this command now defaults to DEBUG builds. Please use `--release` (or simply `-r`) for a release build..."); } } assert_eq!( BuildParachainCommand { - path: Some(path.join(name)), + path: Some(project_path.clone()), package: package.clone(), - release, + profile: Some(profile.clone()), id: None, valid, } diff --git a/crates/pop-cli/src/commands/build/spec.rs b/crates/pop-cli/src/commands/build/spec.rs index 11c94ff0..3cde4c61 100644 --- a/crates/pop-cli/src/commands/build/spec.rs +++ b/crates/pop-cli/src/commands/build/spec.rs @@ -2,11 +2,14 @@ use crate::{ cli, - cli::{traits::Cli as _, Cli}, + cli::{ + traits::{Cli as _, *}, + Cli, + }, style::style, }; use clap::{Args, ValueEnum}; -use cliclack::{confirm, input}; +use cliclack::spinner; use pop_common::Profile; use pop_parachains::{ binary_path, build_parachain, export_wasm_file, generate_genesis_state_file, @@ -22,6 +25,7 @@ use std::{thread::sleep, time::Duration}; use strum::{EnumMessage, VariantArray}; use strum_macros::{AsRefStr, Display, EnumString}; +const DEFAULT_CHAIN: &str = "dev"; const DEFAULT_PARA_ID: u32 = 2000; const DEFAULT_PROTOCOL_ID: &str = "my-protocol"; const DEFAULT_SPEC_NAME: &str = "chain-spec.json"; @@ -127,169 +131,392 @@ pub(crate) enum RelayChain { PolkadotLocal, } -#[derive(Args)] +/// Command for generating a chain specification. +#[derive(Args, Default)] 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")] + /// [DEPRECATED] and will be removed in v0.7.0, use `profile`. + #[arg(short = 'r', long, conflicts_with = "profile")] pub(crate) release: bool, + /// Build profile for the binary to generate the chain specification. + #[arg(long, value_enum)] + pub(crate) profile: Option, /// Parachain ID to be used when generating the chain spec files. - #[arg(short = 'i', long = "id")] + #[arg(short = 'i', long)] pub(crate) id: Option, /// Whether to keep localhost as a bootnode. - #[clap(long, default_value = "true")] + #[arg(long)] pub(crate) default_bootnode: bool, - /// Type of the chain [default: development]. + /// Type of the chain. #[arg(short = 't', long = "type", value_enum)] pub(crate) chain_type: Option, - /// Relay chain this parachain will connect to [default: paseo-local]. + /// Provide the chain specification to use (e.g. dev, local, custom or a path to an existing + /// file). + #[arg(short = 'c', long = "chain")] + pub(crate) chain: Option, + /// Relay chain this parachain will connect to. #[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")] + /// Whether the genesis state file should be generated. + #[arg(long = "genesis-state")] pub(crate) genesis_state: bool, - /// Whether the genesis code file should be generated [default: true]. - #[clap(long = "genesis-code", default_value = "true")] + /// Whether the genesis code file should be generated. + #[arg(long = "genesis-code")] pub(crate) genesis_code: bool, } impl BuildSpecCommand { - /// Executes the command. + /// Executes the build spec command. pub(crate) async fn execute(self) -> anyhow::Result<&'static str> { + let mut cli = Cli; + cli.intro("Generate your chain spec")?; // 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) - }, - }; - Ok("spec") + let build_spec = self.configure_build_spec(&mut cli).await?; + build_spec.build(&mut cli) } else { - Cli.intro("Building your chain spec")?; - Cli.outro_cancel( + cli.outro_cancel( "🚫 Can't build a specification for target. Maybe not a chain project ?", )?; Ok("spec") } } - /// Builds a parachain spec. + /// Configure chain specification requirements by prompting for missing inputs, validating + /// provided values, and preparing a BuildSpec to generate file(s). /// /// # 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)) - } + /// * `cli` - The cli. + async fn configure_build_spec( + self, + cli: &mut impl cli::traits::Cli, + ) -> anyhow::Result { + let BuildSpecCommand { + output_file, + profile, + release, + id, + default_bootnode, + chain_type, + chain, + relay, + protocol_id, + genesis_state, + genesis_code, + } = self; - let spinner = cliclack::spinner(); - spinner.start("Generating chain specification..."); + // Chain. + let chain = match chain { + Some(chain) => chain, + _ => { + // Prompt for chain if not provided. + cli.input("Provide the chain specification to use (e.g. dev, local, custom or a path to an existing file)") + .placeholder(DEFAULT_CHAIN) + .default_input(DEFAULT_CHAIN) + .interact()? + }, + }; - // 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)?; + // Output file. + let maybe_chain_spec_file = PathBuf::from(&chain); + // Check if the provided chain specification is a file. + let (output_file, prompt) = if maybe_chain_spec_file.exists() && + maybe_chain_spec_file.is_file() + { + if output_file.is_some() { + cli.warning("NOTE: If an existing chain spec file is provided it will be used for the output path.")?; } - plain_chain_spec = output_path.join(DEFAULT_SPEC_NAME); + // Prompt whether the user wants to make additional changes to the provided chain spec + // file. + let prompt = cli.confirm("An existing chain spec file is provided. Do you want to make additional changes to it?".to_string()) + .initial_value(false) + .interact()?; + // Set the provided chain specification file as output file and whether to prompt the + // user for additional changes to the provided spec. + (maybe_chain_spec_file, prompt) } 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"); + let output_file = match output_file { + Some(output) => output, + None => { + // Prompt for output file if not provided. + let default_output = format!("./{DEFAULT_SPEC_NAME}"); + PathBuf::from( + cli.input("Name or path for the plain chain spec file:") + .placeholder(&default_output) + .default_input(&default_output) + .interact()?, + ) + }, + }; + (prepare_output_path(&output_file)?, true) + }; + // If chain specification file already exists, obtain values for defaults when prompting. + let chain_spec = ChainSpec::from(&output_file).ok(); - // 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_directory(&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)? + // Para id. + let id = match id { + Some(id) => id, + None => { + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_parachain_id().map(|id| id as u32)) + .unwrap_or(DEFAULT_PARA_ID); + if prompt { + // Prompt for para id. + let default_str = default.to_string(); + cli.input("What parachain ID should be used?") + .default_input(&default_str) + .default_input(&default_str) + .interact()? + .parse::() + .unwrap_or(DEFAULT_PARA_ID) + } else { + default + } }, }; - // Generate plain spec. - spinner.set_message("Generating plain chain specification..."); + // Chain type. + let chain_type = match chain_type { + Some(chain_type) => chain_type, + None => { + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_chain_type()) + .and_then(|r| ChainType::from_str(r, true).ok()) + .unwrap_or_default(); + if prompt { + // Prompt for chain type. + let mut prompt = + cli.select("Choose the chain type: ".to_string()).initial_value(&default); + for chain_type in ChainType::VARIANTS { + prompt = prompt.item( + chain_type, + chain_type.get_message().unwrap_or(chain_type.as_ref()), + chain_type.get_detailed_message().unwrap_or_default(), + ); + } + prompt.interact()?.clone() + } else { + default + } + }, + }; + + // Relay. + let relay = match relay { + Some(relay) => relay, + None => { + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_relay_chain()) + .and_then(|r| RelayChain::from_str(r, true).ok()) + .unwrap_or_default(); + if prompt { + // Prompt for relay. + let mut prompt = cli + .select("Choose the relay your chain will be connecting to: ".to_string()) + .initial_value(&default); + for relay in RelayChain::VARIANTS { + prompt = prompt.item( + relay, + relay.get_message().unwrap_or(relay.as_ref()), + relay.get_detailed_message().unwrap_or_default(), + ); + } + prompt.interact()?.clone() + } else { + default + } + }, + }; + + // Prompt user for build profile. + let mut profile = match profile { + Some(profile) => profile, + None => { + let default = Profile::Release; + if prompt && !release { + // Prompt for build profile. + let mut prompt = cli + .select( + "Choose the build profile of the binary that should be used: " + .to_string(), + ) + .initial_value(&default); + for profile in Profile::VARIANTS { + prompt = prompt.item( + profile, + profile.get_message().unwrap_or(profile.as_ref()), + profile.get_detailed_message().unwrap_or_default(), + ); + } + prompt.interact()?.clone() + } else { + default + } + }, + }; + + // Protocol id. + let protocol_id = match protocol_id { + Some(protocol_id) => protocol_id, + None => { + let default = chain_spec + .as_ref() + .and_then(|cs| cs.get_protocol_id()) + .unwrap_or(DEFAULT_PROTOCOL_ID) + .to_string(); + if prompt { + // Prompt for protocol id. + cli.input("Enter the protocol ID that will identify your network:") + .placeholder(&default) + .default_input(&default) + .interact()? + } else { + default + } + }, + }; + + // Prompt for default bootnode if not provided and chain type is Local or Live. + let default_bootnode = if !default_bootnode { + match chain_type { + ChainType::Development => true, + _ => cli + .confirm("Would you like to use local host as a bootnode ?".to_string()) + .interact()?, + } + } else { + true + }; + + // Prompt for genesis state if not provided. + let genesis_state = if !genesis_state { + cli.confirm("Should the genesis state file be generated ?".to_string()) + .initial_value(true) + .interact()? + } else { + true + }; + + // Prompt for genesis code if not provided. + let genesis_code = if !genesis_code { + cli.confirm("Should the genesis code file be generated ?".to_string()) + .initial_value(true) + .interact()? + } else { + true + }; + + if release { + cli.warning("NOTE: release flag is deprecated. Use `--profile` instead.")?; + #[cfg(not(test))] + sleep(Duration::from_secs(3)); + profile = Profile::Release; + } + + Ok(BuildSpec { + output_file, + profile, + id, + default_bootnode, + chain_type, + chain, + relay, + protocol_id, + genesis_state, + genesis_code, + }) + } +} + +// Represents the configuration for building a chain specification. +#[derive(Debug)] +struct BuildSpec { + output_file: PathBuf, + profile: Profile, + id: u32, + default_bootnode: bool, + chain_type: ChainType, + chain: String, + relay: RelayChain, + protocol_id: String, + genesis_state: bool, + genesis_code: bool, +} + +impl BuildSpec { + // Executes the process of generating the chain specification. + // + // This function generates plain and raw chain spec files based on the provided configuration, + // optionally including genesis state and runtime artifacts. If the node binary is missing, + // it triggers a build process. + fn build(self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<&'static str> { + cli.intro("Building your chain spec")?; let mut generated_files = vec![]; - generate_plain_chain_spec(&binary_path, &plain_chain_spec, self.default_bootnode)?; + let BuildSpec { + ref output_file, + ref profile, + id, + default_bootnode, + ref chain, + genesis_state, + genesis_code, + .. + } = self; + // Ensure binary is built. + let binary_path = ensure_binary_exists(cli, profile)?; + let spinner = spinner(); + spinner.start("Generating chain specification..."); + + // Generate chain spec. + generate_plain_chain_spec(&binary_path, output_file, default_bootnode, chain)?; + // Customize spec based on input. + self.customize()?; generated_files.push(format!( "Plain text chain specification file generated at: {}", - plain_chain_spec.display() + &output_file.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 + let spec_name = &output_file .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)?; + let raw_chain_spec = generate_raw_chain_spec(&binary_path, output_file, &raw_spec_name)?; generated_files.push(format!( "Raw chain specification file generated at: {}", raw_chain_spec.display() )); // Generate genesis artifacts. - if self.genesis_code { + if genesis_code { spinner.set_message("Generating genesis code..."); - let wasm_file_name = format!("para-{}.wasm", para_id); + let wasm_file_name = format!("para-{}.wasm", 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())); } - - if self.genesis_state { + if genesis_state { spinner.set_message("Generating genesis state..."); - let genesis_file_name = format!("para-{}-genesis-state", para_id); + let genesis_file_name = format!("para-{}-genesis-state", 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())); } - cli.intro("Building your chain spec".to_string())?; + spinner.stop("Chain specification built successfully."); let generated_files: Vec<_> = generated_files .iter() .map(|s| style(format!("{} {s}", console::Emoji("●", ">"))).dim().to_string()) @@ -299,156 +526,370 @@ impl BuildSpecCommand { "Need help? Learn more at {}\n", style("https://learn.onpop.io").magenta().underlined() ))?; - Ok("spec") } + + // Customize a chain specification. + fn customize(&self) -> anyhow::Result<()> { + let mut chain_spec = ChainSpec::from(&self.output_file)?; + chain_spec.replace_para_id(self.id)?; + chain_spec.replace_relay_chain(self.relay.as_ref())?; + chain_spec.replace_chain_type(self.chain_type.as_ref())?; + chain_spec.replace_protocol_id(&self.protocol_id)?; + chain_spec.to_file(&self.output_file)?; + Ok(()) + } } -/// 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 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); +// Locate binary, if it doesn't exist trigger build. +fn ensure_binary_exists( + cli: &mut impl cli::traits::Cli, + mode: &Profile, +) -> anyhow::Result { + let cwd = current_dir().unwrap_or(PathBuf::from("./")); + match binary_path(&mode.target_directory(&cwd), &cwd.join("node")) { + Ok(binary_path) => Ok(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).map_err(|e| e.into()) + }, } - for (i, chain_type) in ChainType::VARIANTS.iter().enumerate() { - if default.is_none() && i == 0 { - prompt = prompt.initial_value(chain_type); +} + +// Prepare the output path provided. +fn prepare_output_path(output_path: impl AsRef) -> anyhow::Result { + let mut output_path = output_path.as_ref().to_path_buf(); + // Check if the path ends with '.json' + let is_json_file = output_path + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.eq_ignore_ascii_case("json")) + .unwrap_or(false); + + if !is_json_file { + // Treat as directory. + if !output_path.exists() { + create_dir_all(&output_path)?; + } + output_path.push(DEFAULT_SPEC_NAME); + } else { + // Treat as file. + if let Some(parent_dir) = output_path.parent() { + if !parent_dir.exists() { + create_dir_all(parent_dir)?; + } } - prompt = prompt.item( - chain_type, - chain_type.get_message().unwrap_or(chain_type.as_ref()), - chain_type.get_detailed_message().unwrap_or_default(), - ); } - let chain_type: ChainType = 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); + Ok(output_path) +} + +#[cfg(test)] +mod tests { + use super::{ChainType::*, RelayChain::*, *}; + use crate::cli::MockCli; + use std::{fs::create_dir_all, path::PathBuf}; + use tempfile::{tempdir, TempDir}; + + #[tokio::test] + async fn configure_build_spec_works() -> anyhow::Result<()> { + let chain = "local"; + let chain_type = Live; + let default_bootnode = true; + let genesis_code = true; + let genesis_state = true; + let output_file = "artifacts/chain-spec.json"; + let para_id = 4242; + let protocol_id = "pop"; + let relay = Polkadot; + let release = false; + let profile = Profile::Production; + + for build_spec_cmd in [ + // No flags used. + BuildSpecCommand::default(), + // All flags used. + BuildSpecCommand { + output_file: Some(PathBuf::from(output_file)), + profile: Some(profile.clone()), + release, + id: Some(para_id), + default_bootnode, + chain_type: Some(chain_type.clone()), + chain: Some(chain.to_string()), + relay: Some(relay.clone()), + protocol_id: Some(protocol_id.to_string()), + genesis_state, + genesis_code, + }, + ] { + let mut cli = MockCli::new(); + // If no flags are provided. + if build_spec_cmd.chain.is_none() { + cli = cli + .expect_input("Provide the chain specification to use (e.g. dev, local, custom or a path to an existing file)", chain.to_string()) + .expect_input( + "Name or path for the plain chain spec file:", output_file.to_string()) + .expect_input( + "What parachain ID should be used?", para_id.to_string()) + .expect_input( + "Enter the protocol ID that will identify your network:", protocol_id.to_string()) + .expect_select( + "Choose the chain type: ", + Some(false), + true, + Some(chain_types()), + chain_type.clone() as usize, + ).expect_select( + "Choose the relay your chain will be connecting to: ", + Some(false), + true, + Some(relays()), + relay.clone() as usize, + ).expect_select( + "Choose the build profile of the binary that should be used: ", + Some(false), + true, + Some(profiles()), + profile.clone() as usize + ).expect_confirm("Would you like to use local host as a bootnode ?", default_bootnode + ).expect_confirm("Should the genesis state file be generated ?", genesis_state + ).expect_confirm("Should the genesis code file be generated ?", genesis_code); + } + let build_spec = build_spec_cmd.configure_build_spec(&mut cli).await?; + assert_eq!(build_spec.chain, chain); + assert_eq!(build_spec.output_file, PathBuf::from(output_file)); + assert_eq!(build_spec.id, para_id); + assert_eq!(build_spec.profile, profile); + assert_eq!(build_spec.default_bootnode, default_bootnode); + assert_eq!(build_spec.chain_type, chain_type); + assert_eq!(build_spec.relay, relay); + assert_eq!(build_spec.protocol_id, protocol_id); + assert_eq!(build_spec.genesis_state, genesis_state); + assert_eq!(build_spec.genesis_code, genesis_code); + cli.verify()?; + } + Ok(()) } - // 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(), - ); + + #[tokio::test] + async fn configure_build_spec_with_existing_chain_file() -> anyhow::Result<()> { + let chain_type = Live; + let default_bootnode = true; + let genesis_code = true; + let genesis_state = true; + let output_file = "artifacts/chain-spec.json"; + let para_id = 4242; + let protocol_id = "pop"; + let relay = Polkadot; + let release = false; + let profile = Profile::Production; + + // Create a temporary file to act as the existing chain spec file. + let temp_dir = tempdir()?; + let chain_spec_path = temp_dir.path().join("existing-chain-spec.json"); + std::fs::write(&chain_spec_path, "{}")?; // Write a dummy JSON to the file. + + // Whether to make changes to the provided chain spec file. + for changes in [true, false] { + for build_spec_cmd in [ + // No flags used except the provided chain spec file. + BuildSpecCommand { + chain: Some(chain_spec_path.to_string_lossy().to_string()), + ..Default::default() + }, + // All flags used. + BuildSpecCommand { + output_file: Some(PathBuf::from(output_file)), + profile: Some(profile.clone()), + release, + id: Some(para_id), + default_bootnode, + chain_type: Some(chain_type.clone()), + chain: Some(chain_spec_path.to_string_lossy().to_string()), + relay: Some(relay.clone()), + protocol_id: Some(protocol_id.to_string()), + genesis_state, + genesis_code, + }, + ] { + let mut cli = MockCli::new().expect_confirm( + "An existing chain spec file is provided. Do you want to make additional changes to it?", + changes, + ); + // When user wants to make changes to chain spec file via prompts and no flags + // provided. + let no_flags_used = build_spec_cmd.relay.is_none(); + if changes && no_flags_used { + if build_spec_cmd.id.is_none() { + cli = cli + .expect_input("What parachain ID should be used?", para_id.to_string()); + } + if build_spec_cmd.protocol_id.is_none() { + cli = cli.expect_input( + "Enter the protocol ID that will identify your network:", + protocol_id.to_string(), + ); + } + if build_spec_cmd.chain_type.is_none() { + cli = cli.expect_select( + "Choose the chain type: ", + Some(false), + true, + Some(chain_types()), + chain_type.clone() as usize, + ); + } + if build_spec_cmd.relay.is_none() { + cli = cli.expect_select( + "Choose the relay your chain will be connecting to: ", + Some(false), + true, + Some(relays()), + relay.clone() as usize, + ); + } + if build_spec_cmd.profile.is_none() { + cli = cli.expect_select( + "Choose the build profile of the binary that should be used: ", + Some(false), + true, + Some(profiles()), + profile.clone() as usize, + ); + } + if !build_spec_cmd.default_bootnode { + cli = cli.expect_confirm( + "Would you like to use local host as a bootnode ?", + default_bootnode, + ); + } + if !build_spec_cmd.genesis_state { + cli = cli.expect_confirm( + "Should the genesis state file be generated ?", + genesis_state, + ); + } + if !build_spec_cmd.genesis_code { + cli = cli.expect_confirm( + "Should the genesis code file be generated ?", + genesis_code, + ); + } } - }, - _ => - 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 build_spec = build_spec_cmd.configure_build_spec(&mut cli).await?; + if changes && no_flags_used { + assert_eq!(build_spec.id, para_id); + assert_eq!(build_spec.profile, profile); + assert_eq!(build_spec.default_bootnode, default_bootnode); + assert_eq!(build_spec.chain_type, chain_type); + assert_eq!(build_spec.relay, relay); + assert_eq!(build_spec.protocol_id, protocol_id); + assert_eq!(build_spec.genesis_state, genesis_state); + assert_eq!(build_spec.genesis_code, genesis_code); } - }, + // Assert that the chain spec file is correctly detected and used. + assert_eq!(build_spec.chain, chain_spec_path.to_string_lossy()); + assert_eq!(build_spec.output_file, chain_spec_path); + cli.verify()?; + } + } + Ok(()) } - 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 + #[tokio::test] + async fn configure_build_spec_release_deprecated_works() -> anyhow::Result<()> { + // Create a temporary file to act as the existing chain spec file. + let temp_dir = tempdir()?; + let chain_spec_path = temp_dir.path().join("existing-chain-spec.json"); + std::fs::write(&chain_spec_path, "{}")?; + // Use the deprcrated release flag. + let release = true; + let build_spec_cmd = BuildSpecCommand { + release, + chain: Some(chain_spec_path.to_string_lossy().to_string()), + ..Default::default() }; + let mut cli = + MockCli::new().expect_confirm( + "An existing chain spec file is provided. Do you want to make additional changes to it?", + false, + ).expect_warning("NOTE: release flag is deprecated. Use `--profile` instead."); + let build_spec = build_spec_cmd.configure_build_spec(&mut cli).await?; + assert_eq!(build_spec.profile, release.into()); + cli.verify()?; + Ok(()) + } + + #[test] + fn prepare_output_path_works() -> anyhow::Result<()> { + // Create a temporary directory for testing. + let temp_dir = TempDir::new()?; + let temp_dir_path = temp_dir.path(); + + // No directory path. + let file = temp_dir_path.join("chain-spec.json"); + let result = prepare_output_path(&file)?; + // Expected path: chain-spec.json + assert_eq!(result, file); - 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, - }) + // Existing directory Path. + for dir in ["existing_dir", "existing_dir/", "existing_dir_json"] { + let existing_dir = temp_dir_path.join(dir); + create_dir_all(&existing_dir)?; + let result = prepare_output_path(&existing_dir)?; + // Expected path: existing_dir/chain-spec.json + let expected_path = existing_dir.join(DEFAULT_SPEC_NAME); + assert_eq!(result, expected_path); + } + + // Non-existing directory Path. + for dir in ["non_existing_dir", "non_existing_dir/", "non_existing_dir_json"] { + let non_existing_dir = temp_dir_path.join(dir); + let result = prepare_output_path(&non_existing_dir)?; + // Expected path: non_existing_dir/chain-spec.json + let expected_path = non_existing_dir.join(DEFAULT_SPEC_NAME); + assert_eq!(result, expected_path); + // The directory should now exist. + assert!(result.parent().unwrap().exists()); + } + + Ok(()) + } + + fn relays() -> Vec<(String, String)> { + RelayChain::VARIANTS + .iter() + .map(|variant| { + ( + variant.get_message().unwrap_or(variant.as_ref()).into(), + variant.get_detailed_message().unwrap_or_default().into(), + ) + }) + .collect() + } + + fn chain_types() -> Vec<(String, String)> { + ChainType::VARIANTS + .iter() + .map(|variant| { + ( + variant.get_message().unwrap_or(variant.as_ref()).into(), + variant.get_detailed_message().unwrap_or_default().into(), + ) + }) + .collect() + } + + fn profiles() -> Vec<(String, String)> { + Profile::VARIANTS + .iter() + .map(|variant| { + ( + variant.get_message().unwrap_or(variant.as_ref()).into(), + variant.get_detailed_message().unwrap_or_default().into(), + ) + }) + .collect() + } } diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 4e00e37b..0b2815a8 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -606,13 +606,13 @@ mod tests { .expect_warning("Your call has not been executed.") .expect_confirm( "Do you want to perform another call using the existing smart contract?", - false, + true, ) .expect_confirm( "Do you want to perform another call using the existing smart contract?", - true, + false, ) - .expect_select::( + .expect_select( "Select the message to call:", Some(false), true, @@ -669,8 +669,7 @@ mod tests { ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() - .expect_input("Signer calling the contract:", "//Alice".into()) - .expect_select::( + .expect_select( "Select the message to call:", Some(false), true, @@ -678,17 +677,19 @@ mod tests { 1, // "get" message ) .expect_input( - "Provide the on-chain contract address:", - "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + "Where is your project or contract artifact located?", + temp_dir.path().join("testing").display().to_string(), ) .expect_input( "Where is your contract deployed?", "wss://rpc1.paseo.popnetwork.xyz".into(), ) .expect_input( - "Where is your project or contract artifact located?", - temp_dir.path().join("testing").display().to_string(), - ).expect_info(format!( + "Provide the on-chain contract address:", + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + ) + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_info(format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message get --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice", temp_dir.path().join("testing").display().to_string(), )); @@ -750,13 +751,7 @@ mod tests { // The inputs are processed in reverse order. let mut cli = MockCli::new() .expect_confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)", true) - .expect_input("Signer calling the contract:", "//Alice".into()) - .expect_input("Enter the proof size limit:", "".into()) // Only if call - .expect_input("Enter the gas limit:", "".into()) // Only if call - .expect_input("Value to transfer to the call:", "50".into()) // Only if payable - .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip - .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip - .expect_select::( + .expect_select( "Select the message to call:", Some(false), true, @@ -764,17 +759,24 @@ mod tests { 2, // "specific_flip" message ) .expect_input( - "Provide the on-chain contract address:", - "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + "Where is your project or contract artifact located?", + temp_dir.path().join("testing").display().to_string(), ) .expect_input( "Where is your contract deployed?", "wss://rpc1.paseo.popnetwork.xyz".into(), ) .expect_input( - "Where is your project or contract artifact located?", - temp_dir.path().join("testing").display().to_string(), - ).expect_info(format!( + "Provide the on-chain contract address:", + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + ) + .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip + .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip + .expect_input("Value to transfer to the call:", "50".into()) // Only if payable + .expect_input("Enter the gas limit:", "".into()) // Only if call + .expect_input("Enter the proof size limit:", "".into()) // Only if call + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_info(format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", temp_dir.path().join("testing").display().to_string(), )); @@ -837,11 +839,7 @@ mod tests { ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() - .expect_input("Signer calling the contract:", "//Alice".into()) - .expect_input("Value to transfer to the call:", "50".into()) // Only if payable - .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip - .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip - .expect_select::( + .expect_select( "Select the message to call:", Some(false), true, @@ -849,17 +847,22 @@ mod tests { 2, // "specific_flip" message ) .expect_input( - "Provide the on-chain contract address:", - "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + "Where is your project or contract artifact located?", + temp_dir.path().join("testing").display().to_string(), ) .expect_input( "Where is your contract deployed?", "wss://rpc1.paseo.popnetwork.xyz".into(), ) .expect_input( - "Where is your project or contract artifact located?", - temp_dir.path().join("testing").display().to_string(), - ).expect_info(format!( + "Provide the on-chain contract address:", + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + ) + .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip + .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip + .expect_input("Value to transfer to the call:", "50".into()) // Only if payable + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_info(format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", temp_dir.path().join("testing").display().to_string(), )); diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 34c2f10b..aa385f28 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -46,9 +46,9 @@ pub(crate) enum Command { /// Help message for the build command. fn about_build() -> &'static str { #[cfg(all(feature = "parachain", feature = "contract"))] - return "Build a parachain, smart contract or Rust package."; + return "Build a parachain, chain specification, smart contract or Rust package."; #[cfg(all(feature = "parachain", not(feature = "contract")))] - return "Build a parachain or Rust package."; + return "Build a parachain, chain specification or Rust package."; #[cfg(all(feature = "contract", not(feature = "parachain")))] return "Build a smart contract or Rust package."; } diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 0e17d101..b9baaac5 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -48,7 +48,7 @@ async fn parachain_lifecycle() -> Result<()> { 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" + // paseo-local --protocol-id pop-protocol" --chain local Command::cargo_bin("pop") .unwrap() .current_dir(&temp_parachain_dir) @@ -61,8 +61,12 @@ async fn parachain_lifecycle() -> Result<()> { "2222", "--type", "development", + "--chain", + "local", "--relay", "paseo-local", + "--profile", + "release", "--genesis-state", "--genesis-code", "--protocol-id", @@ -86,6 +90,7 @@ async fn parachain_lifecycle() -> Result<()> { assert!(content.contains("\"tokenSymbol\": \"POP\"")); assert!(content.contains("\"relay_chain\": \"paseo-local\"")); assert!(content.contains("\"protocolId\": \"pop-protocol\"")); + assert!(content.contains("\"id\": \"local_testnet\"")); // pop up parachain -p "./test_parachain" let mut cmd = Cmd::new(cargo_bin("pop")) diff --git a/crates/pop-common/Cargo.toml b/crates/pop-common/Cargo.toml index 108399db..87074a60 100644 --- a/crates/pop-common/Cargo.toml +++ b/crates/pop-common/Cargo.toml @@ -19,14 +19,15 @@ reqwest.workspace = true serde_json.workspace = true serde.workspace = true strum.workspace = true +strum_macros.workspace = true tar.workspace = true tempfile.workspace = true thiserror.workspace = true tokio.workspace = true toml_edit.workspace = true url.workspace = true +toml.workspace = true [dev-dependencies] mockito.workspace = true -strum_macros.workspace = true tempfile.workspace = true diff --git a/crates/pop-common/src/build.rs b/crates/pop-common/src/build.rs index bfe658b9..f871078a 100644 --- a/crates/pop-common/src/build.rs +++ b/crates/pop-common/src/build.rs @@ -2,14 +2,29 @@ use std::{ fmt, path::{Path, PathBuf}, }; +use strum_macros::{AsRefStr, EnumMessage, EnumString, VariantArray}; /// Enum representing a build profile. -#[derive(Debug, PartialEq)] +#[derive(AsRefStr, Clone, Default, Debug, EnumString, EnumMessage, VariantArray, Eq, PartialEq)] pub enum Profile { /// Debug profile, optimized for debugging. + #[strum(serialize = "debug", message = "Debug", detailed_message = "Optimized for debugging.")] Debug, /// Release profile, optimized without any debugging functionality. + #[default] + #[strum( + serialize = "release", + message = "Release", + detailed_message = "Optimized without any debugging functionality." + )] Release, + /// Production profile, optimized for ultimate performance. + #[strum( + serialize = "production", + message = "Production", + detailed_message = "Optimized for ultimate performance." + )] + Production, } impl Profile { @@ -18,13 +33,20 @@ impl Profile { match self { Profile::Release => path.join("target/release"), Profile::Debug => path.join("target/debug"), + Profile::Production => path.join("target/production"), } } } +impl From for bool { + fn from(value: Profile) -> Self { + value != Profile::Debug + } +} + impl From for Profile { - fn from(release: bool) -> Self { - if release { + fn from(value: bool) -> Self { + if value { Profile::Release } else { Profile::Debug @@ -35,8 +57,73 @@ impl From for Profile { impl fmt::Display for Profile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::Debug => write!(f, "DEBUG"), - Self::Release => write!(f, "RELEASE"), + Self::Debug => write!(f, "debug"), + Self::Release => write!(f, "release"), + Self::Production => write!(f, "production"), } } } + +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + use strum::EnumMessage; + + #[test] + fn profile_from_string() { + assert_eq!("debug".parse::().unwrap(), Profile::Debug); + assert_eq!("release".parse::().unwrap(), Profile::Release); + assert_eq!("production".parse::().unwrap(), Profile::Production); + } + + #[test] + fn profile_detailed_message() { + assert_eq!(Profile::Debug.get_detailed_message(), Some("Optimized for debugging.")); + assert_eq!( + Profile::Release.get_detailed_message(), + Some("Optimized without any debugging functionality.") + ); + assert_eq!( + Profile::Production.get_detailed_message(), + Some("Optimized for ultimate performance.") + ); + } + + #[test] + fn profile_target_directory() { + let base_path = Path::new("/example/path"); + + assert_eq!( + Profile::Debug.target_directory(base_path), + Path::new("/example/path/target/debug") + ); + assert_eq!( + Profile::Release.target_directory(base_path), + Path::new("/example/path/target/release") + ); + assert_eq!( + Profile::Production.target_directory(base_path), + Path::new("/example/path/target/production") + ); + } + + #[test] + fn profile_default() { + let default_profile = Profile::default(); + assert_eq!(default_profile, Profile::Release); + } + + #[test] + fn profile_from_bool() { + assert_eq!(Profile::from(true), Profile::Release); + assert_eq!(Profile::from(false), Profile::Debug); + } + + #[test] + fn profile_into_bool() { + assert_eq!(bool::from(Profile::Debug), false); + assert_eq!(bool::from(Profile::Release), true); + assert_eq!(bool::from(Profile::Production), true); + } +} diff --git a/crates/pop-common/src/manifest.rs b/crates/pop-common/src/manifest.rs index ff84c1be..16db4f0d 100644 --- a/crates/pop-common/src/manifest.rs +++ b/crates/pop-common/src/manifest.rs @@ -2,7 +2,7 @@ use crate::Error; use anyhow; -pub use cargo_toml::{Dependency, Manifest}; +pub use cargo_toml::{Dependency, LtoSetting, Manifest, Profile, Profiles}; use std::{ fs::{read_to_string, write}, path::{Path, PathBuf}, @@ -58,6 +58,7 @@ pub fn find_workspace_toml(target_dir: &Path) -> Option { /// This function is used to add a crate to a workspace. /// # Arguments +/// /// * `workspace_toml` - The path to the workspace `Cargo.toml` /// * `crate_path`: The path to the crate that should be added to the workspace pub fn add_crate_to_workspace(workspace_toml: &Path, crate_path: &Path) -> anyhow::Result<()> { @@ -97,6 +98,44 @@ pub fn add_crate_to_workspace(workspace_toml: &Path, crate_path: &Path) -> anyho Ok(()) } +/// Adds a "production" profile to the Cargo.toml manifest if it doesn't already exist. +/// +/// # Arguments +/// * `project` - The path to the root of the Cargo project containing the Cargo.toml. +pub fn add_production_profile(project: &Path) -> anyhow::Result<()> { + let root_toml_path = project.join("Cargo.toml"); + let mut manifest = Manifest::from_path(&root_toml_path)?; + // Check if the `production` profile already exists. + if manifest.profile.custom.contains_key("production") { + return Ok(()); + } + // Create the production profile with required fields. + let production_profile = Profile { + opt_level: None, + debug: None, + split_debuginfo: None, + rpath: None, + lto: Some(LtoSetting::Fat), + debug_assertions: None, + codegen_units: Some(1), + panic: None, + incremental: None, + overflow_checks: None, + strip: None, + package: std::collections::BTreeMap::new(), + build_override: None, + inherits: Some("release".to_string()), + }; + // Insert the new profile into the custom profiles + manifest.profile.custom.insert("production".to_string(), production_profile); + + // Serialize the updated manifest and write it back to the file + let toml_string = toml::to_string(&manifest)?; + write(&root_toml_path, toml_string)?; + + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -419,4 +458,42 @@ mod tests { ); assert!(add_crate.is_err()); } + + #[test] + fn add_production_profile_works() { + let test_builder = TestBuilder::default().add_workspace().add_workspace_cargo_toml( + r#"[profile.release] + opt-level = 3 + "#, + ); + + let binding = test_builder.workspace.expect("Workspace should exist"); + let project_path = binding.path(); + let cargo_toml_path = project_path.join("Cargo.toml"); + + // Call the function to add the production profile + let result = add_production_profile(project_path); + assert!(result.is_ok()); + + // Verify the production profile is added + let manifest = + Manifest::from_path(&cargo_toml_path).expect("Should parse updated Cargo.toml"); + let production_profile = manifest + .profile + .custom + .get("production") + .expect("Production profile should exist"); + assert_eq!(production_profile.codegen_units, Some(1)); + assert_eq!(production_profile.inherits.as_deref(), Some("release")); + assert_eq!(production_profile.lto, Some(LtoSetting::Fat)); + + // Test idempotency: Running the function again should not modify the manifest + let initial_toml_content = + read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable"); + let second_result = add_production_profile(project_path); + assert!(second_result.is_ok()); + let final_toml_content = + read_to_string(&cargo_toml_path).expect("Cargo.toml should be readable"); + assert_eq!(initial_toml_content, final_toml_content); + } } diff --git a/crates/pop-parachains/README.md b/crates/pop-parachains/README.md index 38fa24c1..5d60382f 100644 --- a/crates/pop-parachains/README.md +++ b/crates/pop-parachains/README.md @@ -46,7 +46,7 @@ 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 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); +generate_plain_chain_spec(&binary_path, &plain_chain_spec_path, true, "dev"); // Customize your chain specification let mut chain_spec = ChainSpec::from(&plain_chain_spec_path).unwrap(); chain_spec.replace_para_id(2002); @@ -70,7 +70,7 @@ 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 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); +generate_plain_chain_spec(&binary_path, &plain_chain_spec_path, true, "dev"); // 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. diff --git a/crates/pop-parachains/src/build.rs b/crates/pop-parachains/src/build.rs index 3c25b2a0..1e369f37 100644 --- a/crates/pop-parachains/src/build.rs +++ b/crates/pop-parachains/src/build.rs @@ -1,7 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::Error; -use anyhow::Result; +use crate::Error::{self, *}; +use anyhow::{anyhow, Result}; use duct::cmd; use pop_common::{manifest::from_path, Profile}; use serde_json::{json, Value}; @@ -31,8 +31,10 @@ pub fn build_parachain( args.push("--package"); args.push(package) } - if matches!(profile, &Profile::Release) { + if profile == &Profile::Release { args.push("--release"); + } else if profile == &Profile::Production { + args.push("--profile=production"); } cmd("cargo", args).dir(path).run()?; binary_path(&profile.target_directory(path), node_path.unwrap_or(&path.join("node"))) @@ -75,17 +77,30 @@ pub fn binary_path(target_path: &Path, node_path: &Path) -> Result Result<(), Error> { check_command_exists(binary_path, "build-spec")?; - let mut args = vec!["build-spec"]; + let mut args = vec!["build-spec", "--chain", chain]; if !default_bootnode { args.push("--disable-default-bootnode"); } - cmd(binary_path, args).stdout_path(plain_chain_spec).stderr_null().run()?; + // Create a temporary file. + let temp_file = tempfile::NamedTempFile::new_in(std::env::temp_dir())?; + // Run the command and redirect output to the temporary file. + cmd(binary_path, args).stdout_path(temp_file.path()).stderr_null().run()?; + // Atomically replace the chain spec file with the temporary file. + temp_file.persist(plain_chain_spec).map_err(|e| { + AnyhowError(anyhow!( + "Failed to replace the chain spec file with the temporary file: {}", + e.to_string() + )) + })?; Ok(()) } @@ -323,9 +338,10 @@ mod tests { use anyhow::Result; use pop_common::{manifest::Dependency, set_executable_permission}; use std::{fs, fs::write, io::Write, path::Path}; - use tempfile::{tempdir, Builder}; + use strum::VariantArray; + use tempfile::{tempdir, Builder, TempDir}; - fn setup_template_and_instantiate() -> Result { + fn setup_template_and_instantiate() -> Result { let temp_dir = tempdir().expect("Failed to create temp dir"); let config = Config { symbol: "DOT".to_string(), @@ -348,9 +364,9 @@ mod tests { } // Function that generates a Cargo.toml inside node directory for testing. - fn generate_mock_node(temp_dir: &Path) -> Result<(), Error> { + fn generate_mock_node(temp_dir: &Path, name: Option<&str>) -> Result { // Create a node directory - let target_dir = temp_dir.join("node"); + let target_dir = temp_dir.join(name.unwrap_or("node")); fs::create_dir(&target_dir)?; // Create a Cargo.toml file let mut toml_file = fs::File::create(target_dir.join("Cargo.toml"))?; @@ -365,7 +381,7 @@ mod tests { "# )?; - Ok(()) + Ok(target_dir) } // Function that fetch a binary from pop network @@ -374,13 +390,13 @@ mod tests { writeln!( config.as_file(), r#" -[relaychain] -chain = "paseo-local" + [relaychain] + chain = "paseo-local" -[[parachains]] -id = 4385 -default_command = "pop-node" -"# + [[parachains]] + id = 4385 + default_command = "pop-node" + "# )?; let mut zombienet = Zombienet::new(&cache, config.path().to_str().unwrap(), None, None, None, None, None) @@ -403,20 +419,45 @@ default_command = "pop-node" Ok(binary_path) } + fn add_production_profile(project: &Path) -> Result<()> { + let root_toml_path = project.join("Cargo.toml"); + let mut root_toml_content = fs::read_to_string(&root_toml_path)?; + root_toml_content.push_str( + r#" + [profile.production] + codegen-units = 1 + inherits = "release" + lto = true + "#, + ); + // Write the updated content back to the file + write(&root_toml_path, root_toml_content)?; + Ok(()) + } + #[test] fn build_parachain_works() -> Result<()> { - let temp_dir = tempdir()?; let name = "parachain_template_node"; + let temp_dir = tempdir()?; cmd("cargo", ["new", name, "--bin"]).dir(temp_dir.path()).run()?; - generate_mock_node(&temp_dir.path().join(name))?; - let binary = build_parachain(&temp_dir.path().join(name), None, &Profile::Release, None)?; - let target_directory = temp_dir.path().join(name).join("target/release"); - assert!(target_directory.exists()); - assert!(target_directory.join("parachain_template_node").exists()); - assert_eq!( - binary.display().to_string(), - target_directory.join("parachain_template_node").display().to_string() - ); + let project = temp_dir.path().join(name); + add_production_profile(&project)?; + for node in vec![None, Some("custom_node")] { + let node_path = generate_mock_node(&project, node)?; + for package in vec![None, Some(String::from("parachain_template_node"))] { + for profile in Profile::VARIANTS { + let node_path = node.map(|_| node_path.as_path()); + let binary = build_parachain(&project, package.clone(), &profile, node_path)?; + let target_directory = profile.target_directory(&project); + assert!(target_directory.exists()); + assert!(target_directory.join("parachain_template_node").exists()); + assert_eq!( + binary.display().to_string(), + target_directory.join("parachain_template_node").display().to_string() + ); + } + } + } Ok(()) } @@ -457,7 +498,8 @@ default_command = "pop-node" generate_plain_chain_spec( &binary_path, &temp_dir.path().join("plain-parachain-chainspec.json"), - true, + false, + "local", )?; assert!(plain_chain_spec.exists()); { @@ -473,6 +515,8 @@ default_command = "pop-node" assert!(raw_chain_spec.exists()); let content = fs::read_to_string(raw_chain_spec.clone()).expect("Could not read file"); assert!(content.contains("\"para_id\": 2001")); + assert!(content.contains("\"id\": \"pop-devnet\"")); + assert!(content.contains("\"bootNodes\": []")); // Test export wasm file let wasm_file = export_wasm_file(&binary_path, &raw_chain_spec, "para-2001-wasm")?; assert!(wasm_file.exists()); diff --git a/crates/pop-parachains/src/utils/onboard.rs b/crates/pop-parachains/src/utils/onboard.rs new file mode 100644 index 00000000..e69de29b