Skip to content

Commit

Permalink
feat: dry-run flag to estimate gas (#203)
Browse files Browse the repository at this point in the history
* feat: add dry_run flag to only estimate gas

* feat: add dry_run flag for call contracts

* refactor: custom error if dry-run fails for call contracts

* refactor: remove anyhow

* docs: add flag in the README

* refactor: use Anyhow error for errors that don't need to be controlled

* test: unit tests for call contract

* test: unit tests for error added in call and up

* refactor: newline, comments and other refactors

* docs: message
  • Loading branch information
AlexD10S authored Jun 26, 2024
1 parent e95c42b commit f0e2dfc
Show file tree
Hide file tree
Showing 7 changed files with 238 additions and 84 deletions.
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 {
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;

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

0 comments on commit f0e2dfc

Please sign in to comment.