From 3c9b261a4f855022b5e9ec44c2d6e3eee5630cd5 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 5 Sep 2024 14:09:34 +0200 Subject: [PATCH 01/27] feat: guide user for calling a contract --- Cargo.lock | 2 + Cargo.toml | 2 + crates/pop-cli/src/commands/call/contract.rs | 154 +++++++++++++++--- crates/pop-contracts/Cargo.toml | 3 +- crates/pop-contracts/src/call/metadata.rs | 48 ++++++ .../src/{call.rs => call/mod.rs} | 2 + crates/pop-contracts/src/lib.rs | 4 +- 7 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 crates/pop-contracts/src/call/metadata.rs rename crates/pop-contracts/src/{call.rs => call/mod.rs} (99%) diff --git a/Cargo.lock b/Cargo.lock index 34d1d888..f61d7fe1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4695,6 +4695,7 @@ dependencies = [ "anyhow", "contract-build", "contract-extrinsics", + "contract-transcode", "dirs", "duct", "flate2", @@ -4703,6 +4704,7 @@ dependencies = [ "mockito", "pop-common", "reqwest 0.12.5", + "scale-info", "sp-core", "sp-weights", "strum 0.26.3", diff --git a/Cargo.toml b/Cargo.toml index 02ec5aad..78147f50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,8 @@ sp-core = "31" sp-weights = "30" contract-build = "4.1" contract-extrinsics = "4.1" +contract-transcode = "4.1" +scale-info = { version = "2.11.3", default-features = false, features = ["derive"] } heck = "0.5.0" # parachains diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index d43689c9..ab361fa1 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,31 +1,35 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::style::Theme; -use anyhow::anyhow; +use crate::{ + cli::{traits::Cli as _, Cli}, + style::Theme, +}; +use anyhow::{anyhow, Result}; use clap::Args; -use cliclack::{clear_screen, intro, log, outro, outro_cancel, set_theme}; +use cliclack::{clear_screen, confirm, input, intro, log, outro, outro_cancel, set_theme}; use console::style; use pop_contracts::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, + call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, set_up_call, + CallOpts, Message, }; use sp_weights::Weight; use std::path::PathBuf; -#[derive(Args)] +#[derive(Args, Clone)] pub struct CallContractCommand { /// Path to the contract build directory. #[arg(short = 'p', long)] path: Option, /// The address of the contract to call. #[clap(name = "contract", long, env = "CONTRACT")] - contract: String, + contract: Option, /// The name of the contract message to call. #[clap(long, short)] - message: String, + message: Option, /// The constructor arguments, encoded as strings. #[clap(long, num_args = 0..)] args: Vec, - /// Transfers an initial balance to the instantiated contract. + /// Transfers an initial balance to the contract. #[clap(name = "value", long, default_value = "0")] value: String, /// Maximum amount of gas to be used for this command. @@ -40,7 +44,7 @@ pub struct CallContractCommand { /// Websocket endpoint of a node. #[clap(name = "url", long, value_parser, default_value = "ws://localhost:9944")] url: url::Url, - /// Secret key URI for the account deploying the contract. + /// Secret key URI for the account calling the contract. /// /// e.g. /// - for a dev account "//Alice" @@ -57,26 +61,38 @@ pub struct CallContractCommand { impl CallContractCommand { /// Executes the command. - pub(crate) async fn execute(self) -> anyhow::Result<()> { + pub(crate) async fn execute(self) -> Result<()> { clear_screen()?; intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; set_theme(Theme); + let call_config = if self.contract.is_none() { + guide_user_to_call_contract().await? + } else { + self.clone() + }; + let contract = call_config + .contract + .expect("contract can not be none as fallback above is interactive input; qed"); + let message = call_config + .message + .expect("message can not be none as fallback above is interactive input; qed"); + let call_exec = set_up_call(CallOpts { - path: self.path.clone(), - contract: self.contract.clone(), - message: self.message.clone(), - args: self.args.clone(), - value: self.value.clone(), - gas_limit: self.gas_limit, - proof_size: self.proof_size, - url: self.url.clone(), - suri: self.suri.clone(), - execute: self.execute, + path: call_config.path, + contract, + message, + args: call_config.args, + value: call_config.value, + gas_limit: call_config.gas_limit, + proof_size: call_config.proof_size, + url: call_config.url, + suri: call_config.suri, + execute: call_config.execute, }) .await?; - if self.dry_run { + if call_config.dry_run { let spinner = cliclack::spinner(); spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { @@ -92,16 +108,16 @@ impl CallContractCommand { return Ok(()); } - if !self.execute { + if !call_config.execute { let spinner = cliclack::spinner(); spinner.start("Calling the contract..."); let call_dry_run_result = dry_run_call(&call_exec).await?; log::info(format!("Result: {}", call_dry_run_result))?; log::warning("Your call has not been executed.")?; log::warning(format!( - "To submit the transaction and execute the call on chain, add {} flag to the command.", - "-x/--execute" - ))?; + "To submit the transaction and execute the call on chain, add {} flag to the command.", + "-x/--execute" + ))?; } else { let weight_limit; if self.gas_limit.is_some() && self.proof_size.is_some() { @@ -136,3 +152,91 @@ impl CallContractCommand { Ok(()) } } + +/// Guide the user to call the contract. +async fn guide_user_to_call_contract() -> anyhow::Result { + Cli.intro("Calling a contract")?; + + // Prompt for location of your contract. + let path: String = input("Path to your contract") + .placeholder("./my_contract") + .default_input("./") + .interact()?; + let contract_path = PathBuf::from(&path); + + // Prompt for contract address. + let contract_address: String = input("Paste the on-chain contract address:") + .placeholder("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .interact()?; + + // TODO: Guess the metadata path from the contract path. + let metadata_path = contract_path.join("target/ink/metadata.json"); + let messages = get_messages(metadata_path)?; + let message = display_select_options(&messages)?; + let mut contract_args = Vec::new(); + for arg in &message.args { + contract_args.push(input(arg).placeholder(arg).interact()?); + } + let mut value = "0".to_string(); + if message.payable { + value = input("Value to transfer to the contract: ") + .placeholder("0") + .default_input("0") + .interact()?; + } + // Prompt for gas limit of the call. + let gas_limit_input: u64 = input("Gas Limit:") + .required(false) + .placeholder("By default it will use an Estimation") + .default_input("0") + .interact()?; + let gas_limit: Option = (gas_limit_input != 0).then_some(gas_limit_input); + + // Prompt for proof_size of your contract. + let proof_size_input: u64 = input("Proof size:") + .required(false) + .placeholder("By default it will use an Estimation") + .default_input("0") + .interact()?; + let proof_size: Option = (proof_size_input != 0).then_some(proof_size_input); + + // Prompt for contract location. + let url: String = input("Where is your contract?") + .placeholder("ws://localhost:9944") + .default_input("ws://localhost:9944") + .interact()?; + + // Who is calling the contract. + let suri: String = input("Secret key URI for the account calling the contract:") + .placeholder("//Alice") + .default_input("//Alice") + .interact()?; + + let is_call_confirmed: bool = + confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") + .initial_value(true) + .interact()?; + + Ok(CallContractCommand { + path: Some(contract_path), + contract: Some(contract_address), + message: Some(message.label.clone()), + args: contract_args, + value, + gas_limit, + proof_size, + url: url::Url::parse(&url)?, + suri, + execute: message.mutates, + dry_run: is_call_confirmed, + }) +} + +fn display_select_options(messages: &Vec) -> Result<&Message> { + let mut prompt = cliclack::select("Select the call:".to_string()); + for message in messages { + prompt = prompt.item(message, &message.label, &message.docs); + } + Ok(prompt.interact()?) +} diff --git a/crates/pop-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index 94e3a6d7..19bcbdf3 100644 --- a/crates/pop-contracts/Cargo.toml +++ b/crates/pop-contracts/Cargo.toml @@ -33,7 +33,8 @@ subxt.workspace = true # cargo-contracts contract-build.workspace = true contract-extrinsics.workspace = true - +contract-transcode.workspace = true +scale-info.workspace = true # pop pop-common = { path = "../pop-common", version = "0.3.0" } diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs new file mode 100644 index 00000000..e026b522 --- /dev/null +++ b/crates/pop-contracts/src/call/metadata.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::errors::Error; +use contract_transcode::{ink_metadata::MessageParamSpec, ContractMessageTranscoder}; +use scale_info::form::PortableForm; +use std::path::PathBuf; + +#[derive(Clone, PartialEq, Eq)] +// TODO: We are ignoring selector, return type for now. +/// Describes a contract message. +pub struct Message { + /// The label of the message. + pub label: String, + /// If the message is allowed to mutate the contract state. + pub mutates: bool, + /// If the message accepts any `value` from the caller. + pub payable: bool, + /// The parameters of the deployment handler. + pub args: Vec, + /// The message documentation. + pub docs: String, + /// If the message is the default for off-chain consumers (e.g UIs). + pub default: bool, +} + +pub fn get_messages(metadata_path: PathBuf) -> Result, Error> { + let transcoder = ContractMessageTranscoder::load(metadata_path)?; + let mut messages: Vec = Vec::new(); + for message in transcoder.metadata().spec().messages() { + messages.push(Message { + label: message.label().to_string(), + mutates: message.mutates(), + payable: message.payable(), + args: process_args(message.args()), + docs: message.docs().join("."), + default: *message.default(), + }); + } + Ok(messages) +} +//TODO: We are ignoring the type of the argument. +fn process_args(message_params: &[MessageParamSpec]) -> Vec { + let mut args: Vec = Vec::new(); + for arg in message_params { + args.push(arg.label().to_string()); + } + args +} diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call/mod.rs similarity index 99% rename from crates/pop-contracts/src/call.rs rename to crates/pop-contracts/src/call/mod.rs index 88a3a65b..b5ee9a9d 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call/mod.rs @@ -20,6 +20,8 @@ use subxt::{Config, PolkadotConfig as DefaultConfig}; use subxt_signer::sr25519::Keypair; use url::Url; +pub mod metadata; + /// Attributes for the `call` command. pub struct CallOpts { /// Path to the contract build directory. diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 1d558de3..36272e23 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -13,7 +13,9 @@ mod utils; pub use build::{build_smart_contract, is_supported, Verbosity}; pub use call::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, set_up_call, CallOpts, + call_smart_contract, dry_run_call, dry_run_gas_estimate_call, + metadata::{get_messages, Message}, + set_up_call, CallOpts, }; pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; From f9e305a8da006f7d770fb21fd12d4f07722bc061 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 5 Sep 2024 17:03:06 +0200 Subject: [PATCH 02/27] feat: get metadata contract from the contract path --- crates/pop-cli/src/commands/call/contract.rs | 12 +++++------- crates/pop-contracts/src/call/metadata.rs | 15 +++++++++++---- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index ab361fa1..a1b6b955 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -13,7 +13,7 @@ use pop_contracts::{ CallOpts, Message, }; use sp_weights::Weight; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[derive(Args, Clone)] pub struct CallContractCommand { @@ -158,11 +158,11 @@ async fn guide_user_to_call_contract() -> anyhow::Result { Cli.intro("Calling a contract")?; // Prompt for location of your contract. - let path: String = input("Path to your contract") + let input_path: String = input("Path to your contract") .placeholder("./my_contract") .default_input("./") .interact()?; - let contract_path = PathBuf::from(&path); + let contract_path = Path::new(&input_path); // Prompt for contract address. let contract_address: String = input("Paste the on-chain contract address:") @@ -170,9 +170,7 @@ async fn guide_user_to_call_contract() -> anyhow::Result { .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .interact()?; - // TODO: Guess the metadata path from the contract path. - let metadata_path = contract_path.join("target/ink/metadata.json"); - let messages = get_messages(metadata_path)?; + let messages = get_messages(contract_path)?; let message = display_select_options(&messages)?; let mut contract_args = Vec::new(); for arg in &message.args { @@ -219,7 +217,7 @@ async fn guide_user_to_call_contract() -> anyhow::Result { .interact()?; Ok(CallContractCommand { - path: Some(contract_path), + path: Some(contract_path.to_path_buf()), contract: Some(contract_address), message: Some(message.label.clone()), args: contract_args, diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index e026b522..651c6040 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -1,9 +1,10 @@ // SPDX-License-Identifier: GPL-3.0 use crate::errors::Error; -use contract_transcode::{ink_metadata::MessageParamSpec, ContractMessageTranscoder}; +use contract_extrinsics::ContractArtifacts; +use contract_transcode::ink_metadata::MessageParamSpec; use scale_info::form::PortableForm; -use std::path::PathBuf; +use std::path::Path; #[derive(Clone, PartialEq, Eq)] // TODO: We are ignoring selector, return type for now. @@ -23,8 +24,14 @@ pub struct Message { pub default: bool, } -pub fn get_messages(metadata_path: PathBuf) -> Result, Error> { - let transcoder = ContractMessageTranscoder::load(metadata_path)?; +pub fn get_messages(path: &Path) -> Result, Error> { + let cargo_toml_path = match path.ends_with("Cargo.toml") { + true => path.to_path_buf(), + false => path.join("Cargo.toml"), + }; + let contract_artifacts = + ContractArtifacts::from_manifest_or_file(Some(&cargo_toml_path), None)?; + let transcoder = contract_artifacts.contract_transcoder()?; let mut messages: Vec = Vec::new(); for message in transcoder.metadata().spec().messages() { messages.push(Message { From 5327bf9612ddb5fcb4408730195099d88e68b464 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 5 Sep 2024 18:25:24 +0200 Subject: [PATCH 03/27] refactor: refactor test and validate address input --- crates/pop-cli/src/commands/call/contract.rs | 44 ++++++++++---------- crates/pop-contracts/src/call/metadata.rs | 2 +- crates/pop-contracts/src/lib.rs | 2 +- 3 files changed, 25 insertions(+), 23 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index a1b6b955..bd219bed 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -9,8 +9,8 @@ use clap::Args; use cliclack::{clear_screen, confirm, input, intro, log, outro, outro_cancel, set_theme}; use console::style; use pop_contracts::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, set_up_call, - CallOpts, Message, + call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, parse_account, + set_up_call, CallOpts, Message, }; use sp_weights::Weight; use std::path::{Path, PathBuf}; @@ -155,11 +155,11 @@ impl CallContractCommand { /// Guide the user to call the contract. async fn guide_user_to_call_contract() -> anyhow::Result { - Cli.intro("Calling a contract")?; + Cli.intro("Call a contract")?; // Prompt for location of your contract. - let input_path: String = input("Path to your contract") - .placeholder("./my_contract") + let input_path: String = input("Where is your project located?") + .placeholder("./") .default_input("./") .interact()?; let contract_path = Path::new(&input_path); @@ -167,6 +167,10 @@ async fn guide_user_to_call_contract() -> anyhow::Result { // Prompt for contract address. let contract_address: String = input("Paste the on-chain contract address:") .placeholder("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .validate(|input: &String| match parse_account(input) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid address."), + }) .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .interact()?; @@ -178,35 +182,33 @@ async fn guide_user_to_call_contract() -> anyhow::Result { } let mut value = "0".to_string(); if message.payable { - value = input("Value to transfer to the contract: ") + value = input("Value to transfer to the call:") .placeholder("0") .default_input("0") .interact()?; } - // Prompt for gas limit of the call. - let gas_limit_input: u64 = input("Gas Limit:") + // Prompt for gas limit and proof_size of the call. + let gas_limit_input: String = input("Enter the gas limit:") .required(false) - .placeholder("By default it will use an Estimation") - .default_input("0") + .default_input("") + .placeholder("if left blank, an estimation will be used") .interact()?; - let gas_limit: Option = (gas_limit_input != 0).then_some(gas_limit_input); - - // Prompt for proof_size of your contract. - let proof_size_input: u64 = input("Proof size:") + let gas_limit: Option = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + let proof_size_input: String = input("Enter the proof size limit:") .required(false) - .placeholder("By default it will use an Estimation") - .default_input("0") + .placeholder("if left blank, an estimation will be used") + .default_input("") .interact()?; - let proof_size: Option = (proof_size_input != 0).then_some(proof_size_input); + let proof_size: Option = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. // Prompt for contract location. - let url: String = input("Where is your contract?") + let url: String = input("Where is your contract deployed?") .placeholder("ws://localhost:9944") .default_input("ws://localhost:9944") .interact()?; // Who is calling the contract. - let suri: String = input("Secret key URI for the account calling the contract:") + let suri: String = input("Signer calling the contract:") .placeholder("//Alice") .default_input("//Alice") .interact()?; @@ -227,12 +229,12 @@ async fn guide_user_to_call_contract() -> anyhow::Result { url: url::Url::parse(&url)?, suri, execute: message.mutates, - dry_run: is_call_confirmed, + dry_run: !is_call_confirmed, }) } fn display_select_options(messages: &Vec) -> Result<&Message> { - let mut prompt = cliclack::select("Select the call:".to_string()); + let mut prompt = cliclack::select("Select the message to call:"); for message in messages { prompt = prompt.item(message, &message.label, &message.docs); } diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index 651c6040..0de02cf9 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -39,7 +39,7 @@ pub fn get_messages(path: &Path) -> Result, Error> { mutates: message.mutates(), payable: message.payable(), args: process_args(message.args()), - docs: message.docs().join("."), + docs: message.docs().join(" "), default: *message.default(), }); } diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 36272e23..c883c165 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -25,4 +25,4 @@ pub use up::{ dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, }; -pub use utils::signer::parse_hex_bytes; +pub use utils::{helpers::parse_account, signer::parse_hex_bytes}; From 83e236cc5daeb6aed405a3b545a376f651ef15f1 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 6 Sep 2024 11:53:49 +0200 Subject: [PATCH 04/27] fix: apply feedback --- crates/pop-cli/src/commands/call/contract.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index bd219bed..5dc91aa4 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -166,7 +166,7 @@ async fn guide_user_to_call_contract() -> anyhow::Result { // Prompt for contract address. let contract_address: String = input("Paste the on-chain contract address:") - .placeholder("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .validate(|input: &String| match parse_account(input) { Ok(_) => Ok(()), Err(_) => Err("Invalid address."), @@ -174,7 +174,13 @@ async fn guide_user_to_call_contract() -> anyhow::Result { .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .interact()?; - let messages = get_messages(contract_path)?; + let messages = match get_messages(contract_path) { + Ok(messages) => messages, + Err(e) => { + outro_cancel("Unable to fetch contract metadata.")?; + return Err(anyhow!(format!("{}", e.to_string()))); + }, + }; let message = display_select_options(&messages)?; let mut contract_args = Vec::new(); for arg in &message.args { @@ -191,12 +197,12 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let gas_limit_input: String = input("Enter the gas limit:") .required(false) .default_input("") - .placeholder("if left blank, an estimation will be used") + .placeholder("If left blank, an estimation will be used") .interact()?; let gas_limit: Option = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. let proof_size_input: String = input("Enter the proof size limit:") .required(false) - .placeholder("if left blank, an estimation will be used") + .placeholder("If left blank, an estimation will be used") .default_input("") .interact()?; let proof_size: Option = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. From 7cef633a6712f827046a8d7c7199400314962238 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 6 Sep 2024 14:48:46 +0200 Subject: [PATCH 05/27] feat: prompt to have another call and skip questions for queries --- crates/pop-cli/src/commands/call/contract.rs | 56 ++++++++++++-------- crates/pop-cli/src/commands/mod.rs | 2 +- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 5dc91aa4..68f681af 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -61,7 +61,7 @@ pub struct CallContractCommand { impl CallContractCommand { /// Executes the command. - pub(crate) async fn execute(self) -> Result<()> { + pub(crate) async fn execute(self: Box) -> Result<()> { clear_screen()?; intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; set_theme(Theme); @@ -69,7 +69,7 @@ impl CallContractCommand { let call_config = if self.contract.is_none() { guide_user_to_call_contract().await? } else { - self.clone() + *self.clone() }; let contract = call_config .contract @@ -114,10 +114,6 @@ impl CallContractCommand { let call_dry_run_result = dry_run_call(&call_exec).await?; log::info(format!("Result: {}", call_dry_run_result))?; log::warning("Your call has not been executed.")?; - log::warning(format!( - "To submit the transaction and execute the call on chain, add {} flag to the command.", - "-x/--execute" - ))?; } else { let weight_limit; if self.gas_limit.is_some() && self.proof_size.is_some() { @@ -147,6 +143,13 @@ impl CallContractCommand { log::info(call_result)?; } + if self.contract.is_none() { + let another_call: bool = + confirm("Do you want to do another call?").initial_value(false).interact()?; + if another_call { + Box::pin(self.execute()).await?; + } + } outro("Call completed successfully!")?; Ok(()) @@ -193,19 +196,23 @@ async fn guide_user_to_call_contract() -> anyhow::Result { .default_input("0") .interact()?; } - // Prompt for gas limit and proof_size of the call. - let gas_limit_input: String = input("Enter the gas limit:") - .required(false) - .default_input("") - .placeholder("If left blank, an estimation will be used") - .interact()?; - let gas_limit: Option = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. - let proof_size_input: String = input("Enter the proof size limit:") - .required(false) - .placeholder("If left blank, an estimation will be used") - .default_input("") - .interact()?; - let proof_size: Option = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. + let mut gas_limit: Option = None; + let mut proof_size: Option = None; + if message.mutates { + // Prompt for gas limit and proof_size of the call. + let gas_limit_input: String = input("Enter the gas limit:") + .required(false) + .default_input("") + .placeholder("If left blank, an estimation will be used") + .interact()?; + gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + let proof_size_input: String = input("Enter the proof size limit:") + .required(false) + .placeholder("If left blank, an estimation will be used") + .default_input("") + .interact()?; + proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. + } // Prompt for contract location. let url: String = input("Where is your contract deployed?") @@ -219,10 +226,13 @@ async fn guide_user_to_call_contract() -> anyhow::Result { .default_input("//Alice") .interact()?; - let is_call_confirmed: bool = - confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") - .initial_value(true) - .interact()?; + let mut is_call_confirmed: bool = true; + if message.mutates { + is_call_confirmed = + confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") + .initial_value(true) + .interact()?; + } Ok(CallContractCommand { path: Some(contract_path.to_path_buf()), diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 34c2f10b..8a2941c9 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,7 +98,7 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => cmd.execute().await.map(|_| Value::Null), + call::Command::Contract(cmd) => Box::new(cmd).execute().await.map(|_| Value::Null), }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { From 62eefcfd1d15550ba68b3a0881fdfe78b1164796 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Sun, 8 Sep 2024 22:14:33 +0200 Subject: [PATCH 06/27] refactor: use Cli module instead of cliclack --- crates/pop-cli/src/cli.rs | 94 ++++++++++++++++++++ crates/pop-cli/src/commands/call/contract.rs | 71 ++++++++------- 2 files changed, 132 insertions(+), 33 deletions(-) diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 353a4704..d11f6a47 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -13,6 +13,8 @@ pub(crate) mod traits { fn confirm(&mut self, prompt: impl Display) -> impl Confirm; /// Prints an info message. fn info(&mut self, text: impl Display) -> Result<()>; + /// Constructs a new [`Input`] prompt. + fn input(&mut self, prompt: impl Display) -> impl Input; /// Prints a header of the prompt sequence. fn intro(&mut self, title: impl Display) -> Result<()>; /// Constructs a new [`MultiSelect`] prompt. @@ -21,6 +23,8 @@ pub(crate) mod traits { fn outro(&mut self, message: impl Display) -> Result<()>; /// Prints a footer of the prompt sequence with a failure style. fn outro_cancel(&mut self, message: impl Display) -> Result<()>; + /// Constructs a new [`Select`] prompt. + fn select(&mut self, prompt: impl Display) -> impl Select; /// Prints a success message. fn success(&mut self, message: impl Display) -> Result<()>; /// Prints a warning message. @@ -29,10 +33,29 @@ pub(crate) mod traits { /// A confirmation prompt. pub trait Confirm { + /// Sets the initially selected value. + fn initial_value(self, initial_value: bool) -> Self; /// Starts the prompt interaction. fn interact(&mut self) -> Result; } + /// A text input prompt. + pub trait Input { + /// Sets the default value for the input. + fn default_input(self, value: &str) -> Self; + /// Starts the prompt interaction. + fn interact(&mut self) -> Result; + /// Sets the placeholder (hint) text for the input. + fn placeholder(self, value: &str) -> Self; + /// Sets whether the input is required. + fn required(self, required: bool) -> Self; + /// Sets a validation callback for the input that is called when the user submits. + fn validate( + self, + validator: impl Fn(&String) -> std::result::Result<(), &'static str> + 'static, + ) -> Self; + } + /// A multi-select prompt. pub trait MultiSelect { /// Starts the prompt interaction. @@ -42,6 +65,14 @@ pub(crate) mod traits { /// Sets whether the input is required. fn required(self, required: bool) -> Self; } + + /// A select prompt. + pub trait Select { + /// Starts the prompt interaction. + fn interact(&mut self) -> Result; + /// Adds an item to the selection prompt. + fn item(self, value: T, label: impl Display, hint: impl Display) -> Self; + } } /// A command line interface using cliclack. @@ -57,6 +88,11 @@ impl traits::Cli for Cli { cliclack::log::info(text) } + /// Constructs a new [`Input`] prompt. + fn input(&mut self, prompt: impl Display) -> impl traits::Input { + Input(cliclack::input(prompt)) + } + /// Prints a header of the prompt sequence. fn intro(&mut self, title: impl Display) -> Result<()> { cliclack::clear_screen()?; @@ -79,6 +115,11 @@ impl traits::Cli for Cli { cliclack::outro_cancel(message) } + /// Constructs a new [`Select`] prompt. + fn select(&mut self, prompt: impl Display) -> impl traits::Select { + Select::(cliclack::select(prompt)) + } + /// Prints a success message. fn success(&mut self, message: impl Display) -> Result<()> { cliclack::log::success(message) @@ -97,6 +138,43 @@ impl traits::Confirm for Confirm { 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 + } +} + +/// 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 { + self.0 = self.0.default_input(value); + self + } + /// Starts the prompt interaction. + fn interact(&mut self) -> Result { + self.0.interact() + } + /// Sets the placeholder (hint) text for the input. + fn placeholder(mut self, placeholder: &str) -> Self { + self.0 = self.0.placeholder(placeholder); + self + } + /// Sets whether the input is required. + fn required(mut self, required: bool) -> Self { + self.0 = self.0.required(required); + self + } + /// Sets a validation callback for the input that is called when the user submits. + fn validate( + mut self, + validator: impl Fn(&String) -> std::result::Result<(), &'static str> + 'static, + ) -> Self { + self.0 = self.0.validate(validator); + self + } } /// A multi-select prompt using cliclack. @@ -121,6 +199,22 @@ impl traits::MultiSelect for MultiSelect { } } +/// A select prompt using cliclack. +struct Select(cliclack::Select); + +impl traits::Select for Select { + /// Starts the prompt interaction. + fn interact(&mut self) -> Result { + self.0.interact() + } + + /// Adds an item to the selection prompt. + fn item(mut self, value: T, label: impl Display, hint: impl Display) -> Self { + self.0 = self.0.item(value, label, hint); + self + } +} + #[cfg(test)] pub(crate) mod tests { use super::traits::*; diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 68f681af..0af0fb43 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,13 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::{ - cli::{traits::Cli as _, Cli}, - style::Theme, +use crate::cli::{ + traits::{Cli as _, Confirm, Input, Select}, + Cli, }; use anyhow::{anyhow, Result}; use clap::Args; -use cliclack::{clear_screen, confirm, input, intro, log, outro, outro_cancel, set_theme}; -use console::style; use pop_contracts::{ call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, parse_account, set_up_call, CallOpts, Message, @@ -62,9 +60,7 @@ pub struct CallContractCommand { impl CallContractCommand { /// Executes the command. pub(crate) async fn execute(self: Box) -> Result<()> { - clear_screen()?; - intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; - set_theme(Theme); + Cli.intro("Calling a contract")?; let call_config = if self.contract.is_none() { guide_user_to_call_contract().await? @@ -97,12 +93,12 @@ impl CallContractCommand { spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { - log::info(format!("Gas limit: {:?}", w))?; - log::warning("Your call has not been executed.")?; + Cli.info(format!("Gas limit: {:?}", w))?; + Cli.warning("Your call has not been executed.")?; }, Err(e) => { spinner.error(format!("{e}")); - outro_cancel("Call failed.")?; + Cli.outro_cancel("Call failed.")?; }, }; return Ok(()); @@ -112,8 +108,8 @@ impl CallContractCommand { let spinner = cliclack::spinner(); spinner.start("Calling the contract..."); let call_dry_run_result = dry_run_call(&call_exec).await?; - log::info(format!("Result: {}", call_dry_run_result))?; - log::warning("Your call has not been executed.")?; + Cli.info(format!("Result: {}", call_dry_run_result))?; + Cli.warning("Your call has not been executed.")?; } else { let weight_limit; if self.gas_limit.is_some() && self.proof_size.is_some() { @@ -124,12 +120,12 @@ impl CallContractCommand { spinner.start("Doing a dry run to estimate the gas..."); weight_limit = match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { - log::info(format!("Gas limit: {:?}", w))?; + Cli.info(format!("Gas limit: {:?}", w))?; w }, Err(e) => { spinner.error(format!("{e}")); - outro_cancel("Call failed.")?; + Cli.outro_cancel("Call failed.")?; return Ok(()); }, }; @@ -141,17 +137,17 @@ impl CallContractCommand { .await .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; - log::info(call_result)?; + Cli.info(call_result)?; } if self.contract.is_none() { let another_call: bool = - confirm("Do you want to do another call?").initial_value(false).interact()?; + Cli.confirm("Do you want to do another call?").initial_value(false).interact()?; if another_call { Box::pin(self.execute()).await?; } } - outro("Call completed successfully!")?; + Cli.outro("Call completed successfully!")?; Ok(()) } } @@ -161,14 +157,16 @@ async fn guide_user_to_call_contract() -> anyhow::Result { Cli.intro("Call a contract")?; // Prompt for location of your contract. - let input_path: String = input("Where is your project located?") + let input_path: String = Cli + .input("Where is your project located?") .placeholder("./") .default_input("./") .interact()?; let contract_path = Path::new(&input_path); // Prompt for contract address. - let contract_address: String = input("Paste the on-chain contract address:") + let contract_address: String = Cli + .input("Paste the on-chain contract address:") .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .validate(|input: &String| match parse_account(input) { Ok(_) => Ok(()), @@ -180,18 +178,19 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let messages = match get_messages(contract_path) { Ok(messages) => messages, Err(e) => { - outro_cancel("Unable to fetch contract metadata.")?; + Cli.outro_cancel("Unable to fetch contract metadata.")?; return Err(anyhow!(format!("{}", e.to_string()))); }, }; let message = display_select_options(&messages)?; let mut contract_args = Vec::new(); for arg in &message.args { - contract_args.push(input(arg).placeholder(arg).interact()?); + contract_args.push(Cli.input(arg).placeholder(arg).interact()?); } let mut value = "0".to_string(); if message.payable { - value = input("Value to transfer to the call:") + value = Cli + .input("Value to transfer to the call:") .placeholder("0") .default_input("0") .interact()?; @@ -200,13 +199,15 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let mut proof_size: Option = None; if message.mutates { // Prompt for gas limit and proof_size of the call. - let gas_limit_input: String = input("Enter the gas limit:") + let gas_limit_input: String = Cli + .input("Enter the gas limit:") .required(false) .default_input("") .placeholder("If left blank, an estimation will be used") .interact()?; gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. - let proof_size_input: String = input("Enter the proof size limit:") + let proof_size_input: String = Cli + .input("Enter the proof size limit:") .required(false) .placeholder("If left blank, an estimation will be used") .default_input("") @@ -215,23 +216,25 @@ async fn guide_user_to_call_contract() -> anyhow::Result { } // Prompt for contract location. - let url: String = input("Where is your contract deployed?") + let url: String = Cli + .input("Where is your contract deployed?") .placeholder("ws://localhost:9944") .default_input("ws://localhost:9944") .interact()?; // Who is calling the contract. - let suri: String = input("Signer calling the contract:") + let suri: String = Cli + .input("Signer calling the contract:") .placeholder("//Alice") .default_input("//Alice") .interact()?; let mut is_call_confirmed: bool = true; if message.mutates { - is_call_confirmed = - confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") - .initial_value(true) - .interact()?; + is_call_confirmed = Cli + .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") + .initial_value(true) + .interact()?; } Ok(CallContractCommand { @@ -250,9 +253,11 @@ async fn guide_user_to_call_contract() -> anyhow::Result { } fn display_select_options(messages: &Vec) -> Result<&Message> { - let mut prompt = cliclack::select("Select the message to call:"); + let mut cli = Cli; + let mut prompt = cli.select("Select the message to call:"); for message in messages { prompt = prompt.item(message, &message.label, &message.docs); } - Ok(prompt.interact()?) + let selected_message = prompt.interact()?; + Ok(selected_message) } From fee1e26f54b89f318f495a8e1286a66a9c5dff74 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Mon, 9 Sep 2024 17:21:11 +0200 Subject: [PATCH 07/27] test: unit test pop-cli crate --- crates/pop-cli/src/cli.rs | 131 +++++- crates/pop-cli/src/commands/call/contract.rs | 321 +++++++++++-- crates/pop-cli/src/commands/mod.rs | 7 +- crates/pop-contracts/src/call/mod.rs | 2 +- crates/pop-contracts/src/up.rs | 2 +- .../tests => tests}/files/testing.contract | 0 tests/files/testing.json | 424 ++++++++++++++++++ 7 files changed, 834 insertions(+), 53 deletions(-) rename {crates/pop-contracts/tests => tests}/files/testing.contract (100%) create mode 100644 tests/files/testing.json diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index d11f6a47..65999288 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -218,18 +218,21 @@ impl traits::Select for Select { #[cfg(test)] pub(crate) mod tests { use super::traits::*; - use std::{fmt::Display, io::Result}; + use std::{fmt::Display, io::Result, usize}; /// Mock Cli with optional expectations #[derive(Default)] pub(crate) struct MockCli { confirm_expectation: Option<(String, bool)>, info_expectations: Vec, + input_expectations: Vec<(String, String)>, intro_expectation: Option, outro_expectation: Option, multiselect_expectation: Option<(String, Option, bool, Option>)>, outro_cancel_expectation: Option, + select_expectation: + Option<(String, Option, bool, Option>, usize)>, success_expectations: Vec, warning_expectations: Vec, } @@ -244,6 +247,11 @@ pub(crate) mod tests { self } + pub(crate) fn expect_input(mut self, prompt: impl Display, input: String) -> Self { + self.input_expectations.push((prompt.to_string(), input)); + self + } + pub(crate) fn expect_info(mut self, message: impl Display) -> Self { self.info_expectations.push(message.to_string()); self @@ -275,6 +283,18 @@ pub(crate) mod tests { self } + pub(crate) fn expect_select( + mut self, + prompt: impl Display, + required: Option, + collect: bool, + items: Option>, + item: usize, + ) -> Self { + self.select_expectation = Some((prompt.to_string(), required, collect, items, item)); + self + } + pub(crate) fn expect_success(mut self, message: impl Display) -> Self { self.success_expectations.push(message.to_string()); self @@ -292,6 +312,9 @@ pub(crate) mod tests { if !self.info_expectations.is_empty() { panic!("`{}` info log expectations not satisfied", self.info_expectations.join(",")) } + if !self.input_expectations.is_empty() { + panic!("`{:?}` input expectation not satisfied", self.input_expectations) + } if let Some(expectation) = self.intro_expectation { panic!("`{expectation}` intro expectation not satisfied") } @@ -304,6 +327,9 @@ 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.success_expectations.is_empty() { panic!( "`{}` success log expectations not satisfied", @@ -336,6 +362,20 @@ pub(crate) mod tests { Ok(()) } + fn input(&mut self, prompt: impl Display) -> impl Input { + let prompt = prompt.to_string(); + if let Some((expectation, input)) = self.input_expectations.pop() { + assert_eq!(expectation, prompt, "prompt does not satisfy expectation"); + return MockInput { + prompt: input.clone(), + input, + placeholder: "".to_string(), + required: false, + }; + } + MockInput::default() + } + fn intro(&mut self, title: impl Display) -> Result<()> { if let Some(expectation) = self.intro_expectation.take() { assert_eq!(expectation, title.to_string(), "intro does not satisfy expectation"); @@ -382,6 +422,21 @@ pub(crate) mod tests { Ok(()) } + fn select(&mut self, prompt: impl Display) -> impl Select { + let prompt = prompt.to_string(); + println!("prompt: {}", prompt); + if let Some((expectation, _, collect, items_expectation, item)) = + self.select_expectation.take() + { + println!("expectation: {}", expectation); + println!("items_expectation: {:?}", items_expectation); + assert_eq!(expectation, prompt, "prompt does not satisfy expectation"); + return MockSelect { items_expectation, collect, items: vec![], item }; + } + + MockSelect::default() + } + fn success(&mut self, message: impl Display) -> Result<()> { let message = message.to_string(); self.success_expectations.retain(|x| *x != message); @@ -405,6 +460,46 @@ pub(crate) mod tests { fn interact(&mut self) -> Result { Ok(self.confirm) } + fn initial_value(mut self, initial_value: bool) -> Self { + self.confirm = initial_value; + self + } + } + + /// Mock input prompt + #[derive(Default)] + struct MockInput { + prompt: String, + input: String, + placeholder: String, + required: bool, + } + + impl Input for MockInput { + fn interact(&mut self) -> Result { + Ok(self.prompt.clone()) + } + fn default_input(mut self, value: &str) -> Self { + self.input = value.to_string(); + self + } + + fn placeholder(mut self, value: &str) -> Self { + self.placeholder = value.to_string(); + self + } + + fn required(mut self, value: bool) -> Self { + self.required = value; + self + } + + fn validate( + self, + _validator: impl Fn(&String) -> std::result::Result<(), &'static str> + 'static, + ) -> Self { + self + } } /// Mock multi-select prompt @@ -454,4 +549,38 @@ pub(crate) mod tests { self } } + + /// Mock select prompt + pub(crate) struct MockSelect { + items_expectation: Option>, + collect: bool, + items: Vec, + item: usize, + } + + impl MockSelect { + pub(crate) fn default() -> Self { + Self { items_expectation: None, collect: false, items: vec![], item: 0 } + } + } + + impl Select for MockSelect { + fn interact(&mut self) -> Result { + Ok(self.items[self.item].clone()) + } + + fn item(mut self, value: T, label: impl Display, hint: impl Display) -> Self { + // Check expectations + if let Some(items) = self.items_expectation.as_mut() { + let item = (label.to_string(), hint.to_string()); + assert!(items.contains(&item), "`{item:?}` item does not satisfy any expectations"); + items.retain(|x| *x != item); + } + // Collect if specified + if self.collect { + self.items.push(value); + } + self + } + } } diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 0af0fb43..efaff7db 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,14 +1,11 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::cli::{ - traits::{Cli as _, Confirm, Input, Select}, - Cli, -}; +use crate::cli::traits::*; use anyhow::{anyhow, Result}; use clap::Args; use pop_contracts::{ call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, parse_account, - set_up_call, CallOpts, Message, + set_up_call, CallOpts, }; use sp_weights::Weight; use std::path::{Path, PathBuf}; @@ -57,19 +54,27 @@ pub struct CallContractCommand { dry_run: bool, } -impl CallContractCommand { +pub(crate) struct CallContract<'a, CLI: Cli> { + /// The cli to be used. + pub(crate) cli: &'a mut CLI, + /// The args to call. + pub(crate) args: CallContractCommand, +} + +impl<'a, CLI: Cli> CallContract<'a, CLI> { /// Executes the command. - pub(crate) async fn execute(self: Box) -> Result<()> { - Cli.intro("Calling a contract")?; + pub(crate) async fn execute(mut self: Box) -> Result<()> { + self.cli.intro("Call a contract")?; - let call_config = if self.contract.is_none() { - guide_user_to_call_contract().await? + let call_config = if self.args.contract.is_none() { + guide_user_to_call_contract(&mut self).await? } else { - *self.clone() + self.args.clone() }; let contract = call_config .contract .expect("contract can not be none as fallback above is interactive input; qed"); + // TODO: Can be nill pop call contract --contract let message = call_config .message .expect("message can not be none as fallback above is interactive input; qed"); @@ -82,7 +87,7 @@ impl CallContractCommand { value: call_config.value, gas_limit: call_config.gas_limit, proof_size: call_config.proof_size, - url: call_config.url, + url: call_config.url.clone(), suri: call_config.suri, execute: call_config.execute, }) @@ -93,12 +98,12 @@ impl CallContractCommand { spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { - Cli.info(format!("Gas limit: {:?}", w))?; - Cli.warning("Your call has not been executed.")?; + self.cli.info(format!("Gas limit: {:?}", w))?; + self.cli.warning("Your call has not been executed.")?; }, Err(e) => { spinner.error(format!("{e}")); - Cli.outro_cancel("Call failed.")?; + self.cli.outro_cancel("Call failed.")?; }, }; return Ok(()); @@ -108,24 +113,26 @@ impl CallContractCommand { let spinner = cliclack::spinner(); spinner.start("Calling the contract..."); let call_dry_run_result = dry_run_call(&call_exec).await?; - Cli.info(format!("Result: {}", call_dry_run_result))?; - Cli.warning("Your call has not been executed.")?; + self.cli.info(format!("Result: {}", call_dry_run_result))?; + self.cli.warning("Your call has not been executed.")?; } else { let weight_limit; - if self.gas_limit.is_some() && self.proof_size.is_some() { - weight_limit = - Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()); + if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { + weight_limit = Weight::from_parts( + call_config.gas_limit.unwrap(), + call_config.proof_size.unwrap(), + ); } else { let spinner = cliclack::spinner(); spinner.start("Doing a dry run to estimate the gas..."); weight_limit = match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { - Cli.info(format!("Gas limit: {:?}", w))?; + self.cli.info(format!("Gas limit: {:?}", w))?; w }, Err(e) => { spinner.error(format!("{e}")); - Cli.outro_cancel("Call failed.")?; + self.cli.outro_cancel("Call failed.")?; return Ok(()); }, }; @@ -133,39 +140,49 @@ impl CallContractCommand { let spinner = cliclack::spinner(); spinner.start("Calling the contract..."); - let call_result = call_smart_contract(call_exec, weight_limit, &self.url) + let call_result = call_smart_contract(call_exec, weight_limit, &call_config.url) .await .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; - Cli.info(call_result)?; + self.cli.info(call_result)?; } - if self.contract.is_none() { - let another_call: bool = - Cli.confirm("Do you want to do another call?").initial_value(false).interact()?; + if self.args.contract.is_none() { + let another_call: bool = self + .cli + .confirm("Do you want to do another call?") + .initial_value(false) + .interact()?; if another_call { Box::pin(self.execute()).await?; + } else { + self.cli.outro("Call completed successfully!")?; } + } else { + self.cli.outro("Call completed successfully!")?; } - - Cli.outro("Call completed successfully!")?; Ok(()) } } /// Guide the user to call the contract. -async fn guide_user_to_call_contract() -> anyhow::Result { - Cli.intro("Call a contract")?; +async fn guide_user_to_call_contract<'a, CLI: Cli>( + command: &mut CallContract<'a, CLI>, +) -> anyhow::Result { + command.cli.intro("Call a contract")?; // Prompt for location of your contract. - let input_path: String = Cli + let input_path: String = command + .cli .input("Where is your project located?") .placeholder("./") .default_input("./") .interact()?; let contract_path = Path::new(&input_path); + println!("path: {:?}", contract_path); // Prompt for contract address. - let contract_address: String = Cli + let contract_address: String = command + .cli .input("Paste the on-chain contract address:") .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .validate(|input: &String| match parse_account(input) { @@ -178,18 +195,27 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let messages = match get_messages(contract_path) { Ok(messages) => messages, Err(e) => { - Cli.outro_cancel("Unable to fetch contract metadata.")?; + command.cli.outro_cancel("Unable to fetch contract metadata.")?; return Err(anyhow!(format!("{}", e.to_string()))); }, }; - let message = display_select_options(&messages)?; + let message = { + let mut prompt = command.cli.select("Select the message to call:"); + for select_message in messages { + prompt = + prompt.item(select_message.clone(), &select_message.label, &select_message.docs); + } + prompt.interact()? + }; + let mut contract_args = Vec::new(); for arg in &message.args { - contract_args.push(Cli.input(arg).placeholder(arg).interact()?); + contract_args.push(command.cli.input(arg).placeholder(arg).interact()?); } let mut value = "0".to_string(); if message.payable { - value = Cli + value = command + .cli .input("Value to transfer to the call:") .placeholder("0") .default_input("0") @@ -199,14 +225,16 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let mut proof_size: Option = None; if message.mutates { // Prompt for gas limit and proof_size of the call. - let gas_limit_input: String = Cli + let gas_limit_input: String = command + .cli .input("Enter the gas limit:") .required(false) .default_input("") .placeholder("If left blank, an estimation will be used") .interact()?; gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. - let proof_size_input: String = Cli + let proof_size_input: String = command + .cli .input("Enter the proof size limit:") .required(false) .placeholder("If left blank, an estimation will be used") @@ -216,14 +244,16 @@ async fn guide_user_to_call_contract() -> anyhow::Result { } // Prompt for contract location. - let url: String = Cli + let url: String = command + .cli .input("Where is your contract deployed?") .placeholder("ws://localhost:9944") .default_input("ws://localhost:9944") .interact()?; // Who is calling the contract. - let suri: String = Cli + let suri: String = command + .cli .input("Signer calling the contract:") .placeholder("//Alice") .default_input("//Alice") @@ -231,7 +261,8 @@ async fn guide_user_to_call_contract() -> anyhow::Result { let mut is_call_confirmed: bool = true; if message.mutates { - is_call_confirmed = Cli + is_call_confirmed = command + .cli .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) .interact()?; @@ -252,12 +283,204 @@ async fn guide_user_to_call_contract() -> anyhow::Result { }) } -fn display_select_options(messages: &Vec) -> Result<&Message> { - let mut cli = Cli; - let mut prompt = cli.select("Select the message to call:"); - for message in messages { - prompt = prompt.item(message, &message.label, &message.docs); +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::MockCli; + use pop_contracts::{create_smart_contract, Contract}; + use std::{env, fs}; + use url::Url; + + fn generate_smart_contract_test_environment() -> Result { + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let temp_contract_dir = temp_dir.path().join("testing"); + fs::create_dir(&temp_contract_dir)?; + create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; + Ok(temp_dir) + } + // Function that mocks the build process generating the contract artifacts. + fn mock_build_process(temp_contract_dir: PathBuf) -> Result<()> { + // Create a target directory + let target_contract_dir = temp_contract_dir.join("target"); + fs::create_dir(&target_contract_dir)?; + fs::create_dir(&target_contract_dir.join("ink"))?; + // Copy a mocked testing.contract and testing.json files inside the target directory + let current_dir = env::current_dir().expect("Failed to get current directory"); + let contract_file = current_dir.join("../../tests/files/testing.contract"); + fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; + let metadata_file = current_dir.join("../../tests/files/testing.json"); + fs::copy(metadata_file, &target_contract_dir.join("ink/testing.json"))?; + Ok(()) + } + + #[tokio::test] + async fn call_contract_messages_are_ok() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + mock_build_process(temp_dir.path().join("testing"))?; + + let mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_warning("Your call has not been executed.") + .expect_outro("Call completed successfully!"); + + // Contract deployed on Pop Network testnet, test get + Box::new(CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()), + message: Some("get".to_string()), + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, + }) + .execute() + .await?; + + cli.verify() + } + + // This test only covers the interactive portion of the call contract command, without actually calling the contract. + #[tokio::test] + async fn guide_user_to_query_contract_works() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + mock_build_process(temp_dir.path().join("testing"))?; + + let items = vec![ + ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get".into(), " Simply returns the current value of our `bool`.".into()), + ]; + // The inputs are processed in reverse order. + let mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_input( + "Where is your contract deployed?", + "wss://rpc1.paseo.popnetwork.xyz".into(), + ) + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 1, // "get" message + ) + .expect_input( + "Paste the on-chain contract address:", + "14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".into(), + ) + .expect_input( + "Where is your project located?", + temp_dir.path().join("testing").display().to_string(), + ); + + let call_config = guide_user_to_call_contract(&mut CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("ws://localhost:9944")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, + }) + .await?; + assert_eq!( + call_config.contract, + Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()) + ); + assert_eq!(call_config.message, Some("get".to_string())); + assert_eq!(call_config.args.len(), 0); + assert_eq!(call_config.value, "0".to_string()); + assert_eq!(call_config.gas_limit, None); + assert_eq!(call_config.proof_size, None); + assert_eq!(call_config.url.to_string(), "wss://rpc1.paseo.popnetwork.xyz/"); + assert_eq!(call_config.suri, "//Alice"); + assert!(!call_config.execute); + assert!(!call_config.dry_run); + + cli.verify() + } + + // This test only covers the interactive portion of the call contract command, without actually calling the contract. + #[tokio::test] + async fn guide_user_to_call_contract_works() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + mock_build_process(temp_dir.path().join("testing"))?; + + let items = vec![ + ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get".into(), " Simply returns the current value of our `bool`.".into()), + ]; + // The inputs are processed in reverse order. + let mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_input( + "Where is your contract deployed?", + "wss://rpc1.paseo.popnetwork.xyz".into(), + ) + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 0, // "flip" message + ) + .expect_input("Enter the proof size limit:", "".into()) + .expect_input("Enter the gas limit:", "".into()) + .expect_input( + "Paste the on-chain contract address:", + "14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".into(), + ) + .expect_input( + "Where is your project located?", + temp_dir.path().join("testing").display().to_string(), + ); + + let call_config = guide_user_to_call_contract(&mut CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("ws://localhost:9944")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, + }) + .await?; + assert_eq!( + call_config.contract, + Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()) + ); + assert_eq!(call_config.message, Some("flip".to_string())); + assert_eq!(call_config.args.len(), 0); + assert_eq!(call_config.value, "0".to_string()); + assert_eq!(call_config.gas_limit, None); + assert_eq!(call_config.proof_size, None); + assert_eq!(call_config.url.to_string(), "wss://rpc1.paseo.popnetwork.xyz/"); + assert_eq!(call_config.suri, "//Alice"); + assert!(call_config.execute); + assert!(!call_config.dry_run); + + cli.verify() } - let selected_message = prompt.interact()?; - Ok(selected_message) } diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 8a2941c9..6e896744 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,7 +98,12 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => Box::new(cmd).execute().await.map(|_| Value::Null), + call::Command::Contract(cmd) => { + Box::new(call::contract::CallContract { cli: &mut Cli, args: cmd }) + .execute() + .await + .map(|_| Value::Null) + }, }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { diff --git a/crates/pop-contracts/src/call/mod.rs b/crates/pop-contracts/src/call/mod.rs index b5ee9a9d..591b202c 100644 --- a/crates/pop-contracts/src/call/mod.rs +++ b/crates/pop-contracts/src/call/mod.rs @@ -185,7 +185,7 @@ mod tests { fs::create_dir(&target_contract_dir.join("ink"))?; // Copy a mocked testing.contract file inside the target directory let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("tests/files/testing.contract"); + let contract_file = current_dir.join("../../tests/files/testing.contract"); fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; Ok(()) } diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index f4cc65bd..e387dbe1 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -225,7 +225,7 @@ mod tests { fs::create_dir(&target_contract_dir.join("ink"))?; // Copy a mocked testing.contract file inside the target directory let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("tests/files/testing.contract"); + let contract_file = current_dir.join("../../tests/files/testing.contract"); fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; Ok(()) } diff --git a/crates/pop-contracts/tests/files/testing.contract b/tests/files/testing.contract similarity index 100% rename from crates/pop-contracts/tests/files/testing.contract rename to tests/files/testing.contract diff --git a/tests/files/testing.json b/tests/files/testing.json new file mode 100644 index 00000000..78cff4b6 --- /dev/null +++ b/tests/files/testing.json @@ -0,0 +1,424 @@ +{ + "source": { + "hash": "0x29972a7ea06ca802e5d857dfb7cb6455220679f4e9314a533840b9865ccbb799", + "language": "ink! 5.0.0", + "compiler": "rustc 1.78.0", + "build_info": { + "rust_toolchain": "stable-aarch64-apple-darwin", + "cargo_contract_version": "4.1.1", + "build_mode": "Release", + "wasm_opt_settings": { + "optimization_passes": "Z", + "keep_debug_symbols": false + } + } + }, + "contract": { + "name": "testing", + "version": "0.1.0", + "authors": [ + "[your_name] <[your_email]>" + ] + }, + "image": null, + "version": 5, + "types": [ + { + "id": 0, + "type": { + "def": { + "primitive": "bool" + } + } + }, + { + "id": 1, + "type": { + "path": [ + "testing", + "testing", + "Testing" + ], + "def": { + "composite": { + "fields": [ + { + "name": "value", + "type": 0, + "typeName": ",>>::Type" + } + ] + } + } + } + }, + { + "id": 2, + "type": { + "path": [ + "Result" + ], + "params": [ + { + "name": "T", + "type": 3 + }, + { + "name": "E", + "type": 4 + } + ], + "def": { + "variant": { + "variants": [ + { + "name": "Ok", + "fields": [ + { + "type": 3 + } + ], + "index": 0 + }, + { + "name": "Err", + "fields": [ + { + "type": 4 + } + ], + "index": 1 + } + ] + } + } + } + }, + { + "id": 3, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 4, + "type": { + "path": [ + "ink_primitives", + "LangError" + ], + "def": { + "variant": { + "variants": [ + { + "name": "CouldNotReadInput", + "index": 1 + } + ] + } + } + } + }, + { + "id": 5, + "type": { + "path": [ + "Result" + ], + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 4 + } + ], + "def": { + "variant": { + "variants": [ + { + "name": "Ok", + "fields": [ + { + "type": 0 + } + ], + "index": 0 + }, + { + "name": "Err", + "fields": [ + { + "type": 4 + } + ], + "index": 1 + } + ] + } + } + } + }, + { + "id": 6, + "type": { + "path": [ + "ink_primitives", + "types", + "AccountId" + ], + "def": { + "composite": { + "fields": [ + { + "type": 7, + "typeName": "[u8; 32]" + } + ] + } + } + } + }, + { + "id": 7, + "type": { + "def": { + "array": { + "len": 32, + "type": 8 + } + } + } + }, + { + "id": 8, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 9, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 10, + "type": { + "path": [ + "ink_primitives", + "types", + "Hash" + ], + "def": { + "composite": { + "fields": [ + { + "type": 7, + "typeName": "[u8; 32]" + } + ] + } + } + } + }, + { + "id": 11, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 12, + "type": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 13, + "type": { + "path": [ + "ink_env", + "types", + "NoChainExtension" + ], + "def": { + "variant": {} + } + } + } + ], + "storage": { + "root": { + "root_key": "0x00000000", + "layout": { + "struct": { + "name": "Testing", + "fields": [ + { + "name": "value", + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 0 + } + } + } + ] + } + }, + "ty": 1 + } + }, + "spec": { + "constructors": [ + { + "label": "new", + "selector": "0x9bae9d5e", + "payable": false, + "args": [ + { + "label": "init_value", + "type": { + "type": 0, + "displayName": [ + "bool" + ] + } + } + ], + "returnType": { + "type": 2, + "displayName": [ + "ink_primitives", + "ConstructorResult" + ] + }, + "docs": [ + "Constructor that initializes the `bool` value to the given `init_value`." + ], + "default": false + }, + { + "label": "default", + "selector": "0xed4b9d1b", + "payable": false, + "args": [], + "returnType": { + "type": 2, + "displayName": [ + "ink_primitives", + "ConstructorResult" + ] + }, + "docs": [ + "Constructor that initializes the `bool` value to `false`.", + "", + "Constructors can delegate to other constructors." + ], + "default": false + } + ], + "messages": [ + { + "label": "flip", + "selector": "0x633aa551", + "mutates": true, + "payable": false, + "args": [], + "returnType": { + "type": 2, + "displayName": [ + "ink", + "MessageResult" + ] + }, + "docs": [ + " A message that can be called on instantiated contracts.", + " This one flips the value of the stored `bool` from `true`", + " to `false` and vice versa." + ], + "default": false + }, + { + "label": "get", + "selector": "0x2f865bd9", + "mutates": false, + "payable": false, + "args": [], + "returnType": { + "type": 5, + "displayName": [ + "ink", + "MessageResult" + ] + }, + "docs": [ + " Simply returns the current value of our `bool`." + ], + "default": false + } + ], + "events": [], + "docs": [], + "lang_error": { + "type": 4, + "displayName": [ + "ink", + "LangError" + ] + }, + "environment": { + "accountId": { + "type": 6, + "displayName": [ + "AccountId" + ] + }, + "balance": { + "type": 9, + "displayName": [ + "Balance" + ] + }, + "hash": { + "type": 10, + "displayName": [ + "Hash" + ] + }, + "timestamp": { + "type": 11, + "displayName": [ + "Timestamp" + ] + }, + "blockNumber": { + "type": 12, + "displayName": [ + "BlockNumber" + ] + }, + "chainExtension": { + "type": 13, + "displayName": [ + "ChainExtension" + ] + }, + "maxEventTopics": 4, + "staticBufferSize": 16384 + } + } +} \ No newline at end of file From 6c9aa7711d39ebb6059bae563b8ac846273e7d99 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Mon, 9 Sep 2024 17:36:46 +0200 Subject: [PATCH 08/27] test: unit contracts crate --- crates/pop-contracts/src/call/metadata.rs | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index 0de02cf9..cdc1071a 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -53,3 +53,43 @@ fn process_args(message_params: &[MessageParamSpec]) -> Vec Result { + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let temp_contract_dir = temp_dir.path().join("testing"); + fs::create_dir(&temp_contract_dir)?; + create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; + Ok(temp_dir) + } + // Function that mocks the build process generating the contract artifacts. + fn mock_build_process(temp_contract_dir: PathBuf) -> Result<(), Error> { + // Create a target directory + let target_contract_dir = temp_contract_dir.join("target"); + fs::create_dir(&target_contract_dir)?; + fs::create_dir(&target_contract_dir.join("ink"))?; + // Copy a mocked testing.contract file inside the target directory + let current_dir = env::current_dir().expect("Failed to get current directory"); + let contract_file = current_dir.join("../../tests/files/testing.contract"); + fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; + Ok(()) + } + #[test] + fn get_messages_work() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + mock_build_process(temp_dir.path().join("testing"))?; + let message = get_messages(&temp_dir.path().join("testing"))?; + assert_eq!(message.len(), 2); + assert_eq!(message[0].label, "flip"); + assert_eq!(message[0].docs, " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa."); + assert_eq!(message[1].label, "get"); + assert_eq!(message[1].docs, " Simply returns the current value of our `bool`."); + Ok(()) + } +} From cd4c20fa3517b32dde18f97a06142a8644181d84 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Mon, 9 Sep 2024 22:14:50 +0200 Subject: [PATCH 09/27] chore: format --- crates/pop-cli/src/commands/call/contract.rs | 6 ++++-- crates/pop-cli/src/commands/mod.rs | 5 ++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index efaff7db..32ec5698 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -346,7 +346,8 @@ mod tests { cli.verify() } - // This test only covers the interactive portion of the call contract command, without actually calling the contract. + // This test only covers the interactive portion of the call contract command, without actually + // calling the contract. #[tokio::test] async fn guide_user_to_query_contract_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; @@ -414,7 +415,8 @@ mod tests { cli.verify() } - // This test only covers the interactive portion of the call contract command, without actually calling the contract. + // This test only covers the interactive portion of the call contract command, without actually + // calling the contract. #[tokio::test] async fn guide_user_to_call_contract_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 6e896744..4530ce4c 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,12 +98,11 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => { + call::Command::Contract(cmd) => Box::new(call::contract::CallContract { cli: &mut Cli, args: cmd }) .execute() .await - .map(|_| Value::Null) - }, + .map(|_| Value::Null), }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { From d59259c39210e8d3bbc6ffa5020d15f88bef0232 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 10 Sep 2024 11:00:55 +0200 Subject: [PATCH 10/27] test: refactor and improve test cases --- crates/pop-cli/src/cli.rs | 3 - crates/pop-cli/src/commands/call/contract.rs | 77 +++++++++---------- crates/pop-contracts/src/call/metadata.rs | 38 ++++----- crates/pop-contracts/src/call/mod.rs | 56 +++++++------- crates/pop-contracts/src/init_tests.rs | 29 +++++++ crates/pop-contracts/src/lib.rs | 2 + crates/pop-contracts/src/up.rs | 68 +++++++++------- .../tests/files/testing.contract | 1 + .../pop-contracts/tests}/files/testing.json | 33 +++++++- tests/files/testing.contract | 1 - 10 files changed, 182 insertions(+), 126 deletions(-) create mode 100644 crates/pop-contracts/src/init_tests.rs create mode 100644 crates/pop-contracts/tests/files/testing.contract rename {tests => crates/pop-contracts/tests}/files/testing.json (90%) delete mode 100644 tests/files/testing.contract diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 65999288..29598c7e 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -424,12 +424,9 @@ pub(crate) mod tests { fn select(&mut self, prompt: impl Display) -> impl Select { let prompt = prompt.to_string(); - println!("prompt: {}", prompt); if let Some((expectation, _, collect, items_expectation, item)) = self.select_expectation.take() { - println!("expectation: {}", expectation); - println!("items_expectation: {:?}", items_expectation); assert_eq!(expectation, prompt, "prompt does not satisfy expectation"); return MockSelect { items_expectation, collect, items: vec![], item }; } diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 32ec5698..e8312fe0 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -178,7 +178,6 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .default_input("./") .interact()?; let contract_path = Path::new(&input_path); - println!("path: {:?}", contract_path); // Prompt for contract address. let contract_address: String = command @@ -287,36 +286,20 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( mod tests { use super::*; use crate::cli::MockCli; - use pop_contracts::{create_smart_contract, Contract}; - use std::{env, fs}; + use pop_contracts::{generate_smart_contract_test_environment, mock_build_process}; + use std::env; use url::Url; - fn generate_smart_contract_test_environment() -> Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("testing"); - fs::create_dir(&temp_contract_dir)?; - create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; - Ok(temp_dir) - } - // Function that mocks the build process generating the contract artifacts. - fn mock_build_process(temp_contract_dir: PathBuf) -> Result<()> { - // Create a target directory - let target_contract_dir = temp_contract_dir.join("target"); - fs::create_dir(&target_contract_dir)?; - fs::create_dir(&target_contract_dir.join("ink"))?; - // Copy a mocked testing.contract and testing.json files inside the target directory - let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("../../tests/files/testing.contract"); - fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; - let metadata_file = current_dir.join("../../tests/files/testing.json"); - fs::copy(metadata_file, &target_contract_dir.join("ink/testing.json"))?; - Ok(()) - } - #[tokio::test] async fn call_contract_messages_are_ok() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("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 mut cli = MockCli::new() .expect_intro(&"Call a contract") @@ -328,7 +311,7 @@ mod tests { cli: &mut cli, args: CallContractCommand { path: Some(temp_dir.path().join("testing")), - contract: Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), message: Some("get".to_string()), args: vec![].to_vec(), value: "0".to_string(), @@ -351,7 +334,13 @@ mod tests { #[tokio::test] async fn guide_user_to_query_contract_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("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 items = vec![ ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), @@ -374,7 +363,7 @@ mod tests { ) .expect_input( "Paste the on-chain contract address:", - "14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".into(), + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) .expect_input( "Where is your project located?", @@ -400,7 +389,7 @@ mod tests { .await?; assert_eq!( call_config.contract, - Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()) + Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) ); assert_eq!(call_config.message, Some("get".to_string())); assert_eq!(call_config.args.len(), 0); @@ -420,11 +409,18 @@ mod tests { #[tokio::test] async fn guide_user_to_call_contract_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("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 items = vec![ ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), ("get".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() @@ -434,18 +430,20 @@ mod tests { "Where is your contract deployed?", "wss://rpc1.paseo.popnetwork.xyz".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("new_value", "true".into()) // Args for specific_flip .expect_select::( "Select the message to call:", Some(false), true, Some(items), - 0, // "flip" message + 2, // "specific_flip" message ) - .expect_input("Enter the proof size limit:", "".into()) - .expect_input("Enter the gas limit:", "".into()) .expect_input( "Paste the on-chain contract address:", - "14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".into(), + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) .expect_input( "Where is your project located?", @@ -471,11 +469,12 @@ mod tests { .await?; assert_eq!( call_config.contract, - Some("14BfwaXddoarT9Z6LcfXBmunutk6Ssmy1kBdWX6sy9KRskJz".to_string()) + Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) ); - assert_eq!(call_config.message, Some("flip".to_string())); - assert_eq!(call_config.args.len(), 0); - assert_eq!(call_config.value, "0".to_string()); + assert_eq!(call_config.message, Some("specific_flip".to_string())); + assert_eq!(call_config.args.len(), 1); + assert_eq!(call_config.args[0], "true".to_string()); + assert_eq!(call_config.value, "50".to_string()); assert_eq!(call_config.gas_limit, None); assert_eq!(call_config.proof_size, None); assert_eq!(call_config.url.to_string(), "wss://rpc1.paseo.popnetwork.xyz/"); diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index cdc1071a..a2b378e6 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -7,7 +7,6 @@ use scale_info::form::PortableForm; use std::path::Path; #[derive(Clone, PartialEq, Eq)] -// TODO: We are ignoring selector, return type for now. /// Describes a contract message. pub struct Message { /// The label of the message. @@ -56,40 +55,31 @@ fn process_args(message_params: &[MessageParamSpec]) -> Vec Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("testing"); - fs::create_dir(&temp_contract_dir)?; - create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; - Ok(temp_dir) - } - // Function that mocks the build process generating the contract artifacts. - fn mock_build_process(temp_contract_dir: PathBuf) -> Result<(), Error> { - // Create a target directory - let target_contract_dir = temp_contract_dir.join("target"); - fs::create_dir(&target_contract_dir)?; - fs::create_dir(&target_contract_dir.join("ink"))?; - // Copy a mocked testing.contract file inside the target directory - let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("../../tests/files/testing.contract"); - fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; - Ok(()) - } #[test] fn get_messages_work() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let message = get_messages(&temp_dir.path().join("testing"))?; - assert_eq!(message.len(), 2); + assert_eq!(message.len(), 3); assert_eq!(message[0].label, "flip"); assert_eq!(message[0].docs, " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa."); assert_eq!(message[1].label, "get"); assert_eq!(message[1].docs, " Simply returns the current value of our `bool`."); + assert_eq!(message[2].label, "specific_flip"); + assert_eq!(message[2].docs, " A message for testing, flips the value of the stored `bool` with `new_value` and is payable"); + // assert parsed arguments + assert_eq!(message[2].args, vec!["new_value".to_string()]); Ok(()) } } diff --git a/crates/pop-contracts/src/call/mod.rs b/crates/pop-contracts/src/call/mod.rs index 591b202c..fdac5fd0 100644 --- a/crates/pop-contracts/src/call/mod.rs +++ b/crates/pop-contracts/src/call/mod.rs @@ -160,40 +160,25 @@ pub async fn call_smart_contract( mod tests { use super::*; use crate::{ - contracts_node_generator, create_smart_contract, dry_run_gas_estimate_instantiate, - errors::Error, instantiate_smart_contract, run_contracts_node, set_up_deployment, Contract, - UpOpts, + contracts_node_generator, dry_run_gas_estimate_instantiate, errors::Error, + generate_smart_contract_test_environment, instantiate_smart_contract, mock_build_process, + run_contracts_node, set_up_deployment, UpOpts, }; use anyhow::Result; use sp_core::Bytes; - use std::{env, fs, process::Command}; + use std::{env, process::Command}; const CONTRACTS_NETWORK_URL: &str = "wss://rpc2.paseo.popnetwork.xyz"; - fn generate_smart_contract_test_environment() -> Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("testing"); - fs::create_dir(&temp_contract_dir)?; - create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; - Ok(temp_dir) - } - // Function that mocks the build process generating the contract artifacts. - fn mock_build_process(temp_contract_dir: PathBuf) -> Result<(), Error> { - // Create a target directory - let target_contract_dir = temp_contract_dir.join("target"); - fs::create_dir(&target_contract_dir)?; - fs::create_dir(&target_contract_dir.join("ink"))?; - // Copy a mocked testing.contract file inside the target directory - let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("../../tests/files/testing.contract"); - fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; - Ok(()) - } - #[tokio::test] async fn test_set_up_call() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let call_opts = CallOpts { path: Some(temp_dir.path().join("testing")), @@ -259,7 +244,12 @@ mod tests { #[tokio::test] async fn test_dry_run_call_error_contract_not_deployed() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let call_opts = CallOpts { path: Some(temp_dir.path().join("testing")), @@ -281,7 +271,12 @@ mod tests { #[tokio::test] async fn test_dry_run_estimate_call_error_contract_not_deployed() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let call_opts = CallOpts { path: Some(temp_dir.path().join("testing")), @@ -307,7 +302,12 @@ mod tests { async fn call_works() -> Result<()> { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let cache = temp_dir.path().join(""); diff --git a/crates/pop-contracts/src/init_tests.rs b/crates/pop-contracts/src/init_tests.rs new file mode 100644 index 00000000..f1c24c7b --- /dev/null +++ b/crates/pop-contracts/src/init_tests.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{create_smart_contract, Contract}; +use anyhow::Result; +use std::{fs, path::PathBuf}; + +pub fn generate_smart_contract_test_environment() -> Result { + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let temp_contract_dir = temp_dir.path().join("testing"); + fs::create_dir(&temp_contract_dir)?; + create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; + Ok(temp_dir) +} + +// Function that mocks the build process generating the contract artifacts. +pub fn mock_build_process( + temp_contract_dir: PathBuf, + contract_file: PathBuf, + metadata_file: PathBuf, +) -> Result<()> { + // Create a target directory + let target_contract_dir = temp_contract_dir.join("target"); + fs::create_dir(&target_contract_dir)?; + fs::create_dir(&target_contract_dir.join("ink"))?; + // Copy a mocked testing.contract and testing.json files inside the target directory + fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; + fs::copy(metadata_file, &target_contract_dir.join("ink/testing.json"))?; + Ok(()) +} diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index c883c165..4fd24bd4 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -4,6 +4,7 @@ mod build; mod call; mod errors; +mod init_tests; mod new; mod node; mod templates; @@ -17,6 +18,7 @@ pub use call::{ metadata::{get_messages, Message}, set_up_call, CallOpts, }; +pub use init_tests::{generate_smart_contract_test_environment, mock_build_process}; pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; pub use templates::{Contract, ContractType}; diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index e387dbe1..a903c0b4 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -201,39 +201,24 @@ pub async fn upload_smart_contract( mod tests { use super::*; use crate::{ - contracts_node_generator, create_smart_contract, errors::Error, run_contracts_node, - templates::Contract, + contracts_node_generator, errors::Error, generate_smart_contract_test_environment, + mock_build_process, run_contracts_node, }; use anyhow::Result; - use std::{env, fs, process::Command}; + use std::{env, process::Command}; use url::Url; const CONTRACTS_NETWORK_URL: &str = "wss://rpc2.paseo.popnetwork.xyz"; - fn generate_smart_contract_test_environment() -> Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("testing"); - fs::create_dir(&temp_contract_dir)?; - create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; - Ok(temp_dir) - } - // Function that mocks the build process generating the contract artifacts. - fn mock_build_process(temp_contract_dir: PathBuf) -> Result<(), Error> { - // Create a target directory - let target_contract_dir = temp_contract_dir.join("target"); - fs::create_dir(&target_contract_dir)?; - fs::create_dir(&target_contract_dir.join("ink"))?; - // Copy a mocked testing.contract file inside the target directory - let current_dir = env::current_dir().expect("Failed to get current directory"); - let contract_file = current_dir.join("../../tests/files/testing.contract"); - fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; - Ok(()) - } - #[tokio::test] async fn set_up_deployment_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let up_opts = UpOpts { path: Some(temp_dir.path().join("testing")), constructor: "new".to_string(), @@ -252,7 +237,12 @@ mod tests { #[tokio::test] async fn set_up_upload_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let up_opts = UpOpts { path: Some(temp_dir.path().join("testing")), constructor: "new".to_string(), @@ -271,7 +261,12 @@ mod tests { #[tokio::test] async fn dry_run_gas_estimate_instantiate_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let up_opts = UpOpts { path: Some(temp_dir.path().join("testing")), constructor: "new".to_string(), @@ -293,7 +288,12 @@ mod tests { #[tokio::test] async fn dry_run_gas_estimate_instantiate_throw_custom_error() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let up_opts = UpOpts { path: Some(temp_dir.path().join("testing")), constructor: "new".to_string(), @@ -316,7 +316,12 @@ mod tests { #[tokio::test] async fn dry_run_upload_throw_custom_error() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let up_opts = UpOpts { path: Some(temp_dir.path().join("testing")), constructor: "new".to_string(), @@ -339,7 +344,12 @@ mod tests { async fn instantiate_and_upload() -> Result<()> { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; let temp_dir = generate_smart_contract_test_environment()?; - mock_build_process(temp_dir.path().join("testing"))?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; let cache = temp_dir.path().join(""); diff --git a/crates/pop-contracts/tests/files/testing.contract b/crates/pop-contracts/tests/files/testing.contract new file mode 100644 index 00000000..5fb54f3c --- /dev/null +++ b/crates/pop-contracts/tests/files/testing.contract @@ -0,0 +1 @@ +{"source":{"hash":"0x80776e58b218850d7d86447b2edea78d827ed0ed2499ff3a92b7ea10e4f95eb5","language":"ink! 5.0.0","compiler":"rustc 1.78.0","wasm":"0x0061736d01000000012b0860027f7f0060037f7f7f017f60000060047f7f7f7f017f60037f7f7f0060017f006000017f60017f017f027406057365616c310b6765745f73746f726167650003057365616c3005696e7075740000057365616c320b7365745f73746f726167650003057365616c300b7365616c5f72657475726e0004057365616c301176616c75655f7472616e73666572726564000003656e76066d656d6f72790201021003100f0101010105040006070002050002020616037f01418080040b7f00418080050b7f00418080050b0711020463616c6c0012066465706c6f7900130aa80c0f2b01017f037f2002200346047f200005200020036a200120036a2d00003a0000200341016a21030c010b0b0b6f01017f0240200020014d04402000210303402002450d02200320012d00003a0000200341016a2103200141016a2101200241016b21020c000b000b200041016b2103200141016b210103402002450d01200220036a200120026a2d00003a0000200241016b21020c000b000b20000b2501017f037f2002200346047f200005200020036a20013a0000200341016a21030c010b0b0b3f01027f0340200245044041000f0b200241016b210220012d0000210320002d00002104200141016a2101200041016a210020032004460d000b200420036b0b2601017f230041106b220124002001410036020c20002001410c6a4104100a200141106a24000b4801027f024002402000280208220320026a22042003490d00200420002802044b0d00200420036b2002470d01200028020020036a2001200210051a200020043602080f0b000b000b2601017f230041106b22022400200220003a000f20012002410f6a4101100a200241106a24000b6102027f027e230041206b22002400200041106a22014200370300200042003703082000411036021c200041086a2000411c6a1004200028021c41114f0440000b2001290300210220002903082103200041206a2400410541042002200384501b0b3f01017f2000280204220145044041020f0b2000200141016b36020420002000280200220041016a3602004101410220002d000022004101461b410020001b0b3c01027f027f200145044041808004210141010c010b410121024180800441013a000041818004210141020b2103200120023a0000200020031011000b12004180800441003b0100410041021011000b8a0101057f230041106b22012400200142808001370208200141808004360204200141046a22041009024020012802082205200128020c2202490d00200128020421032001410036020c2001200520026b3602082001200220036a36020420002004100b200128020c220020012802084b0d00200320022001280204200010021a200141106a24000f0b000b0d0020004180800420011003000b990401067f230041106b2200240020004180800136020441808004200041046a10010240024020002802042202418180014f0d000240024020024104490d002000418480043602042000200241046b360208418380042d00002101418280042d00002104418180042d00002103418080042d00002202412f470440200241ec00470440200241e300470d02410221022003413a46200441a5014671200141d10046710d030c020b2003410f472004411d4772200141f70147720d01200041046a100d220241ff01714102460d010c020b41032102200341860146200441db004671200141d90146710d010b41014101100e000b200042808001370208200041808004360204200041046a2204100920002802082205200028020c2201490d00200028020421032000200520016b220536020420032001200120036a2201200410002000280204220320054b720d0020002003360208200020013602042004100d220141ff01714102460d0020002802080d000240024002404102200241026b41ff01712200200041024f1b41016b0e020100020b2002410171101041004100100e000b100c41ff01714105470d01230041106b220024002000418080043602044180800441003a00002000428080818010370208200141ff0171410047200041046a100b200028020c2200418180014f0440000b410020001011000b100c41ff01714105460d010b000b200141ff017145101041004100100e000be50101057f230041106b2200240002400240100c41ff01714105470d0020004180800136020c418080042000410c6a1001200028020c2201418180014f0d0020014104490d012000418480043602042000200141046b360208418380042d00002101418280042d00002102418180042d000021030240418080042d0000220441ed014704402004419b0147200341ae0147722002419d0147200141de004772720d03200041046a100d220041ff01714102470d010c030b200341cb00462002419d0146712001411b4671450d0241001010100f000b20001010100f000b000b41014101100e000b","build_info":{"rust_toolchain":"stable-aarch64-apple-darwin","cargo_contract_version":"5.0.0-alpha","build_mode":"Release","wasm_opt_settings":{"optimization_passes":"Z","keep_debug_symbols":false}}},"contract":{"name":"testing","version":"0.1.0","authors":["[your_name] <[your_email]>"]},"image":null,"version":5,"types":[{"id":0,"type":{"def":{"primitive":"bool"}}},{"id":1,"type":{"path":["testing","testing","Testing"],"def":{"composite":{"fields":[{"name":"value","type":0,"typeName":",>>::Type"}]}}}},{"id":2,"type":{"path":["Result"],"params":[{"name":"T","type":3},{"name":"E","type":4}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":3}],"index":0},{"name":"Err","fields":[{"type":4}],"index":1}]}}}},{"id":3,"type":{"def":{"tuple":[]}}},{"id":4,"type":{"path":["ink_primitives","LangError"],"def":{"variant":{"variants":[{"name":"CouldNotReadInput","index":1}]}}}},{"id":5,"type":{"path":["Result"],"params":[{"name":"T","type":0},{"name":"E","type":4}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":0}],"index":0},{"name":"Err","fields":[{"type":4}],"index":1}]}}}},{"id":6,"type":{"path":["ink_primitives","types","AccountId"],"def":{"composite":{"fields":[{"type":7,"typeName":"[u8; 32]"}]}}}},{"id":7,"type":{"def":{"array":{"len":32,"type":8}}}},{"id":8,"type":{"def":{"primitive":"u8"}}},{"id":9,"type":{"def":{"primitive":"u128"}}},{"id":10,"type":{"path":["ink_primitives","types","Hash"],"def":{"composite":{"fields":[{"type":7,"typeName":"[u8; 32]"}]}}}},{"id":11,"type":{"def":{"primitive":"u64"}}},{"id":12,"type":{"def":{"primitive":"u32"}}},{"id":13,"type":{"path":["ink_env","types","NoChainExtension"],"def":{"variant":{}}}}],"storage":{"root":{"root_key":"0x00000000","layout":{"struct":{"name":"Testing","fields":[{"name":"value","layout":{"leaf":{"key":"0x00000000","ty":0}}}]}},"ty":1}},"spec":{"constructors":[{"label":"new","selector":"0x9bae9d5e","payable":false,"args":[{"label":"init_value","type":{"type":0,"displayName":["bool"]}}],"returnType":{"type":2,"displayName":["ink_primitives","ConstructorResult"]},"docs":["Constructor that initializes the `bool` value to the given `init_value`."],"default":false},{"label":"default","selector":"0xed4b9d1b","payable":false,"args":[],"returnType":{"type":2,"displayName":["ink_primitives","ConstructorResult"]},"docs":["Constructor that initializes the `bool` value to `false`.","","Constructors can delegate to other constructors."],"default":false}],"messages":[{"label":"flip","selector":"0x633aa551","mutates":true,"payable":false,"args":[],"returnType":{"type":2,"displayName":["ink","MessageResult"]},"docs":[" A message that can be called on instantiated contracts."," This one flips the value of the stored `bool` from `true`"," to `false` and vice versa."],"default":false},{"label":"get","selector":"0x2f865bd9","mutates":false,"payable":false,"args":[],"returnType":{"type":5,"displayName":["ink","MessageResult"]},"docs":[" Simply returns the current value of our `bool`."],"default":false},{"label":"specific_flip","selector":"0x6c0f1df7","mutates":true,"payable":true,"args":[{"label":"new_value","type":{"type":0,"displayName":["bool"]}}],"returnType":{"type":2,"displayName":["ink","MessageResult"]},"docs":[" A message for testing, flips the value of the stored `bool` with `new_value`"," and is payable"],"default":false}],"events":[],"docs":[],"lang_error":{"type":4,"displayName":["ink","LangError"]},"environment":{"accountId":{"type":6,"displayName":["AccountId"]},"balance":{"type":9,"displayName":["Balance"]},"hash":{"type":10,"displayName":["Hash"]},"timestamp":{"type":11,"displayName":["Timestamp"]},"blockNumber":{"type":12,"displayName":["BlockNumber"]},"chainExtension":{"type":13,"displayName":["ChainExtension"]},"maxEventTopics":4,"staticBufferSize":16384}}} \ No newline at end of file diff --git a/tests/files/testing.json b/crates/pop-contracts/tests/files/testing.json similarity index 90% rename from tests/files/testing.json rename to crates/pop-contracts/tests/files/testing.json index 78cff4b6..ed230c98 100644 --- a/tests/files/testing.json +++ b/crates/pop-contracts/tests/files/testing.json @@ -1,11 +1,11 @@ { "source": { - "hash": "0x29972a7ea06ca802e5d857dfb7cb6455220679f4e9314a533840b9865ccbb799", + "hash": "0x80776e58b218850d7d86447b2edea78d827ed0ed2499ff3a92b7ea10e4f95eb5", "language": "ink! 5.0.0", "compiler": "rustc 1.78.0", "build_info": { "rust_toolchain": "stable-aarch64-apple-darwin", - "cargo_contract_version": "4.1.1", + "cargo_contract_version": "5.0.0-alpha", "build_mode": "Release", "wasm_opt_settings": { "optimization_passes": "Z", @@ -369,6 +369,35 @@ " Simply returns the current value of our `bool`." ], "default": false + }, + { + "label": "specific_flip", + "selector": "0x6c0f1df7", + "mutates": true, + "payable": true, + "args": [ + { + "label": "new_value", + "type": { + "type": 0, + "displayName": [ + "bool" + ] + } + } + ], + "returnType": { + "type": 2, + "displayName": [ + "ink", + "MessageResult" + ] + }, + "docs": [ + " A message for testing, flips the value of the stored `bool` with `new_value`", + " and is payable" + ], + "default": false } ], "events": [], diff --git a/tests/files/testing.contract b/tests/files/testing.contract deleted file mode 100644 index 8701e656..00000000 --- a/tests/files/testing.contract +++ /dev/null @@ -1 +0,0 @@ -{"source":{"hash":"0xb15348075722f8ac92352b8fcfd6fa3506e2a3f430adadcc79fa73cf23bfe9e7","language":"ink! 5.0.0","compiler":"rustc 1.78.0","wasm":"0x0061736d0100000001400b60037f7f7f017f60027f7f017f60027f7f0060037f7f7f0060017f0060047f7f7f7f017f60047f7f7f7f0060000060057f7f7f7f7f006000017f60017f017f028a0107057365616c310b6765745f73746f726167650005057365616c3005696e7075740002057365616c320b7365745f73746f726167650005057365616c300d64656275675f6d6573736167650001057365616c300b7365616c5f72657475726e0003057365616c301176616c75655f7472616e73666572726564000203656e76066d656d6f7279020102100335340000000006030403020901020a00010702040302020704070306020301010004000101010104020101080506050802010103000104050170010e0e0616037f01418080040b7f0041c092050b7f0041ba92050b0711020463616c6c001b066465706c6f79001d0913010041010b0d103528392a362529252627232c0ae63b342b01017f037f2002200346047f200005200020036a200120036a2d00003a0000200341016a21030c010b0b0b6f01017f0240200020014d04402000210303402002450d02200320012d00003a0000200341016a2103200141016a2101200241016b21020c000b000b200041016b2103200141016b210103402002450d01200220036a200120026a2d00003a0000200241016b21020c000b000b20000b2501017f037f2002200346047f200005200020036a20013a0000200341016a21030c010b0b0b3f01027f0340200245044041000f0b200241016b210220012d0000210320002d00002104200141016a2101200041016a210020032004460d000b200420036b0b2300200120034b04402001200341f49004100b000b20002001360204200020023602000b6801017f230041306b2203240020032001360204200320003602002003412c6a41033602002003410236020c200341a88a0436020820034202370214200341033602242003200341206a3602102003200341046a36022820032003360220200341086a20021011000b2601017f230041106b220124002001410036020c20002001410c6a4104100d200141106a24000b920101037f02402000280208220420026a220320044f04402003200028020422054b0d01200028020020046a200320046b2001200241d08f041033200020033602080f0b230041206b22002400200041013602042000420037020c200041988e043602082000412b36021c200041f286043602182000200041186a360200200041b08f041011000b2003200541c08f04100b000b2601017f230041106b22022400200220003a000f20012002410f6a4101100d200241106a24000b6d02037f027e230041206b22002400200041106a22014200370300200042003703082000411036021c200041086a2000411c6a1005200028021c220241114f04402002411041f49004100b000b2001290300210320002903082104200041206a2400410541042003200484501b0b810101017f230041306b220224002002410136020c200241988d043602082002420137021420024102360224200220002d00004102742200418092046a28020036022c20022000419492046a2802003602282002200241206a3602102002200241286a36022020012802142001280218200241086a10242100200241306a240020000b3c01017f230041206b22022400200241013b011c2002200136021820022000360214200241b88704360210200241988e0436020c2002410c6a102b000b4901017f230041106b22012400200141003a000f027f20002001410f6a410110134504404101410220012d000f22004101461b410020001b0c010b41020b2100200141106a240020000b3d01027f2000280204220320024922044504402001200220002802002201200241f0910410332000200320026b3602042000200120026a3602000b20040b11002001410036000020002001410410130b5301027f230041106b22002400200042808001370208200041ba9204360204200041046a220141001019200141001019200028020c2200418180014f044020004180800141f08004100b000b41002000101a000b5b01027f230041106b22022400200242808001370208200241ba9204360204200241046a22032001047f20034101101941010541000b1019200228020c2201418180014f044020014180800141f08004100b000b20002001101a000ba70102057f017e230041306b2201240020014100360220200142808001370228200141ba9204360224200141246a2202100c20012001290224370218200141106a200141186a2203200128022c10182001280214210420012802102105200129021821062001410036022c2001200637022420002002100e20012001290224370218200141086a2003200128022c1018200520042001280208200128020c10021a200141306a24000b7401027f230041206b220324002002200128020422044b04402003410136020c200341a48e0436020820034200370214200341988e04360210200341086a41f08f041011000b2001200420026b36020420012001280200220120026a3602002000200236020420002001360200200341206a24000b940101027f20002802082202200028020422034904402000200241016a360208200028020020026a20013a00000f0b230041306b2200240020002003360204200020023602002000412c6a41033602002000410236020c20004188880436020820004202370214200041033602242000200041206a360210200020003602282000200041046a360220200041086a41e08f041011000b0d00200041ba920420011004000bef0401077f230041406a220024000240024002400240100f41ff0171410546044020004180800136022841ba9204200041286a22011001200041106a200028022841ba920441808001100a2000200029031037022820012000411c6a10140d0320002d001f210120002d001e210220002d001d2103024020002d001c2204412f470440200441e300470d05410121042003413a47200241a5014772200141d1004772450d010c050b41002104200341860147200241db004772200141d90147720d040b2000410036022420004280800137022c200041ba9204360228200041286a2202100c2000200029022837021c200041086a2000411c6a20002802301018200028020c210320002802082105200028021c21012000200028022022063602282005200320012002100021022000200028022820012006100a02400240024020020e0400040401040b200028020021012000200028020436022c20002001360228200041286a1012220141ff01714102470440200028022c450d020b2000410136022c200041bc82043602280c060b2000410136022c2000418c82043602280c050b20040d02230041106b22002400200042808001370208200041ba9204360204200041046a220241001019200141ff01714100472002100e200028020c2200418180014f044020004180800141f08004100b000b41002000101a000b200041043a0028200041286a101c000b2000410136022c2000419c810436022820004200370234200041988e04360230200041286a41a481041011000b200141ff0171451017410041001016000b410141011016000b20004200370234200041988e04360230200041286a41e481041011000b4501017f230041206b2201240020014101360204200141988d043602002001420137020c2001410136021c200120003602182001200141186a360208200141e481041011000be90101057f230041206b220024000240100f220141ff0171410546044020004180800136021441ba9204200041146a22011001200041086a200028021441ba920441808001100a2000200029030837021420012000411c6a10140d0120002d001f210120002d001e210220002d001d210320002d001c2204419b01470440200341cb00462002419d0146712001411b467145200441ed0147720d02410010171015000b200341ae01472002419d014772200141de0047720d01200041146a1012220041ff01714102460d01200010171015000b200020013a0014200041146a101c000b410141011016000b6001027f230041106b2203240020022000280200200028020822046b4b0440200341086a200020042002101f2003280208200328020c1020200028020821040b200028020420046a2001200210061a2000200220046a360208200341106a24000b9a0301077f230041206b220624000240200220036a22032002490d00410121044108200128020022024101742205200320032005491b2203200341084d1b2203417f73411f76210502402002450440410021040c010b2006200236021c200620012802043602140b20062004360218200641086a210820032102200641146a2104230041106b22072400027f027f024020050440200241004e0d01410121054100210241040c030b2008410036020441010c010b027f2004280204044020042802082209450440200741086a20052002102120072802082104200728020c0c020b2004280200210a02402005200210222204450440410021040c010b2004200a200910061a0b20020c010b20072005200210212007280200210420072802040b210920082004200520041b3602042009200220041b21022004450b210541080b20086a200236020020082005360200200741106a2400200628020c21042006280208450440200120033602002001200436020441818080807821040c010b200628021021030b2000200336020420002004360200200641206a24000bcb0100024020004181808080784704402000450d01230041306b220024002000200136020c20004102360214200041a085043602102000420137021c2000410336022c2000200041286a36021820002000410c6a360228230041206b22012400200141003b011c200141b085043602182001200041106a360214200141b88704360210200141988e0436020c2001410c6a102b000b0f0b230041206b220024002000410136020c20004188830436020820004200370214200041988e04360210200041086a418084041011000b200041a892042d00001a200120021022210120002002360204200020013602000bc50101017f027f41ac92042d0000044041b092042802000c010b3f00210241b0920441c0920536020041ac920441013a000041b49204200241107436020041c092050b21020240027f4100200020026a41016b410020006b71220020016a22022000490d001a41b492042802002002490440200141ffff036a220241107640002200417f460d022000411074220020024180807c716a22022000490d0241b4920420023602004100200020016a22022000490d011a0b41b09204200236020020000b0f0b41000b0c00200041dc8204200110240b850401077f230041406a22032400200341033a003c2003412036022c200341003602382003200136023420032000360230200341003602242003410036021c027f0240024020022802102201450440200228020c22004103742105200041ffffffff01712106200228020421082002280200210720022802082101034020042005460d02200420076a220041046a28020022020440200328023020002802002002200328023428020c1100000d040b200441086a21042001280200210020012802042102200141086a210120002003411c6a2002110100450d000b0c020b200228021422044105742100200441ffffff3f712106200228020c2109200228020821052002280204210820022802002207210403402000450d01200441046a28020022020440200328023020042802002002200328023428020c1100000d030b2003200128021036022c200320012d001c3a003c20032001280218360238200341106a2005200141086a10372003200329031037021c200341086a20052001103720032003290308370224200441086a2104200041206b210020012802142102200141206a2101200520024103746a22022802002003411c6a2002280204110100450d000b0c010b200620084904402003280230200720064103746a22002802002000280204200328023428020c1100000d010b41000c010b41010b2101200341406b240020010b0300010b0c00200020012002101e41000bb20201047f230041106b220224000240027f0240024020014180014f04402002410036020c2001418010490d012001418080044f0d0220022001410c7641e001723a000c20022001410676413f71418001723a000d4102210341030c030b200028020822032000280200460440230041106b22042400200441086a200020034101101f2004280208200428020c1020200441106a2400200028020821030b2000200341016a360208200028020420036a20013a00000c030b2002200141067641c001723a000c4101210341020c010b20022001410676413f71418001723a000e20022001410c76413f71418001723a000d2002200141127641077141f001723a000c4103210341040b210420032002410c6a2205722001413f71418001723a0000200020052004101e0b200241106a240041000bdb05020b7f027e230041406a220324004127210202402000350200220d4290ce00540440200d210e0c010b0340200341196a20026a220041046b200d4290ce0080220e42f0b1037e200d7ca7220441ffff037141e4006e220641017441ac88046a2f00003b0000200041026b2006419c7f6c20046a41ffff037141017441ac88046a2f00003b0000200241046b2102200d42ffc1d72f562100200e210d20000d000b0b200ea7220041e3004b0440200241026b2202200341196a6a200ea7220441ffff037141e4006e2200419c7f6c20046a41ffff037141017441ac88046a2f00003b00000b02402000410a4f0440200241026b2202200341196a6a200041017441ac88046a2f00003b00000c010b200241016b2202200341196a6a20004130723a00000b200128021c22054101712207412720026b22066a2100410021042005410471044041988e04210441988e0441988e04102d20006a21000b412b418080c40020071b2107200341196a20026a2108024020012802004504404101210220012802142200200128021822012007200410300d01200020082006200128020c11000021020c010b2000200128020422094f04404101210220012802142200200128021822012007200410300d01200020082006200128020c11000021020c010b200541087104402001280210210b2001413036021020012d0020210c41012102200141013a0020200128021422052001280218220a2007200410300d01200341106a2001200920006b4101103120032802102200418080c400460d0120032802142104200520082006200a28020c1100000d01200020042005200a10320d012001200c3a00202001200b360210410021020c010b41012102200341086a2001200920006b4101103120032802082205418080c400460d00200328020c210920012802142200200128021822012007200410300d00200020082006200128020c1100000d002005200920002001103221020b200341406b240020020b1800200128021441d482044105200128021828020c1100000b0e0020002802001a03400c000b000baa0201017f230041406a220124002001200036020c20014102360214200141b08e043602102001420137021c2001410436022c2001200141286a36021820012001410c6a360228200141003602382001428080808010370230200141306a200141106a10234504402001280234210020012802382101024041b892042d000045044041b992042d00000d010b200020011003410947044041b8920441013a00000b41b9920441013a00000b000b230041406a220024002000413336020c200041c08504360208200041c4820436021420002001413f6a3602102000413c6a41063602002000410236021c2000419c880436021820004202370224200041023602342000200041306a3602202000200041106a3602382000200041086a360230200041186a41e086041011000b2200200042eeb4d39ded9bae93907f370308200042d4ce8f88d3c5f6dba47f3703000ba10301067f230041106b220224000240200120006b220141104f04402000200041036a417c71220520006b2200102e2005200120006b2200417c716a2000410371102e6a21042000410276210303402003450d0220022005200341c0012003200341c0014f1b41ac8b04102f200228020c21032002280208210520022002280200200228020422002000417c7141888d04102f024020022802042200450440410021010c010b2002280200220620004102746a21074100210103404100210003402001200020066a2802002201417f734107762001410676724181828408716a2101200041046a22004110470d000b200641106a22062007470d000b0b200141087641ff81fc0771200141ff81fc07716a418180046c41107620046a2104200228020c2201450d000b2002280208210020014102742103410021010340200120002802002201417f734107762001410676724181828408716a2101200041046a2100200341046b22030d000b200141087641ff81fc0771200141ff81fc07716a418180046c41107620046a21040c010b20002001102e21040b200241106a240020040b2c01017f200104400340200220002c000041bf7f4a6a2102200041016a2100200141016b22010d000b0b20020b6b01017f230041206b22052400200220034904402005410136020c200541a48e0436020820054200370214200541988e04360210200541086a20041011000b20002003360204200020013602002000200220036b36020c2000200120034102746a360208200541206a24000b39000240027f2002418080c40047044041012000200220012802101101000d011a0b20030d0141000b0f0b200020034100200128020c1100000b990101027f024002400240024020012d0020220441016b0e03010200030b200341ff01710d00410021040c020b20022104410021020c010b20024101762104200241016a41017621020b200441016a210420012802102103200128021821052001280214210102400340200441016b2204450d01200120032005280210110100450d000b418080c40021030b20002002360204200020033602000b3201017f027f0340200120012004460d011a200441016a2104200220002003280210110100450d000b200441016b0b2001490b78002001200346044020002002200110061a0f0b230041306b2200240020002003360204200020013602002000412c6a41033602002000410336020c200041fc8b0436020820004202370214200041033602242000200041206a360210200020003602282000200041046a360220200041086a20041011000bf60101067f2000027f418080c400200128020022022001280204460d001a2001200241016a2205360200024020022d0000220341187441187541004e0d002001200241026a220536020020022d0001413f7121042003411f712106200341df014d0440200641067420047221030c010b2001200241036a220536020020022d0002413f712004410674722104200341f00149044020042006410c747221030c010b2001200241046a2205360200418080c4002006411274418080f0007120022d0003413f71200441067472722203418080c400460d011a0b200120012802082207200520026b6a36020820030b360204200020073602000baa0301067f230041306b22022400200028020421042000280200210302400240200128020022062001280208220072044002402000450d00200128020c21002002410036022c200220033602242002200320046a360228200041016a21000340200041016b22000440200241186a200241246a1034200228021c418080c400470d010c020b0b200241106a200241246a10342002280214418080c400460d000240024020022802102205450d00200420054d04404100210020042005460d010c020b41002100200320056a2c00004140480d010b200321000b2005200420001b21042000200320001b21030b2006450440200128021420032004200128021828020c11000021000c030b200128020422002003200320046a102d22054d0d01200241086a2001200020056b410010314101210020022802082205418080c400460d02200228020c210620012802142207200320042001280218220128020c1100000d022005200620072001103221000c020b200128021420032004200128021828020c11000021000c010b200128021420032004200128021828020c11000021000b200241306a240020000b140020002802002001200028020428020c1101000b5501027f0240027f02400240200228020041016b0e020103000b200241046a0c010b200120022802044103746a22012802044105470d0120012802000b2802002104410121030b20002004360204200020033602000b0a0020002001200210240be00201067f230041406a22022400200028020021054101210002402001280214220441c88704410c2001280218220628020c22011100000d00200528020c21032002413c6a4103360200200241346a410336020020024103360214200241a087043602102002420337021c20022003410c6a3602382002200341086a3602302002410236022c200220033602282002200241286a220736021820042006200241106a10380d00200528020822030440200441d48704410220011100000d01200241386a200341106a290200370300200241306a200341086a29020037030020022003290200370328200420062007103821000c010b200220052802002203200528020428020c11020041002100200229030042e4dec78590d085de7d520d00200229030842c1f7f9e8cc93b2d141520d0041012100200441d48704410220011100000d00200420032802002003280204200111000021000b200241406b240020000b0bb0120100418080040ba7122f55736572732f616c65786265616e2f2e636172676f2f72656769737472792f7372632f696e6465782e6372617465732e696f2d366631376432326262613135303031662f696e6b5f656e762d352e302e302f7372632f656e67696e652f6f6e5f636861696e2f696d706c732e727300000001006f0000001a01000032000000656e636f756e746572656420756e6578706563746564206572726f72800001001c000000000001006f000000e3000000170000002f55736572732f616c65786265616e2f446f63756d656e74732f726f6775652f74657374696e672f6c69622e72730000b40001002e000000060000000500000073746f7261676520656e7472792077617320656d70747900f400010017000000636f756c64206e6f742070726f7065726c79206465636f64652073746f7261676520656e747279001401010027000000070000000000000001000000080000004572726f72000000090000000c000000040000000a0000000b0000000c0000006361706163697479206f766572666c6f7700000074010100110000002f55736572732f616c65786265616e2f2e7275737475702f746f6f6c636861696e732f737461626c652d616172636836342d6170706c652d64617277696e2f6c69622f727573746c69622f7372632f727573742f6c6962726172792f616c6c6f632f7372632f7261775f7665632e7273900101007000000019000000050000002f55736572732f616c65786265616e2f2e7275737475702f746f6f6c636861696e732f737461626c652d616172636836342d6170706c652d64617277696e2f6c69622f727573746c69622f7372632f727573742f6c6962726172792f616c6c6f632f7372632f616c6c6f632e72736d656d6f727920616c6c6f636174696f6e206f6620206279746573206661696c65647e02010015000000930201000d000000100201006e000000a50100000d0000006120666f726d617474696e6720747261697420696d706c656d656e746174696f6e2072657475726e656420616e206572726f722f55736572732f616c65786265616e2f2e7275737475702f746f6f6c636861696e732f737461626c652d616172636836342d6170706c652d64617277696e2f6c69622f727573746c69622f7372632f727573742f6c6962726172792f616c6c6f632f7372632f666d742e727300f30201006c0000007902000020000000293a63616c6c656420604f7074696f6e3a3a756e77726170282960206f6e206120604e6f6e65602076616c75650000001807010000000000710301000100000071030100010000000700000000000000010000000d00000070616e69636b6564206174203a0a696e646578206f7574206f6620626f756e64733a20746865206c656e20697320206275742074686520696e64657820697320d603010020000000f6030100120000003a200000180701000000000018040100020000003030303130323033303430353036303730383039313031313132313331343135313631373138313932303231323232333234323532363237323832393330333133323333333433353336333733383339343034313432343334343435343634373438343935303531353235333534353535363537353835393630363136323633363436353636363736383639373037313732373337343735373637373738373938303831383238333834383538363837383838393930393139323933393439353936393739383939206f7574206f662072616e676520666f7220736c696365206f66206c656e6774682072616e676520656e6420696e6465782000001605010010000000f4040100220000002f55736572732f616c65786265616e2f2e7275737475702f746f6f6c636861696e732f737461626c652d616172636836342d6170706c652d64617277696e2f6c69622f727573746c69622f7372632f727573742f6c6962726172792f636f72652f7372632f736c6963652f697465722e727300003805010072000000ce05000025000000736f7572636520736c696365206c656e67746820282920646f6573206e6f74206d617463682064657374696e6174696f6e20736c696365206c656e6774682028bc05010015000000d10501002b00000070030100010000002f55736572732f616c65786265616e2f2e7275737475702f746f6f6c636861696e732f737461626c652d616172636836342d6170706c652d64617277696e2f6c69622f727573746c69622f7372632f727573742f6c6962726172792f636f72652f7372632f7374722f636f756e742e727300000014060100710000004f000000320000001807010000000000756e61626c6520746f206465636f64652073656c6563746f72656e636f756e746572656420756e6b6e6f776e2073656c6563746f72756e61626c6520746f206465636f646520696e707574636f756c64206e6f74207265616420696e7075747061696420616e20756e70617961626c65206d6573736167656d6964203e206c656e00000018070100090000000a00000018070100000000002c070100010000002f55736572732f616c65786265616e2f2e636172676f2f72656769737472792f7372632f696e6465782e6372617465732e696f2d366631376432326262613135303031662f696e6b5f656e762d352e302e302f7372632f656e67696e652f6f6e5f636861696e2f6275666665722e727340070100700000005c0000003b00000040070100700000005c0000001400000040070100700000005d0000000e00000040070100700000006800000009000000400701007000000090000000210000002f55736572732f616c65786265616e2f2e636172676f2f72656769737472792f7372632f696e6465782e6372617465732e696f2d366631376432326262613135303031662f70616c6c65742d636f6e7472616374732d756170692d6e6578742d362e302e332f7372632f686f73742e727300000000080100710000002d000000170000002f55736572732f616c65786265616e2f2e636172676f2f72656769737472792f7372632f696e6465782e6372617465732e696f2d366631376432326262613135303031662f7061726974792d7363616c652d636f6465632d332e362e31322f7372632f636f6465632e727300840801006b000000770000000e000000190000001c000000160000001400000019000000a0060100b9060100d5060100eb060100ff0601","build_info":{"build_mode":"Debug","cargo_contract_version":"4.1.1","rust_toolchain":"stable-aarch64-apple-darwin","wasm_opt_settings":{"keep_debug_symbols":false,"optimization_passes":"Z"}}},"contract":{"name":"testing","version":"0.1.0","authors":["[your_name] <[your_email]>"]},"image":null,"spec":{"constructors":[{"args":[{"label":"init_value","type":{"displayName":["bool"],"type":0}}],"default":false,"docs":["Constructor that initializes the `bool` value to the given `init_value`."],"label":"new","payable":false,"returnType":{"displayName":["ink_primitives","ConstructorResult"],"type":2},"selector":"0x9bae9d5e"},{"args":[],"default":false,"docs":["Constructor that initializes the `bool` value to `false`.","","Constructors can delegate to other constructors."],"label":"default","payable":false,"returnType":{"displayName":["ink_primitives","ConstructorResult"],"type":2},"selector":"0xed4b9d1b"}],"docs":[],"environment":{"accountId":{"displayName":["AccountId"],"type":6},"balance":{"displayName":["Balance"],"type":9},"blockNumber":{"displayName":["BlockNumber"],"type":12},"chainExtension":{"displayName":["ChainExtension"],"type":13},"hash":{"displayName":["Hash"],"type":10},"maxEventTopics":4,"staticBufferSize":16384,"timestamp":{"displayName":["Timestamp"],"type":11}},"events":[],"lang_error":{"displayName":["ink","LangError"],"type":4},"messages":[{"args":[],"default":false,"docs":[" A message that can be called on instantiated contracts."," This one flips the value of the stored `bool` from `true`"," to `false` and vice versa."],"label":"flip","mutates":true,"payable":false,"returnType":{"displayName":["ink","MessageResult"],"type":2},"selector":"0x633aa551"},{"args":[],"default":false,"docs":[" Simply returns the current value of our `bool`."],"label":"get","mutates":false,"payable":false,"returnType":{"displayName":["ink","MessageResult"],"type":5},"selector":"0x2f865bd9"}]},"storage":{"root":{"layout":{"struct":{"fields":[{"layout":{"leaf":{"key":"0x00000000","ty":0}},"name":"value"}],"name":"Testing"}},"root_key":"0x00000000","ty":1}},"types":[{"id":0,"type":{"def":{"primitive":"bool"}}},{"id":1,"type":{"def":{"composite":{"fields":[{"name":"value","type":0,"typeName":",>>::Type"}]}},"path":["testing","testing","Testing"]}},{"id":2,"type":{"def":{"variant":{"variants":[{"fields":[{"type":3}],"index":0,"name":"Ok"},{"fields":[{"type":4}],"index":1,"name":"Err"}]}},"params":[{"name":"T","type":3},{"name":"E","type":4}],"path":["Result"]}},{"id":3,"type":{"def":{"tuple":[]}}},{"id":4,"type":{"def":{"variant":{"variants":[{"index":1,"name":"CouldNotReadInput"}]}},"path":["ink_primitives","LangError"]}},{"id":5,"type":{"def":{"variant":{"variants":[{"fields":[{"type":0}],"index":0,"name":"Ok"},{"fields":[{"type":4}],"index":1,"name":"Err"}]}},"params":[{"name":"T","type":0},{"name":"E","type":4}],"path":["Result"]}},{"id":6,"type":{"def":{"composite":{"fields":[{"type":7,"typeName":"[u8; 32]"}]}},"path":["ink_primitives","types","AccountId"]}},{"id":7,"type":{"def":{"array":{"len":32,"type":8}}}},{"id":8,"type":{"def":{"primitive":"u8"}}},{"id":9,"type":{"def":{"primitive":"u128"}}},{"id":10,"type":{"def":{"composite":{"fields":[{"type":7,"typeName":"[u8; 32]"}]}},"path":["ink_primitives","types","Hash"]}},{"id":11,"type":{"def":{"primitive":"u64"}}},{"id":12,"type":{"def":{"primitive":"u32"}}},{"id":13,"type":{"def":{"variant":{}},"path":["ink_env","types","NoChainExtension"]}}],"version":5} \ No newline at end of file From c4221921bfced361edde5632904eeadedb261e12 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 10 Sep 2024 11:13:36 +0200 Subject: [PATCH 11/27] fix: fix todos and refactor --- crates/pop-cli/src/commands/call/contract.rs | 49 ++++++++++++++++++-- crates/pop-contracts/src/call/metadata.rs | 6 ++- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index e8312fe0..f6908f9b 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -74,10 +74,13 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { let contract = call_config .contract .expect("contract can not be none as fallback above is interactive input; qed"); - // TODO: Can be nill pop call contract --contract - let message = call_config - .message - .expect("message can not be none as fallback above is interactive input; qed"); + let message = match call_config.message { + Some(m) => m, + None => { + self.cli.outro_cancel("Please specify the message to call.")?; + return Ok(()); + }, + }; let call_exec = set_up_call(CallOpts { path: call_config.path, @@ -484,4 +487,42 @@ mod tests { cli.verify() } + + #[tokio::test] + async fn call_contract_messages_fails_no_message() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + 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 mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_outro_cancel("Please specify the message to call."); + + // Contract deployed on Pop Network testnet, test get + Box::new(CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, + }) + .execute() + .await?; + + cli.verify() + } } diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index a2b378e6..43c6997b 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -23,6 +23,10 @@ pub struct Message { pub default: bool, } +/// Extracts a list of smart contract messages parsing the metadata file. +/// +/// # Arguments +/// * `path` - Location path of the project. pub fn get_messages(path: &Path) -> Result, Error> { let cargo_toml_path = match path.ends_with("Cargo.toml") { true => path.to_path_buf(), @@ -44,7 +48,7 @@ pub fn get_messages(path: &Path) -> Result, Error> { } Ok(messages) } -//TODO: We are ignoring the type of the argument. +// Parse the message parameters into a vector of argument labels. fn process_args(message_params: &[MessageParamSpec]) -> Vec { let mut args: Vec = Vec::new(); for arg in message_params { From 22dd4f6b0e9c0afb8ecd4c9310a7504988b76d0f Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 10 Sep 2024 11:50:01 +0200 Subject: [PATCH 12/27] test: fix unit test --- crates/pop-cli/src/commands/call/contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index f6908f9b..7ba1e0dc 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -348,6 +348,7 @@ mod tests { let items = vec![ ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), ("get".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() From c8fedef18f4e23bb27931991ae96ce07d8a9b5ad Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Wed, 11 Sep 2024 18:51:30 +0200 Subject: [PATCH 13/27] feat: parse types of parameters and display it to the user in the placeholder --- crates/pop-cli/src/commands/call/contract.rs | 10 +++++++-- crates/pop-contracts/src/call/metadata.rs | 23 +++++++++++++++----- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 7ba1e0dc..dc13d1d5 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -212,7 +212,13 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( let mut contract_args = Vec::new(); for arg in &message.args { - contract_args.push(command.cli.input(arg).placeholder(arg).interact()?); + contract_args.push( + command + .cli + .input(format!("Enter the value for the parameter: {}", arg.label)) + .placeholder(&format!("Type required: {}", &arg.type_name)) + .interact()?, + ); } let mut value = "0".to_string(); if message.payable { @@ -437,7 +443,7 @@ mod tests { .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("new_value", "true".into()) // Args for specific_flip + .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip .expect_select::( "Select the message to call:", Some(false), diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index 43c6997b..82e7589b 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -6,6 +6,14 @@ use contract_transcode::ink_metadata::MessageParamSpec; use scale_info::form::PortableForm; use std::path::Path; +#[derive(Clone, PartialEq, Eq)] +/// Describes a contract message. +pub struct Param { + /// The label of the parameter. + pub label: String, + /// The type name of the parameter. + pub type_name: String, +} #[derive(Clone, PartialEq, Eq)] /// Describes a contract message. pub struct Message { @@ -16,7 +24,7 @@ pub struct Message { /// If the message accepts any `value` from the caller. pub payable: bool, /// The parameters of the deployment handler. - pub args: Vec, + pub args: Vec, /// The message documentation. pub docs: String, /// If the message is the default for off-chain consumers (e.g UIs). @@ -49,10 +57,13 @@ pub fn get_messages(path: &Path) -> Result, Error> { Ok(messages) } // Parse the message parameters into a vector of argument labels. -fn process_args(message_params: &[MessageParamSpec]) -> Vec { - let mut args: Vec = Vec::new(); +fn process_args(message_params: &[MessageParamSpec]) -> Vec { + let mut args: Vec = Vec::new(); for arg in message_params { - args.push(arg.label().to_string()); + args.push(Param { + label: arg.label().to_string(), + type_name: arg.ty().display_name().to_string(), + }); } args } @@ -83,7 +94,9 @@ mod tests { assert_eq!(message[2].label, "specific_flip"); assert_eq!(message[2].docs, " A message for testing, flips the value of the stored `bool` with `new_value` and is payable"); // assert parsed arguments - assert_eq!(message[2].args, vec!["new_value".to_string()]); + assert_eq!(message[2].args.len(), 1); + assert_eq!(message[2].args[0].label, "new_value".to_string()); + assert_eq!(message[2].args[0].type_name, "bool".to_string()); Ok(()) } } From b0b3573db230dbca387bf52cdb20fd2d1cef85fd Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 12 Sep 2024 11:30:06 +0200 Subject: [PATCH 14/27] refactor: error handling for pop call --- crates/pop-cli/src/commands/call/contract.rs | 29 ++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index dc13d1d5..748614f3 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -67,7 +67,13 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { self.cli.intro("Call a contract")?; let call_config = if self.args.contract.is_none() { - guide_user_to_call_contract(&mut self).await? + match guide_user_to_call_contract(&mut self).await { + Ok(config) => config, + Err(e) => { + self.cli.outro_cancel(format!("{}", e.to_string()))?; + return Ok(()); + }, + } } else { self.args.clone() }; @@ -82,7 +88,7 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { }, }; - let call_exec = set_up_call(CallOpts { + let call_exec = match set_up_call(CallOpts { path: call_config.path, contract, message, @@ -94,7 +100,14 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { suri: call_config.suri, execute: call_config.execute, }) - .await?; + .await + { + Ok(call_exec) => call_exec, + Err(e) => { + self.cli.outro_cancel(format!("{}", e.root_cause().to_string()))?; + return Ok(()); + }, + }; if call_config.dry_run { let spinner = cliclack::spinner(); @@ -197,8 +210,10 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( let messages = match get_messages(contract_path) { Ok(messages) => messages, Err(e) => { - command.cli.outro_cancel("Unable to fetch contract metadata.")?; - return Err(anyhow!(format!("{}", e.to_string()))); + return Err(anyhow!(format!( + "Unable to fetch contract metadata: {}", + e.to_string().replace("Anyhow error: ", "") + ))); }, }; let message = { @@ -227,6 +242,10 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .input("Value to transfer to the call:") .placeholder("0") .default_input("0") + .validate(|input: &String| match input.parse::() { + Ok(_) => Ok(()), + Err(_) => Err("Invalid value."), + }) .interact()?; } let mut gas_limit: Option = None; From 8e60da84e3244e4720b92a22412927e1720b6ff9 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 12 Sep 2024 12:57:55 +0200 Subject: [PATCH 15/27] refactor: display call to be executed after guide and reorder --- crates/pop-cli/src/commands/call/contract.rs | 149 +++++++++++++++---- 1 file changed, 117 insertions(+), 32 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 748614f3..743dc3fc 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -53,6 +53,40 @@ pub struct CallContractCommand { #[clap(long, conflicts_with = "execute")] dry_run: bool, } +impl CallContractCommand { + fn display(&self) -> String { + let mut full_message = format!("pop call contract"); + if let Some(path) = &self.path { + full_message.push_str(&format!(" --path {}", path.display().to_string())); + } + if let Some(contract) = &self.contract { + full_message.push_str(&format!(" --contract {}", contract)); + } + if let Some(message) = &self.message { + full_message.push_str(&format!(" --message {}", message)); + } + if !self.args.is_empty() { + full_message.push_str(&format!(" --args {}", self.args.join(" "))); + } + if self.value != "0" { + full_message.push_str(&format!(" --value {}", self.value)); + } + if let Some(gas_limit) = self.gas_limit { + full_message.push_str(&format!(" --gas {}", gas_limit)); + } + if let Some(proof_size) = self.proof_size { + full_message.push_str(&format!(" --proof_size {}", proof_size)); + } + full_message.push_str(&format!(" --url {} --suri {}", self.url, self.suri)); + if self.execute { + full_message.push_str(" --execute"); + } + if self.dry_run { + full_message.push_str(" --dry_run"); + } + full_message + } +} pub(crate) struct CallContract<'a, CLI: Cli> { /// The cli to be used. @@ -65,7 +99,6 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { /// Executes the command. pub(crate) async fn execute(mut self: Box) -> Result<()> { self.cli.intro("Call a contract")?; - let call_config = if self.args.contract.is_none() { match guide_user_to_call_contract(&mut self).await { Ok(config) => config, @@ -195,6 +228,24 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .interact()?; let contract_path = Path::new(&input_path); + let messages = match get_messages(contract_path) { + Ok(messages) => messages, + Err(e) => { + return Err(anyhow!(format!( + "Unable to fetch contract metadata: {}", + e.to_string().replace("Anyhow error: ", "") + ))); + }, + }; + + // Prompt for contract location. + let url: String = command + .cli + .input("Where is your contract deployed?") + .placeholder("ws://localhost:9944") + .default_input("ws://localhost:9944") + .interact()?; + // Prompt for contract address. let contract_address: String = command .cli @@ -207,15 +258,6 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .interact()?; - let messages = match get_messages(contract_path) { - Ok(messages) => messages, - Err(e) => { - return Err(anyhow!(format!( - "Unable to fetch contract metadata: {}", - e.to_string().replace("Anyhow error: ", "") - ))); - }, - }; let message = { let mut prompt = command.cli.select("Select the message to call:"); for select_message in messages { @@ -270,14 +312,6 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. } - // Prompt for contract location. - let url: String = command - .cli - .input("Where is your contract deployed?") - .placeholder("ws://localhost:9944") - .default_input("ws://localhost:9944") - .interact()?; - // Who is calling the contract. let suri: String = command .cli @@ -294,8 +328,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .initial_value(true) .interact()?; } - - Ok(CallContractCommand { + let call_command = CallContractCommand { path: Some(contract_path.to_path_buf()), contract: Some(contract_address), message: Some(message.label.clone()), @@ -307,7 +340,9 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( suri, execute: message.mutates, dry_run: !is_call_confirmed, - }) + }; + command.cli.info(call_command.display())?; + Ok(call_command) } #[cfg(test)] @@ -379,10 +414,6 @@ mod tests { let mut cli = MockCli::new() .expect_intro(&"Call a contract") .expect_input("Signer calling the contract:", "//Alice".into()) - .expect_input( - "Where is your contract deployed?", - "wss://rpc1.paseo.popnetwork.xyz".into(), - ) .expect_select::( "Select the message to call:", Some(false), @@ -394,10 +425,17 @@ mod tests { "Paste the on-chain contract address:", "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) + .expect_input( + "Where is your contract deployed?", + "wss://rpc1.paseo.popnetwork.xyz".into(), + ) .expect_input( "Where is your project located?", temp_dir.path().join("testing").display().to_string(), - ); + ).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(), + )); let call_config = guide_user_to_call_contract(&mut CallContract { cli: &mut cli, @@ -429,6 +467,10 @@ mod tests { assert_eq!(call_config.suri, "//Alice"); assert!(!call_config.execute); assert!(!call_config.dry_run); + assert_eq!(call_config.display(), 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(), + )); cli.verify() } @@ -455,10 +497,6 @@ mod tests { let mut cli = MockCli::new() .expect_intro(&"Call a contract") .expect_input("Signer calling the contract:", "//Alice".into()) - .expect_input( - "Where is your contract deployed?", - "wss://rpc1.paseo.popnetwork.xyz".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 @@ -474,10 +512,17 @@ mod tests { "Paste the on-chain contract address:", "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), ) + .expect_input( + "Where is your contract deployed?", + "wss://rpc1.paseo.popnetwork.xyz".into(), + ) .expect_input( "Where is your project located?", temp_dir.path().join("testing").display().to_string(), - ); + ).expect_info(format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args true --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); let call_config = guide_user_to_call_contract(&mut CallContract { cli: &mut cli, @@ -510,6 +555,10 @@ mod tests { assert_eq!(call_config.suri, "//Alice"); assert!(call_config.execute); assert!(!call_config.dry_run); + assert_eq!(call_config.display(), format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args true --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); cli.verify() } @@ -529,7 +578,6 @@ mod tests { .expect_intro(&"Call a contract") .expect_outro_cancel("Please specify the message to call."); - // Contract deployed on Pop Network testnet, test get Box::new(CallContract { cli: &mut cli, args: CallContractCommand { @@ -551,4 +599,41 @@ mod tests { cli.verify() } + + #[tokio::test] + async fn call_contract_messages_fails_parse_metadata() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + 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 mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_outro_cancel("Unable to fetch contract metadata: No 'ink' dependency found"); + + Box::new(CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("wrong-testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("ws://localhost:9944")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, + }) + .execute() + .await?; + + cli.verify() + } } From 08f24548f2e758a04698212809b50dfbe788e5ba Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 12 Sep 2024 21:55:11 +0200 Subject: [PATCH 16/27] refactor: when repeat call use same contract values and dont clean screen --- crates/pop-cli/src/commands/call/contract.rs | 259 ++++++++++--------- crates/pop-cli/src/commands/mod.rs | 7 +- 2 files changed, 137 insertions(+), 129 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 743dc3fc..0a7b71f7 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -8,7 +8,7 @@ use pop_contracts::{ set_up_call, CallOpts, }; use sp_weights::Weight; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; #[derive(Args, Clone)] pub struct CallContractCommand { @@ -97,10 +97,10 @@ pub(crate) struct CallContract<'a, CLI: Cli> { impl<'a, CLI: Cli> CallContract<'a, CLI> { /// Executes the command. - pub(crate) async fn execute(mut self: Box) -> Result<()> { + pub(crate) async fn execute(mut self: Self) -> Result<()> { self.cli.intro("Call a contract")?; let call_config = if self.args.contract.is_none() { - match guide_user_to_call_contract(&mut self).await { + match guide_user_to_call_contract(&mut self, None, None, None).await { Ok(config) => config, Err(e) => { self.cli.outro_cancel(format!("{}", e.to_string()))?; @@ -110,19 +110,29 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { } else { self.args.clone() }; + match self.execute_call(call_config.clone()).await { + Ok(_) => Ok(()), + Err(e) => { + self.cli.outro_cancel(format!("{}", e.to_string()))?; + return Ok(()); + }, + } + } + /// Executes the call. + async fn execute_call(&mut self, call_config: CallContractCommand) -> Result<()> { let contract = call_config .contract + .clone() .expect("contract can not be none as fallback above is interactive input; qed"); let message = match call_config.message { Some(m) => m, None => { - self.cli.outro_cancel("Please specify the message to call.")?; - return Ok(()); + return Err(anyhow!("Please specify the message to call.")); }, }; let call_exec = match set_up_call(CallOpts { - path: call_config.path, + path: call_config.path.clone(), contract, message, args: call_config.args, @@ -137,8 +147,7 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { { Ok(call_exec) => call_exec, Err(e) => { - self.cli.outro_cancel(format!("{}", e.root_cause().to_string()))?; - return Ok(()); + return Err(anyhow!(format!("{}", e.root_cause().to_string()))); }, }; @@ -181,8 +190,7 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { }, Err(e) => { spinner.error(format!("{e}")); - self.cli.outro_cancel("Call failed.")?; - return Ok(()); + return Err(anyhow!("Call failed.")); }, }; } @@ -198,37 +206,52 @@ impl<'a, CLI: Cli> CallContract<'a, CLI> { if self.args.contract.is_none() { let another_call: bool = self .cli - .confirm("Do you want to do another call?") + .confirm("Do you want to do another call using the existing smart contract?") .initial_value(false) .interact()?; if another_call { - Box::pin(self.execute()).await?; + // Remove only the prompt asking for another call. + console::Term::stderr().clear_last_lines(2)?; + let new_call_config = guide_user_to_call_contract( + self, + call_config.path, + Some(call_config.url), + call_config.contract, + ) + .await?; + Box::pin(self.execute_call(new_call_config)).await?; } else { self.cli.outro("Call completed successfully!")?; } } else { self.cli.outro("Call completed successfully!")?; } - Ok(()) + return Ok(()); } } /// Guide the user to call the contract. async fn guide_user_to_call_contract<'a, CLI: Cli>( command: &mut CallContract<'a, CLI>, + contract_path: Option, + url: Option, + contract_address: Option, ) -> anyhow::Result { - command.cli.intro("Call a contract")?; - - // Prompt for location of your contract. - let input_path: String = command - .cli - .input("Where is your project located?") - .placeholder("./") - .default_input("./") - .interact()?; - let contract_path = Path::new(&input_path); - - let messages = match get_messages(contract_path) { + let contract_path: PathBuf = match contract_path { + Some(path) => path, + None => { + // Prompt for path. + let input_path: String = command + .cli + .input("Where is your project located?") + .placeholder("./") + .default_input("./") + .interact()?; + PathBuf::from(input_path) + }, + }; + // Parse the contract metadata provided. If there error, do not prompt for more. + let messages = match get_messages(&contract_path) { Ok(messages) => messages, Err(e) => { return Err(anyhow!(format!( @@ -237,32 +260,45 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( ))); }, }; - - // Prompt for contract location. - let url: String = command - .cli - .input("Where is your contract deployed?") - .placeholder("ws://localhost:9944") - .default_input("ws://localhost:9944") - .interact()?; - - // Prompt for contract address. - let contract_address: String = command - .cli - .input("Paste the on-chain contract address:") - .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") - .validate(|input: &String| match parse_account(input) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid address."), - }) - .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") - .interact()?; + let url: url::Url = match url { + Some(url) => url, + None => { + // Prompt for url. + let url: String = command + .cli + .input("Where is your contract deployed?") + .placeholder("ws://localhost:9944") + .default_input("ws://localhost:9944") + .interact()?; + url::Url::parse(&url)? + }, + }; + let contract_address: String = match contract_address { + Some(contract_address) => contract_address, + None => { + // Prompt for contract address. + let contract_address: String = command + .cli + .input("Paste the on-chain contract address:") + .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .validate(|input: &String| match parse_account(input) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid address."), + }) + .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .interact()?; + contract_address + }, + }; let message = { let mut prompt = command.cli.select("Select the message to call:"); for select_message in messages { - prompt = - prompt.item(select_message.clone(), &select_message.label, &select_message.docs); + prompt = prompt.item( + select_message.clone(), + format!("{}\n", &select_message.label), + &select_message.docs, + ); } prompt.interact()? }; @@ -329,14 +365,14 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( .interact()?; } let call_command = CallContractCommand { - path: Some(contract_path.to_path_buf()), + path: Some(contract_path), contract: Some(contract_address), message: Some(message.label.clone()), args: contract_args, value, gas_limit, proof_size, - url: url::Url::parse(&url)?, + url, suri, execute: message.mutates, dry_run: !is_call_confirmed, @@ -406,13 +442,12 @@ mod tests { )?; let items = vec![ - ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), - ("get".into(), " Simply returns the current value of our `bool`.".into()), - ("specific_flip".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) + ("flip\n".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get\n".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip\n".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() - .expect_intro(&"Call a contract") .expect_input("Signer calling the contract:", "//Alice".into()) .expect_select::( "Select the message to call:", @@ -437,22 +472,27 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(&mut CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("ws://localhost:9944")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, + let call_config = guide_user_to_call_contract( + &mut CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("ws://localhost:9944")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, }, - }) + None, + None, + None, + ) .await?; assert_eq!( call_config.contract, @@ -489,13 +529,12 @@ mod tests { )?; let items = vec![ - ("flip".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), - ("get".into(), " Simply returns the current value of our `bool`.".into()), - ("specific_flip".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) + ("flip\n".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get\n".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip\n".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) ]; // The inputs are processed in reverse order. let mut cli = MockCli::new() - .expect_intro(&"Call a contract") .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 @@ -524,22 +563,27 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(&mut CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("ws://localhost:9944")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, + let call_config = guide_user_to_call_contract( + &mut CallContract { + cli: &mut cli, + args: CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("ws://localhost:9944")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + }, }, - }) + None, + None, + None, + ) .await?; assert_eq!( call_config.contract, @@ -578,7 +622,7 @@ mod tests { .expect_intro(&"Call a contract") .expect_outro_cancel("Please specify the message to call."); - Box::new(CallContract { + CallContract { cli: &mut cli, args: CallContractCommand { path: Some(temp_dir.path().join("testing")), @@ -593,44 +637,7 @@ mod tests { dry_run: false, execute: false, }, - }) - .execute() - .await?; - - cli.verify() - } - - #[tokio::test] - async fn call_contract_messages_fails_parse_metadata() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; - 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 mut cli = MockCli::new() - .expect_intro(&"Call a contract") - .expect_outro_cancel("Unable to fetch contract metadata: No 'ink' dependency found"); - - Box::new(CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("wrong-testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("ws://localhost:9944")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - }, - }) + } .execute() .await?; diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 4530ce4c..a7797845 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,11 +98,12 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => - Box::new(call::contract::CallContract { cli: &mut Cli, args: cmd }) + call::Command::Contract(cmd) => { + call::contract::CallContract { cli: &mut Cli, args: cmd } .execute() .await - .map(|_| Value::Null), + .map(|_| Value::Null) + }, }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { From 68848beedcb01416515174fdbf352e7359843c8d Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 13 Sep 2024 11:23:06 +0200 Subject: [PATCH 17/27] test: add dry-run test --- crates/pop-cli/src/commands/call/contract.rs | 49 ++++++++++++++++++-- crates/pop-cli/src/commands/mod.rs | 5 +- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 0a7b71f7..9782af73 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -24,12 +24,12 @@ pub struct CallContractCommand { /// The constructor arguments, encoded as strings. #[clap(long, num_args = 0..)] args: Vec, - /// Transfers an initial balance to the contract. + /// The value to be transferred as part of the call. #[clap(name = "value", long, default_value = "0")] 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. + /// call. #[clap(name = "gas", long)] gas_limit: Option, /// Maximum proof size for this command. @@ -374,7 +374,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( proof_size, url, suri, - execute: message.mutates, + execute: if is_call_confirmed { message.mutates } else { false }, dry_run: !is_call_confirmed, }; command.cli.info(call_command.display())?; @@ -406,7 +406,7 @@ mod tests { .expect_outro("Call completed successfully!"); // Contract deployed on Pop Network testnet, test get - Box::new(CallContract { + CallContract { cli: &mut cli, args: CallContractCommand { path: Some(temp_dir.path().join("testing")), @@ -421,13 +421,52 @@ mod tests { dry_run: false, execute: false, }, - }) + } .execute() .await?; cli.verify() } + #[tokio::test] + async fn call_contract_dry_run_works() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + 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 mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_warning("Your call has not been executed.") + .expect_info("Gas limit: Weight { ref_time: 100, proof_size: 10 }"); + + let call_config = CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: Some("flip".to_string()), + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: Some(100), + proof_size: Some(10), + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: true, + execute: false, + }; + assert_eq!(call_config.display(), format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", + temp_dir.path().join("testing").display().to_string(), + )); + // Contract deployed on Pop Network testnet, test dry-run + CallContract { cli: &mut cli, args: call_config }.execute().await?; + + cli.verify() + } + // This test only covers the interactive portion of the call contract command, without actually // calling the contract. #[tokio::test] diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index a7797845..234b8d57 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,12 +98,11 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => { + call::Command::Contract(cmd) => call::contract::CallContract { cli: &mut Cli, args: cmd } .execute() .await - .map(|_| Value::Null) - }, + .map(|_| Value::Null), }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { From 1aaea92060dc27dd5c71463fbedfa64c1d5d525e Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Thu, 19 Sep 2024 21:15:01 +0200 Subject: [PATCH 18/27] test: refactor and add more test coverage --- crates/pop-cli/src/commands/call/contract.rs | 468 +++++++++---------- crates/pop-cli/src/commands/mod.rs | 6 +- 2 files changed, 224 insertions(+), 250 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 9782af73..a3a0f240 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::cli::traits::*; +use crate::cli::{self, traits::*}; use anyhow::{anyhow, Result}; use clap::Args; use pop_contracts::{ @@ -54,6 +54,24 @@ pub struct CallContractCommand { dry_run: bool, } impl CallContractCommand { + /// Executes the command. + pub(crate) async fn execute(self) -> Result<()> { + let call_config: CallContractCommand = match self.set_up_call_config(&mut cli::Cli).await { + Ok(call_config) => call_config, + Err(e) => { + display_message(&format!("{}", e.to_string()), false, &mut cli::Cli)?; + return Ok(()); + }, + }; + match execute_call(call_config, self.contract.is_none(), &mut cli::Cli).await { + Ok(_) => Ok(()), + Err(e) => { + display_message(&format!("{}", e.to_string()), false, &mut cli::Cli)?; + Ok(()) + }, + } + } + fn display(&self) -> String { let mut full_message = format!("pop call contract"); if let Some(path) = &self.path { @@ -86,163 +104,39 @@ impl CallContractCommand { } full_message } -} -pub(crate) struct CallContract<'a, CLI: Cli> { - /// The cli to be used. - pub(crate) cli: &'a mut CLI, - /// The args to call. - pub(crate) args: CallContractCommand, -} - -impl<'a, CLI: Cli> CallContract<'a, CLI> { - /// Executes the command. - pub(crate) async fn execute(mut self: Self) -> Result<()> { - self.cli.intro("Call a contract")?; - let call_config = if self.args.contract.is_none() { - match guide_user_to_call_contract(&mut self, None, None, None).await { + /// Set up the config call. + async fn set_up_call_config( + &self, + cli: &mut impl cli::traits::Cli, + ) -> anyhow::Result { + cli.intro("Call a contract")?; + let call_config = if self.contract.is_none() { + match guide_user_to_call_contract(None, None, None, cli).await { Ok(config) => config, Err(e) => { - self.cli.outro_cancel(format!("{}", e.to_string()))?; - return Ok(()); + return Err(anyhow!(format!("{}", e.to_string()))); }, } } else { - self.args.clone() + self.clone() }; - match self.execute_call(call_config.clone()).await { - Ok(_) => Ok(()), - Err(e) => { - self.cli.outro_cancel(format!("{}", e.to_string()))?; - return Ok(()); - }, - } - } - /// Executes the call. - async fn execute_call(&mut self, call_config: CallContractCommand) -> Result<()> { - let contract = call_config - .contract - .clone() - .expect("contract can not be none as fallback above is interactive input; qed"); - let message = match call_config.message { - Some(m) => m, - None => { - return Err(anyhow!("Please specify the message to call.")); - }, - }; - - let call_exec = match set_up_call(CallOpts { - path: call_config.path.clone(), - contract, - message, - args: call_config.args, - value: call_config.value, - gas_limit: call_config.gas_limit, - proof_size: call_config.proof_size, - url: call_config.url.clone(), - suri: call_config.suri, - execute: call_config.execute, - }) - .await - { - Ok(call_exec) => call_exec, - Err(e) => { - return Err(anyhow!(format!("{}", e.root_cause().to_string()))); - }, - }; - - if call_config.dry_run { - let spinner = cliclack::spinner(); - spinner.start("Doing a dry run to estimate the gas..."); - match dry_run_gas_estimate_call(&call_exec).await { - Ok(w) => { - self.cli.info(format!("Gas limit: {:?}", w))?; - self.cli.warning("Your call has not been executed.")?; - }, - Err(e) => { - spinner.error(format!("{e}")); - self.cli.outro_cancel("Call failed.")?; - }, - }; - return Ok(()); - } - - if !call_config.execute { - let spinner = cliclack::spinner(); - spinner.start("Calling the contract..."); - let call_dry_run_result = dry_run_call(&call_exec).await?; - self.cli.info(format!("Result: {}", call_dry_run_result))?; - self.cli.warning("Your call has not been executed.")?; - } else { - let weight_limit; - if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { - weight_limit = Weight::from_parts( - call_config.gas_limit.unwrap(), - call_config.proof_size.unwrap(), - ); - } else { - let spinner = cliclack::spinner(); - spinner.start("Doing a dry run to estimate the gas..."); - weight_limit = match dry_run_gas_estimate_call(&call_exec).await { - Ok(w) => { - self.cli.info(format!("Gas limit: {:?}", w))?; - w - }, - Err(e) => { - spinner.error(format!("{e}")); - return Err(anyhow!("Call failed.")); - }, - }; - } - let spinner = cliclack::spinner(); - spinner.start("Calling the contract..."); - - let call_result = call_smart_contract(call_exec, weight_limit, &call_config.url) - .await - .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; - - self.cli.info(call_result)?; - } - if self.args.contract.is_none() { - let another_call: bool = self - .cli - .confirm("Do you want to do another call using the existing smart contract?") - .initial_value(false) - .interact()?; - if another_call { - // Remove only the prompt asking for another call. - console::Term::stderr().clear_last_lines(2)?; - let new_call_config = guide_user_to_call_contract( - self, - call_config.path, - Some(call_config.url), - call_config.contract, - ) - .await?; - Box::pin(self.execute_call(new_call_config)).await?; - } else { - self.cli.outro("Call completed successfully!")?; - } - } else { - self.cli.outro("Call completed successfully!")?; - } - return Ok(()); + Ok(call_config) } } /// Guide the user to call the contract. -async fn guide_user_to_call_contract<'a, CLI: Cli>( - command: &mut CallContract<'a, CLI>, +async fn guide_user_to_call_contract( contract_path: Option, url: Option, contract_address: Option, + cli: &mut impl cli::traits::Cli, ) -> anyhow::Result { let contract_path: PathBuf = match contract_path { Some(path) => path, None => { // Prompt for path. - let input_path: String = command - .cli + let input_path: String = cli .input("Where is your project located?") .placeholder("./") .default_input("./") @@ -264,8 +158,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( Some(url) => url, None => { // Prompt for url. - let url: String = command - .cli + let url: String = cli .input("Where is your contract deployed?") .placeholder("ws://localhost:9944") .default_input("ws://localhost:9944") @@ -277,8 +170,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( Some(contract_address) => contract_address, None => { // Prompt for contract address. - let contract_address: String = command - .cli + let contract_address: String = cli .input("Paste the on-chain contract address:") .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .validate(|input: &String| match parse_account(input) { @@ -292,7 +184,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( }; let message = { - let mut prompt = command.cli.select("Select the message to call:"); + let mut prompt = cli.select("Select the message to call:"); for select_message in messages { prompt = prompt.item( select_message.clone(), @@ -306,17 +198,14 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( let mut contract_args = Vec::new(); for arg in &message.args { contract_args.push( - command - .cli - .input(format!("Enter the value for the parameter: {}", arg.label)) + cli.input(format!("Enter the value for the parameter: {}", arg.label)) .placeholder(&format!("Type required: {}", &arg.type_name)) .interact()?, ); } let mut value = "0".to_string(); if message.payable { - value = command - .cli + value = cli .input("Value to transfer to the call:") .placeholder("0") .default_input("0") @@ -330,16 +219,14 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( let mut proof_size: Option = None; if message.mutates { // Prompt for gas limit and proof_size of the call. - let gas_limit_input: String = command - .cli + let gas_limit_input: String = cli .input("Enter the gas limit:") .required(false) .default_input("") .placeholder("If left blank, an estimation will be used") .interact()?; gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. - let proof_size_input: String = command - .cli + let proof_size_input: String = cli .input("Enter the proof size limit:") .required(false) .placeholder("If left blank, an estimation will be used") @@ -349,8 +236,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( } // Who is calling the contract. - let suri: String = command - .cli + let suri: String = cli .input("Signer calling the contract:") .placeholder("//Alice") .default_input("//Alice") @@ -358,8 +244,7 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( let mut is_call_confirmed: bool = true; if message.mutates { - is_call_confirmed = command - .cli + is_call_confirmed = cli .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) .interact()?; @@ -377,10 +262,131 @@ async fn guide_user_to_call_contract<'a, CLI: Cli>( execute: if is_call_confirmed { message.mutates } else { false }, dry_run: !is_call_confirmed, }; - command.cli.info(call_command.display())?; + cli.info(call_command.display())?; Ok(call_command) } +/// Executes the call. +async fn execute_call( + call_config: CallContractCommand, + prompt_to_repeat_call: bool, + cli: &mut impl cli::traits::Cli, +) -> anyhow::Result<()> { + let contract = call_config + .contract + .clone() + .expect("contract can not be none as fallback above is interactive input; qed"); + let message = match call_config.message { + Some(m) => m, + None => { + return Err(anyhow!("Please specify the message to call.")); + }, + }; + + let call_exec = match set_up_call(CallOpts { + path: call_config.path.clone(), + contract, + message, + args: call_config.args, + value: call_config.value, + gas_limit: call_config.gas_limit, + proof_size: call_config.proof_size, + url: call_config.url.clone(), + suri: call_config.suri, + execute: call_config.execute, + }) + .await + { + Ok(call_exec) => call_exec, + Err(e) => { + return Err(anyhow!(format!("{}", e.root_cause().to_string()))); + }, + }; + + if call_config.dry_run { + let spinner = cliclack::spinner(); + spinner.start("Doing a dry run to estimate the gas..."); + match dry_run_gas_estimate_call(&call_exec).await { + Ok(w) => { + cli.info(format!("Gas limit: {:?}", w))?; + cli.warning("Your call has not been executed.")?; + }, + Err(e) => { + spinner.error(format!("{e}")); + display_message("Call failed.", false, cli)?; + }, + }; + return Ok(()); + } + + if !call_config.execute { + let spinner = cliclack::spinner(); + spinner.start("Calling the contract..."); + let call_dry_run_result = dry_run_call(&call_exec).await?; + cli.info(format!("Result: {}", call_dry_run_result))?; + cli.warning("Your call has not been executed.")?; + } else { + let weight_limit; + if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { + weight_limit = + Weight::from_parts(call_config.gas_limit.unwrap(), call_config.proof_size.unwrap()); + } else { + let spinner = cliclack::spinner(); + spinner.start("Doing a dry run to estimate the gas..."); + weight_limit = match dry_run_gas_estimate_call(&call_exec).await { + Ok(w) => { + cli.info(format!("Gas limit: {:?}", w))?; + w + }, + Err(e) => { + spinner.error(format!("{e}")); + return Err(anyhow!("Call failed.")); + }, + }; + } + let spinner = cliclack::spinner(); + spinner.start("Calling the contract..."); + + let call_result = call_smart_contract(call_exec, weight_limit, &call_config.url) + .await + .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; + + cli.info(call_result)?; + } + if prompt_to_repeat_call { + let another_call: bool = cli + .confirm("Do you want to do another call using the existing smart contract?") + .initial_value(false) + .interact()?; + if another_call { + // Remove only the prompt asking for another call. + console::Term::stderr().clear_last_lines(2)?; + let new_call_config = guide_user_to_call_contract( + call_config.path, + Some(call_config.url), + call_config.contract, + cli, + ) + .await?; + Box::pin(execute_call(new_call_config, prompt_to_repeat_call, cli)).await?; + } else { + display_message("Call completed successfully!", true, cli)?; + } + } else { + display_message("Call completed successfully!", true, cli)?; + } + return Ok(()); +} + +fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli) -> Result<()> { + if success { + cli.outro(message)?; + } else { + cli.outro_cancel(message)?; + } + Ok(()) +} + #[cfg(test)] mod tests { use super::*; @@ -390,7 +396,7 @@ mod tests { use url::Url; #[tokio::test] - async fn call_contract_messages_are_ok() -> Result<()> { + async fn call_contract_query_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; let mut current_dir = env::current_dir().expect("Failed to get current directory"); current_dir.pop(); @@ -403,27 +409,30 @@ mod tests { let mut cli = MockCli::new() .expect_intro(&"Call a contract") .expect_warning("Your call has not been executed.") + .expect_confirm( + "Do you want to do another call using the existing smart contract?", + false, + ) .expect_outro("Call completed successfully!"); // Contract deployed on Pop Network testnet, test get - CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), - message: Some("get".to_string()), - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - }, + let config_call = CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: Some("get".to_string()), + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, } - .execute() + .set_up_call_config(&mut cli) .await?; + // Test the query. With true, it will prompt for another call. + execute_call(config_call, true, &mut cli).await?; cli.verify() } @@ -456,13 +465,15 @@ mod tests { suri: "//Alice".to_string(), dry_run: true, execute: false, - }; + } + .set_up_call_config(&mut cli) + .await?; assert_eq!(call_config.display(), format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", temp_dir.path().join("testing").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run - CallContract { cli: &mut cli, args: call_config }.execute().await?; + execute_call(call_config, false, &mut cli).await?; cli.verify() } @@ -507,32 +518,11 @@ mod tests { "Where is your project located?", temp_dir.path().join("testing").display().to_string(), ).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(), - )); - - let call_config = guide_user_to_call_contract( - &mut CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("ws://localhost:9944")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - }, - }, - None, - None, - None, - ) - .await?; + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message get --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice", + temp_dir.path().join("testing").display().to_string(), + )); + + let call_config = guide_user_to_call_contract(None, None, None, &mut cli).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -602,28 +592,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract( - &mut CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("ws://localhost:9944")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - }, - }, - None, - None, - None, - ) - .await?; + let call_config = guide_user_to_call_contract(None, None, None, &mut cli).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -657,29 +626,38 @@ mod tests { current_dir.join("pop-contracts/tests/files/testing.json"), )?; - let mut cli = MockCli::new() - .expect_intro(&"Call a contract") - .expect_outro_cancel("Please specify the message to call."); - - CallContract { - cli: &mut cli, - args: CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - }, + let mut cli = MockCli::new().expect_intro(&"Call a contract"); + + let call_config = CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, } - .execute() + .set_up_call_config(&mut cli) .await?; + assert!(matches!( + execute_call(call_config, false, &mut cli).await, + anyhow::Result::Err(message) if message.to_string() == "Please specify the message to call." + )); + + cli.verify() + } + #[test] + fn test_display_message() -> Result<()> { + let mut cli = MockCli::new().expect_outro(&"Call completed successfully!"); + display_message("Call completed successfully!", true, &mut cli)?; + cli.verify()?; + let mut cli = MockCli::new().expect_outro_cancel("Call failed."); + display_message("Call failed.", false, &mut cli)?; cli.verify() } } diff --git a/crates/pop-cli/src/commands/mod.rs b/crates/pop-cli/src/commands/mod.rs index 234b8d57..34c2f10b 100644 --- a/crates/pop-cli/src/commands/mod.rs +++ b/crates/pop-cli/src/commands/mod.rs @@ -98,11 +98,7 @@ impl Command { }, #[cfg(feature = "contract")] Self::Call(args) => match args.command { - call::Command::Contract(cmd) => - call::contract::CallContract { cli: &mut Cli, args: cmd } - .execute() - .await - .map(|_| Value::Null), + call::Command::Contract(cmd) => cmd.execute().await.map(|_| Value::Null), }, #[cfg(any(feature = "parachain", feature = "contract"))] Self::Up(args) => match args.command { From 05db96fb4a68dcc09092a4019ef5d71814948683 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 20 Sep 2024 09:21:49 +0200 Subject: [PATCH 19/27] test: more coverage --- crates/pop-cli/src/cli.rs | 18 +-- crates/pop-cli/src/commands/call/contract.rs | 120 ++++++++++++++----- crates/pop-contracts/src/init_tests.rs | 6 +- 3 files changed, 101 insertions(+), 43 deletions(-) diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 29598c7e..968c1a4f 100644 --- a/crates/pop-cli/src/cli.rs +++ b/crates/pop-cli/src/cli.rs @@ -223,7 +223,7 @@ pub(crate) mod tests { /// Mock Cli with optional expectations #[derive(Default)] pub(crate) struct MockCli { - confirm_expectation: Option<(String, bool)>, + confirm_expectation: Vec<(String, bool)>, info_expectations: Vec, input_expectations: Vec<(String, String)>, intro_expectation: Option, @@ -243,7 +243,7 @@ pub(crate) mod tests { } pub(crate) fn expect_confirm(mut self, prompt: impl Display, confirm: bool) -> Self { - self.confirm_expectation = Some((prompt.to_string(), confirm)); + self.confirm_expectation.push((prompt.to_string(), confirm)); self } @@ -306,8 +306,8 @@ pub(crate) mod tests { } pub(crate) fn verify(self) -> anyhow::Result<()> { - if let Some((expectation, _)) = self.confirm_expectation { - panic!("`{expectation}` confirm expectation not satisfied") + if !self.confirm_expectation.is_empty() { + panic!("`{:?}` confirm expectations not satisfied", self.confirm_expectation) } if !self.info_expectations.is_empty() { panic!("`{}` info log expectations not satisfied", self.info_expectations.join(",")) @@ -349,7 +349,7 @@ pub(crate) mod tests { impl Cli for MockCli { fn confirm(&mut self, prompt: impl Display) -> impl Confirm { let prompt = prompt.to_string(); - if let Some((expectation, confirm)) = self.confirm_expectation.take() { + if let Some((expectation, confirm)) = self.confirm_expectation.pop() { assert_eq!(expectation, prompt, "prompt does not satisfy expectation"); return MockConfirm { confirm }; } @@ -454,13 +454,13 @@ pub(crate) mod tests { } impl Confirm for MockConfirm { + fn initial_value(mut self, _initial_value: bool) -> Self { + self.confirm = self.confirm; // Ignore initial value and always return mock value + self + } fn interact(&mut self) -> Result { Ok(self.confirm) } - fn initial_value(mut self, initial_value: bool) -> Self { - self.confirm = initial_value; - self - } } /// Mock input prompt diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index a3a0f240..897e41ab 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -59,23 +59,23 @@ impl CallContractCommand { let call_config: CallContractCommand = match self.set_up_call_config(&mut cli::Cli).await { Ok(call_config) => call_config, Err(e) => { - display_message(&format!("{}", e.to_string()), false, &mut cli::Cli)?; + display_message(&e.to_string(), false, &mut cli::Cli)?; return Ok(()); }, }; match execute_call(call_config, self.contract.is_none(), &mut cli::Cli).await { Ok(_) => Ok(()), Err(e) => { - display_message(&format!("{}", e.to_string()), false, &mut cli::Cli)?; + display_message(&e.to_string(), false, &mut cli::Cli)?; Ok(()) }, } } fn display(&self) -> String { - let mut full_message = format!("pop call contract"); + let mut full_message = "pop call contract".to_string(); if let Some(path) = &self.path { - full_message.push_str(&format!(" --path {}", path.display().to_string())); + full_message.push_str(&format!(" --path {}", path.display())); } if let Some(contract) = &self.contract { full_message.push_str(&format!(" --contract {}", contract)); @@ -326,14 +326,12 @@ async fn execute_call( cli.info(format!("Result: {}", call_dry_run_result))?; cli.warning("Your call has not been executed.")?; } else { - let weight_limit; - if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { - weight_limit = - Weight::from_parts(call_config.gas_limit.unwrap(), call_config.proof_size.unwrap()); + let weight_limit = if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { + Weight::from_parts(call_config.gas_limit.unwrap(), call_config.proof_size.unwrap()) } else { let spinner = cliclack::spinner(); spinner.start("Doing a dry run to estimate the gas..."); - weight_limit = match dry_run_gas_estimate_call(&call_exec).await { + match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { cli.info(format!("Gas limit: {:?}", w))?; w @@ -342,8 +340,8 @@ async fn execute_call( spinner.error(format!("{e}")); return Err(anyhow!("Call failed.")); }, - }; - } + } + }; let spinner = cliclack::spinner(); spinner.start("Calling the contract..."); @@ -375,7 +373,7 @@ async fn execute_call( } else { display_message("Call completed successfully!", true, cli)?; } - return Ok(()); + Ok(()) } fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli) -> Result<()> { @@ -396,7 +394,7 @@ mod tests { use url::Url; #[tokio::test] - async fn call_contract_query_works() -> Result<()> { + async fn execute_query_works() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; let mut current_dir = env::current_dir().expect("Failed to get current directory"); current_dir.pop(); @@ -405,18 +403,8 @@ mod tests { current_dir.join("pop-contracts/tests/files/testing.contract"), current_dir.join("pop-contracts/tests/files/testing.json"), )?; - - let mut cli = MockCli::new() - .expect_intro(&"Call a contract") - .expect_warning("Your call has not been executed.") - .expect_confirm( - "Do you want to do another call using the existing smart contract?", - false, - ) - .expect_outro("Call completed successfully!"); - // Contract deployed on Pop Network testnet, test get - let config_call = CallContractCommand { + CallContractCommand { path: Some(temp_dir.path().join("testing")), contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), message: Some("get".to_string()), @@ -429,12 +417,9 @@ mod tests { dry_run: false, execute: false, } - .set_up_call_config(&mut cli) + .execute() .await?; - // Test the query. With true, it will prompt for another call. - execute_call(config_call, true, &mut cli).await?; - - cli.verify() + Ok(()) } #[tokio::test] @@ -478,6 +463,69 @@ mod tests { cli.verify() } + #[tokio::test] + async fn call_contract_query_duplicate_call_works() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + 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 items = vec![ + ("flip\n".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get\n".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip\n".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) + ]; + let mut cli = MockCli::new() + .expect_intro(&"Call a contract") + .expect_warning("Your call has not been executed.") + .expect_confirm( + "Do you want to do another call using the existing smart contract?", + false, + ) + .expect_confirm( + "Do you want to do another call using the existing smart contract?", + true, + ) + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 1, // "get" message + ) + .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(), + )) + .expect_warning("Your call has not been executed.") + .expect_outro("Call completed successfully!"); + + // Contract deployed on Pop Network testnet, test get + let config_call = CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: Some("get".to_string()), + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + } + .set_up_call_config(&mut cli) + .await?; + // Test the query. With true, it will prompt for another call. + execute_call(config_call, true, &mut cli).await?; + + cli.verify() + } + // This test only covers the interactive portion of the call contract command, without actually // calling the contract. #[tokio::test] @@ -616,7 +664,17 @@ mod tests { } #[tokio::test] - async fn call_contract_messages_fails_no_message() -> Result<()> { + async fn guide_user_to_call_contract_fails_not_build() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + let mut cli = MockCli::new(); + assert!( + matches!(guide_user_to_call_contract(Some(temp_dir.path().join("testing")), None, None, &mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")) + ); + cli.verify() + } + + #[tokio::test] + async fn call_contract_fails_no_message() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; let mut current_dir = env::current_dir().expect("Failed to get current directory"); current_dir.pop(); @@ -652,7 +710,7 @@ mod tests { } #[test] - fn test_display_message() -> Result<()> { + fn display_message_works() -> Result<()> { let mut cli = MockCli::new().expect_outro(&"Call completed successfully!"); display_message("Call completed successfully!", true, &mut cli)?; cli.verify()?; diff --git a/crates/pop-contracts/src/init_tests.rs b/crates/pop-contracts/src/init_tests.rs index f1c24c7b..0541220f 100644 --- a/crates/pop-contracts/src/init_tests.rs +++ b/crates/pop-contracts/src/init_tests.rs @@ -21,9 +21,9 @@ pub fn mock_build_process( // Create a target directory let target_contract_dir = temp_contract_dir.join("target"); fs::create_dir(&target_contract_dir)?; - fs::create_dir(&target_contract_dir.join("ink"))?; + fs::create_dir(target_contract_dir.join("ink"))?; // Copy a mocked testing.contract and testing.json files inside the target directory - fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?; - fs::copy(metadata_file, &target_contract_dir.join("ink/testing.json"))?; + fs::copy(contract_file, target_contract_dir.join("ink/testing.contract"))?; + fs::copy(metadata_file, target_contract_dir.join("ink/testing.json"))?; Ok(()) } From 8df2058910af6b48d9f9c2c09a125cb0c019830e Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Fri, 20 Sep 2024 10:21:06 +0200 Subject: [PATCH 20/27] fix: unit test --- crates/pop-cli/src/commands/call/contract.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 897e41ab..2219c88e 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -612,6 +612,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 From 96ed5f173205d3ca3cfad24d5b14f95b5ab46e87 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 5 Nov 2024 11:09:26 +0100 Subject: [PATCH 21/27] feat: dev mode to skip certain user prompts --- crates/pop-cli/src/commands/call/contract.rs | 93 ++++++++++++++++++-- 1 file changed, 87 insertions(+), 6 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 2219c88e..06c96d5b 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -52,6 +52,10 @@ pub struct CallContractCommand { /// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction. #[clap(long, conflicts_with = "execute")] dry_run: bool, + /// Enables developer mode, bypassing certain user prompts for faster testing. + /// Recommended for testing and local development only. + #[clap(name = "dev", long, short, default_value = "false")] + dev_mode: bool, } impl CallContractCommand { /// Executes the command. @@ -112,7 +116,7 @@ impl CallContractCommand { ) -> anyhow::Result { cli.intro("Call a contract")?; let call_config = if self.contract.is_none() { - match guide_user_to_call_contract(None, None, None, cli).await { + match guide_user_to_call_contract(None, None, None, self.dev_mode, cli).await { Ok(config) => config, Err(e) => { return Err(anyhow!(format!("{}", e.to_string()))); @@ -130,6 +134,7 @@ async fn guide_user_to_call_contract( contract_path: Option, url: Option, contract_address: Option, + dev_mode: bool, cli: &mut impl cli::traits::Cli, ) -> anyhow::Result { let contract_path: PathBuf = match contract_path { @@ -217,7 +222,7 @@ async fn guide_user_to_call_contract( } let mut gas_limit: Option = None; let mut proof_size: Option = None; - if message.mutates { + if message.mutates && !dev_mode { // Prompt for gas limit and proof_size of the call. let gas_limit_input: String = cli .input("Enter the gas limit:") @@ -243,7 +248,7 @@ async fn guide_user_to_call_contract( .interact()?; let mut is_call_confirmed: bool = true; - if message.mutates { + if message.mutates && !dev_mode { is_call_confirmed = cli .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) @@ -261,6 +266,7 @@ async fn guide_user_to_call_contract( suri, execute: if is_call_confirmed { message.mutates } else { false }, dry_run: !is_call_confirmed, + dev_mode, }; cli.info(call_command.display())?; Ok(call_command) @@ -363,6 +369,7 @@ async fn execute_call( call_config.path, Some(call_config.url), call_config.contract, + call_config.dev_mode, cli, ) .await?; @@ -416,6 +423,7 @@ mod tests { suri: "//Alice".to_string(), dry_run: false, execute: false, + dev_mode: false, } .execute() .await?; @@ -450,6 +458,7 @@ mod tests { suri: "//Alice".to_string(), dry_run: true, execute: false, + dev_mode: false, } .set_up_call_config(&mut cli) .await?; @@ -517,6 +526,7 @@ mod tests { suri: "//Alice".to_string(), dry_run: false, execute: false, + dev_mode: false, } .set_up_call_config(&mut cli) .await?; @@ -570,7 +580,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(None, None, None, &mut cli).await?; + let call_config = guide_user_to_call_contract(None, None, None, false, &mut cli).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -641,7 +651,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(None, None, None, &mut cli).await?; + let call_config = guide_user_to_call_contract(None, None, None, false, &mut cli).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -664,12 +674,82 @@ mod tests { cli.verify() } + // This test only covers the interactive portion of the call contract command, without actually + // calling the contract. + #[tokio::test] + async fn guide_user_to_call_contract_in_dev_mode_works() -> Result<()> { + let temp_dir = generate_smart_contract_test_environment()?; + 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 items = vec![ + ("flip\n".into(), " A message that can be called on instantiated contracts. This one flips the value of the stored `bool` from `true` to `false` and vice versa.".into()), + ("get\n".into(), " Simply returns the current value of our `bool`.".into()), + ("specific_flip\n".into(), " A message for testing, flips the value of the stored `bool` with `new_value` and is payable".into()) + ]; + // 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: new_value", "true".into()) // Args for specific_flip + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 2, // "specific_flip" message + ) + .expect_input( + "Paste the on-chain contract address:", + "15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".into(), + ) + .expect_input( + "Where is your contract deployed?", + "wss://rpc1.paseo.popnetwork.xyz".into(), + ) + .expect_input( + "Where is your project located?", + temp_dir.path().join("testing").display().to_string(), + ).expect_info(format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args true --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); + + let call_config = guide_user_to_call_contract(None, None, None, true, &mut cli).await?; + assert_eq!( + call_config.contract, + Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) + ); + assert_eq!(call_config.message, Some("specific_flip".to_string())); + assert_eq!(call_config.args.len(), 1); + assert_eq!(call_config.args[0], "true".to_string()); + assert_eq!(call_config.value, "50".to_string()); + assert_eq!(call_config.gas_limit, None); + assert_eq!(call_config.proof_size, None); + assert_eq!(call_config.url.to_string(), "wss://rpc1.paseo.popnetwork.xyz/"); + assert_eq!(call_config.suri, "//Alice"); + assert!(call_config.execute); + assert!(!call_config.dry_run); + assert!(call_config.dev_mode); + assert_eq!(call_config.display(), format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args true --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); + + cli.verify() + } + #[tokio::test] async fn guide_user_to_call_contract_fails_not_build() -> Result<()> { let temp_dir = generate_smart_contract_test_environment()?; let mut cli = MockCli::new(); assert!( - matches!(guide_user_to_call_contract(Some(temp_dir.path().join("testing")), None, None, &mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")) + matches!(guide_user_to_call_contract(Some(temp_dir.path().join("testing")), None, None, false, &mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")) ); cli.verify() } @@ -699,6 +779,7 @@ mod tests { suri: "//Alice".to_string(), dry_run: false, execute: false, + dev_mode: false, } .set_up_call_config(&mut cli) .await?; From 0e5bf32f1111a829c23fb4f9befe28607c09be3e Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 5 Nov 2024 15:39:49 +0100 Subject: [PATCH 22/27] refactor: test functions, renaming and fix clippy --- crates/pop-cli/src/commands/call/contract.rs | 18 ++++----- crates/pop-contracts/src/call/metadata.rs | 4 +- crates/pop-contracts/src/call/mod.rs | 14 +++---- crates/pop-contracts/src/init_tests.rs | 29 --------------- crates/pop-contracts/src/lib.rs | 4 +- crates/pop-contracts/src/testing.rs | 39 ++++++++++++++++++++ crates/pop-contracts/src/up.rs | 16 ++++---- 7 files changed, 67 insertions(+), 57 deletions(-) delete mode 100644 crates/pop-contracts/src/init_tests.rs create mode 100644 crates/pop-contracts/src/testing.rs diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 06c96d5b..85edaf17 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -396,13 +396,13 @@ fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli mod tests { use super::*; use crate::cli::MockCli; - use pop_contracts::{generate_smart_contract_test_environment, mock_build_process}; + use pop_contracts::{mock_build_process, new_environment}; use std::env; use url::Url; #[tokio::test] async fn execute_query_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -432,7 +432,7 @@ mod tests { #[tokio::test] async fn call_contract_dry_run_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -474,7 +474,7 @@ mod tests { #[tokio::test] async fn call_contract_query_duplicate_call_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -540,7 +540,7 @@ mod tests { // calling the contract. #[tokio::test] async fn guide_user_to_query_contract_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -606,7 +606,7 @@ mod tests { // calling the contract. #[tokio::test] async fn guide_user_to_call_contract_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -678,7 +678,7 @@ mod tests { // calling the contract. #[tokio::test] async fn guide_user_to_call_contract_in_dev_mode_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( @@ -746,7 +746,7 @@ mod tests { #[tokio::test] async fn guide_user_to_call_contract_fails_not_build() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let mut cli = MockCli::new(); assert!( matches!(guide_user_to_call_contract(Some(temp_dir.path().join("testing")), None, None, false, &mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")) @@ -756,7 +756,7 @@ mod tests { #[tokio::test] async fn call_contract_fails_no_message() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + 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( diff --git a/crates/pop-contracts/src/call/metadata.rs b/crates/pop-contracts/src/call/metadata.rs index 82e7589b..605d0620 100644 --- a/crates/pop-contracts/src/call/metadata.rs +++ b/crates/pop-contracts/src/call/metadata.rs @@ -73,12 +73,12 @@ mod tests { use std::env; use super::*; - use crate::{generate_smart_contract_test_environment, mock_build_process}; + use crate::{mock_build_process, new_environment}; use anyhow::Result; #[test] fn get_messages_work() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), diff --git a/crates/pop-contracts/src/call/mod.rs b/crates/pop-contracts/src/call/mod.rs index 3b97220c..2c363301 100644 --- a/crates/pop-contracts/src/call/mod.rs +++ b/crates/pop-contracts/src/call/mod.rs @@ -161,8 +161,8 @@ mod tests { use super::*; use crate::{ contracts_node_generator, dry_run_gas_estimate_instantiate, errors::Error, - generate_smart_contract_test_environment, instantiate_smart_contract, mock_build_process, - run_contracts_node, set_up_deployment, UpOpts, + instantiate_smart_contract, mock_build_process, new_environment, run_contracts_node, + set_up_deployment, UpOpts, }; use anyhow::Result; use sp_core::Bytes; @@ -172,7 +172,7 @@ mod tests { #[tokio::test] async fn test_set_up_call() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -199,7 +199,7 @@ mod tests { #[tokio::test] async fn test_set_up_call_error_contract_not_build() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let call_opts = CallOpts { path: Some(temp_dir.path().join("testing")), contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(), @@ -243,7 +243,7 @@ mod tests { #[tokio::test] async fn test_dry_run_call_error_contract_not_deployed() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -270,7 +270,7 @@ mod tests { #[tokio::test] async fn test_dry_run_estimate_call_error_contract_not_deployed() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -301,7 +301,7 @@ mod tests { #[tokio::test] async fn call_works() -> Result<()> { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), diff --git a/crates/pop-contracts/src/init_tests.rs b/crates/pop-contracts/src/init_tests.rs deleted file mode 100644 index 0541220f..00000000 --- a/crates/pop-contracts/src/init_tests.rs +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -use crate::{create_smart_contract, Contract}; -use anyhow::Result; -use std::{fs, path::PathBuf}; - -pub fn generate_smart_contract_test_environment() -> Result { - let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); - let temp_contract_dir = temp_dir.path().join("testing"); - fs::create_dir(&temp_contract_dir)?; - create_smart_contract("testing", temp_contract_dir.as_path(), &Contract::Standard)?; - Ok(temp_dir) -} - -// Function that mocks the build process generating the contract artifacts. -pub fn mock_build_process( - temp_contract_dir: PathBuf, - contract_file: PathBuf, - metadata_file: PathBuf, -) -> Result<()> { - // Create a target directory - let target_contract_dir = temp_contract_dir.join("target"); - fs::create_dir(&target_contract_dir)?; - fs::create_dir(target_contract_dir.join("ink"))?; - // Copy a mocked testing.contract and testing.json files inside the target directory - fs::copy(contract_file, target_contract_dir.join("ink/testing.contract"))?; - fs::copy(metadata_file, target_contract_dir.join("ink/testing.json"))?; - Ok(()) -} diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 4fd24bd4..a3ff0175 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -4,11 +4,11 @@ mod build; mod call; mod errors; -mod init_tests; mod new; mod node; mod templates; mod test; +mod testing; mod up; mod utils; @@ -18,11 +18,11 @@ pub use call::{ metadata::{get_messages, Message}, set_up_call, CallOpts, }; -pub use init_tests::{generate_smart_contract_test_environment, mock_build_process}; pub use new::{create_smart_contract, is_valid_contract_name}; pub use node::{contracts_node_generator, is_chain_alive, run_contracts_node}; pub use templates::{Contract, ContractType}; pub use test::{test_e2e_smart_contract, test_smart_contract}; +pub use testing::{mock_build_process, new_environment}; pub use up::{ dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, set_up_deployment, set_up_upload, upload_smart_contract, UpOpts, diff --git a/crates/pop-contracts/src/testing.rs b/crates/pop-contracts/src/testing.rs new file mode 100644 index 00000000..6a8abfcd --- /dev/null +++ b/crates/pop-contracts/src/testing.rs @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::{create_smart_contract, Contract}; +use anyhow::Result; +use std::{ + fs::{copy, create_dir}, + path::Path, +}; + +/// Generates a smart contract test environment. +/// +/// * `name` - The name of the contract to be created. +pub fn new_environment(name: &str) -> Result { + let temp_dir = tempfile::tempdir().expect("Could not create temp dir"); + let temp_contract_dir = temp_dir.path().join(name); + create_dir(&temp_contract_dir)?; + create_smart_contract(name, temp_contract_dir.as_path(), &Contract::Standard)?; + Ok(temp_dir) +} + +/// Mocks the build process by generating contract artifacts in a specified temporary directory. +/// +/// * `temp_contract_dir` - The root directory where the `target` folder and artifacts will be +/// created. +/// * `contract_file` - The path to the mocked contract file to be copied. +/// * `metadata_file` - The path to the mocked metadata file to be copied. +pub fn mock_build_process

(temp_contract_dir: P, contract_file: P, metadata_file: P) -> Result<()> +where + P: AsRef, +{ + // Create a target directory + let target_contract_dir = temp_contract_dir.as_ref().join("target"); + create_dir(&target_contract_dir)?; + create_dir(target_contract_dir.join("ink"))?; + // Copy a mocked testing.contract and testing.json files inside the target directory + copy(contract_file, target_contract_dir.join("ink/testing.contract"))?; + copy(metadata_file, target_contract_dir.join("ink/testing.json"))?; + Ok(()) +} diff --git a/crates/pop-contracts/src/up.rs b/crates/pop-contracts/src/up.rs index b0c0edc5..6d2f0ec0 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -204,8 +204,8 @@ pub async fn upload_smart_contract( mod tests { use super::*; use crate::{ - contracts_node_generator, errors::Error, generate_smart_contract_test_environment, - mock_build_process, run_contracts_node, + contracts_node_generator, errors::Error, mock_build_process, new_environment, + run_contracts_node, }; use anyhow::Result; use std::{env, process::Command}; @@ -215,7 +215,7 @@ mod tests { #[tokio::test] async fn set_up_deployment_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -239,7 +239,7 @@ mod tests { #[tokio::test] async fn set_up_upload_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -263,7 +263,7 @@ mod tests { #[tokio::test] async fn dry_run_gas_estimate_instantiate_works() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -290,7 +290,7 @@ mod tests { #[tokio::test] async fn dry_run_gas_estimate_instantiate_throw_custom_error() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -318,7 +318,7 @@ mod tests { #[tokio::test] async fn dry_run_upload_throw_custom_error() -> Result<()> { - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), @@ -346,7 +346,7 @@ mod tests { #[tokio::test] async fn instantiate_and_upload() -> Result<()> { const LOCALHOST_URL: &str = "ws://127.0.0.1:9944"; - let temp_dir = generate_smart_contract_test_environment()?; + let temp_dir = new_environment("testing")?; let current_dir = env::current_dir().expect("Failed to get current directory"); mock_build_process( temp_dir.path().join("testing"), From 29848b42b6d8d9c93a9e3f1929291b83a3762b1f Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 5 Nov 2024 16:30:58 +0100 Subject: [PATCH 23/27] refactor: improve devex of pop call contract --- crates/pop-cli/src/commands/call/contract.rs | 446 +++++++++---------- 1 file changed, 221 insertions(+), 225 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 85edaf17..a7dbbf1c 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -10,6 +10,10 @@ use pop_contracts::{ use sp_weights::Weight; use std::path::PathBuf; +const DEFAULT_URL: &str = "ws://localhost:9944"; +const DEFAULT_URI: &str = "//Alice"; +const DEFAULT_PAYABLE_VALUE: &str = "0"; + #[derive(Args, Clone)] pub struct CallContractCommand { /// Path to the contract build directory. @@ -25,7 +29,7 @@ pub struct CallContractCommand { #[clap(long, num_args = 0..)] args: Vec, /// The value to be transferred as part of the call. - #[clap(name = "value", long, default_value = "0")] + #[clap(name = "value", long, default_value = DEFAULT_PAYABLE_VALUE)] 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 @@ -37,14 +41,14 @@ pub struct CallContractCommand { #[clap(long)] proof_size: Option, /// Websocket endpoint of a node. - #[clap(name = "url", long, value_parser, default_value = "ws://localhost:9944")] + #[clap(name = "url", long, value_parser, default_value = DEFAULT_URL)] url: url::Url, /// Secret key URI for the account calling the contract. /// /// e.g. /// - for a dev account "//Alice" /// - with a password "//Alice///SECRET_PASSWORD" - #[clap(name = "suri", long, short, default_value = "//Alice")] + #[clap(name = "suri", long, short, default_value = DEFAULT_URI)] suri: String, /// Submit an extrinsic for on-chain execution. #[clap(short('x'), long)] @@ -67,7 +71,7 @@ impl CallContractCommand { return Ok(()); }, }; - match execute_call(call_config, self.contract.is_none(), &mut cli::Cli).await { + match call_config.execute_call(self.message.is_none(), &mut cli::Cli).await { Ok(_) => Ok(()), Err(e) => { display_message(&e.to_string(), false, &mut cli::Cli)?; @@ -115,8 +119,8 @@ impl CallContractCommand { cli: &mut impl cli::traits::Cli, ) -> anyhow::Result { cli.intro("Call a contract")?; - let call_config = if self.contract.is_none() { - match guide_user_to_call_contract(None, None, None, self.dev_mode, cli).await { + let call_config = if self.message.is_none() { + match self.guide_user_to_call_contract(cli).await { Ok(config) => config, Err(e) => { return Err(anyhow!(format!("{}", e.to_string()))); @@ -127,41 +131,35 @@ impl CallContractCommand { }; Ok(call_config) } -} -/// Guide the user to call the contract. -async fn guide_user_to_call_contract( - contract_path: Option, - url: Option, - contract_address: Option, - dev_mode: bool, - cli: &mut impl cli::traits::Cli, -) -> anyhow::Result { - let contract_path: PathBuf = match contract_path { - Some(path) => path, - None => { - // Prompt for path. - let input_path: String = cli - .input("Where is your project located?") - .placeholder("./") - .default_input("./") - .interact()?; - PathBuf::from(input_path) - }, - }; - // Parse the contract metadata provided. If there error, do not prompt for more. - let messages = match get_messages(&contract_path) { - Ok(messages) => messages, - Err(e) => { - return Err(anyhow!(format!( - "Unable to fetch contract metadata: {}", - e.to_string().replace("Anyhow error: ", "") - ))); - }, - }; - let url: url::Url = match url { - Some(url) => url, - None => { + /// Guide the user to call the contract. + async fn guide_user_to_call_contract( + &self, + cli: &mut impl cli::traits::Cli, + ) -> anyhow::Result { + let contract_path: PathBuf = match &self.path { + Some(path) => path.to_path_buf(), + None => { + // Prompt for path. + let input_path: String = cli + .input("Where is your project located?") + .placeholder("./") + .default_input("./") + .interact()?; + PathBuf::from(input_path) + }, + }; + // Parse the contract metadata provided. If there is an error, do not prompt for more. + let messages = match get_messages(&contract_path) { + Ok(messages) => messages, + Err(e) => { + return Err(anyhow!(format!( + "Unable to fetch contract metadata: {}", + e.to_string().replace("Anyhow error: ", "") + ))); + }, + }; + let url = if self.url.as_str() == DEFAULT_URL { // Prompt for url. let url: String = cli .input("Where is your contract deployed?") @@ -169,218 +167,216 @@ async fn guide_user_to_call_contract( .default_input("ws://localhost:9944") .interact()?; url::Url::parse(&url)? - }, - }; - let contract_address: String = match contract_address { - Some(contract_address) => contract_address, - None => { - // Prompt for contract address. - let contract_address: String = cli - .input("Paste the on-chain contract address:") - .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") - .validate(|input: &String| match parse_account(input) { + } else { + self.url.clone() + }; + let contract_address: String = match &self.contract { + Some(contract_address) => contract_address.to_string(), + None => { + // Prompt for contract address. + let contract_address: String = cli + .input("Paste the on-chain contract address:") + .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .validate(|input: &String| match parse_account(input) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid address."), + }) + .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .interact()?; + contract_address + }, + }; + + let message = { + let mut prompt = cli.select("Select the message to call:"); + for select_message in messages { + prompt = prompt.item( + select_message.clone(), + format!("{}\n", &select_message.label), + &select_message.docs, + ); + } + prompt.interact()? + }; + + let mut contract_args = Vec::new(); + for arg in &message.args { + contract_args.push( + cli.input(format!("Enter the value for the parameter: {}", arg.label)) + .placeholder(&format!("Type required: {}", &arg.type_name)) + .interact()?, + ); + } + let mut value = "0".to_string(); + if message.payable { + value = cli + .input("Value to transfer to the call:") + .placeholder("0") + .default_input("0") + .validate(|input: &String| match input.parse::() { Ok(_) => Ok(()), - Err(_) => Err("Invalid address."), + Err(_) => Err("Invalid value."), }) - .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") .interact()?; - contract_address - }, - }; - - let message = { - let mut prompt = cli.select("Select the message to call:"); - for select_message in messages { - prompt = prompt.item( - select_message.clone(), - format!("{}\n", &select_message.label), - &select_message.docs, - ); } - prompt.interact()? - }; - - let mut contract_args = Vec::new(); - for arg in &message.args { - contract_args.push( - cli.input(format!("Enter the value for the parameter: {}", arg.label)) - .placeholder(&format!("Type required: {}", &arg.type_name)) - .interact()?, - ); - } - let mut value = "0".to_string(); - if message.payable { - value = cli - .input("Value to transfer to the call:") - .placeholder("0") - .default_input("0") - .validate(|input: &String| match input.parse::() { - Ok(_) => Ok(()), - Err(_) => Err("Invalid value."), - }) - .interact()?; - } - let mut gas_limit: Option = None; - let mut proof_size: Option = None; - if message.mutates && !dev_mode { - // Prompt for gas limit and proof_size of the call. - let gas_limit_input: String = cli - .input("Enter the gas limit:") - .required(false) - .default_input("") - .placeholder("If left blank, an estimation will be used") - .interact()?; - gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. - let proof_size_input: String = cli - .input("Enter the proof size limit:") - .required(false) - .placeholder("If left blank, an estimation will be used") - .default_input("") - .interact()?; - proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. - } + let mut gas_limit: Option = None; + let mut proof_size: Option = None; + if message.mutates && !self.dev_mode { + // Prompt for gas limit and proof_size of the call. + let gas_limit_input: String = cli + .input("Enter the gas limit:") + .required(false) + .default_input("") + .placeholder("If left blank, an estimation will be used") + .interact()?; + gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + let proof_size_input: String = cli + .input("Enter the proof size limit:") + .required(false) + .placeholder("If left blank, an estimation will be used") + .default_input("") + .interact()?; + proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. + } - // Who is calling the contract. - let suri: String = cli - .input("Signer calling the contract:") - .placeholder("//Alice") - .default_input("//Alice") - .interact()?; - - let mut is_call_confirmed: bool = true; - if message.mutates && !dev_mode { - is_call_confirmed = cli - .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") - .initial_value(true) - .interact()?; + // Who is calling the contract. + let suri = if self.suri == DEFAULT_URI { + // Prompt for uri. + cli.input("Signer calling the contract:") + .placeholder("//Alice") + .default_input("//Alice") + .interact()? + } else { + self.suri.clone() + }; + + let mut is_call_confirmed: bool = true; + if message.mutates && !self.dev_mode { + is_call_confirmed = cli + .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") + .initial_value(true) + .interact()?; + } + let call_command = CallContractCommand { + path: Some(contract_path), + contract: Some(contract_address), + message: Some(message.label.clone()), + args: contract_args, + value, + gas_limit, + proof_size, + url, + suri, + execute: if is_call_confirmed { message.mutates } else { false }, + dry_run: !is_call_confirmed, + dev_mode: self.dev_mode, + }; + cli.info(call_command.display())?; + Ok(call_command) } - let call_command = CallContractCommand { - path: Some(contract_path), - contract: Some(contract_address), - message: Some(message.label.clone()), - args: contract_args, - value, - gas_limit, - proof_size, - url, - suri, - execute: if is_call_confirmed { message.mutates } else { false }, - dry_run: !is_call_confirmed, - dev_mode, - }; - cli.info(call_command.display())?; - Ok(call_command) -} -/// Executes the call. -async fn execute_call( - call_config: CallContractCommand, - prompt_to_repeat_call: bool, - cli: &mut impl cli::traits::Cli, -) -> anyhow::Result<()> { - let contract = call_config - .contract - .clone() - .expect("contract can not be none as fallback above is interactive input; qed"); - let message = match call_config.message { - Some(m) => m, - None => { - return Err(anyhow!("Please specify the message to call.")); - }, - }; - - let call_exec = match set_up_call(CallOpts { - path: call_config.path.clone(), - contract, - message, - args: call_config.args, - value: call_config.value, - gas_limit: call_config.gas_limit, - proof_size: call_config.proof_size, - url: call_config.url.clone(), - suri: call_config.suri, - execute: call_config.execute, - }) - .await - { - Ok(call_exec) => call_exec, - Err(e) => { - return Err(anyhow!(format!("{}", e.root_cause().to_string()))); - }, - }; - - if call_config.dry_run { - let spinner = cliclack::spinner(); - spinner.start("Doing a dry run to estimate the gas..."); - match dry_run_gas_estimate_call(&call_exec).await { - Ok(w) => { - cli.info(format!("Gas limit: {:?}", w))?; - cli.warning("Your call has not been executed.")?; + /// Executes the call. + async fn execute_call( + &self, + prompt_to_repeat_call: bool, + cli: &mut impl cli::traits::Cli, + ) -> anyhow::Result<()> { + let message = self + .message + .clone() + .expect("message can not be none as fallback above is interactive input; qed"); + let contract = match &self.contract { + Some(contract) => contract.to_string(), + None => { + return Err(anyhow!("Please specify the contract address.")); }, + }; + let call_exec = match set_up_call(CallOpts { + path: self.path.clone(), + contract, + message, + args: self.args.clone(), + value: self.value.clone(), + gas_limit: self.gas_limit, + proof_size: self.proof_size, + url: self.url.clone(), + suri: self.suri.clone(), + execute: self.execute, + }) + .await + { + Ok(call_exec) => call_exec, Err(e) => { - spinner.error(format!("{e}")); - display_message("Call failed.", false, cli)?; + return Err(anyhow!(format!("{}", e.root_cause().to_string()))); }, }; - return Ok(()); - } - if !call_config.execute { - let spinner = cliclack::spinner(); - spinner.start("Calling the contract..."); - let call_dry_run_result = dry_run_call(&call_exec).await?; - cli.info(format!("Result: {}", call_dry_run_result))?; - cli.warning("Your call has not been executed.")?; - } else { - let weight_limit = if call_config.gas_limit.is_some() && call_config.proof_size.is_some() { - Weight::from_parts(call_config.gas_limit.unwrap(), call_config.proof_size.unwrap()) - } else { + if self.dry_run { let spinner = cliclack::spinner(); spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { cli.info(format!("Gas limit: {:?}", w))?; - w + cli.warning("Your call has not been executed.")?; }, Err(e) => { spinner.error(format!("{e}")); - return Err(anyhow!("Call failed.")); + display_message("Call failed.", false, cli)?; }, - } - }; - let spinner = cliclack::spinner(); - spinner.start("Calling the contract..."); + }; + return Ok(()); + } - let call_result = call_smart_contract(call_exec, weight_limit, &call_config.url) - .await - .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; + if !self.execute { + let spinner = cliclack::spinner(); + spinner.start("Calling the contract..."); + let call_dry_run_result = dry_run_call(&call_exec).await?; + cli.info(format!("Result: {}", call_dry_run_result))?; + cli.warning("Your call has not been executed.")?; + } else { + let weight_limit = if self.gas_limit.is_some() && self.proof_size.is_some() { + Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()) + } else { + let spinner = cliclack::spinner(); + spinner.start("Doing a dry run to estimate the gas..."); + match dry_run_gas_estimate_call(&call_exec).await { + Ok(w) => { + cli.info(format!("Gas limit: {:?}", w))?; + w + }, + Err(e) => { + spinner.error(format!("{e}")); + return Err(anyhow!("Call failed.")); + }, + } + }; + let spinner = cliclack::spinner(); + spinner.start("Calling the contract..."); - cli.info(call_result)?; - } - if prompt_to_repeat_call { - let another_call: bool = cli - .confirm("Do you want to do another call using the existing smart contract?") - .initial_value(false) - .interact()?; - if another_call { - // Remove only the prompt asking for another call. - console::Term::stderr().clear_last_lines(2)?; - let new_call_config = guide_user_to_call_contract( - call_config.path, - Some(call_config.url), - call_config.contract, - call_config.dev_mode, - cli, - ) - .await?; - Box::pin(execute_call(new_call_config, prompt_to_repeat_call, cli)).await?; + let call_result = call_smart_contract(call_exec, weight_limit, &self.url) + .await + .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; + + cli.info(call_result)?; + } + if prompt_to_repeat_call { + let another_call: bool = cli + .confirm("Do you want to do another call using the existing smart contract?") + .initial_value(false) + .interact()?; + if another_call { + // Remove only the prompt asking for another call. + console::Term::stderr().clear_last_lines(2)?; + let new_call_config = self.guide_user_to_call_contract(cli).await?; + Box::pin(new_call_config.execute_call(prompt_to_repeat_call, cli)).await?; + } else { + display_message("Call completed successfully!", true, cli)?; + } } else { display_message("Call completed successfully!", true, cli)?; } - } else { - display_message("Call completed successfully!", true, cli)?; + Ok(()) } - Ok(()) } fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli) -> Result<()> { From 5f479fa15bfb5835bffaf2f693766990d66e27df Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Tue, 5 Nov 2024 17:30:57 +0100 Subject: [PATCH 24/27] test: adjust tests to refactor --- crates/pop-cli/src/commands/call/contract.rs | 183 +++++++++++++------ 1 file changed, 125 insertions(+), 58 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index a7dbbf1c..3a8ccf34 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -10,7 +10,7 @@ use pop_contracts::{ use sp_weights::Weight; use std::path::PathBuf; -const DEFAULT_URL: &str = "ws://localhost:9944"; +const DEFAULT_URL: &str = "ws://localhost:9944/"; const DEFAULT_URI: &str = "//Alice"; const DEFAULT_PAYABLE_VALUE: &str = "0"; @@ -94,7 +94,7 @@ impl CallContractCommand { if !self.args.is_empty() { full_message.push_str(&format!(" --args {}", self.args.join(" "))); } - if self.value != "0" { + if self.value != DEFAULT_PAYABLE_VALUE { full_message.push_str(&format!(" --value {}", self.value)); } if let Some(gas_limit) = self.gas_limit { @@ -119,17 +119,11 @@ impl CallContractCommand { cli: &mut impl cli::traits::Cli, ) -> anyhow::Result { cli.intro("Call a contract")?; - let call_config = if self.message.is_none() { - match self.guide_user_to_call_contract(cli).await { - Ok(config) => config, - Err(e) => { - return Err(anyhow!(format!("{}", e.to_string()))); - }, - } + if self.message.is_none() { + self.guide_user_to_call_contract(cli).await } else { - self.clone() - }; - Ok(call_config) + Ok(self.clone()) + } } /// Guide the user to call the contract. @@ -138,7 +132,7 @@ impl CallContractCommand { cli: &mut impl cli::traits::Cli, ) -> anyhow::Result { let contract_path: PathBuf = match &self.path { - Some(path) => path.to_path_buf(), + Some(path) => path.clone(), None => { // Prompt for path. let input_path: String = cli @@ -171,7 +165,7 @@ impl CallContractCommand { self.url.clone() }; let contract_address: String = match &self.contract { - Some(contract_address) => contract_address.to_string(), + Some(contract_address) => contract_address.clone(), None => { // Prompt for contract address. let contract_address: String = cli @@ -189,9 +183,9 @@ impl CallContractCommand { let message = { let mut prompt = cli.select("Select the message to call:"); - for select_message in messages { + for select_message in &messages { prompt = prompt.item( - select_message.clone(), + select_message, format!("{}\n", &select_message.label), &select_message.docs, ); @@ -250,13 +244,14 @@ impl CallContractCommand { self.suri.clone() }; - let mut is_call_confirmed: bool = true; - if message.mutates && !self.dev_mode { - is_call_confirmed = cli - .confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") + let is_call_confirmed = if message.mutates && !self.dev_mode { + cli.confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) - .interact()?; - } + .interact()? + } else { + true + }; + let call_command = CallContractCommand { path: Some(contract_path), contract: Some(contract_address), @@ -281,10 +276,12 @@ impl CallContractCommand { prompt_to_repeat_call: bool, cli: &mut impl cli::traits::Cli, ) -> anyhow::Result<()> { - let message = self - .message - .clone() - .expect("message can not be none as fallback above is interactive input; qed"); + let message = match &self.message { + Some(message) => message.to_string(), + None => { + return Err(anyhow!("Please specify the message to call.")); + }, + }; let contract = match &self.contract { Some(contract) => contract.to_string(), None => { @@ -360,11 +357,11 @@ impl CallContractCommand { cli.info(call_result)?; } if prompt_to_repeat_call { - let another_call: bool = cli + if cli .confirm("Do you want to do another call using the existing smart contract?") .initial_value(false) - .interact()?; - if another_call { + .interact()? + { // Remove only the prompt asking for another call. console::Term::stderr().clear_last_lines(2)?; let new_call_config = self.guide_user_to_call_contract(cli).await?; @@ -463,7 +460,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run - execute_call(call_config, false, &mut cli).await?; + call_config.execute_call(false, &mut cli).await?; cli.verify() } @@ -510,7 +507,7 @@ mod tests { .expect_outro("Call completed successfully!"); // Contract deployed on Pop Network testnet, test get - let config_call = CallContractCommand { + let call_config = CallContractCommand { path: Some(temp_dir.path().join("testing")), contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), message: Some("get".to_string()), @@ -527,7 +524,7 @@ mod tests { .set_up_call_config(&mut cli) .await?; // Test the query. With true, it will prompt for another call. - execute_call(config_call, true, &mut cli).await?; + call_config.execute_call(true, &mut cli).await?; cli.verify() } @@ -576,7 +573,22 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(None, None, None, false, &mut cli).await?; + let call_config = CallContractCommand { + path: None, + contract: None, + message: None, + args: vec![].to_vec(), + value: DEFAULT_PAYABLE_VALUE.to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse(DEFAULT_URL)?, + suri: DEFAULT_URI.to_string(), + dry_run: false, + execute: false, + dev_mode: false, + } + .guide_user_to_call_contract(&mut cli) + .await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -647,7 +659,22 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(None, None, None, false, &mut cli).await?; + let call_config = CallContractCommand { + path: None, + contract: None, + message: None, + args: vec![].to_vec(), + value: DEFAULT_PAYABLE_VALUE.to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse(DEFAULT_URL)?, + suri: DEFAULT_URI.to_string(), + dry_run: false, + execute: false, + dev_mode: false, + } + .guide_user_to_call_contract(&mut cli) + .await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -716,7 +743,22 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = guide_user_to_call_contract(None, None, None, true, &mut cli).await?; + let call_config = CallContractCommand { + path: None, + contract: None, + message: None, + args: vec![].to_vec(), + value: DEFAULT_PAYABLE_VALUE.to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse(DEFAULT_URL)?, + suri: DEFAULT_URI.to_string(), + dry_run: false, + execute: false, + dev_mode: true, + } + .guide_user_to_call_contract(&mut cli) + .await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -744,14 +786,25 @@ mod tests { async fn guide_user_to_call_contract_fails_not_build() -> Result<()> { let temp_dir = new_environment("testing")?; let mut cli = MockCli::new(); - assert!( - matches!(guide_user_to_call_contract(Some(temp_dir.path().join("testing")), None, None, false, &mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")) - ); + assert!(matches!(CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + dev_mode: false, + }.guide_user_to_call_contract(&mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory."))); cli.verify() } #[tokio::test] - async fn call_contract_fails_no_message() -> Result<()> { + async fn execute_contract_fails_no_message_or_contract() -> Result<()> { let temp_dir = new_environment("testing")?; let mut current_dir = env::current_dir().expect("Failed to get current directory"); current_dir.pop(); @@ -761,29 +814,43 @@ mod tests { current_dir.join("pop-contracts/tests/files/testing.json"), )?; - let mut cli = MockCli::new().expect_intro(&"Call a contract"); - - let call_config = CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - dev_mode: false, - } - .set_up_call_config(&mut cli) - .await?; + let mut cli = MockCli::new(); assert!(matches!( - execute_call(call_config, false, &mut cli).await, + CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + dev_mode: false, + }.execute_call(false, &mut cli).await, anyhow::Result::Err(message) if message.to_string() == "Please specify the message to call." )); + assert!(matches!( + CallContractCommand { + path: Some(temp_dir.path().join("testing")), + contract: None, + message: Some("get".to_string()), + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + dev_mode: false, + }.execute_call(false, &mut cli).await, + anyhow::Result::Err(message) if message.to_string() == "Please specify the contract address." + )); + cli.verify() } From 5be201b233311a8fd509199954d047cdb6607932 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Wed, 6 Nov 2024 11:39:24 +0100 Subject: [PATCH 25/27] chore: reset_for_new_call fields --- crates/pop-cli/src/commands/call/contract.rs | 46 ++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 3a8ccf34..132966e7 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -64,13 +64,14 @@ pub struct CallContractCommand { impl CallContractCommand { /// Executes the command. pub(crate) async fn execute(self) -> Result<()> { - let call_config: CallContractCommand = match self.set_up_call_config(&mut cli::Cli).await { - Ok(call_config) => call_config, - Err(e) => { - display_message(&e.to_string(), false, &mut cli::Cli)?; - return Ok(()); - }, - }; + let mut call_config: CallContractCommand = + match self.set_up_call_config(&mut cli::Cli).await { + Ok(call_config) => call_config, + Err(e) => { + display_message(&e.to_string(), false, &mut cli::Cli)?; + return Ok(()); + }, + }; match call_config.execute_call(self.message.is_none(), &mut cli::Cli).await { Ok(_) => Ok(()), Err(e) => { @@ -201,8 +202,8 @@ impl CallContractCommand { .interact()?, ); } - let mut value = "0".to_string(); - if message.payable { + let mut value = self.value.clone(); + if message.payable && value == DEFAULT_PAYABLE_VALUE { value = cli .input("Value to transfer to the call:") .placeholder("0") @@ -213,9 +214,9 @@ impl CallContractCommand { }) .interact()?; } - let mut gas_limit: Option = None; - let mut proof_size: Option = None; - if message.mutates && !self.dev_mode { + let mut gas_limit: Option = self.gas_limit; + let mut proof_size: Option = self.proof_size; + if message.mutates && !self.dev_mode && gas_limit.is_none() { // Prompt for gas limit and proof_size of the call. let gas_limit_input: String = cli .input("Enter the gas limit:") @@ -224,6 +225,9 @@ impl CallContractCommand { .placeholder("If left blank, an estimation will be used") .interact()?; gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + } + + if message.mutates && !self.dev_mode && proof_size.is_none() { let proof_size_input: String = cli .input("Enter the proof size limit:") .required(false) @@ -272,7 +276,7 @@ impl CallContractCommand { /// Executes the call. async fn execute_call( - &self, + &mut self, prompt_to_repeat_call: bool, cli: &mut impl cli::traits::Cli, ) -> anyhow::Result<()> { @@ -304,7 +308,7 @@ impl CallContractCommand { { Ok(call_exec) => call_exec, Err(e) => { - return Err(anyhow!(format!("{}", e.root_cause().to_string()))); + return Err(anyhow!(format!("{}", e.to_string()))); }, }; @@ -364,7 +368,8 @@ impl CallContractCommand { { // Remove only the prompt asking for another call. console::Term::stderr().clear_last_lines(2)?; - let new_call_config = self.guide_user_to_call_contract(cli).await?; + self.reset_for_new_call(); + let mut new_call_config = self.guide_user_to_call_contract(cli).await?; Box::pin(new_call_config.execute_call(prompt_to_repeat_call, cli)).await?; } else { display_message("Call completed successfully!", true, cli)?; @@ -374,6 +379,13 @@ impl CallContractCommand { } Ok(()) } + + /// Resets message specific fields to default values for a new call. + fn reset_for_new_call(&mut self) { + self.value = DEFAULT_PAYABLE_VALUE.to_string(); + self.gas_limit = None; + self.proof_size = None; + } } fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli) -> Result<()> { @@ -439,7 +451,7 @@ mod tests { .expect_warning("Your call has not been executed.") .expect_info("Gas limit: Weight { ref_time: 100, proof_size: 10 }"); - let call_config = CallContractCommand { + let mut call_config = CallContractCommand { path: Some(temp_dir.path().join("testing")), contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), message: Some("flip".to_string()), @@ -507,7 +519,7 @@ mod tests { .expect_outro("Call completed successfully!"); // Contract deployed on Pop Network testnet, test get - let call_config = CallContractCommand { + let mut call_config = CallContractCommand { path: Some(temp_dir.path().join("testing")), contract: Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()), message: Some("get".to_string()), From e55d1a645185a90ea6c6a172130915f7c54abb84 Mon Sep 17 00:00:00 2001 From: AlexD10S Date: Wed, 6 Nov 2024 19:44:30 +0100 Subject: [PATCH 26/27] fix: build contract if has not been built --- crates/pop-cli/src/commands/call/contract.rs | 36 +++++++++++++-- crates/pop-cli/src/commands/up/contract.rs | 45 +----------------- crates/pop-cli/src/common/build.rs | 48 ++++++++++++++++++++ crates/pop-cli/src/common/mod.rs | 2 + 4 files changed, 85 insertions(+), 46 deletions(-) create mode 100644 crates/pop-cli/src/common/build.rs diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 132966e7..24bfc06a 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,11 +1,15 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::cli::{self, traits::*}; +use crate::{ + cli::{self, traits::*}, + common::build::has_contract_been_built, +}; use anyhow::{anyhow, Result}; use clap::Args; +use cliclack::spinner; use pop_contracts::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, get_messages, parse_account, - set_up_call, CallOpts, + build_smart_contract, call_smart_contract, dry_run_call, dry_run_gas_estimate_call, + get_messages, parse_account, set_up_call, CallOpts, Verbosity, }; use sp_weights::Weight; use std::path::PathBuf; @@ -64,6 +68,7 @@ pub struct CallContractCommand { impl CallContractCommand { /// Executes the command. pub(crate) async fn execute(self) -> Result<()> { + self.ensure_contract_built(&mut cli::Cli).await?; let mut call_config: CallContractCommand = match self.set_up_call_config(&mut cli::Cli).await { Ok(call_config) => call_config, @@ -114,6 +119,31 @@ impl CallContractCommand { full_message } + /// Checks if the contract has been built; if not, builds it. + async fn ensure_contract_built(&self, cli: &mut impl cli::traits::Cli) -> Result<()> { + // Check if build exists in the specified "Contract build directory" + 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(self.path.as_deref(), true, Verbosity::Quiet) { + Ok(result) => result, + Err(e) => { + return Err(anyhow!(format!( + "🚫 An error occurred building your contract: {}\nUse `pop build` to retry with build output.", + e.to_string() + ))); + }, + }; + spinner.stop(format!( + "Your contract artifacts are ready. You can find them in: {}", + result.target_directory.display() + )); + } + Ok(()) + } + /// Set up the config call. async fn set_up_call_config( &self, diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 72a09697..d357d958 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -2,13 +2,12 @@ use crate::{ cli::{traits::Cli as _, Cli}, - common::contracts::check_contracts_node_and_prompt, + common::{build::has_contract_been_built, contracts::check_contracts_node_and_prompt}, style::style, }; use clap::Args; use cliclack::{confirm, log, log::error, spinner}; use console::{Emoji, Style}; -use pop_common::manifest::from_path; use pop_contracts::{ build_smart_contract, dry_run_gas_estimate_instantiate, dry_run_upload, instantiate_smart_contract, is_chain_alive, parse_hex_bytes, run_contracts_node, @@ -17,7 +16,7 @@ use pop_contracts::{ use sp_core::Bytes; use sp_weights::Weight; use std::{ - path::{Path, PathBuf}, + path::PathBuf, process::{Child, Command}, }; use tempfile::NamedTempFile; @@ -312,28 +311,9 @@ impl From for UpOpts { } } -/// Checks if a contract has been built by verifying the existence of the build directory and the -/// .contract file. -/// -/// # Arguments -/// * `path` - An optional path to the project directory. If no path is provided, the current -/// directory is used. -pub fn has_contract_been_built(path: Option<&Path>) -> bool { - let project_path = path.unwrap_or_else(|| Path::new("./")); - let manifest = match from_path(Some(project_path)) { - Ok(manifest) => manifest, - Err(_) => return false, - }; - let contract_name = manifest.package().name(); - project_path.join("target/ink").exists() && - project_path.join(format!("target/ink/{}.contract", contract_name)).exists() -} - #[cfg(test)] mod tests { use super::*; - use duct::cmd; - use std::fs::{self, File}; use url::Url; #[test] @@ -369,25 +349,4 @@ mod tests { ); Ok(()) } - - #[test] - fn has_contract_been_built_works() -> anyhow::Result<()> { - let temp_dir = tempfile::tempdir()?; - let path = temp_dir.path(); - - // Standard rust project - let name = "hello_world"; - cmd("cargo", ["new", name]).dir(&path).run()?; - let contract_path = path.join(name); - assert!(!has_contract_been_built(Some(&contract_path))); - - cmd("cargo", ["build"]).dir(&contract_path).run()?; - // Mock build directory - fs::create_dir(&contract_path.join("target/ink"))?; - assert!(!has_contract_been_built(Some(&path.join(name)))); - // Create a mocked .contract file inside the target directory - File::create(contract_path.join(format!("target/ink/{}.contract", name)))?; - assert!(has_contract_been_built(Some(&path.join(name)))); - Ok(()) - } } diff --git a/crates/pop-cli/src/common/build.rs b/crates/pop-cli/src/common/build.rs new file mode 100644 index 00000000..6bc7fd4d --- /dev/null +++ b/crates/pop-cli/src/common/build.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: GPL-3.0 + +use pop_common::manifest::from_path; +use std::path::Path; +/// Checks if a contract has been built by verifying the existence of the build directory and the +/// .contract file. +/// +/// # Arguments +/// * `path` - An optional path to the project directory. If no path is provided, the current +/// directory is used. +pub fn has_contract_been_built(path: Option<&Path>) -> bool { + let project_path = path.unwrap_or_else(|| Path::new("./")); + let manifest = match from_path(Some(project_path)) { + Ok(manifest) => manifest, + Err(_) => return false, + }; + let contract_name = manifest.package().name(); + project_path.join("target/ink").exists() && + project_path.join(format!("target/ink/{}.contract", contract_name)).exists() +} + +#[cfg(test)] +mod tests { + use super::*; + use duct::cmd; + use std::fs::{self, File}; + + #[test] + fn has_contract_been_built_works() -> anyhow::Result<()> { + let temp_dir = tempfile::tempdir()?; + let path = temp_dir.path(); + + // Standard rust project + let name = "hello_world"; + cmd("cargo", ["new", name]).dir(&path).run()?; + let contract_path = path.join(name); + assert!(!has_contract_been_built(Some(&contract_path))); + + cmd("cargo", ["build"]).dir(&contract_path).run()?; + // Mock build directory + fs::create_dir(&contract_path.join("target/ink"))?; + assert!(!has_contract_been_built(Some(&path.join(name)))); + // Create a mocked .contract file inside the target directory + File::create(contract_path.join(format!("target/ink/{}.contract", name)))?; + assert!(has_contract_been_built(Some(&path.join(name)))); + Ok(()) + } +} diff --git a/crates/pop-cli/src/common/mod.rs b/crates/pop-cli/src/common/mod.rs index 1cb3ee57..0469db60 100644 --- a/crates/pop-cli/src/common/mod.rs +++ b/crates/pop-cli/src/common/mod.rs @@ -1,5 +1,7 @@ // SPDX-License-Identifier: GPL-3.0 +#[cfg(feature = "contract")] +pub mod build; #[cfg(feature = "contract")] pub mod contracts; pub mod helpers; From eaad4be11e7798d8169f3839fed8c41e57026eba Mon Sep 17 00:00:00 2001 From: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Date: Wed, 6 Nov 2024 22:03:35 +0000 Subject: [PATCH 27/27] refactor: use command state (#338) Merged set_up_call_config and guide_user_to_call_contract into a single function. Also adds short symbols for arguments. --- crates/pop-cli/src/commands/call/contract.rs | 312 +++++++++---------- 1 file changed, 152 insertions(+), 160 deletions(-) diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 24bfc06a..1a559ebc 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -24,7 +24,7 @@ pub struct CallContractCommand { #[arg(short = 'p', long)] path: Option, /// The address of the contract to call. - #[clap(name = "contract", long, env = "CONTRACT")] + #[clap(name = "contract", short = 'c', long, env = "CONTRACT")] contract: Option, /// The name of the contract message to call. #[clap(long, short)] @@ -33,19 +33,19 @@ pub struct CallContractCommand { #[clap(long, num_args = 0..)] args: Vec, /// The value to be transferred as part of the call. - #[clap(name = "value", long, default_value = DEFAULT_PAYABLE_VALUE)] + #[clap(name = "value", short = 'v', long, default_value = DEFAULT_PAYABLE_VALUE)] 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 /// call. - #[clap(name = "gas", long)] + #[clap(name = "gas", short = 'g', long)] gas_limit: Option, /// Maximum proof size for this command. /// If not specified it will perform a dry-run to estimate the proof size required. - #[clap(long)] + #[clap(short = 'P', long)] proof_size: Option, /// Websocket endpoint of a node. - #[clap(name = "url", long, value_parser, default_value = DEFAULT_URL)] + #[clap(name = "url", short = 'u', long, value_parser, default_value = DEFAULT_URL)] url: url::Url, /// Secret key URI for the account calling the contract. /// @@ -58,7 +58,7 @@ pub struct CallContractCommand { #[clap(short('x'), long)] execute: bool, /// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction. - #[clap(long, conflicts_with = "execute")] + #[clap(short = 'D', long, conflicts_with = "execute")] dry_run: bool, /// Enables developer mode, bypassing certain user prompts for faster testing. /// Recommended for testing and local development only. @@ -67,23 +67,21 @@ pub struct CallContractCommand { } impl CallContractCommand { /// Executes the command. - pub(crate) async fn execute(self) -> Result<()> { + pub(crate) async fn execute(mut self) -> Result<()> { + // Ensure contract is built. self.ensure_contract_built(&mut cli::Cli).await?; - let mut call_config: CallContractCommand = - match self.set_up_call_config(&mut cli::Cli).await { - Ok(call_config) => call_config, - Err(e) => { - display_message(&e.to_string(), false, &mut cli::Cli)?; - return Ok(()); - }, - }; - match call_config.execute_call(self.message.is_none(), &mut cli::Cli).await { - Ok(_) => Ok(()), - Err(e) => { - display_message(&e.to_string(), false, &mut cli::Cli)?; - Ok(()) - }, + // Check if message specified via command line argument. + let prompt_to_repeat_call = self.message.is_none(); + // Configure the call based on command line arguments/call UI. + if let Err(e) = self.configure(&mut cli::Cli, false).await { + display_message(&e.to_string(), false, &mut cli::Cli)?; + return Ok(()); + }; + // Finally execute the call. + if let Err(e) = self.execute_call(&mut cli::Cli, prompt_to_repeat_call).await { + display_message(&e.to_string(), false, &mut cli::Cli)?; } + Ok(()) } fn display(&self) -> String { @@ -120,7 +118,7 @@ impl CallContractCommand { } /// Checks if the contract has been built; if not, builds it. - async fn ensure_contract_built(&self, cli: &mut impl cli::traits::Cli) -> Result<()> { + async fn ensure_contract_built(&self, cli: &mut impl Cli) -> Result<()> { // Check if build exists in the specified "Contract build directory" if !has_contract_been_built(self.path.as_deref()) { // Build the contract in release mode @@ -144,36 +142,39 @@ impl CallContractCommand { Ok(()) } - /// Set up the config call. - async fn set_up_call_config( - &self, - cli: &mut impl cli::traits::Cli, - ) -> anyhow::Result { - cli.intro("Call a contract")?; - if self.message.is_none() { - self.guide_user_to_call_contract(cli).await - } else { - Ok(self.clone()) + /// Configure the call based on command line arguments/call UI. + async fn configure(&mut self, cli: &mut impl Cli, repeat: bool) -> Result<()> { + // Show intro on first run. + if !repeat { + cli.intro("Call a contract")?; + } + + // If message has been specified via command line arguments, return early. + if self.message.is_some() { + return Ok(()); } - } - /// Guide the user to call the contract. - async fn guide_user_to_call_contract( - &self, - cli: &mut impl cli::traits::Cli, - ) -> anyhow::Result { - let contract_path: PathBuf = match &self.path { - Some(path) => path.clone(), + // Resolve path. + let contract_path = match self.path.as_ref() { None => { - // Prompt for path. - let input_path: String = cli - .input("Where is your project located?") - .placeholder("./") - .default_input("./") - .interact()?; - PathBuf::from(input_path) + let path = Some(PathBuf::from("./")); + if has_contract_been_built(path.as_deref()) { + self.path = path; + } else { + // Prompt for path. + let input_path: String = cli + .input("Where is your project located?") + .placeholder("./") + .default_input("./") + .interact()?; + self.path = Some(PathBuf::from(input_path)); + } + + self.path.as_ref().unwrap() }, + Some(p) => p, }; + // Parse the contract metadata provided. If there is an error, do not prompt for more. let messages = match get_messages(&contract_path) { Ok(messages) => messages, @@ -184,34 +185,34 @@ impl CallContractCommand { ))); }, }; - let url = if self.url.as_str() == DEFAULT_URL { + + // Resolve url. + if !repeat && self.url.as_str() == DEFAULT_URL { // Prompt for url. let url: String = cli .input("Where is your contract deployed?") .placeholder("ws://localhost:9944") .default_input("ws://localhost:9944") .interact()?; - url::Url::parse(&url)? - } else { - self.url.clone() + self.url = url::Url::parse(&url)? }; - let contract_address: String = match &self.contract { - Some(contract_address) => contract_address.clone(), - None => { - // Prompt for contract address. - let contract_address: String = cli - .input("Paste the on-chain contract address:") - .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") - .validate(|input: &String| match parse_account(input) { - Ok(_) => Ok(()), - Err(_) => Err("Invalid address."), - }) - .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") - .interact()?; - contract_address - }, + + // Resolve contract address. + if let None = self.contract { + // Prompt for contract address. + let contract_address: String = cli + .input("Paste the on-chain contract address:") + .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .validate(|input: &String| match parse_account(input) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid address."), + }) + .default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .interact()?; + self.contract = Some(contract_address); }; + // Resolve message. let message = { let mut prompt = cli.select("Select the message to call:"); for select_message in &messages { @@ -221,9 +222,12 @@ impl CallContractCommand { &select_message.docs, ); } - prompt.interact()? + let message = prompt.interact()?; + self.message = Some(message.label.clone()); + message }; + // Resolve message arguments. let mut contract_args = Vec::new(); for arg in &message.args { contract_args.push( @@ -232,9 +236,11 @@ impl CallContractCommand { .interact()?, ); } - let mut value = self.value.clone(); - if message.payable && value == DEFAULT_PAYABLE_VALUE { - value = cli + self.args = contract_args; + + // Resolve value. + if message.payable && self.value == DEFAULT_PAYABLE_VALUE { + self.value = cli .input("Value to transfer to the call:") .placeholder("0") .default_input("0") @@ -244,9 +250,9 @@ impl CallContractCommand { }) .interact()?; } - let mut gas_limit: Option = self.gas_limit; - let mut proof_size: Option = self.proof_size; - if message.mutates && !self.dev_mode && gas_limit.is_none() { + + // Resolve gas limit. + if message.mutates && !self.dev_mode && self.gas_limit.is_none() { // Prompt for gas limit and proof_size of the call. let gas_limit_input: String = cli .input("Enter the gas limit:") @@ -254,30 +260,31 @@ impl CallContractCommand { .default_input("") .placeholder("If left blank, an estimation will be used") .interact()?; - gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + self.gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. } - if message.mutates && !self.dev_mode && proof_size.is_none() { + // Resolve proof size. + if message.mutates && !self.dev_mode && self.proof_size.is_none() { let proof_size_input: String = cli .input("Enter the proof size limit:") .required(false) .placeholder("If left blank, an estimation will be used") .default_input("") .interact()?; - proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. + self.proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. } - // Who is calling the contract. - let suri = if self.suri == DEFAULT_URI { + // Resolve who is calling the contract. + if self.suri == DEFAULT_URI { // Prompt for uri. - cli.input("Signer calling the contract:") + self.suri = cli + .input("Signer calling the contract:") .placeholder("//Alice") .default_input("//Alice") - .interact()? - } else { - self.suri.clone() + .interact()?; }; + // Finally prompt for confirmation. let is_call_confirmed = if message.mutates && !self.dev_mode { cli.confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)") .initial_value(true) @@ -285,31 +292,19 @@ impl CallContractCommand { } else { true }; + self.execute = is_call_confirmed && message.mutates; + self.dry_run = !is_call_confirmed; - let call_command = CallContractCommand { - path: Some(contract_path), - contract: Some(contract_address), - message: Some(message.label.clone()), - args: contract_args, - value, - gas_limit, - proof_size, - url, - suri, - execute: if is_call_confirmed { message.mutates } else { false }, - dry_run: !is_call_confirmed, - dev_mode: self.dev_mode, - }; - cli.info(call_command.display())?; - Ok(call_command) + cli.info(self.display())?; + Ok(()) } - /// Executes the call. + /// Execute the call. async fn execute_call( &mut self, + cli: &mut impl Cli, prompt_to_repeat_call: bool, - cli: &mut impl cli::traits::Cli, - ) -> anyhow::Result<()> { + ) -> Result<()> { let message = match &self.message { Some(message) => message.to_string(), None => { @@ -343,7 +338,7 @@ impl CallContractCommand { }; if self.dry_run { - let spinner = cliclack::spinner(); + let spinner = spinner(); spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { @@ -359,7 +354,7 @@ impl CallContractCommand { } if !self.execute { - let spinner = cliclack::spinner(); + let spinner = spinner(); spinner.start("Calling the contract..."); let call_dry_run_result = dry_run_call(&call_exec).await?; cli.info(format!("Result: {}", call_dry_run_result))?; @@ -368,7 +363,7 @@ impl CallContractCommand { let weight_limit = if self.gas_limit.is_some() && self.proof_size.is_some() { Weight::from_parts(self.gas_limit.unwrap(), self.proof_size.unwrap()) } else { - let spinner = cliclack::spinner(); + let spinner = spinner(); spinner.start("Doing a dry run to estimate the gas..."); match dry_run_gas_estimate_call(&call_exec).await { Ok(w) => { @@ -381,7 +376,7 @@ impl CallContractCommand { }, } }; - let spinner = cliclack::spinner(); + let spinner = spinner(); spinner.start("Calling the contract..."); let call_result = call_smart_contract(call_exec, weight_limit, &self.url) @@ -390,35 +385,37 @@ impl CallContractCommand { cli.info(call_result)?; } - if prompt_to_repeat_call { - if cli - .confirm("Do you want to do another call using the existing smart contract?") - .initial_value(false) - .interact()? - { - // Remove only the prompt asking for another call. - console::Term::stderr().clear_last_lines(2)?; - self.reset_for_new_call(); - let mut new_call_config = self.guide_user_to_call_contract(cli).await?; - Box::pin(new_call_config.execute_call(prompt_to_repeat_call, cli)).await?; - } else { - display_message("Call completed successfully!", true, cli)?; - } - } else { + + // Prompt for any additional calls. + if !prompt_to_repeat_call { display_message("Call completed successfully!", true, cli)?; + return Ok(()); + } + if cli + .confirm("Do you want to perform another call using the existing smart contract?") + .initial_value(false) + .interact()? + { + // Reset specific items from the last call and repeat. + self.reset_for_new_call(); + self.configure(cli, true).await?; + Box::pin(self.execute_call(cli, prompt_to_repeat_call)).await + } else { + display_message("Contract calling complete.", true, cli)?; + Ok(()) } - Ok(()) } /// Resets message specific fields to default values for a new call. fn reset_for_new_call(&mut self) { + self.message = None; self.value = DEFAULT_PAYABLE_VALUE.to_string(); self.gas_limit = None; self.proof_size = None; } } -fn display_message(message: &str, success: bool, cli: &mut impl cli::traits::Cli) -> Result<()> { +fn display_message(message: &str, success: bool, cli: &mut impl Cli) -> Result<()> { if success { cli.outro(message)?; } else { @@ -494,15 +491,14 @@ mod tests { dry_run: true, execute: false, dev_mode: false, - } - .set_up_call_config(&mut cli) - .await?; + }; + call_config.configure(&mut cli, false).await?; assert_eq!(call_config.display(), format!( "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message flip --gas 100 --proof_size 10 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --dry_run", temp_dir.path().join("testing").display().to_string(), )); // Contract deployed on Pop Network testnet, test dry-run - call_config.execute_call(false, &mut cli).await?; + call_config.execute_call(&mut cli, false).await?; cli.verify() } @@ -526,11 +522,11 @@ mod tests { .expect_intro(&"Call a contract") .expect_warning("Your call has not been executed.") .expect_confirm( - "Do you want to do another call using the existing smart contract?", + "Do you want to perform another call using the existing smart contract?", false, ) .expect_confirm( - "Do you want to do another call using the existing smart contract?", + "Do you want to perform another call using the existing smart contract?", true, ) .expect_select::( @@ -546,7 +542,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )) .expect_warning("Your call has not been executed.") - .expect_outro("Call completed successfully!"); + .expect_outro("Contract calling complete."); // Contract deployed on Pop Network testnet, test get let mut call_config = CallContractCommand { @@ -562,11 +558,10 @@ mod tests { dry_run: false, execute: false, dev_mode: false, - } - .set_up_call_config(&mut cli) - .await?; + }; + call_config.configure(&mut cli, false).await?; // Test the query. With true, it will prompt for another call. - call_config.execute_call(true, &mut cli).await?; + call_config.execute_call(&mut cli, true).await?; cli.verify() } @@ -615,7 +610,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = CallContractCommand { + let mut call_config = CallContractCommand { path: None, contract: None, message: None, @@ -628,9 +623,8 @@ mod tests { dry_run: false, execute: false, dev_mode: false, - } - .guide_user_to_call_contract(&mut cli) - .await?; + }; + call_config.configure(&mut cli, false).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -701,7 +695,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = CallContractCommand { + let mut call_config = CallContractCommand { path: None, contract: None, message: None, @@ -714,9 +708,8 @@ mod tests { dry_run: false, execute: false, dev_mode: false, - } - .guide_user_to_call_contract(&mut cli) - .await?; + }; + call_config.configure(&mut cli, false).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -785,7 +778,7 @@ mod tests { temp_dir.path().join("testing").display().to_string(), )); - let call_config = CallContractCommand { + let mut call_config = CallContractCommand { path: None, contract: None, message: None, @@ -798,9 +791,8 @@ mod tests { dry_run: false, execute: false, dev_mode: true, - } - .guide_user_to_call_contract(&mut cli) - .await?; + }; + call_config.configure(&mut cli, false).await?; assert_eq!( call_config.contract, Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".to_string()) @@ -829,19 +821,19 @@ mod tests { let temp_dir = new_environment("testing")?; let mut cli = MockCli::new(); assert!(matches!(CallContractCommand { - path: Some(temp_dir.path().join("testing")), - contract: None, - message: None, - args: vec![].to_vec(), - value: "0".to_string(), - gas_limit: None, - proof_size: None, - url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, - suri: "//Alice".to_string(), - dry_run: false, - execute: false, - dev_mode: false, - }.guide_user_to_call_contract(&mut cli).await, anyhow::Result::Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory."))); + path: Some(temp_dir.path().join("testing")), + contract: None, + message: None, + args: vec![].to_vec(), + value: "0".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse("wss://rpc1.paseo.popnetwork.xyz")?, + suri: "//Alice".to_string(), + dry_run: false, + execute: false, + dev_mode: false, + }.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory."))); cli.verify() } @@ -871,7 +863,7 @@ mod tests { dry_run: false, execute: false, dev_mode: false, - }.execute_call(false, &mut cli).await, + }.execute_call(&mut cli, false).await, anyhow::Result::Err(message) if message.to_string() == "Please specify the message to call." )); @@ -889,7 +881,7 @@ mod tests { dry_run: false, execute: false, dev_mode: false, - }.execute_call(false, &mut cli).await, + }.execute_call(&mut cli, false).await, anyhow::Result::Err(message) if message.to_string() == "Please specify the contract address." ));