Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dry-run flag to estimate gas #203

Merged
merged 13 commits into from
Jun 26, 2024
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ Some of the options available are:

- You also can specify the url of your node with `--url ws://your-endpoint`, by default it is
using `ws://localhost:9944`.
- To perform a dry-run via RPC to estimate the gas usage without submitting a transaction use the `--dry-run` flag.

For more information about the options,
check [cargo-contract documentation](https://github.com/paritytech/cargo-contract/blob/master/crates/extrinsics/README.md#instantiate)
Expand Down
23 changes: 21 additions & 2 deletions crates/pop-cli/src/commands/call/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ pub struct CallContractCommand {
/// Submit an extrinsic for on-chain execution.
#[clap(short('x'), long)]
execute: bool,
/// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction.
#[clap(long, conflicts_with = "execute")]
dry_run: bool,
}

impl CallContractCommand {
Expand All @@ -73,6 +76,22 @@ impl CallContractCommand {
})
.await?;

if self.dry_run {
let spinner = cliclack::spinner();
spinner.start("Doing a dry run to estimate the gas...");
match dry_run_gas_estimate_call(&call_exec).await {
Ok(w) => {
log::info(format!("Gas limit: {:?}", w))?;
log::warning("Your call has not been executed.")?;
},
Err(e) => {
spinner.error(format!("{e}"));
outro_cancel("Call failed.")?;
},
};
return Ok(());
}

if !self.execute {
let spinner = cliclack::spinner();
spinner.start("Calling the contract...");
Expand All @@ -93,12 +112,12 @@ impl CallContractCommand {
spinner.start("Doing a dry run to estimate the gas...");
weight_limit = match dry_run_gas_estimate_call(&call_exec).await {
Ok(w) => {
log::info(format!("Gas limit {:?}", w))?;
log::info(format!("Gas limit: {:?}", w))?;
w
},
Err(e) => {
spinner.error(format!("{e}"));
outro_cancel("Deployment failed.")?;
outro_cancel("Call failed.")?;
return Ok(());
},
};
Expand Down
29 changes: 16 additions & 13 deletions crates/pop-cli/src/commands/up/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub struct UpContractCommand {
/// - with a password "//Alice///SECRET_PASSWORD"
#[clap(name = "suri", long, short, default_value = "//Alice")]
suri: String,
/// Perform a dry-run via RPC to estimate the gas usage. This does not submit a transaction.
#[clap(long)]
dry_run: bool,
/// Before start a local node, do not ask the user for confirmation.
#[clap(short('y'), long)]
skip_confirm: bool,
Expand Down Expand Up @@ -92,8 +95,6 @@ impl UpContractCommand {
// if build exists then proceed
intro(format!("{}: Deploy a smart contract", style(" Pop CLI ").black().on_magenta()))?;

println!("{}: Deploying a smart contract", style(" Pop CLI ").black().on_magenta());

let instantiate_exec = set_up_deployment(UpOpts {
path: self.path.clone(),
constructor: self.constructor.clone(),
Expand All @@ -115,7 +116,7 @@ impl UpContractCommand {
spinner.start("Doing a dry run to estimate the gas...");
weight_limit = match dry_run_gas_estimate_instantiate(&instantiate_exec).await {
Ok(w) => {
log::info(format!("Gas limit {:?}", w))?;
log::info(format!("Gas limit: {:?}", w))?;
w
},
Err(e) => {
Expand All @@ -125,16 +126,18 @@ impl UpContractCommand {
},
};
}
let spinner = cliclack::spinner();
spinner.start("Uploading and instantiating the contract...");
let contract_address = instantiate_smart_contract(instantiate_exec, weight_limit)
.await
.map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?;
spinner.stop(format!(
"Contract deployed and instantiated: The Contract Address is {:?}",
contract_address
));
outro("Deployment complete")?;
if !self.dry_run {
Daanvdplas marked this conversation as resolved.
Show resolved Hide resolved
let spinner = cliclack::spinner();
spinner.start("Uploading and instantiating the contract...");
let contract_address = instantiate_smart_contract(instantiate_exec, weight_limit)
.await
.map_err(|err| anyhow!("{} {}", "ERROR:", format!("{err:?}")))?;
spinner.stop(format!(
"Contract deployed and instantiated: The Contract Address is {:?}",
contract_address
));
outro("Deployment complete")?;
}
Ok(())
}
}
159 changes: 103 additions & 56 deletions crates/pop-contracts/src/call.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// SPDX-License-Identifier: GPL-3.0

use crate::{
errors::Error,
utils::{
helpers::{get_manifest_path, parse_account, parse_balance},
signer::create_signer,
},
};
use anyhow::Context;
use contract_build::Verbosity;
use contract_extrinsics::{
Expand All @@ -13,11 +20,6 @@ use subxt::{Config, PolkadotConfig as DefaultConfig};
use subxt_signer::sr25519::Keypair;
use url::Url;

AlexD10S marked this conversation as resolved.
Show resolved Hide resolved
use crate::utils::{
helpers::{get_manifest_path, parse_account, parse_balance},
signer::create_signer,
};

/// Attributes for the `call` command.
pub struct CallOpts {
/// Path to the contract build folder.
Expand Down Expand Up @@ -84,28 +86,22 @@ pub async fn set_up_call(
///
pub async fn dry_run_call(
call_exec: &CallExec<DefaultConfig, DefaultEnvironment, Keypair>,
) -> anyhow::Result<String> {
) -> Result<String, Error> {
let call_result = call_exec.call_dry_run().await?;
match call_result.result {
Ok(ref ret_val) => {
let value = call_exec
Ok(ref ret_val) => {
let value = call_exec
.transcoder()
.decode_message_return(
call_exec.message(),
&mut &ret_val.data[..],
)
.context(format!(
"Failed to decode return value {:?}",
&ret_val
))?;
.decode_message_return(call_exec.message(), &mut &ret_val.data[..])
.context(format!("Failed to decode return value {:?}", &ret_val))?;
Ok(value.to_string())
}
Err(ref _err) => {
Err(anyhow::anyhow!(
"Pre-submission dry-run failed. Add gas_limit and proof_size manually to skip this step."
))
}
}
},
Err(ref err) => {
let error_variant =
ErrorVariant::from_dispatch_error(err, &call_exec.client().metadata())?;
Err(Error::DryRunCallContractError(format!("{error_variant}")))
},
}
}

/// Estimate the gas required for a contract call without modifying the state of the blockchain.
Expand All @@ -116,25 +112,23 @@ pub async fn dry_run_call(
///
pub async fn dry_run_gas_estimate_call(
call_exec: &CallExec<DefaultConfig, DefaultEnvironment, Keypair>,
) -> anyhow::Result<Weight> {
) -> Result<Weight, Error> {
let call_result = call_exec.call_dry_run().await?;
match call_result.result {
Ok(_) => {
// use user specified values where provided, otherwise use the estimates
let ref_time = call_exec
.gas_limit()
.unwrap_or_else(|| call_result.gas_required.ref_time());
let proof_size = call_exec
.proof_size()
.unwrap_or_else(|| call_result.gas_required.proof_size());
Ok(Weight::from_parts(ref_time, proof_size))
}
Err(ref _err) => {
Err(anyhow::anyhow!(
"Pre-submission dry-run failed. Add gas_limit and proof_size manually to skip this step."
))
}
}
Ok(_) => {
// Use user specified values where provided, otherwise use the estimates.
let ref_time =
call_exec.gas_limit().unwrap_or_else(|| call_result.gas_required.ref_time());
let proof_size =
call_exec.proof_size().unwrap_or_else(|| call_result.gas_required.proof_size());
Ok(Weight::from_parts(ref_time, proof_size))
},
Err(ref err) => {
let error_variant =
ErrorVariant::from_dispatch_error(err, &call_exec.client().metadata())?;
Err(Error::DryRunCallContractError(format!("{error_variant}")))
},
}
}

/// Call a smart contract on the blockchain.
Expand All @@ -161,36 +155,42 @@ pub async fn call_smart_contract(
Ok(output)
}

#[cfg(feature = "unit_contract")]
#[cfg(test)]
mod tests {
use super::*;
use crate::{build_smart_contract, create_smart_contract};
use anyhow::{Error, Result};
use std::fs;
use tempfile::TempDir;
use crate::{create_smart_contract, errors::Error, Template};
use anyhow::Result;
use std::{env, fs};

const CONTRACTS_NETWORK_URL: &str = "wss://rococo-contracts-rpc.polkadot.io";

fn generate_smart_contract_test_environment() -> Result<tempfile::TempDir, Error> {
fn generate_smart_contract_test_environment() -> Result<tempfile::TempDir> {
let temp_dir = tempfile::tempdir().expect("Could not create temp dir");
let temp_contract_dir = temp_dir.path().join("test_contract");
let temp_contract_dir = temp_dir.path().join("testing");
fs::create_dir(&temp_contract_dir)?;
create_smart_contract("test_contract", temp_contract_dir.as_path())?;
create_smart_contract("testing", temp_contract_dir.as_path(), &Template::Standard)?;
Ok(temp_dir)
}
fn build_smart_contract_test_environment(temp_dir: &TempDir) -> Result<(), Error> {
build_smart_contract(&Some(temp_dir.path().join("test_contract")), true)?;
// Function that mocks the build process generating the contract artifacts.
fn mock_build_process(temp_contract_dir: PathBuf) -> Result<(), Error> {
// Create a target directory
let target_contract_dir = temp_contract_dir.join("target");
fs::create_dir(&target_contract_dir)?;
fs::create_dir(&target_contract_dir.join("ink"))?;
// Copy a mocked testing.contract file inside the target directory
let current_dir = env::current_dir().expect("Failed to get current directory");
let contract_file = current_dir.join("tests/files/testing.contract");
fs::copy(contract_file, &target_contract_dir.join("ink/testing.contract"))?;
Ok(())
}

#[tokio::test]
async fn test_set_up_call() -> Result<(), Error> {
async fn test_set_up_call() -> Result<()> {
let temp_dir = generate_smart_contract_test_environment()?;
build_smart_contract_test_environment(&temp_dir)?;
mock_build_process(temp_dir.path().join("testing"))?;

let call_opts = CallOpts {
path: Some(temp_dir.path().join("test_contract")),
path: Some(temp_dir.path().join("testing")),
contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(),
message: "get".to_string(),
args: [].to_vec(),
Expand All @@ -207,10 +207,10 @@ mod tests {
}

#[tokio::test]
async fn test_set_up_call_error_contract_not_build() -> Result<(), Error> {
async fn test_set_up_call_error_contract_not_build() -> Result<()> {
let temp_dir = generate_smart_contract_test_environment()?;
let call_opts = CallOpts {
path: Some(temp_dir.path().join("test_contract")),
path: Some(temp_dir.path().join("testing")),
contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(),
message: "get".to_string(),
args: [].to_vec(),
Expand All @@ -229,7 +229,7 @@ mod tests {
Ok(())
}
#[tokio::test]
async fn test_set_up_call_fails_no_smart_contract_folder() -> Result<(), Error> {
async fn test_set_up_call_fails_no_smart_contract_folder() -> Result<()> {
let call_opts = CallOpts {
path: None,
contract: "5CLPm1CeUvJhZ8GCDZCR7nWZ2m3XXe4X5MtAQK69zEjut36A".to_string(),
Expand All @@ -249,4 +249,51 @@ mod tests {

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 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!(matches!(dry_run_call(&call).await, Err(Error::DryRunCallContractError(..))));
Ok(())
}

#[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 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!(matches!(
dry_run_gas_estimate_call(&call).await,
Err(Error::DryRunCallContractError(..))
));
Ok(())
}
}
15 changes: 12 additions & 3 deletions crates/pop-contracts/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,18 @@ pub enum Error {
#[error("Failed to parse hex encoded bytes: {0}")]
HexParsing(String),

#[error("Pre-submission dry-run failed: {0}")]
DryRunUploadContractError(String),

#[error("Pre-submission dry-run failed: {0}")]
DryRunCallContractError(String),

#[error("Anyhow error: {0}")]
AnyhowError(#[from] anyhow::Error),

#[error("Failed to install {0}")]
InstallContractsNode(String),

#[error("ParseError error: {0}")]
ParseError(#[from] url::ParseError),

Expand All @@ -43,9 +55,6 @@ pub enum Error {
#[error("Unsupported platform: {os}")]
UnsupportedPlatform { os: &'static str },

#[error("Anyhow error: {0}")]
AnyhowError(#[from] anyhow::Error),

#[error("HTTP error: {0}")]
HttpError(#[from] reqwest::Error),
}
Loading
Loading