From ef3594d5e4d70b8fd053b336b74ef315f175e80d Mon Sep 17 00:00:00 2001 From: Alex Bean Date: Fri, 29 Nov 2024 15:16:00 +0100 Subject: [PATCH] feat: guide user to call a contract (#306) * feat: guide user for calling a contract * feat: get metadata contract from the contract path * refactor: refactor test and validate address input * fix: apply feedback * feat: prompt to have another call and skip questions for queries * refactor: use Cli module instead of cliclack * test: unit test pop-cli crate * test: unit contracts crate * chore: format * test: refactor and improve test cases * fix: fix todos and refactor * test: fix unit test * feat: parse types of parameters and display it to the user in the placeholder * refactor: error handling for pop call * refactor: display call to be executed after guide and reorder * refactor: when repeat call use same contract values and dont clean screen * test: add dry-run test * test: refactor and add more test coverage * test: more coverage * fix: unit test * feat: dev mode to skip certain user prompts * refactor: test functions, renaming and fix clippy * refactor: improve devex of pop call contract * test: adjust tests to refactor * chore: reset_for_new_call fields * fix: build contract if has not been built * 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. * fix: parse user inputs for Option arguments (#332) * fix: automatically add some or none to Option argument * test: refactor and tests * refactor: improve code and comments * fix: renaming and clean code * chore: option params not mandatory * fix: parse user inputs for Option arguments in constructor (#335) * fix: automatically add some or none to Option argument * fix: tests * refactor: process_function_args * test: update tests accordingly last changes * fix: issue with delimiter * test: fix unit test * refactor: renaming and fix comments * refactor: format types (#339) Shows the full type representation, making it easier to see the entry format of parameter values. * fix: logo doesn't show in README --------- Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Co-authored-by: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> * test: fix unit test * feat: allow users to specify custom contract metadata files (#347) * chore: allow the user specify the metadata file to call a contract * test: unit test to parse metadata from a file * docs: fix docs * refactor: ensure_contract_built after user input path * fix: call contract when metadata file * fix: remove default_input in contract address * docs: rename metadata with artifact * fix: panic at has_contract_been_built * fix: clippy * refactor: keep ensure_contract_built as a CallContractCommand function * fix: ensure_contract_built * docs: improve comments * fix: feedback and include wasm file for testing * fix: clippy * chore: after build contract prompt the user if the contract is already deployed * refactor: ensure_contract_built * refactor: has_contract_been_built function * docs: fix comments and messages * refactor: get_messages and get_constructors * test: fix unit tests call ui --------- Co-authored-by: Frank Bell <60948618+evilrobot-01@users.noreply.github.com> Co-authored-by: Alejandro Martinez Andres <11448715+al3mart@users.noreply.github.com> --- Cargo.lock | 12 +- Cargo.toml | 2 + crates/pop-cli/src/cli.rs | 232 +++- crates/pop-cli/src/commands/call/contract.rs | 1048 ++++++++++++++++- crates/pop-cli/src/commands/up/contract.rs | 51 +- crates/pop-cli/src/common/contracts.rs | 48 +- crates/pop-contracts/Cargo.toml | 3 +- crates/pop-contracts/src/call.rs | 138 ++- crates/pop-contracts/src/errors.rs | 12 +- crates/pop-contracts/src/lib.rs | 8 +- crates/pop-contracts/src/testing.rs | 39 + crates/pop-contracts/src/up.rs | 90 +- crates/pop-contracts/src/utils/metadata.rs | 549 +++++++++ crates/pop-contracts/src/utils/mod.rs | 1 + .../tests/files/testing.contract | 2 +- crates/pop-contracts/tests/files/testing.json | 528 +++++++++ crates/pop-contracts/tests/files/testing.wasm | Bin 0 -> 3710 bytes 17 files changed, 2562 insertions(+), 201 deletions(-) create mode 100644 crates/pop-contracts/src/testing.rs create mode 100644 crates/pop-contracts/src/utils/metadata.rs create mode 100644 crates/pop-contracts/tests/files/testing.json create mode 100644 crates/pop-contracts/tests/files/testing.wasm diff --git a/Cargo.lock b/Cargo.lock index 1313b0cb..d273e058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4667,7 +4667,7 @@ dependencies = [ [[package]] name = "pop-cli" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "assert_cmd", @@ -4697,7 +4697,7 @@ dependencies = [ [[package]] name = "pop-common" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "cargo_toml", @@ -4722,11 +4722,12 @@ dependencies = [ [[package]] name = "pop-contracts" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "contract-build", "contract-extrinsics", + "contract-transcode", "dirs", "duct", "flate2", @@ -4735,6 +4736,7 @@ dependencies = [ "mockito", "pop-common", "reqwest 0.12.7", + "scale-info", "sp-core", "sp-weights", "strum 0.26.3", @@ -4751,7 +4753,7 @@ dependencies = [ [[package]] name = "pop-parachains" -version = "0.4.0" +version = "0.5.0" dependencies = [ "anyhow", "askama", @@ -4780,7 +4782,7 @@ dependencies = [ [[package]] name = "pop-telemetry" -version = "0.4.0" +version = "0.5.0" dependencies = [ "dirs", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 30f4ac63..5a515c6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,8 @@ sp-core = "31" sp-weights = "30" contract-build = "5.0.0-alpha" contract-extrinsics = "5.0.0-alpha" +contract-transcode = "5.0.0-alpha" +scale-info = { version = "2.11.3", default-features = false, features = ["derive"] } heck = "0.5.0" # parachains diff --git a/crates/pop-cli/src/cli.rs b/crates/pop-cli/src/cli.rs index 353a4704..968c1a4f 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,21 +199,40 @@ 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::*; - 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)>, + confirm_expectation: Vec<(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, } @@ -146,7 +243,12 @@ 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 + } + + pub(crate) fn expect_input(mut self, prompt: impl Display, input: String) -> Self { + self.input_expectations.push((prompt.to_string(), input)); self } @@ -181,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 @@ -192,12 +306,15 @@ 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(",")) } + 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") } @@ -210,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", @@ -229,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 }; } @@ -242,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"); @@ -288,6 +422,18 @@ pub(crate) mod tests { Ok(()) } + fn select(&mut self, prompt: impl Display) -> impl Select { + let prompt = prompt.to_string(); + if let Some((expectation, _, collect, items_expectation, item)) = + self.select_expectation.take() + { + 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); @@ -308,11 +454,51 @@ 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) } } + /// 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 pub(crate) struct MockMultiSelect { required_expectation: Option, @@ -360,4 +546,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 52210cf0..4e00e37b 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -1,71 +1,355 @@ // SPDX-License-Identifier: GPL-3.0 -use crate::style::Theme; -use anyhow::anyhow; +use crate::{ + cli::{self, traits::*}, + common::contracts::has_contract_been_built, +}; +use anyhow::{anyhow, Result}; use clap::Args; -use cliclack::{clear_screen, intro, log, outro, outro_cancel, set_theme}; -use console::style; +use cliclack::spinner; use pop_contracts::{ - call_smart_contract, dry_run_call, dry_run_gas_estimate_call, 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; -#[derive(Args)] +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. - #[arg(short = 'p', long)] + /// Path to the contract build directory or a contract artifact. + #[arg(short, long)] path: Option, /// The address of the contract to call. - #[clap(name = "contract", long, env = "CONTRACT")] - contract: String, + #[arg(name = "contract", short, long, env = "CONTRACT")] + contract: Option, /// The name of the contract message to call. - #[clap(long, short)] - message: String, - /// The constructor arguments, encoded as strings. - #[clap(long, num_args = 0..)] + #[arg(long, short)] + message: Option, + /// The message arguments, encoded as strings. + #[arg(short, long, num_args = 0..,)] args: Vec, - /// Transfers an initial balance to the instantiated contract. - #[clap(name = "value", long, default_value = "0")] + /// The value to be transferred as part of the call. + #[arg(name = "value", short, 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 - /// instantiation. - #[clap(name = "gas", long)] + /// call. + #[arg(name = "gas", short, 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)] + #[arg(short = 'P', long)] proof_size: Option, /// Websocket endpoint of a node. - #[clap(name = "url", long, value_parser, default_value = "ws://localhost:9944")] + #[arg(name = "url", short, long, value_parser, default_value = DEFAULT_URL)] 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" /// - with a password "//Alice///SECRET_PASSWORD" - #[clap(name = "suri", long, short, default_value = "//Alice")] + #[arg(name = "suri", long, short, default_value = DEFAULT_URI)] suri: String, /// Submit an extrinsic for on-chain execution. - #[clap(short('x'), long)] + #[arg(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")] + #[arg(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. + #[arg(name = "dev", long, short, default_value = "false")] + dev_mode: bool, } - impl CallContractCommand { /// Executes the command. - pub(crate) async fn execute(self) -> anyhow::Result<()> { - clear_screen()?; - intro(format!("{}: Calling a contract", style(" Pop CLI ").black().on_magenta()))?; - set_theme(Theme); + pub(crate) async fn execute(mut self) -> Result<()> { + // 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 { + match e.to_string().as_str() { + "Contract not deployed." => { + display_message( + "Use `pop up contract` to deploy your contract.", + true, // Not an error, just a message. + &mut cli::Cli, + )?; + }, + _ => { + 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 { + let mut full_message = "pop call contract".to_string(); + if let Some(path) = &self.path { + full_message.push_str(&format!(" --path {}", path.display())); + } + 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() { + let args: Vec<_> = self.args.iter().map(|a| format!("\"{a}\"")).collect(); + full_message.push_str(&format!(" --args {}", args.join(", "))); + } + if self.value != DEFAULT_PAYABLE_VALUE { + 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 + } + + /// If the contract has not been built, build it in release mode. + async fn ensure_contract_built(&self, cli: &mut impl Cli) -> Result<()> { + // 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(()) + } + + /// Prompts the user to confirm if the contract has already been deployed. + fn confirm_contract_deployment(&self, cli: &mut impl Cli) -> Result<()> { + let is_contract_deployed = cli + .confirm("Has the contract already been deployed?") + .initial_value(false) + .interact()?; + if !is_contract_deployed { + return Err(anyhow!("Contract not deployed.")); + } + Ok(()) + } + + /// Checks whether building the contract is required + fn is_contract_build_required(&self) -> bool { + self.path + .as_ref() + .map(|p| p.is_dir() && !has_contract_been_built(Some(&p))) + .unwrap_or_default() + } - let call_exec = set_up_call(CallOpts { + /// 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(()); + } + + // Resolve path. + if self.path.is_none() { + let input_path: String = cli + .input("Where is your project or contract artifact located?") + .placeholder("./") + .default_input("./") + .interact()?; + self.path = Some(PathBuf::from(input_path)); + } + let contract_path = self + .path + .as_ref() + .expect("path is guaranteed to be set as input as prompted when None; qed"); + + // Ensure contract is built and check if deployed. + if self.is_contract_build_required() { + self.ensure_contract_built(&mut cli::Cli).await?; + self.confirm_contract_deployment(&mut cli::Cli)?; + } + + // 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: ", "") + ))); + }, + }; + + // 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()?; + self.url = url::Url::parse(&url)? + }; + + // Resolve contract address. + if self.contract.is_none() { + // Prompt for contract address. + let contract_address: String = cli + .input("Provide the on-chain contract address:") + .placeholder("e.g. 5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt") + .validate(|input: &String| match parse_account(input) { + Ok(_) => Ok(()), + Err(_) => Err("Invalid address."), + }) + .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 { + prompt = prompt.item( + select_message, + format!("{}\n", &select_message.label), + &select_message.docs, + ); + } + 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 { + let mut input = cli + .input(format!("Enter the value for the parameter: {}", arg.label)) + .placeholder(&format!("Type required: {}", arg.type_name)); + + // Set default input only if the parameter type is `Option` (Not mandatory) + if arg.type_name.starts_with("Option<") { + input = input.default_input(""); + } + contract_args.push(input.interact()?); + } + 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") + .validate(|input: &String| match input.parse::() { + Ok(_) => Ok(()), + Err(_) => Err("Invalid value."), + }) + .interact()?; + } + + // 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:") + .required(false) + .default_input("") + .placeholder("If left blank, an estimation will be used") + .interact()?; + self.gas_limit = gas_limit_input.parse::().ok(); // If blank or bad input, estimate it. + } + + // 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()?; + self.proof_size = proof_size_input.parse::().ok(); // If blank or bad input, estimate it. + } + + // Resolve who is calling the contract. + if self.suri == DEFAULT_URI { + // Prompt for uri. + self.suri = cli + .input("Signer calling the contract:") + .placeholder("//Alice") + .default_input("//Alice") + .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) + .interact()? + } else { + true + }; + self.execute = is_call_confirmed && message.mutates; + self.dry_run = !is_call_confirmed; + + cli.info(self.display())?; + Ok(()) + } + + /// Execute the call. + async fn execute_call( + &mut self, + cli: &mut impl Cli, + prompt_to_repeat_call: bool, + ) -> Result<()> { + 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 => { + return Err(anyhow!("Please specify the contract address.")); + }, + }; + let call_exec = match set_up_call(CallOpts { path: self.path.clone(), - contract: self.contract.clone(), - message: self.message.clone(), + contract, + message, args: self.args.clone(), value: self.value.clone(), gas_limit: self.gas_limit, @@ -74,63 +358,725 @@ impl CallContractCommand { suri: self.suri.clone(), execute: self.execute, }) - .await?; + .await + { + Ok(call_exec) => call_exec, + Err(e) => { + return Err(anyhow!(format!("{}", e.to_string()))); + }, + }; 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) => { - 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.")?; + display_message("Call failed.", false, cli)?; }, }; return Ok(()); } 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?; - 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" - ))?; + 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(); + let spinner = spinner(); 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))?; + cli.info(format!("Gas limit: {:?}", w))?; w }, Err(e) => { spinner.error(format!("{e}")); - outro_cancel("Call failed.")?; - return Ok(()); + return Err(anyhow!("Call failed.")); }, } }; - let spinner = cliclack::spinner(); + let spinner = spinner(); spinner.start("Calling the contract..."); let call_result = call_smart_contract(call_exec, weight_limit, &self.url) .await .map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?; - log::info(call_result)?; + cli.info(call_result)?; } - outro("Call completed successfully!")?; + // 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(()) + } + } + + /// 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) -> Result<()> { + if success { + cli.outro(message)?; + } else { + cli.outro_cancel(message)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cli::MockCli; + use pop_contracts::{mock_build_process, new_environment}; + use std::{env, fs::write}; + use url::Url; + + #[tokio::test] + async fn execute_query_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + // Contract deployed on Pop Network testnet, test get + 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, + dev_mode: false, + } + .execute() + .await?; Ok(()) } + + #[tokio::test] + async fn call_contract_dry_run_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + + let 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 mut 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, + dev_mode: false, + }; + 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(&mut cli, false).await?; + + cli.verify() + } + + #[tokio::test] + async fn call_contract_dry_run_with_artifact_file_works() -> Result<()> { + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + + 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 }"); + + // From .contract file + let mut call_config = CallContractCommand { + path: Some(current_dir.join("pop-contracts/tests/files/testing.contract")), + 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, + dev_mode: false, + }; + 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", + current_dir.join("pop-contracts/tests/files/testing.contract").display().to_string(), + )); + // Contract deployed on Pop Network testnet, test dry-run + call_config.execute_call(&mut cli, false).await?; + + // From .json file + call_config.path = Some(current_dir.join("pop-contracts/tests/files/testing.json")); + 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", + current_dir.join("pop-contracts/tests/files/testing.json").display().to_string(), + )); + + // From .wasm file + call_config.path = Some(current_dir.join("pop-contracts/tests/files/testing.wasm")); + 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", + current_dir.join("pop-contracts/tests/files/testing.wasm").display().to_string(), + )); + // Contract deployed on Pop Network testnet, test dry-run + call_config.execute_call(&mut cli, false).await?; + + cli.verify() + } + + #[tokio::test] + async fn call_contract_query_duplicate_call_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + let 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 perform another call using the existing smart contract?", + false, + ) + .expect_confirm( + "Do you want to perform 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("Contract calling complete."); + + // Contract deployed on Pop Network testnet, test get + let mut call_config = 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, + dev_mode: false, + }; + call_config.configure(&mut cli, false).await?; + // Test the query. With true, it will prompt for another call. + call_config.execute_call(&mut cli, true).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 = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + + let 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_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 1, // "get" message + ) + .expect_input( + "Provide 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 or contract artifact 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 mut 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, + }; + call_config.configure(&mut cli, false).await?; + assert_eq!( + call_config.contract, + Some("15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm".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); + 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() + } + + // 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 = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + + let 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_confirm("Do you want to execute the call? (Selecting 'No' will perform a dry run)", true) + .expect_input("Signer calling the contract:", "//Alice".into()) + .expect_input("Enter the proof size limit:", "".into()) // Only if call + .expect_input("Enter the gas limit:", "".into()) // Only if call + .expect_input("Value to transfer to the call:", "50".into()) // Only if payable + .expect_input("Enter the value for the parameter: number", "2".into()) // Args for specific_flip + .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 2, // "specific_flip" message + ) + .expect_input( + "Provide 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 or contract artifact located?", + temp_dir.path().join("testing").display().to_string(), + ).expect_info(format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); + + let mut 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, + }; + call_config.configure(&mut cli, false).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(), 2); + assert_eq!(call_config.args[0], "true".to_string()); + assert_eq!(call_config.args[1], "2".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_eq!(call_config.display(), format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); + + 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 = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("pop-contracts/tests/files/testing.contract"), + current_dir.join("pop-contracts/tests/files/testing.json"), + )?; + + let 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: number", "2".into()) // Args for specific_flip + .expect_input("Enter the value for the parameter: new_value", "true".into()) // Args for specific_flip + .expect_select::( + "Select the message to call:", + Some(false), + true, + Some(items), + 2, // "specific_flip" message + ) + .expect_input( + "Provide 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 or contract artifact located?", + temp_dir.path().join("testing").display().to_string(), + ).expect_info(format!( + "pop call contract --path {} --contract 15XausWjFLBBFLDXUSBRfSfZk25warm4wZRV4ZxhZbfvjrJm --message specific_flip --args \"true\", \"2\" --value 50 --url wss://rpc1.paseo.popnetwork.xyz/ --suri //Alice --execute", + temp_dir.path().join("testing").display().to_string(), + )); + + let mut 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, + }; + call_config.configure(&mut cli, false).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(), 2); + assert_eq!(call_config.args[0], "true".to_string()); + assert_eq!(call_config.args[1], "2".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\", \"2\" --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 = new_environment("testing")?; + let mut current_dir = env::current_dir().expect("Failed to get current directory"); + current_dir.pop(); + // Create invalid `.json`, `.contract` and `.wasm` files for testing + let invalid_contract_path = temp_dir.path().join("testing.contract"); + let invalid_json_path = temp_dir.path().join("testing.json"); + let invalid_wasm_path = temp_dir.path().join("testing.wasm"); + write(&invalid_contract_path, b"This is an invalid contract file")?; + write(&invalid_json_path, b"This is an invalid JSON file")?; + write(&invalid_wasm_path, b"This is an invalid WASM file")?; + // Mock the build process to simulate a scenario where the contract is not properly built. + mock_build_process( + temp_dir.path().join("testing"), + invalid_contract_path.clone(), + invalid_contract_path.clone(), + )?; + // Test the path is a folder with an invalid build. + let mut command = 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, + }; + let mut cli = MockCli::new(); + assert!( + matches!(command.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata")) + ); + // Test the path is a file with invalid `.contract` file. + command.path = Some(invalid_contract_path); + assert!( + matches!(command.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata")) + ); + // Test the path is a file with invalid `.json` file. + command.path = Some(invalid_json_path); + assert!( + matches!(command.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata")) + ); + // Test the path is a file with invalid `.wasm` file. + command.path = Some(invalid_wasm_path); + assert!( + matches!(command.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata")) + ); + cli.verify() + } + + #[tokio::test] + 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(); + 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(); + assert!(matches!( + 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(&mut cli, false).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(&mut cli, false).await, + anyhow::Result::Err(message) if message.to_string() == "Please specify the contract address." + )); + + cli.verify() + } + + #[tokio::test] + async fn confirm_contract_deployment_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + 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, + }; + // Contract is not deployed. + let mut cli = + MockCli::new().expect_confirm("Has the contract already been deployed?", false); + assert!( + matches!(call_config.confirm_contract_deployment(&mut cli), anyhow::Result::Err(message) if message.to_string() == "Contract not deployed.") + ); + cli.verify()?; + // Contract is deployed. + cli = MockCli::new().expect_confirm("Has the contract already been deployed?", true); + call_config.confirm_contract_deployment(&mut cli)?; + cli.verify() + } + + #[tokio::test] + async fn is_contract_build_required_works() -> Result<()> { + let temp_dir = new_environment("testing")?; + 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, + }; + // Contract not build. Build is required. + assert!(call_config.is_contract_build_required()); + // Mock build process. Build is not required. + 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"), + )?; + assert!(!call_config.is_contract_build_required()); + Ok(()) + } + + #[test] + 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()?; + 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/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index 7b3a2603..3b7353b3 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::contracts::{check_contracts_node_and_prompt, has_contract_been_built}, 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; @@ -36,7 +35,7 @@ pub struct UpContractCommand { #[clap(name = "constructor", long, default_value = "new")] constructor: String, /// The constructor arguments, encoded as strings. - #[clap(long, num_args = 0..)] + #[clap(long, num_args = 0..,)] args: Vec, /// Transfers an initial balance to the instantiated contract. #[clap(name = "value", long, default_value = "0")] @@ -330,28 +329,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] @@ -359,7 +339,7 @@ mod tests { let command = UpContractCommand { path: None, constructor: "new".to_string(), - args: vec!["false".to_string()].to_vec(), + args: vec![], value: "0".to_string(), gas_limit: None, proof_size: None, @@ -376,7 +356,7 @@ mod tests { UpOpts { path: None, constructor: "new".to_string(), - args: vec!["false".to_string()].to_vec(), + args: vec![].to_vec(), value: "0".to_string(), gas_limit: None, proof_size: None, @@ -387,25 +367,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/contracts.rs b/crates/pop-cli/src/common/contracts.rs index e04f51b7..338ca84b 100644 --- a/crates/pop-cli/src/common/contracts.rs +++ b/crates/pop-cli/src/common/contracts.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: GPL-3.0 use cliclack::{confirm, log::warning, spinner}; +use pop_common::manifest::from_path; use pop_contracts::contracts_node_generator; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; /// Checks the status of the `substrate-contracts-node` binary, sources it if necessary, and /// prompts the user to update it if the existing binary is not the latest version. @@ -65,3 +66,48 @@ pub async fn check_contracts_node_and_prompt(skip_confirm: bool) -> anyhow::Resu Ok(node_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 Ok(manifest) = from_path(Some(project_path)) else { + return false; + }; + manifest + .package + .map(|p| project_path.join(format!("target/ink/{}.contract", p.name())).exists()) + .unwrap_or_default() +} + +#[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-contracts/Cargo.toml b/crates/pop-contracts/Cargo.toml index 23c6f0a6..3d4f15fb 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.5.0" } diff --git a/crates/pop-contracts/src/call.rs b/crates/pop-contracts/src/call.rs index 92ef55c6..a554bc2f 100644 --- a/crates/pop-contracts/src/call.rs +++ b/crates/pop-contracts/src/call.rs @@ -4,13 +4,14 @@ use crate::{ errors::Error, utils::{ helpers::{get_manifest_path, parse_account, parse_balance}, + metadata::{process_function_args, FunctionType}, signer::create_signer, }, }; use anyhow::Context; use contract_build::Verbosity; use contract_extrinsics::{ - BalanceVariant, CallCommandBuilder, CallExec, DisplayEvents, ErrorVariant, + BalanceVariant, CallCommandBuilder, CallExec, ContractArtifacts, DisplayEvents, ErrorVariant, ExtrinsicOptsBuilder, TokenMetadata, }; use ink_env::{DefaultEnvironment, Environment}; @@ -28,7 +29,7 @@ pub struct CallOpts { pub contract: String, /// The name of the contract message to call. pub message: String, - /// The constructor arguments, encoded as strings. + /// The message arguments, encoded as strings. pub args: Vec, /// Transfers an initial balance to the instantiated contract. pub value: String, @@ -51,24 +52,43 @@ pub struct CallOpts { /// * `call_opts` - options for the `call` command. pub async fn set_up_call( call_opts: CallOpts, -) -> anyhow::Result> { +) -> Result, Error> { let token_metadata = TokenMetadata::query::(&call_opts.url).await?; - let manifest_path = get_manifest_path(call_opts.path.as_deref())?; let signer = create_signer(&call_opts.suri)?; - let extrinsic_opts = ExtrinsicOptsBuilder::new(signer) - .manifest_path(Some(manifest_path)) - .url(call_opts.url.clone()) - .done(); + let extrinsic_opts = match &call_opts.path { + // If path is a file construct the ExtrinsicOptsBuilder from the file. + Some(path) if path.is_file() => { + let artifacts = ContractArtifacts::from_manifest_or_file(None, Some(path))?; + ExtrinsicOptsBuilder::new(signer) + .file(Some(artifacts.artifact_path())) + .url(call_opts.url.clone()) + .done() + }, + _ => { + let manifest_path = get_manifest_path(call_opts.path.as_deref())?; + ExtrinsicOptsBuilder::new(signer) + .manifest_path(Some(manifest_path)) + .url(call_opts.url.clone()) + .done() + }, + }; let value: BalanceVariant<::Balance> = parse_balance(&call_opts.value)?; let contract: ::AccountId = parse_account(&call_opts.contract)?; + // Process the provided argument values. + let args = process_function_args( + call_opts.path.unwrap_or_else(|| PathBuf::from("./")), + &call_opts.message, + call_opts.args, + FunctionType::Message, + )?; let call_exec: CallExec = CallCommandBuilder::new(contract.clone(), &call_opts.message, extrinsic_opts) - .args(call_opts.args.clone()) + .args(args) .value(value.denominate_balance(&token_metadata)?) .gas_limit(call_opts.gas_limit) .proof_size(call_opts.proof_size) @@ -158,43 +178,48 @@ 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, + instantiate_smart_contract, mock_build_process, new_environment, 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 + #[tokio::test] + async fn test_set_up_call() -> Result<()> { + let temp_dir = new_environment("testing")?; 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"))?; + 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")), + contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(), + message: "get".to_string(), + args: [].to_vec(), + value: "1000".to_string(), + gas_limit: None, + proof_size: None, + url: Url::parse(CONTRACTS_NETWORK_URL)?, + suri: "//Alice".to_string(), + execute: false, + }; + let call = set_up_call(call_opts).await?; + assert_eq!(call.message(), "get"); 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"))?; - + async fn test_set_up_call_from_artifact_file() -> Result<()> { + let current_dir = env::current_dir().expect("Failed to get current directory"); let call_opts = CallOpts { - path: Some(temp_dir.path().join("testing")), + path: Some(current_dir.join("./tests/files/testing.json")), contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(), message: "get".to_string(), args: [].to_vec(), @@ -212,7 +237,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(), @@ -225,11 +250,9 @@ mod tests { suri: "//Alice".to_string(), execute: false, }; - let call = set_up_call(call_opts).await; - assert!(call.is_err()); - let error = call.err().unwrap(); - assert_eq!(error.root_cause().to_string(), "Failed to find any contract artifacts in target directory. \nRun `cargo contract build --release` to generate the artifacts."); - + assert!( + matches!(set_up_call(call_opts).await, Err(Error::AnyhowError(message)) if message.root_cause().to_string() == "Failed to find any contract artifacts in target directory. \nRun `cargo contract build --release` to generate the artifacts.") + ); Ok(()) } #[tokio::test] @@ -246,18 +269,21 @@ mod tests { suri: "//Alice".to_string(), execute: false, }; - let call = set_up_call(call_opts).await; - assert!(call.is_err()); - let error = call.err().unwrap(); - assert_eq!(error.root_cause().to_string(), "No 'ink' dependency found"); - + assert!( + matches!(set_up_call(call_opts).await, Err(Error::AnyhowError(message)) if message.root_cause().to_string() == "No 'ink' dependency found") + ); Ok(()) } #[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 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"), + 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")), @@ -278,8 +304,13 @@ 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 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"), + 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")), @@ -304,8 +335,13 @@ 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()?; - mock_build_process(temp_dir.path().join("testing"))?; + 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"), + 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/errors.rs b/crates/pop-contracts/src/errors.rs index a0ea14ad..b59031a0 100644 --- a/crates/pop-contracts/src/errors.rs +++ b/crates/pop-contracts/src/errors.rs @@ -24,10 +24,16 @@ pub enum Error { HexParsing(String), #[error("HTTP error: {0}")] HttpError(#[from] reqwest::Error), + #[error("Incorrect number of arguments provided. Expecting {expected}, {provided} provided")] + IncorrectArguments { expected: usize, provided: usize }, #[error("Failed to install {0}")] InstallContractsNode(String), #[error("{0}")] InstantiateContractError(String), + #[error("Invalid constructor name: {0}")] + InvalidConstructorName(String), + #[error("Invalid message name: {0}")] + InvalidMessageName(String), #[error("Invalid name: {0}")] InvalidName(String), #[error("IO error: {0}")] @@ -36,6 +42,8 @@ pub enum Error { KeyPairCreation(String), #[error("Failed to get manifest path: {0}")] ManifestPath(String), + #[error("Argument {0} is required")] + MissingArgument(String), #[error("Failed to create new contract project: {0}")] NewContract(String), #[error("ParseError error: {0}")] @@ -44,12 +52,12 @@ pub enum Error { ParseSecretURI(String), #[error("The `Repository` property is missing from the template variant")] RepositoryMissing, + #[error("Sourcing error {0}")] + SourcingError(SourcingError), #[error("Failed to execute test command: {0}")] TestCommand(String), #[error("Unsupported platform: {os}")] UnsupportedPlatform { os: &'static str }, #[error("{0}")] UploadContractError(String), - #[error("Sourcing error {0}")] - SourcingError(SourcingError), } diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 1d558de3..14228399 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -8,6 +8,7 @@ mod new; mod node; mod templates; mod test; +mod testing; mod up; mod utils; @@ -19,8 +20,13 @@ 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, }; -pub use utils::signer::parse_hex_bytes; +pub use utils::{ + helpers::parse_account, + metadata::{get_messages, ContractFunction}, + signer::parse_hex_bytes, +}; 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 93a2742e..586a962f 100644 --- a/crates/pop-contracts/src/up.rs +++ b/crates/pop-contracts/src/up.rs @@ -3,6 +3,7 @@ use crate::{ errors::Error, utils::{ helpers::{get_manifest_path, parse_balance}, + metadata::{process_function_args, FunctionType}, signer::create_signer, }, }; @@ -62,10 +63,17 @@ pub async fn set_up_deployment( let value: BalanceVariant<::Balance> = parse_balance(&up_opts.value)?; + // Process the provided argument values. + let args = process_function_args( + up_opts.path.unwrap_or_else(|| PathBuf::from("./")), + &up_opts.constructor, + up_opts.args, + FunctionType::Constructor, + )?; let instantiate_exec: InstantiateExec = InstantiateCommandBuilder::new(extrinsic_opts) .constructor(up_opts.constructor.clone()) - .args(up_opts.args.clone()) + .args(args) .value(value.denominate_balance(&token_metadata)?) .gas_limit(up_opts.gas_limit) .proof_size(up_opts.proof_size) @@ -215,39 +223,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, mock_build_process, new_environment, + 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 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"), + 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(), @@ -265,8 +258,13 @@ 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 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"), + 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(), @@ -284,8 +282,13 @@ 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 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"), + 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(), @@ -306,8 +309,13 @@ 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 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"), + 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(), @@ -329,8 +337,13 @@ 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 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"), + 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(), @@ -352,8 +365,13 @@ 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()?; - mock_build_process(temp_dir.path().join("testing"))?; + 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"), + 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/utils/metadata.rs b/crates/pop-contracts/src/utils/metadata.rs new file mode 100644 index 00000000..38cdc12d --- /dev/null +++ b/crates/pop-contracts/src/utils/metadata.rs @@ -0,0 +1,549 @@ +// SPDX-License-Identifier: GPL-3.0 + +use crate::errors::Error; +use contract_extrinsics::ContractArtifacts; +use contract_transcode::ink_metadata::MessageParamSpec; +use scale_info::{form::PortableForm, PortableRegistry, Type, TypeDef, TypeDefPrimitive}; +use std::path::Path; + +/// Describes a parameter. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Param { + /// The label of the parameter. + pub label: String, + /// The type name of the parameter. + pub type_name: String, +} + +/// Describes a contract function. +#[derive(Clone, PartialEq, Eq)] +pub struct ContractFunction { + /// The label of the function. + pub label: String, + /// If the function accepts any `value` from the caller. + pub payable: bool, + /// The parameters of the deployment handler. + pub args: Vec, + /// The function documentation. + pub docs: String, + /// If the message/constructor is the default for off-chain consumers (e.g UIs). + pub default: bool, + /// If the message is allowed to mutate the contract state. true for constructors. + pub mutates: bool, +} + +/// Specifies the type of contract funtion, either a constructor or a message. +#[derive(Clone, PartialEq, Eq)] +pub enum FunctionType { + Constructor, + Message, +} + +/// Extracts a list of smart contract messages parsing the contract artifact. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +pub fn get_messages

(path: P) -> Result, Error> +where + P: AsRef, +{ + get_contract_functions(path.as_ref(), FunctionType::Message) +} + +/// Extracts a list of smart contract contructors parsing the contract artifact. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +pub fn get_constructors

(path: P) -> Result, Error> +where + P: AsRef, +{ + get_contract_functions(path.as_ref(), FunctionType::Constructor) +} + +/// Extracts a list of smart contract functions (messages or constructors) parsing the contract +/// artifact. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +/// * `function_type` - Specifies whether to extract messages or constructors. +fn get_contract_functions( + path: &Path, + function_type: FunctionType, +) -> Result, Error> { + let contract_artifacts = if path.is_dir() || path.ends_with("Cargo.toml") { + let cargo_toml_path = + if path.ends_with("Cargo.toml") { path.to_path_buf() } else { path.join("Cargo.toml") }; + ContractArtifacts::from_manifest_or_file(Some(&cargo_toml_path), None)? + } else { + ContractArtifacts::from_manifest_or_file(None, Some(&path.to_path_buf()))? + }; + let transcoder = contract_artifacts.contract_transcoder()?; + let metadata = transcoder.metadata(); + + Ok(match function_type { + FunctionType::Message => metadata + .spec() + .messages() + .iter() + .map(|message| ContractFunction { + label: message.label().to_string(), + mutates: message.mutates(), + payable: message.payable(), + args: process_args(message.args(), metadata.registry()), + docs: message.docs().join(" "), + default: *message.default(), + }) + .collect(), + FunctionType::Constructor => metadata + .spec() + .constructors() + .iter() + .map(|constructor| ContractFunction { + label: constructor.label().to_string(), + payable: *constructor.payable(), + args: process_args(constructor.args(), metadata.registry()), + docs: constructor.docs().join(" "), + default: *constructor.default(), + mutates: true, + }) + .collect(), + }) +} + +/// Extracts the information of a smart contract message parsing the contract artifact. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +/// * `message` - The label of the contract message. +fn get_message

(path: P, message: &str) -> Result +where + P: AsRef, +{ + get_messages(path.as_ref())? + .into_iter() + .find(|msg| msg.label == message) + .ok_or_else(|| Error::InvalidMessageName(message.to_string())) +} + +/// Extracts the information of a smart contract constructor parsing the contract artifact. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +/// * `constructor` - The label of the constructor. +fn get_constructor

(path: P, constructor: &str) -> Result +where + P: AsRef, +{ + get_constructors(path.as_ref())? + .into_iter() + .find(|c| c.label == constructor) + .ok_or_else(|| Error::InvalidConstructorName(constructor.to_string())) +} + +// Parse the parameters into a vector of argument labels. +fn process_args( + params: &[MessageParamSpec], + registry: &PortableRegistry, +) -> Vec { + let mut args: Vec = Vec::new(); + for arg in params { + // Resolve type from registry to provide full type representation. + let type_name = + format_type(registry.resolve(arg.ty().ty().id).expect("type not found"), registry); + args.push(Param { label: arg.label().to_string(), type_name }); + } + args +} + +// Formats a specified type, using the registry to output its full type representation. +fn format_type(ty: &Type, registry: &PortableRegistry) -> String { + let mut name = ty + .path + .segments + .last() + .map(|s| s.to_owned()) + .unwrap_or_else(|| ty.path.to_string()); + + if !ty.type_params.is_empty() { + let params: Vec<_> = ty + .type_params + .iter() + .filter_map(|p| registry.resolve(p.ty.unwrap().id)) + .map(|t| format_type(t, registry)) + .collect(); + name = format!("{name}<{}>", params.join(",")); + } + + name = format!( + "{name}{}", + match &ty.type_def { + TypeDef::Composite(composite) => { + if composite.fields.is_empty() { + return "".to_string(); + } + + let mut named = false; + let fields: Vec<_> = composite + .fields + .iter() + .filter_map(|f| match f.name.as_ref() { + None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), + Some(field) => { + named = true; + f.type_name.as_ref().map(|t| format!("{field}: {t}")) + }, + }) + .collect(); + match named { + true => format!(" {{ {} }}", fields.join(", ")), + false => format!(" ({})", fields.join(", ")), + } + }, + TypeDef::Variant(variant) => { + let variants: Vec<_> = variant + .variants + .iter() + .map(|v| { + if v.fields.is_empty() { + return v.name.clone(); + } + + let name = v.name.as_str(); + let mut named = false; + let fields: Vec<_> = v + .fields + .iter() + .filter_map(|f| match f.name.as_ref() { + None => registry.resolve(f.ty.id).map(|t| format_type(t, registry)), + Some(field) => { + named = true; + f.type_name.as_ref().map(|t| format!("{field}: {t}")) + }, + }) + .collect(); + format!( + "{name}{}", + match named { + true => format!("{{ {} }}", fields.join(", ")), + false => format!("({})", fields.join(", ")), + } + ) + }) + .collect(); + format!(": {}", variants.join(", ")) + }, + TypeDef::Sequence(sequence) => { + format!( + "[{}]", + format_type( + registry.resolve(sequence.type_param.id).expect("sequence type not found"), + registry + ) + ) + }, + TypeDef::Array(array) => { + format!( + "[{};{}]", + format_type( + registry.resolve(array.type_param.id).expect("array type not found"), + registry + ), + array.len + ) + }, + TypeDef::Tuple(tuple) => { + let fields: Vec<_> = tuple + .fields + .iter() + .filter_map(|p| registry.resolve(p.id)) + .map(|t| format_type(t, registry)) + .collect(); + format!("({})", fields.join(",")) + }, + TypeDef::Primitive(primitive) => { + use TypeDefPrimitive::*; + match primitive { + Bool => "bool", + Char => "char", + Str => "str", + U8 => "u8", + U16 => "u16", + U32 => "u32", + U64 => "u64", + U128 => "u128", + U256 => "u256", + I8 => "i8", + I16 => "i16", + I32 => "i32", + I64 => "i64", + I128 => "i128", + I256 => "i256", + } + .to_string() + }, + TypeDef::Compact(compact) => { + format!( + "Compact<{}>", + format_type( + registry.resolve(compact.type_param.id).expect("compact type not found"), + registry + ) + ) + }, + TypeDef::BitSequence(_) => { + unimplemented!("bit sequence not currently supported") + }, + } + ); + + name +} + +/// Processes a list of argument values for a specified contract function, +/// wrapping each value in `Some(...)` or replacing it with `None` if the argument is optional. +/// +/// # Arguments +/// * `path` - Location path of the project or contract artifact. +/// * `label` - Label of the contract message to retrieve. +/// * `args` - Argument values provided by the user. +/// * `function_type` - Specifies whether to process arguments of messages or constructors. +pub fn process_function_args

( + path: P, + label: &str, + args: Vec, + function_type: FunctionType, +) -> Result, Error> +where + P: AsRef, +{ + let function = match function_type { + FunctionType::Message => get_message(path, label)?, + FunctionType::Constructor => get_constructor(path, label)?, + }; + if args.len() != function.args.len() { + return Err(Error::IncorrectArguments { + expected: function.args.len(), + provided: args.len(), + }); + } + Ok(args + .into_iter() + .zip(&function.args) + .map(|(arg, param)| match (param.type_name.starts_with("Option<"), arg.is_empty()) { + // If the argument is Option and empty, replace it with `None` + (true, true) => "None".to_string(), + // If the argument is Option and not empty, wrap it in `Some(...)` + (true, false) => format!("Some({})", arg), + // If the argument is not Option, return it as is + _ => arg, + }) + .collect::>()) +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + use crate::{mock_build_process, new_environment}; + use anyhow::Result; + + #[test] + fn get_messages_work() -> Result<()> { + let temp_dir = new_environment("testing")?; + let current_dir = env::current_dir().expect("Failed to get current directory"); + + // Helper function to avoid duplicated code + fn assert_contract_metadata_parsed(message: Vec) -> Result<()> { + 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.len(), 2); + assert_eq!(message[2].args[0].label, "new_value".to_string()); + assert_eq!(message[2].args[0].type_name, "bool".to_string()); + assert_eq!(message[2].args[1].label, "number".to_string()); + assert_eq!(message[2].args[1].type_name, "Option: None, Some(u32)".to_string()); + Ok(()) + } + + mock_build_process( + temp_dir.path().join("testing"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + + // Test with a directory path + let message = get_messages(&temp_dir.path().join("testing"))?; + assert_contract_metadata_parsed(message)?; + + // Test with a metadata file path + let message = get_messages(¤t_dir.join("./tests/files/testing.contract"))?; + assert_contract_metadata_parsed(message)?; + + Ok(()) + } + + #[test] + fn get_message_work() -> Result<()> { + 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"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + assert!(matches!( + get_message(&temp_dir.path().join("testing"), "wrong_flip"), + Err(Error::InvalidMessageName(name)) if name == "wrong_flip".to_string())); + let message = get_message(&temp_dir.path().join("testing"), "specific_flip")?; + assert_eq!(message.label, "specific_flip"); + assert_eq!(message.docs, " A message for testing, flips the value of the stored `bool` with `new_value` and is payable"); + // assert parsed arguments + assert_eq!(message.args.len(), 2); + assert_eq!(message.args[0].label, "new_value".to_string()); + assert_eq!(message.args[0].type_name, "bool".to_string()); + assert_eq!(message.args[1].label, "number".to_string()); + assert_eq!(message.args[1].type_name, "Option: None, Some(u32)".to_string()); + Ok(()) + } + + #[test] + fn get_constructors_work() -> Result<()> { + 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"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + let constructor = get_constructors(&temp_dir.path().join("testing"))?; + assert_eq!(constructor.len(), 2); + assert_eq!(constructor[0].label, "new"); + assert_eq!( + constructor[0].docs, + "Constructor that initializes the `bool` value to the given `init_value`." + ); + assert_eq!(constructor[1].label, "default"); + assert_eq!( + constructor[1].docs, + "Constructor that initializes the `bool` value to `false`. Constructors can delegate to other constructors." + ); + // assert parsed arguments + assert_eq!(constructor[0].args.len(), 1); + assert_eq!(constructor[0].args[0].label, "init_value".to_string()); + assert_eq!(constructor[0].args[0].type_name, "bool".to_string()); + assert_eq!(constructor[1].args.len(), 2); + assert_eq!(constructor[1].args[0].label, "init_value".to_string()); + assert_eq!(constructor[1].args[0].type_name, "bool".to_string()); + assert_eq!(constructor[1].args[1].label, "number".to_string()); + assert_eq!(constructor[1].args[1].type_name, "Option: None, Some(u32)".to_string()); + Ok(()) + } + + #[test] + fn get_constructor_work() -> Result<()> { + 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"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + assert!(matches!( + get_constructor(&temp_dir.path().join("testing"), "wrong_constructor"), + Err(Error::InvalidConstructorName(name)) if name == "wrong_constructor".to_string())); + let constructor = get_constructor(&temp_dir.path().join("testing"), "default")?; + assert_eq!(constructor.label, "default"); + assert_eq!( + constructor.docs, + "Constructor that initializes the `bool` value to `false`. Constructors can delegate to other constructors." + ); + // assert parsed arguments + assert_eq!(constructor.args.len(), 2); + assert_eq!(constructor.args[0].label, "init_value".to_string()); + assert_eq!(constructor.args[0].type_name, "bool".to_string()); + assert_eq!(constructor.args[1].label, "number".to_string()); + assert_eq!(constructor.args[1].type_name, "Option: None, Some(u32)".to_string()); + Ok(()) + } + + #[test] + fn process_function_args_work() -> Result<()> { + 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"), + current_dir.join("./tests/files/testing.contract"), + current_dir.join("./tests/files/testing.json"), + )?; + assert!(matches!( + process_function_args(temp_dir.path().join("testing"),"wrong_flip", Vec::new(), FunctionType::Message), + Err(Error::InvalidMessageName(error)) if error == "wrong_flip".to_string())); + assert!(matches!( + process_function_args( + temp_dir.path().join("testing"), + "specific_flip", + Vec::new(), + FunctionType::Message + ), + Err(Error::IncorrectArguments {expected, provided }) if expected == 2 && provided == 0 + )); + assert_eq!( + process_function_args( + temp_dir.path().join("testing"), + "specific_flip", + ["true".to_string(), "2".to_string()].to_vec(), + FunctionType::Message + )?, + ["true".to_string(), "Some(2)".to_string()] + ); + assert_eq!( + process_function_args( + temp_dir.path().join("testing"), + "specific_flip", + ["true".to_string(), "".to_string()].to_vec(), + FunctionType::Message + )?, + ["true".to_string(), "None".to_string()] + ); + + // Test constructors + assert!(matches!( + process_function_args(temp_dir.path().join("testing"),"wrong_constructor", Vec::new(), FunctionType::Constructor), + Err(Error::InvalidConstructorName(error)) if error == "wrong_constructor".to_string())); + assert!(matches!( + process_function_args( + temp_dir.path().join("testing"), + "default", + Vec::new(), + FunctionType::Constructor + ), + Err(Error::IncorrectArguments {expected, provided }) if expected == 2 && provided == 0 + )); + assert_eq!( + process_function_args( + temp_dir.path().join("testing"), + "default", + ["true".to_string(), "2".to_string()].to_vec(), + FunctionType::Constructor + )?, + ["true".to_string(), "Some(2)".to_string()] + ); + assert_eq!( + process_function_args( + temp_dir.path().join("testing"), + "default", + ["true".to_string(), "".to_string()].to_vec(), + FunctionType::Constructor + )?, + ["true".to_string(), "None".to_string()] + ); + Ok(()) + } +} diff --git a/crates/pop-contracts/src/utils/mod.rs b/crates/pop-contracts/src/utils/mod.rs index 357c6608..ad49e2dc 100644 --- a/crates/pop-contracts/src/utils/mod.rs +++ b/crates/pop-contracts/src/utils/mod.rs @@ -1,4 +1,5 @@ // SPDX-License-Identifier: GPL-3.0 pub mod helpers; +pub mod metadata; pub mod signer; diff --git a/crates/pop-contracts/tests/files/testing.contract b/crates/pop-contracts/tests/files/testing.contract index 8701e656..fbd8e6fb 100644 --- a/crates/pop-contracts/tests/files/testing.contract +++ b/crates/pop-contracts/tests/files/testing.contract @@ -1 +1 @@ -{"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 +{"source":{"hash":"0xf4f0cafd08d8e362141b3c64e3c651ad6a38225dbf0b66f691c15bb0ea00eac3","language":"ink! 5.0.0","compiler":"rustc 1.78.0","wasm":"0x0061736d0100000001270760027f7f0060037f7f7f017f60000060047f7f7f7f017f60037f7f7f0060017f017f6000017f027406057365616c310b6765745f73746f726167650003057365616c3005696e7075740000057365616c320b7365745f73746f726167650003057365616c300b7365616c5f72657475726e0004057365616c301176616c75655f7472616e73666572726564000003656e76066d656d6f7279020102100313120101010100050000040006000200000002020616037f01418080040b7f00418080050b7f00418080050b0711020463616c6c0015066465706c6f7900160ad910122b01017f037f2002200346047f200005200020036a200120036a2d00003a0000200341016a21030c010b0b0b6f01017f0240200020014d04402000210303402002450d02200320012d00003a0000200341016a2103200141016a2101200241016b21020c000b000b200041016b2103200141016b210103402002450d01200220036a200120026a2d00003a0000200241016b21020c000b000b20000b2501017f037f2002200346047f200005200020036a20013a0000200341016a21030c010b0b0b3f01027f0340200245044041000f0b200241016b210220012d0000210320002d00002104200141016a2101200041016a210020032004460d000b200420036b0bc00101057f230041106b22022400410221044104210502402001100a220641ff01714102460d00200241086a2001100b20022d00080d000240024020022d000922030e020100020b20012802042203410449047f4101052001200341046b36020420012001280200220141046a3602002001280000210341000b2101200220033602042002200136020020022802000d0141012103200228020421040b20002003360204200020063a0000410821050b200020056a2004360200200241106a24000b3f01027f230041106b22012400200141086a2000100b20012d0009210020012d00082102200141106a240041024101410220004101461b410020001b20021b0b3c01017f200020012802042202047f2001200241016b36020420012001280200220141016a36020020012d00000520010b3a000120002002453a00000b2601017f230041106b220224002002200036020c20012002410c6a4104100d200241106a24000b4801027f024002402000280208220320026a22042003490d00200420002802044b0d00200420036b2002470d01200028020020036a2001200210051a200020043602080f0b000b000b2601017f230041106b22022400200220003a000f20012002410f6a4101100d200241106a24000b6102027f027e230041206b22002400200041106a22014200370300200042003703082000411036021c200041086a2000411c6a1004200028021c41114f0440000b2001290300210220002903082103200041206a2400410541042002200384501b0b3c01027f027f200145044041808004210141010c010b410121024180800441013a000041818004210141020b2103200120023a0000200020031013000b12004180800441003b0100410041021013000b920101057f230041106b220224002002428080013702082002418080043602044100200241046a2204100c024020022802082206200228020c2203490d00200228020421052002410036020c2002200620036b3602082002200320056a36020420012004100e20002004100c200228020c220020022802084b0d00200520032002280204200010021a200241106a24000f0b000b0d0020004180800420011003000b08002000200110120bb60501087f230041206b2200240020004180800136021441808004200041146a100102400240024020002802142201418180014f0d0041042104027f410420014104490d001a20004184800436020c2000200141046b360210418380042d00002101418280042d00002102418180042d00002103024002400240418080042d00002205412f470440200541ec00460d014104200541e300470d041a41042003413a470d041a4104200241a501470d041a41042202200141d100470d041a410221010c020b41042003418601470d031a4104200241db00470d031a41042202200141d901470d031a410321010c010b41042003410f470d021a41042002411d470d021a4104200141f701470d021a200041146a2000410c6a1009200028021822014102460d01200028021c210420002d001421020b20002001360204200020023a000041080c010b41040b20006a2004360200200028020422024104460d012000280208210520002d000021072000428080013702182000418080043602144100200041146a2203100c20002802182206200028021c2201490d00200028021421042000200620016b220636021420042001200120046a2201200310002000280214220420064b720d0020002004360218200020013602142003100a220141ff01714102460d002000280218220341034d2003410447720d00200028021428000021000240024002404102200241026b2203200341024f1b41016b0e020100020b2005200020021b20074100471014410041001010000b100f41ff01714105470d01230041106b220024002000418080043602044180800441003a00002000428080818010370208200141ff0171410047200041046a100e200028020c2200418180014f0440000b410020001013000b100f41ff01714105460d020b000b410141011010000b2000200141ff0171451014410041001010000be90201067f230041206b22002400024002400240100f41ff01714105470d0020004180800136021441808004200041146a100120002802142201418180014f0d004104210220014104490d0120004184800436020c2000200141046b360210418380042d00002101418280042d00002103418180042d00002104027f418080042d0000220541ed014704402005419b0147200441ae0147722003419d0147200141de004772720d0341022103410322012000410c6a100a220441ff01714102470d011a0c040b200441cb00472003419d0147722001411b47720d02200041146a2000410c6a1009200028021822034102460d0220002d00142104200028021c0b210120002003360204200020043a0000410821020c020b000b410321010b200020026a2001360200024020002802042201410347044020002d0000210220014102460d012000280208410020011b200241004710121011000b410141011010000b4100200210121011000b","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":{"def":{"primitive":"u32"}}},{"id":2,"type":{"path":["testing","testing","Testing"],"def":{"composite":{"fields":[{"name":"value","type":0,"typeName":",>>::Type"},{"name":"number","type":1,"typeName":",>>::Type"}]}}}},{"id":3,"type":{"path":["Result"],"params":[{"name":"T","type":4},{"name":"E","type":5}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":4}],"index":0},{"name":"Err","fields":[{"type":5}],"index":1}]}}}},{"id":4,"type":{"def":{"tuple":[]}}},{"id":5,"type":{"path":["ink_primitives","LangError"],"def":{"variant":{"variants":[{"name":"CouldNotReadInput","index":1}]}}}},{"id":6,"type":{"path":["Option"],"params":[{"name":"T","type":1}],"def":{"variant":{"variants":[{"name":"None","index":0},{"name":"Some","fields":[{"type":1}],"index":1}]}}}},{"id":7,"type":{"path":["Result"],"params":[{"name":"T","type":0},{"name":"E","type":5}],"def":{"variant":{"variants":[{"name":"Ok","fields":[{"type":0}],"index":0},{"name":"Err","fields":[{"type":5}],"index":1}]}}}},{"id":8,"type":{"path":["ink_primitives","types","AccountId"],"def":{"composite":{"fields":[{"type":9,"typeName":"[u8; 32]"}]}}}},{"id":9,"type":{"def":{"array":{"len":32,"type":10}}}},{"id":10,"type":{"def":{"primitive":"u8"}}},{"id":11,"type":{"def":{"primitive":"u128"}}},{"id":12,"type":{"path":["ink_primitives","types","Hash"],"def":{"composite":{"fields":[{"type":9,"typeName":"[u8; 32]"}]}}}},{"id":13,"type":{"def":{"primitive":"u64"}}},{"id":14,"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}}},{"name":"number","layout":{"leaf":{"key":"0x00000000","ty":1}}}]}},"ty":2}},"spec":{"constructors":[{"label":"new","selector":"0x9bae9d5e","payable":false,"args":[{"label":"init_value","type":{"type":0,"displayName":["bool"]}}],"returnType":{"type":3,"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":[{"label":"init_value","type":{"type":0,"displayName":["bool"]}},{"label":"number","type":{"type":6,"displayName":["Option"]}}],"returnType":{"type":3,"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":3,"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":7,"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"]}},{"label":"number","type":{"type":6,"displayName":["Option"]}}],"returnType":{"type":3,"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":5,"displayName":["ink","LangError"]},"environment":{"accountId":{"type":8,"displayName":["AccountId"]},"balance":{"type":11,"displayName":["Balance"]},"hash":{"type":12,"displayName":["Hash"]},"timestamp":{"type":13,"displayName":["Timestamp"]},"blockNumber":{"type":1,"displayName":["BlockNumber"]},"chainExtension":{"type":14,"displayName":["ChainExtension"]},"maxEventTopics":4,"staticBufferSize":16384}}} \ No newline at end of file diff --git a/crates/pop-contracts/tests/files/testing.json b/crates/pop-contracts/tests/files/testing.json new file mode 100644 index 00000000..61921986 --- /dev/null +++ b/crates/pop-contracts/tests/files/testing.json @@ -0,0 +1,528 @@ +{ + "source": { + "hash": "0xf4f0cafd08d8e362141b3c64e3c651ad6a38225dbf0b66f691c15bb0ea00eac3", + "language": "ink! 5.0.0", + "compiler": "rustc 1.78.0", + "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": { + "def": { + "primitive": "u32" + } + } + }, + { + "id": 2, + "type": { + "path": [ + "testing", + "testing", + "Testing" + ], + "def": { + "composite": { + "fields": [ + { + "name": "value", + "type": 0, + "typeName": ",>>::Type" + }, + { + "name": "number", + "type": 1, + "typeName": ",>>::Type" + } + ] + } + } + } + }, + { + "id": 3, + "type": { + "path": [ + "Result" + ], + "params": [ + { + "name": "T", + "type": 4 + }, + { + "name": "E", + "type": 5 + } + ], + "def": { + "variant": { + "variants": [ + { + "name": "Ok", + "fields": [ + { + "type": 4 + } + ], + "index": 0 + }, + { + "name": "Err", + "fields": [ + { + "type": 5 + } + ], + "index": 1 + } + ] + } + } + } + }, + { + "id": 4, + "type": { + "def": { + "tuple": [] + } + } + }, + { + "id": 5, + "type": { + "path": [ + "ink_primitives", + "LangError" + ], + "def": { + "variant": { + "variants": [ + { + "name": "CouldNotReadInput", + "index": 1 + } + ] + } + } + } + }, + { + "id": 6, + "type": { + "path": [ + "Option" + ], + "params": [ + { + "name": "T", + "type": 1 + } + ], + "def": { + "variant": { + "variants": [ + { + "name": "None", + "index": 0 + }, + { + "name": "Some", + "fields": [ + { + "type": 1 + } + ], + "index": 1 + } + ] + } + } + } + }, + { + "id": 7, + "type": { + "path": [ + "Result" + ], + "params": [ + { + "name": "T", + "type": 0 + }, + { + "name": "E", + "type": 5 + } + ], + "def": { + "variant": { + "variants": [ + { + "name": "Ok", + "fields": [ + { + "type": 0 + } + ], + "index": 0 + }, + { + "name": "Err", + "fields": [ + { + "type": 5 + } + ], + "index": 1 + } + ] + } + } + } + }, + { + "id": 8, + "type": { + "path": [ + "ink_primitives", + "types", + "AccountId" + ], + "def": { + "composite": { + "fields": [ + { + "type": 9, + "typeName": "[u8; 32]" + } + ] + } + } + } + }, + { + "id": 9, + "type": { + "def": { + "array": { + "len": 32, + "type": 10 + } + } + } + }, + { + "id": 10, + "type": { + "def": { + "primitive": "u8" + } + } + }, + { + "id": 11, + "type": { + "def": { + "primitive": "u128" + } + } + }, + { + "id": 12, + "type": { + "path": [ + "ink_primitives", + "types", + "Hash" + ], + "def": { + "composite": { + "fields": [ + { + "type": 9, + "typeName": "[u8; 32]" + } + ] + } + } + } + }, + { + "id": 13, + "type": { + "def": { + "primitive": "u64" + } + } + }, + { + "id": 14, + "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 + } + } + }, + { + "name": "number", + "layout": { + "leaf": { + "key": "0x00000000", + "ty": 1 + } + } + } + ] + } + }, + "ty": 2 + } + }, + "spec": { + "constructors": [ + { + "label": "new", + "selector": "0x9bae9d5e", + "payable": false, + "args": [ + { + "label": "init_value", + "type": { + "type": 0, + "displayName": [ + "bool" + ] + } + } + ], + "returnType": { + "type": 3, + "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": [ + { + "label": "init_value", + "type": { + "type": 0, + "displayName": [ + "bool" + ] + } + }, + { + "label": "number", + "type": { + "type": 6, + "displayName": [ + "Option" + ] + } + } + ], + "returnType": { + "type": 3, + "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": 3, + "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": 7, + "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" + ] + } + }, + { + "label": "number", + "type": { + "type": 6, + "displayName": [ + "Option" + ] + } + } + ], + "returnType": { + "type": 3, + "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": 5, + "displayName": [ + "ink", + "LangError" + ] + }, + "environment": { + "accountId": { + "type": 8, + "displayName": [ + "AccountId" + ] + }, + "balance": { + "type": 11, + "displayName": [ + "Balance" + ] + }, + "hash": { + "type": 12, + "displayName": [ + "Hash" + ] + }, + "timestamp": { + "type": 13, + "displayName": [ + "Timestamp" + ] + }, + "blockNumber": { + "type": 1, + "displayName": [ + "BlockNumber" + ] + }, + "chainExtension": { + "type": 14, + "displayName": [ + "ChainExtension" + ] + }, + "maxEventTopics": 4, + "staticBufferSize": 16384 + } + } +} \ No newline at end of file diff --git a/crates/pop-contracts/tests/files/testing.wasm b/crates/pop-contracts/tests/files/testing.wasm new file mode 100644 index 0000000000000000000000000000000000000000..430888669464e770d310139f29e8af1ebf865ba4 GIT binary patch literal 3710 zcmb_fO>ZPu6}|7Hx~pC8aXC1S84uE{iiI30?T;bxhzSyYkzz9%A+uqL>>j(uxZBen zSNAv>DN1{y2#GCQ#0H5i8-4|1#S$U0VGA2J2nk5ed9T|aN+LFl<*Daa@1A?_x#!;3 zE#2WWB_isb)`1?6<-m-`V>LbyIj~r9$(_kxa-hOX?BcK*>ro>aR^8Lh?6?~34@YNp z_qY-h_BN7}!Sfd*k?`03Z2099*z2C|*VX7nJrEnM<}bRZFRJ}f-5m^{R&`w+iI{3| z(RfxpJF8!6rE~MfVxsU*8i-r0G*gWXMC-<)87qHzX|u8Tf4@rjxcoH9n)BKoc27^` zn~kID`RUn9xjXaUrN!69DQ9-iF>FHZX2 zoR>+jFisVgZwwAP!(-anj>tn1XT9p-t>%NKnKQ28l0E=GJ}*;j0$#bKS0;e_0Jce? zBG3=pKyzwU%a%rT-u4QF$cLE~1m_Y$*tietQQ7oM=S2&#HTzsOorQ5>r*N=#*0nAI zW@%k>RDTU}1F;kc(;+~lt6c+01Q;9BQP zJgAPeA85lN+6ms3h;YK42}eV!npF4877Hr2Db zo1sVqd%(S+6dZ{Xi7JaWKpB-nVI%>cE9+891aw{lswp5C9Yw7emt0J>a*ge>9z@cj zRuWVRe?gRi06;+FfnDStV+^_9(9IMFNhU!hR7ew%LA;<2+=j*qX$FO$nQk`~_=L+q zAOduHLf{NG0m>?slmMGb2c>}f|JhX3Rhh-6g7pXu2A)#HWkXXXE}J%0fQwC)UNu$n zwM}(J6awT7LDNqe+G~@s8|1L5Ch3J%Z)RTUu^yXk1foA^tGm!0l!j3SPS{I)U*7pqR3A@Pcalxt>?~of6Z>3xev%T=q~D zUvjcc{Quy}|HOVv&+6@9Ka1wd?*zH5>e(nGAUK)9ct_o%b`_XbX5B9xmfqyPQZ}u9mW9#KT5f2sSZ%Fn% zW{TMk3i+;ps?gQoUWcPBpZ`zT*BPgR(<~@br|~at$=d1nFE7;toq|O`fqOO#+PSp( zyxyi%)3U+GY#G=L+J#yWBq+iFN3VcH#*YSN?Ub`QB?f?K2SA{(00|3>VnD*09Z7|g ze%6NW(I`f;G$fxdX20fj0=74%Y$Mbyy%J7}U%_X9TE;Wxrm8s z#1kk+nYtwZ29+1i8^3f>pzVErftnbfUK9=XI1HD1_&8ufn;XBr3k&1_D)IR@*+p&s zsdj5jZwvxIw~Ji3_||S+w=L34^BKBWMM#(tK>}|Rw<9a~G=kPw{*OS3<_`fB?SBKz zw!x@Tv%GQrpVais&*&{4`m^8=Mr~}k>u`A=gv6H+{{rI2^$_oPM6!fwh2f?c)dEtJ zSoYN^M=Kl{q*S45A#$|is(!oFY$2>x7(Oz?t&yMx=qimL=!DZPo!Gse;VlgqpFk1T<3Se(<8FPCwND(&g%kyOvL!IYK=m*JP zmn?rRu%GO+T7t~3Wx7;Cb352U zVE2gGk%gcx0Q?1J5#YDif+qqbh$}I~I=K&n)}dPr8cqZo|G0B3lr{xdIV0 z)*uXc#b5}-hXn@X=lNp3FeRT=fVksU_Ni86)@J`w>pvb=^>Dp=T7C4S>JHY|4!ia7 z*?L_apA1L!%k^P>xPCG?syR+XyDr;P_-vt)C6{4}aP{8LU5f@f6>v*6N{%lIJ4w6a3!6@BhXB^KN}Idf6G` hi&oV+JUglmJKtH`THCxIVhI~`eF@)v`23K*zW{;(I0gU! literal 0 HcmV?d00001