Skip to content

Commit

Permalink
feat: allow users to specify custom contract metadata files (#347)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
AlexD10S authored Nov 26, 2024
1 parent a6f8247 commit 6761f6b
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 74 deletions.
179 changes: 133 additions & 46 deletions crates/pop-cli/src/commands/call/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const DEFAULT_PAYABLE_VALUE: &str = "0";

#[derive(Args, Clone)]
pub struct CallContractCommand {
/// Path to the contract build directory.
/// Path to the contract build directory or a contract artifact.
#[arg(short = 'p', long)]
path: Option<PathBuf>,
/// The address of the contract to call.
Expand Down Expand Up @@ -68,8 +68,6 @@ pub struct CallContractCommand {
impl CallContractCommand {
/// Executes the command.
pub(crate) async fn execute(mut self) -> Result<()> {
// Ensure contract is built.
self.ensure_contract_built(&mut cli::Cli).await?;
// 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.
Expand Down Expand Up @@ -119,27 +117,33 @@ impl CallContractCommand {
}

/// Checks if the contract has been built; if not, builds it.
/// If the path is a contract artifact file, skips the build process
async fn ensure_contract_built(&self, cli: &mut impl Cli) -> Result<()> {
// Check if build exists in the specified "Contract build directory"
if !has_contract_been_built(self.path.as_deref()) {
// Build the contract in release mode
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!(
// The path is expected to be set. If it is not, exit early without attempting to build the
// contract.
let Some(path) = self.path.as_deref() else { return Ok(()) };
// Check if the path is a file or the build exists in the specified "Contract build
// directory"
if path.is_file() || has_contract_been_built(self.path.as_deref()) {
return Ok(());
}
// 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()
));
}
},
};
spinner.stop(format!(
"Your contract artifacts are ready. You can find them in: {}",
result.target_directory.display()
));
Ok(())
}

Expand All @@ -156,25 +160,21 @@ impl CallContractCommand {
}

// Resolve path.
let contract_path = match self.path.as_ref() {
None => {
let path = Some(PathBuf::from("./"));
if has_contract_been_built(path.as_deref()) {
self.path = path;
} else {
// Prompt for path.
let input_path: String = cli
.input("Where is your project located?")
.placeholder("./")
.default_input("./")
.interact()?;
self.path = Some(PathBuf::from(input_path));
}
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 is prompted when None; qed");

self.path.as_ref().unwrap()
},
Some(p) => p,
};
// Ensure contract is built.
self.ensure_contract_built(&mut cli::Cli).await?;

// Parse the contract metadata provided. If there is an error, do not prompt for more.
let messages = match get_messages(contract_path) {
Expand Down Expand Up @@ -208,7 +208,6 @@ impl CallContractCommand {
Ok(_) => Ok(()),
Err(_) => Err("Invalid address."),
})
.default_input("5DYs7UGBm2LuX4ryvyqfksozNAW5V47tPbGiVgnjYWCZ29bt")
.interact()?;
self.contract = Some(contract_address);
};
Expand Down Expand Up @@ -434,7 +433,7 @@ mod tests {
use super::*;
use crate::cli::MockCli;
use pop_contracts::{mock_build_process, new_environment};
use std::env;
use std::{env, fs::write};
use url::Url;

#[tokio::test]
Expand Down Expand Up @@ -508,6 +507,60 @@ mod tests {
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")?;
Expand Down Expand Up @@ -608,7 +661,7 @@ mod tests {
"wss://rpc1.paseo.popnetwork.xyz".into(),
)
.expect_input(
"Where is your project located?",
"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",
Expand Down Expand Up @@ -694,7 +747,7 @@ mod tests {
"wss://rpc1.paseo.popnetwork.xyz".into(),
)
.expect_input(
"Where is your project located?",
"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",
Expand Down Expand Up @@ -779,7 +832,7 @@ mod tests {
"wss://rpc1.paseo.popnetwork.xyz".into(),
)
.expect_input(
"Where is your project located?",
"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",
Expand Down Expand Up @@ -828,8 +881,23 @@ mod tests {
#[tokio::test]
async fn guide_user_to_call_contract_fails_not_build() -> Result<()> {
let temp_dir = new_environment("testing")?;
let mut cli = MockCli::new();
assert!(matches!(CallContractCommand {
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,
Expand All @@ -842,7 +910,26 @@ mod tests {
dry_run: false,
execute: false,
dev_mode: false,
}.configure(&mut cli, false).await, Err(message) if message.to_string().contains("Unable to fetch contract metadata: Failed to find any contract artifacts in target directory.")));
};
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()
}

Expand Down
7 changes: 4 additions & 3 deletions crates/pop-cli/src/common/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,10 @@ pub fn has_contract_been_built(path: Option<&Path>) -> bool {
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()
manifest
.package
.map(|p| project_path.join(format!("target/ink/{}.contract", p.name())).exists())
.unwrap_or_default()
}

#[cfg(test)]
Expand Down
44 changes: 38 additions & 6 deletions crates/pop-contracts/src/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
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};
Expand Down Expand Up @@ -54,13 +54,25 @@ pub async fn set_up_call(
call_opts: CallOpts,
) -> Result<CallExec<DefaultConfig, DefaultEnvironment, Keypair>, Error> {
let token_metadata = TokenMetadata::query::<DefaultConfig>(&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<<DefaultEnvironment as Environment>::Balance> =
parse_balance(&call_opts.value)?;
Expand Down Expand Up @@ -203,6 +215,26 @@ mod tests {
Ok(())
}

#[tokio::test]
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(current_dir.join("./tests/files/testing.json")),
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_error_contract_not_build() -> Result<()> {
let temp_dir = new_environment("testing")?;
Expand Down
Loading

0 comments on commit 6761f6b

Please sign in to comment.