diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 064a34d0..bdf09c4c 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{cache, cli::Cli}; +use crate::{cache, cli::Cli, common::builds::get_project_path}; use clap::Subcommand; use pop_common::templates::Template; use serde_json::{json, Value}; @@ -30,8 +30,7 @@ pub(crate) enum Command { #[clap(alias = "c")] #[cfg(any(feature = "parachain", feature = "contract"))] Call(call::CallArgs), - /// Launch a local network or deploy a smart contract. - #[clap(alias = "u")] + #[clap(alias = "u", about = about_up())] #[cfg(any(feature = "parachain", feature = "contract"))] Up(up::UpArgs), /// Test a smart contract. @@ -53,6 +52,16 @@ fn about_build() -> &'static str { return "Build a smart contract or Rust package."; } +/// Help message for the up command. +fn about_up() -> &'static str { + #[cfg(all(feature = "parachain", feature = "contract"))] + return "Deploy a smart contract or launch a local network."; + #[cfg(all(feature = "parachain", not(feature = "contract")))] + return "Launch a local network."; + #[cfg(all(feature = "contract", not(feature = "parachain")))] + return "Deploy a smart contract."; +} + impl Command { /// Executes the command. pub(crate) async fn execute(self) -> anyhow::Result { @@ -101,10 +110,23 @@ impl Command { }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { - #[cfg(feature = "parachain")] - up::Command::Parachain(cmd) => cmd.execute().await.map(|_| Value::Null), - #[cfg(feature = "contract")] - up::Command::Contract(cmd) => cmd.execute().await.map(|_| Value::Null), + None => up::Command::execute(args).await.map(|t| json!(t)), + Some(cmd) => match cmd { + #[cfg(feature = "parachain")] + up::Command::Network(mut cmd) => { + cmd.valid = true; + cmd.execute().await.map(|_| Value::Null) + }, + // TODO: Deprecated, will be removed in v0.8.0. + #[cfg(feature = "parachain")] + up::Command::Parachain(cmd) => cmd.execute().await.map(|_| Value::Null), + // TODO: Deprecated, will be removed in v0.8.0. + #[cfg(feature = "contract")] + up::Command::Contract(mut cmd) => { + cmd.path = get_project_path(args.path, args.path_pos); + cmd.execute().await.map(|_| Value::Null) + }, + }, }, #[cfg(feature = "contract")] Self::Test(args) => match args.command { diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index c11e55e5..8e92c7b2 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -3,7 +3,6 @@ use crate::{ cli::{traits::Cli as _, Cli}, common::{ - builds::get_project_path, contracts::{check_contracts_node_and_prompt, has_contract_been_built, terminate_node}, wallet::request_signature, }, @@ -29,47 +28,46 @@ const COMPLETE: &str = "🚀 Deployment complete"; const DEFAULT_URL: &str = "ws://localhost:9944/"; const DEFAULT_PORT: u16 = 9944; const FAILED: &str = "🚫 Deployment failed."; +const HELP_HEADER: &str = "Smart contract deployment options"; #[derive(Args, Clone)] +#[clap(next_help_heading = HELP_HEADER)] pub struct UpContractCommand { /// Path to the contract build directory. - #[arg(short, long)] - path: Option, - /// Directory path without flag for your project [default: current directory] - #[arg(value_name = "PATH", index = 1, conflicts_with = "path")] - pub path_pos: Option, + #[clap(skip)] + pub(crate) path: Option, /// The name of the contract constructor to call. #[clap(short, long, default_value = "new")] - constructor: String, + pub(crate) constructor: String, /// The constructor arguments, encoded as strings. #[clap(short, long, num_args = 0..,)] - args: Vec, + pub(crate) args: Vec, /// Transfers an initial balance to the instantiated contract. #[clap(short, long, default_value = "0")] - value: String, + pub(crate) value: String, /// Maximum amount of gas to be used for this command. /// If not specified it will perform a dry-run to estimate the gas consumed for the /// instantiation. #[clap(name = "gas", short, long)] - gas_limit: Option, + pub(crate) gas_limit: Option, /// Maximum proof size for the instantiation. /// If not specified it will perform a dry-run to estimate the proof size required. #[clap(short = 'P', long)] - proof_size: Option, + pub(crate) proof_size: Option, /// A salt used in the address derivation of the new contract. Use to create multiple /// instances of the same contract code from the same account. #[clap(short = 'S', long, value_parser = parse_hex_bytes)] - salt: Option, + pub(crate) salt: Option, /// Websocket endpoint of a chain. #[clap(short, long, value_parser, default_value = DEFAULT_URL)] - url: Url, + pub(crate) url: Url, /// Secret key URI for the account deploying the contract. /// /// e.g. /// - for a dev account "//Alice" /// - with a password "//Alice///SECRET_PASSWORD" #[clap(short, long, default_value = "//Alice")] - suri: String, + pub(crate) suri: String, /// Use a browser extension wallet to sign the extrinsic. #[clap( name = "use-wallet", @@ -78,36 +76,37 @@ pub struct UpContractCommand { short('w'), conflicts_with = "suri" )] - use_wallet: bool, + pub(crate) use_wallet: bool, /// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction. #[clap(short = 'D', long)] - dry_run: bool, + pub(crate) dry_run: bool, /// Uploads the contract only, without instantiation. #[clap(short = 'U', long)] - upload_only: bool, + pub(crate) upload_only: bool, /// Automatically source or update the needed binary required without prompting for /// confirmation. #[clap(short = 'y', long)] - skip_confirm: bool, + pub(crate) skip_confirm: bool, + // Deprecation flag, used to specify whether the deprecation warning is shown. + #[clap(skip)] + pub(crate) valid: bool, } impl UpContractCommand { /// Executes the command. pub(crate) async fn execute(mut self) -> anyhow::Result<()> { Cli.intro("Deploy a smart contract")?; - - let project_path = get_project_path(self.path.clone(), self.path_pos.clone()); + // Show warning if specified as deprecated. + if !self.valid { + Cli.warning("NOTE: this command is deprecated. Please use `pop up` (or simply `pop u`) in future...")?; + } // Check if build exists in the specified "Contract build directory" - if !has_contract_been_built(project_path.as_deref()) { + if !has_contract_been_built(self.path.as_deref()) { // Build the contract in release mode Cli.warning("NOTE: contract has not yet been built.")?; let spinner = spinner(); spinner.start("Building contract in RELEASE mode..."); - let result = match build_smart_contract( - project_path.as_deref().map(|v| v), - true, - Verbosity::Quiet, - ) { + let result = match build_smart_contract(self.path.as_deref(), true, Verbosity::Quiet) { Ok(result) => result, Err(e) => { Cli.outro_cancel(format!("🚫 An error occurred building your contract: {e}\nUse `pop build` to retry with build output."))?; @@ -369,8 +368,7 @@ impl UpContractCommand { // get the call data and contract code hash async fn get_contract_data(&self) -> anyhow::Result<(Vec, [u8; 32])> { - let project_path = get_project_path(self.path.clone(), self.path_pos.clone()); - let contract_code = get_contract_code(project_path.as_ref())?; + let contract_code = get_contract_code(self.path.as_ref())?; let hash = contract_code.code_hash(); if self.upload_only { let call_data = get_upload_payload(contract_code, self.url.as_str()).await?; @@ -445,7 +443,6 @@ mod tests { fn default_up_contract_command() -> UpContractCommand { UpContractCommand { path: None, - path_pos: None, constructor: "new".to_string(), args: vec![], value: "0".to_string(), @@ -458,6 +455,7 @@ mod tests { upload_only: false, skip_confirm: false, use_wallet: false, + valid: true, } } @@ -521,8 +519,7 @@ mod tests { let localhost_url = format!("ws://127.0.0.1:{}", port); let up_contract_opts = UpContractCommand { - path: Some(temp_dir.clone()), - path_pos: Some(temp_dir), + path: Some(temp_dir), constructor: "new".to_string(), args: vec![], value: "0".to_string(), @@ -535,6 +532,7 @@ mod tests { upload_only: true, skip_confirm: true, use_wallet: true, + valid: true, }; let rpc_client = subxt::backend::rpc::RpcClient::from_url(&up_contract_opts.url).await?; @@ -573,8 +571,7 @@ mod tests { let localhost_url = format!("ws://127.0.0.1:{}", port); let up_contract_opts = UpContractCommand { - path: Some(temp_dir.clone()), - path_pos: Some(temp_dir), + path: Some(temp_dir), constructor: "new".to_string(), args: vec!["false".to_string()], value: "0".to_string(), @@ -587,6 +584,7 @@ mod tests { upload_only: false, skip_confirm: true, use_wallet: true, + valid: true, }; // Retrieve call data based on the above command options. diff --git a/crates/pop-cli/src/commands/up/mod.rs b/crates/pop-cli/src/commands/up/mod.rs index 500e2e20..877618ba 100644 --- a/crates/pop-cli/src/commands/up/mod.rs +++ b/crates/pop-cli/src/commands/up/mod.rs @@ -1,29 +1,143 @@ // SPDX-License-Identifier: GPL-3.0 +use crate::{ + cli::{self, Cli}, + common::builds::get_project_path, +}; use clap::{Args, Subcommand}; +use std::path::PathBuf; #[cfg(feature = "contract")] mod contract; #[cfg(feature = "parachain")] -mod parachain; +mod network; -/// Arguments for launching or deploying. -#[derive(Args)] +/// Arguments for launching or deploying a project. +#[derive(Args, Clone)] #[command(args_conflicts_with_subcommands = true)] pub(crate) struct UpArgs { + /// Path to the project directory. + // TODO: Introduce the short option in v0.8.0 once deprecated parachain command is removed. + #[arg(long, global = true)] + pub path: Option, + + /// Directory path without flag for your project [default: current directory] + #[arg(value_name = "PATH", index = 1, global = true, conflicts_with = "path")] + pub path_pos: Option, + + #[command(flatten)] + #[cfg(feature = "contract")] + pub(crate) contract: contract::UpContractCommand, + #[command(subcommand)] - pub(crate) command: Command, + pub(crate) command: Option, } /// Launch a local network or deploy a smart contract. -#[derive(Subcommand)] +#[derive(Subcommand, Clone)] pub(crate) enum Command { #[cfg(feature = "parachain")] /// Launch a local network. - #[clap(alias = "p")] - Parachain(parachain::ZombienetCommand), + #[clap(alias = "n")] + Network(network::ZombienetCommand), + #[cfg(feature = "parachain")] + /// [DEPRECATED] Launch a local network (will be removed in v0.8.0). + #[clap(alias = "p", hide = true)] + Parachain(network::ZombienetCommand), #[cfg(feature = "contract")] - /// Deploy a smart contract. - #[clap(alias = "c")] + /// [DEPRECATED] Deploy a smart contract (will be removed in v0.8.0). + #[clap(alias = "c", hide = true)] Contract(contract::UpContractCommand), } + +impl Command { + /// Executes the command. + pub(crate) async fn execute(args: UpArgs) -> anyhow::Result<&'static str> { + Self::execute_project_deployment(args, &mut Cli).await + } + + /// Identifies the project type and executes the appropriate deployment process. + async fn execute_project_deployment( + args: UpArgs, + cli: &mut impl cli::traits::Cli, + ) -> anyhow::Result<&'static str> { + let project_path = get_project_path(args.path.clone(), args.path_pos.clone()); + // If only contract feature enabled, deploy a contract + #[cfg(feature = "contract")] + if pop_contracts::is_supported(project_path.as_deref())? { + let mut cmd = args.contract; + cmd.path = project_path; + cmd.valid = true; // To handle deprecated command, remove in v0.8.0. + cmd.execute().await?; + return Ok("contract"); + } + cli.warning("No contract detected. Ensure you are in a valid project directory.")?; + Ok("") + } +} + +#[cfg(test)] +mod tests { + use super::{contract::UpContractCommand, *}; + + use cli::MockCli; + use duct::cmd; + use pop_contracts::{mock_build_process, new_environment}; + use std::env; + use url::Url; + + fn create_up_args(project_path: PathBuf) -> anyhow::Result { + Ok(UpArgs { + path: Some(project_path), + path_pos: None, + contract: UpContractCommand { + path: None, + constructor: "new".to_string(), + args: vec!["false".to_string()], + value: "0".to_string(), + gas_limit: None, + proof_size: None, + salt: None, + url: Url::parse("wss://rpc2.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + use_wallet: false, + dry_run: true, + upload_only: true, + skip_confirm: false, + valid: false, + }, + command: None, + }) + } + + #[tokio::test] + async fn detects_contract_correctly() -> anyhow::Result<()> { + let temp_dir = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + let args = create_up_args(temp_dir.path().join("testing"))?; + let mut cli = MockCli::new(); + assert_eq!(Command::execute_project_deployment(args, &mut cli).await?, "contract"); + cli.verify() + } + + #[tokio::test] + async fn detects_rust_project_correctly() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir()?; + let name = "hello_world"; + let path = temp_dir.path(); + let project_path = path.join(name); + let args = create_up_args(project_path)?; + + cmd("cargo", ["new", name, "--bin"]).dir(&path).run()?; + let mut cli = MockCli::new() + .expect_warning("No contract detected. Ensure you are in a valid project directory."); + assert_eq!(Command::execute_project_deployment(args, &mut cli).await?, ""); + cli.verify() + } +} diff --git a/crates/pop-cli/src/commands/up/parachain.rs b/crates/pop-cli/src/commands/up/network.rs similarity index 97% rename from crates/pop-cli/src/commands/up/parachain.rs rename to crates/pop-cli/src/commands/up/network.rs index 30fc6fce..c1c114b3 100644 --- a/crates/pop-cli/src/commands/up/parachain.rs +++ b/crates/pop-cli/src/commands/up/network.rs @@ -13,7 +13,7 @@ use pop_parachains::{clear_dmpq, Error, IndexSet, NetworkNode, RelayChain, Zombi use std::{path::Path, time::Duration}; use tokio::time::sleep; -#[derive(Args)] +#[derive(Args, Clone)] pub(crate) struct ZombienetCommand { /// The Zombienet network configuration file to be used. #[arg(short, long)] @@ -48,6 +48,9 @@ pub(crate) struct ZombienetCommand { /// Automatically source all needed binaries required without prompting for confirmation. #[clap(short = 'y', long)] skip_confirm: bool, + // Deprecation flag, used to specify whether the deprecation warning is shown. + #[clap(skip)] + pub(crate) valid: bool, } impl ZombienetCommand { @@ -57,6 +60,11 @@ impl ZombienetCommand { intro(format!("{}: Launch a local network", style(" Pop CLI ").black().on_magenta()))?; set_theme(Theme); + // Show warning if specified as deprecated. + if !self.valid { + log::warning("NOTE: this command is deprecated. Please use `pop up network` (or simply `pop up n`) in future...")?; + } + // Parse arguments let cache = crate::cache()?; let mut zombienet = match Zombienet::new( diff --git a/crates/pop-cli/tests/contract.rs b/crates/pop-cli/tests/contract.rs index a095702a..535b0502 100644 --- a/crates/pop-cli/tests/contract.rs +++ b/crates/pop-cli/tests/contract.rs @@ -89,19 +89,11 @@ async fn contract_lifecycle() -> Result<()> { sleep(Duration::from_secs(5)).await; // Only upload the contract - // pop up contract --path ./test_contract --upload-only + // pop up --path ./test_contract --upload-only Command::cargo_bin("pop") .unwrap() - .current_dir(&temp_dir.join("test_contract")) - .args(&[ - "up", - "contract", - "--path", - "./test_contract", - "--upload-only", - "--url", - default_endpoint, - ]) + .current_dir(&temp_dir) + .args(&["up", "--path", "./test_contract", "--upload-only", "--url", default_endpoint]) .assert() .success(); // Instantiate contract, only dry-run @@ -110,7 +102,6 @@ async fn contract_lifecycle() -> Result<()> { .current_dir(&temp_dir.join("test_contract")) .args(&[ "up", - "contract", "--constructor", "new", "--args", @@ -180,7 +171,7 @@ async fn contract_lifecycle() -> Result<()> { .assert() .success(); - // pop up contract --upload-only --use-wallet + // pop up --upload-only --use-wallet // Will run http server for wallet integration. // Using `cargo run --` as means for the CI to pass. // Possibly there's room for improvement here. @@ -189,12 +180,11 @@ async fn contract_lifecycle() -> Result<()> { "run", "--", "up", - "contract", "--upload-only", "--use-wallet", "--skip-confirm", "--dry-run", - "-p", + "--path", temp_dir.join("test_contract").to_str().expect("to_str"), "--url", default_endpoint, diff --git a/crates/pop-cli/tests/parachain.rs b/crates/pop-cli/tests/parachain.rs index 31ad6bde..d8d3b5b5 100644 --- a/crates/pop-cli/tests/parachain.rs +++ b/crates/pop-cli/tests/parachain.rs @@ -123,10 +123,10 @@ name = "collator-01" ), )?; - // `pop up parachain -f ./network.toml --skip-confirm` + // `pop up network -f ./network.toml --skip-confirm` let mut cmd = Cmd::new(cargo_bin("pop")) .current_dir(&temp_parachain_dir) - .args(&["up", "parachain", "-f", "./network.toml", "--skip-confirm"]) + .args(&["up", "network", "-f", "./network.toml", "--skip-confirm"]) .spawn() .unwrap();