From 069f8a4edf0d615b13ccd1bdebe25ad23adee837 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 11 Aug 2022 14:51:29 +0100 Subject: [PATCH] Dry run gas limit estimation (#484) * Dry run instantiate gas estimation * Fmt * Prompt instantiate dry run estimate * Fix gas estimation and fmt * Dry run gas estimation for instantiate with code * Dry run gas estimation for call * Fmt * Clippy * Fmt * Remove deprecated rustfmt configuration options * If else instead of early return * WIP tx confirm * Prompt instantiate with code hash * Dry run status * Dry run call pre dispatch * Align and prettify dry run gas estimate * Factor out success print fns * Remove unused imports * Clippy * Fmt * Update comment * Use --gas argument override if specified * Fix dry run result formatting * Print pre-submission dry run details * Fmt * Make confirm prompt case sensitive and default Y * Fmt --- src/cmd/extrinsics/call.rs | 141 +++++++++++++++++++++--------- src/cmd/extrinsics/events.rs | 2 + src/cmd/extrinsics/instantiate.rs | 133 +++++++++++++++++++++------- src/cmd/extrinsics/mod.rs | 86 +++++++++++++----- src/util.rs | 2 +- 5 files changed, 268 insertions(+), 96 deletions(-) diff --git a/src/cmd/extrinsics/call.rs b/src/cmd/extrinsics/call.rs index a09cccbea..8a31a96e8 100644 --- a/src/cmd/extrinsics/call.rs +++ b/src/cmd/extrinsics/call.rs @@ -20,6 +20,7 @@ use super::{ dry_run_error_details, load_metadata, parse_balance, + prompt_confirm_tx, wait_for_success_and_handle_error, Balance, ContractMessageTranscoder, @@ -27,10 +28,16 @@ use super::{ PairSigner, RuntimeApi, RuntimeDispatchError, - EXEC_RESULT_MAX_KEY_COL_WIDTH, + MAX_KEY_COL_WIDTH, +}; +use crate::{ + name_value_println, + DEFAULT_KEY_COL_WIDTH, +}; +use anyhow::{ + anyhow, + Result, }; -use crate::name_value_println; -use anyhow::Result; use jsonrpsee::{ core::client::ClientT, rpc_params, @@ -71,8 +78,9 @@ pub struct CallCommand { #[clap(flatten)] extrinsic_opts: ExtrinsicOpts, /// Maximum amount of gas to be used for this command. - #[clap(name = "gas", long, default_value = "50000000000")] - gas_limit: u64, + /// If not specified will perform a dry-run to estimate the gas consumed for the instantiation. + #[clap(name = "gas", long)] + gas_limit: Option, /// The value to be transferred as part of the call. #[clap(name = "value", long, parse(try_from_str = parse_balance), default_value = "0")] value: Balance, @@ -97,22 +105,49 @@ impl CallCommand { .to_runtime_api::(); if self.extrinsic_opts.dry_run { - self.call_rpc(&api, call_data, &signer, &transcoder).await + let result = self.call_dry_run(call_data, &signer).await?; + + match result.result { + Ok(ref ret_val) => { + let value = transcoder + .decode_return(&self.message, &mut &ret_val.data.0[..])?; + name_value_println!( + "Result", + String::from("Success!"), + DEFAULT_KEY_COL_WIDTH + ); + name_value_println!( + "Reverted", + format!("{:?}", ret_val.did_revert()), + DEFAULT_KEY_COL_WIDTH + ); + name_value_println!( + "Data", + format!("{}", value), + DEFAULT_KEY_COL_WIDTH + ); + display_contract_exec_result::<_, DEFAULT_KEY_COL_WIDTH>(&result) + } + Err(ref err) => { + let err = dry_run_error_details(&api, err).await?; + name_value_println!("Result", err, MAX_KEY_COL_WIDTH); + display_contract_exec_result::<_, MAX_KEY_COL_WIDTH>(&result) + } + } } else { self.call(&api, call_data, &signer, &transcoder).await } }) } - async fn call_rpc( + async fn call_dry_run( &self, - api: &RuntimeApi, data: Vec, signer: &PairSigner, - transcoder: &ContractMessageTranscoder<'_>, - ) -> Result<()> { + ) -> Result { let url = self.extrinsic_opts.url_to_string(); let cli = WsClientBuilder::default().build(&url).await?; + let gas_limit = self.gas_limit.as_ref().unwrap_or(&5_000_000_000_000); let storage_deposit_limit = self .extrinsic_opts .storage_deposit_limit @@ -122,40 +157,13 @@ impl CallCommand { origin: signer.account_id().clone(), dest: self.contract.clone(), value: NumberOrHex::Hex(self.value.into()), - gas_limit: NumberOrHex::Number(self.gas_limit), + gas_limit: NumberOrHex::Number(*gas_limit), storage_deposit_limit, input_data: Bytes(data), }; let params = rpc_params![call_request]; - let result: ContractExecResult = cli.request("contracts_call", params).await?; - - match result.result { - Ok(ref ret_val) => { - let value = - transcoder.decode_return(&self.message, &mut &ret_val.data.0[..])?; - name_value_println!( - "Result", - String::from("Success!"), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); - name_value_println!( - "Reverted", - format!("{:?}", ret_val.did_revert()), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); - name_value_println!( - "Data", - format!("{}", value), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); - } - Err(ref err) => { - let err = dry_run_error_details(api, err).await?; - name_value_println!("Result", err, EXEC_RESULT_MAX_KEY_COL_WIDTH); - } - } - display_contract_exec_result(&result)?; - Ok(()) + let result = cli.request("contracts_call", params).await?; + Ok(result) } async fn call( @@ -166,13 +174,30 @@ impl CallCommand { transcoder: &ContractMessageTranscoder<'_>, ) -> Result<()> { log::debug!("calling contract {:?}", self.contract); + + let gas_limit = self + .pre_submit_dry_run_gas_estimate(api, data.clone(), signer) + .await?; + + if !self.extrinsic_opts.skip_confirm { + prompt_confirm_tx(|| { + name_value_println!("Message", self.message, DEFAULT_KEY_COL_WIDTH); + name_value_println!("Args", self.args.join(" "), DEFAULT_KEY_COL_WIDTH); + name_value_println!( + "Gas limit", + gas_limit.to_string(), + DEFAULT_KEY_COL_WIDTH + ); + })?; + } + let tx_progress = api .tx() .contracts() .call( self.contract.clone().into(), self.value, - self.gas_limit, + gas_limit, self.extrinsic_opts.storage_deposit_limit, data, )? @@ -188,6 +213,40 @@ impl CallCommand { &self.extrinsic_opts.verbosity()?, ) } + + /// Dry run the call before tx submission. Returns the gas required estimate. + async fn pre_submit_dry_run_gas_estimate( + &self, + api: &RuntimeApi, + data: Vec, + signer: &PairSigner, + ) -> Result { + if self.extrinsic_opts.skip_dry_run { + return match self.gas_limit { + Some(gas) => Ok(gas), + None => { + Err(anyhow!( + "Gas limit `--gas` argument required if `--skip-dry-run` specified" + )) + } + } + } + super::print_dry_running_status(&self.message); + let call_result = self.call_dry_run(data, signer).await?; + match call_result.result { + Ok(_) => { + super::print_gas_required_success(call_result.gas_required); + let gas_limit = self.gas_limit.unwrap_or(call_result.gas_required); + Ok(gas_limit) + } + Err(ref err) => { + let err = dry_run_error_details(api, err).await?; + name_value_println!("Result", err, MAX_KEY_COL_WIDTH); + display_contract_exec_result::<_, MAX_KEY_COL_WIDTH>(&call_result)?; + Err(anyhow!("Pre-submission dry-run failed. Use --skip-dry-run to skip this step.")) + } + } + } } /// A struct that encodes RPC parameters required for a call to a smart contract. diff --git a/src/cmd/extrinsics/events.rs b/src/cmd/extrinsics/events.rs index 575a8d0e5..6dcbe89fa 100644 --- a/src/cmd/extrinsics/events.rs +++ b/src/cmd/extrinsics/events.rs @@ -47,6 +47,8 @@ pub fn display_events( return Ok(()) } + println!(); + if matches!(verbosity, Verbosity::Verbose) { println!("VERBOSE") } diff --git a/src/cmd/extrinsics/instantiate.rs b/src/cmd/extrinsics/instantiate.rs index 2c848e764..e2edfa674 100644 --- a/src/cmd/extrinsics/instantiate.rs +++ b/src/cmd/extrinsics/instantiate.rs @@ -19,6 +19,7 @@ use super::{ display_events, dry_run_error_details, parse_balance, + prompt_confirm_tx, runtime_api::api, wait_for_success_and_handle_error, Balance, @@ -29,12 +30,13 @@ use super::{ PairSigner, RuntimeApi, RuntimeDispatchError, - EXEC_RESULT_MAX_KEY_COL_WIDTH, + MAX_KEY_COL_WIDTH, }; use crate::{ name_value_println, util::decode_hex, Verbosity, + DEFAULT_KEY_COL_WIDTH, }; use anyhow::{ anyhow, @@ -98,9 +100,10 @@ pub struct InstantiateCommand { /// Transfers an initial balance to the instantiated contract #[clap(name = "value", long, default_value = "0", parse(try_from_str = parse_balance))] value: Balance, - /// Maximum amount of gas to be used for this command - #[clap(name = "gas", long, default_value = "50000000000")] - gas_limit: u64, + /// Maximum amount of gas to be used for this command. + /// If not specified will perform a dry-run to estimate the gas consumed for the instantiation. + #[clap(name = "gas", long)] + gas_limit: Option, /// A salt used in the address derivation of the new contract. Use to create multiple instances /// of the same contract code from the same account. #[clap(long, parse(try_from_str = parse_hex_bytes))] @@ -163,6 +166,8 @@ impl InstantiateCommand { let salt = self.salt.clone().unwrap_or_else(|| Bytes(Vec::new())); let args = InstantiateArgs { + constructor: self.constructor.clone(), + raw_args: self.args.clone(), value: self.value, gas_limit: self.gas_limit, storage_deposit_limit: self.extrinsic_opts.storage_deposit_limit, @@ -172,6 +177,7 @@ impl InstantiateCommand { let exec = Exec { args, + opts: self.extrinsic_opts.clone(), url, verbosity, signer, @@ -185,14 +191,17 @@ impl InstantiateCommand { } struct InstantiateArgs { - value: super::Balance, - gas_limit: u64, + constructor: String, + raw_args: Vec, + value: Balance, + gas_limit: Option, storage_deposit_limit: Option, data: Vec, salt: Bytes, } pub struct Exec<'a> { + opts: ExtrinsicOpts, args: InstantiateArgs, verbosity: Verbosity, url: String, @@ -219,49 +228,49 @@ impl<'a> Exec<'a> { name_value_println!( "Result", String::from("Success!"), - EXEC_RESULT_MAX_KEY_COL_WIDTH + DEFAULT_KEY_COL_WIDTH ); name_value_println!( "Contract", ret_val.account_id.to_ss58check(), - EXEC_RESULT_MAX_KEY_COL_WIDTH + DEFAULT_KEY_COL_WIDTH ); name_value_println!( "Reverted", format!("{:?}", ret_val.result.did_revert()), - EXEC_RESULT_MAX_KEY_COL_WIDTH + DEFAULT_KEY_COL_WIDTH ); name_value_println!( "Data", format!("{:?}", ret_val.result.data), - EXEC_RESULT_MAX_KEY_COL_WIDTH + DEFAULT_KEY_COL_WIDTH ); + display_contract_exec_result::<_, DEFAULT_KEY_COL_WIDTH>(&result) } Err(ref err) => { let err = dry_run_error_details(&self.subxt_api().await?, err).await?; - name_value_println!("Result", err, EXEC_RESULT_MAX_KEY_COL_WIDTH); + name_value_println!("Result", err, MAX_KEY_COL_WIDTH); + display_contract_exec_result::<_, MAX_KEY_COL_WIDTH>(&result) } } - display_contract_exec_result(&result)?; - return Ok(()) - } - - match code { - Code::Upload(code) => { - let (code_hash, contract_account) = - self.instantiate_with_code(code).await?; - if let Some(code_hash) = code_hash { - name_value_println!("Code hash", format!("{:?}", code_hash)); + } else { + match code { + Code::Upload(code) => { + let (code_hash, contract_account) = + self.instantiate_with_code(code).await?; + if let Some(code_hash) = code_hash { + name_value_println!("Code hash", format!("{:?}", code_hash)); + } + name_value_println!("Contract", contract_account.to_ss58check()); + } + Code::Existing(code_hash) => { + let contract_account = self.instantiate(code_hash).await?; + name_value_println!("Contract", contract_account.to_ss58check()); } - name_value_println!("Contract", contract_account.to_ss58check()); - } - Code::Existing(code_hash) => { - let contract_account = self.instantiate(code_hash).await?; - name_value_println!("Contract", contract_account.to_ss58check()); } + Ok(()) } - Ok(()) } async fn instantiate_with_code( @@ -269,12 +278,20 @@ impl<'a> Exec<'a> { code: Bytes, ) -> Result<(Option, ContractAccount)> { let api = self.subxt_api().await?; + let gas_limit = self + .pre_submit_dry_run_gas_estimate(Code::Upload(code.clone())) + .await?; + + if !self.opts.skip_confirm { + prompt_confirm_tx(|| self.print_default_instantiate_preview(gas_limit))?; + } + let tx_progress = api .tx() .contracts() .instantiate_with_code( self.args.value, - self.args.gas_limit, + gas_limit, self.args.storage_deposit_limit, code.to_vec(), self.args.data.clone(), @@ -306,12 +323,27 @@ impl<'a> Exec<'a> { async fn instantiate(&self, code_hash: CodeHash) -> Result { let api = self.subxt_api().await?; + let gas_limit = self + .pre_submit_dry_run_gas_estimate(Code::Existing(code_hash)) + .await?; + + if !self.opts.skip_confirm { + prompt_confirm_tx(|| { + self.print_default_instantiate_preview(gas_limit); + name_value_println!( + "Code hash", + format!("{:?}", code_hash), + DEFAULT_KEY_COL_WIDTH + ); + })?; + } + let tx_progress = api .tx() .contracts() .instantiate( self.args.value, - self.args.gas_limit, + gas_limit, self.args.storage_deposit_limit, code_hash, self.args.data.clone(), @@ -336,8 +368,15 @@ impl<'a> Exec<'a> { Ok(instantiated.contract) } + fn print_default_instantiate_preview(&self, gas_limit: u64) { + name_value_println!("Constructor", self.args.constructor, DEFAULT_KEY_COL_WIDTH); + name_value_println!("Args", self.args.raw_args.join(" "), DEFAULT_KEY_COL_WIDTH); + name_value_println!("Gas limit", gas_limit.to_string(), DEFAULT_KEY_COL_WIDTH); + } + async fn instantiate_dry_run(&self, code: Code) -> Result { let cli = WsClientBuilder::default().build(&self.url).await?; + let gas_limit = self.args.gas_limit.as_ref().unwrap_or(&5_000_000_000_000); let storage_deposit_limit = self .args .storage_deposit_limit @@ -346,7 +385,7 @@ impl<'a> Exec<'a> { let call_request = InstantiateRequest { origin: self.signer.account_id().clone(), value: NumberOrHex::Hex(self.args.value.into()), - gas_limit: NumberOrHex::Number(self.args.gas_limit), + gas_limit: NumberOrHex::Number(*gas_limit), storage_deposit_limit, code, data: self.args.data.clone().into(), @@ -360,6 +399,40 @@ impl<'a> Exec<'a> { Ok(result) } + + /// Dry run the instantiation before tx submission. Returns the gas required estimate. + async fn pre_submit_dry_run_gas_estimate(&self, code: Code) -> Result { + if self.opts.skip_dry_run { + return match self.args.gas_limit { + Some(gas) => Ok(gas), + None => { + Err(anyhow!( + "Gas limit `--gas` argument required if `--skip-dry-run` specified" + )) + } + } + } + super::print_dry_running_status(&self.args.constructor); + let instantiate_result = self.instantiate_dry_run(code).await?; + match instantiate_result.result { + Ok(_) => { + super::print_gas_required_success(instantiate_result.gas_required); + let gas_limit = self + .args + .gas_limit + .unwrap_or(instantiate_result.gas_required); + Ok(gas_limit) + } + Err(ref err) => { + let err = dry_run_error_details(&self.subxt_api().await?, err).await?; + name_value_println!("Result", err, MAX_KEY_COL_WIDTH); + display_contract_exec_result::<_, MAX_KEY_COL_WIDTH>( + &instantiate_result, + )?; + Err(anyhow!("Pre-submission dry-run failed. Use --skip-dry-run to skip this step.")) + } + } + } } /// A struct that encodes RPC parameters required to instantiate a new smart contract. diff --git a/src/cmd/extrinsics/mod.rs b/src/cmd/extrinsics/mod.rs index a54d8cc24..4fc468f5d 100644 --- a/src/cmd/extrinsics/mod.rs +++ b/src/cmd/extrinsics/mod.rs @@ -29,8 +29,13 @@ use anyhow::{ Context, Result, }; +use colored::Colorize; use std::{ fs::File, + io::{ + self, + Write, + }, path::PathBuf, }; @@ -41,6 +46,7 @@ use crate::{ workspace::ManifestPath, Verbosity, VerbosityFlags, + DEFAULT_KEY_COL_WIDTH, }; use pallet_contracts_primitives::ContractResult; use sp_core::{ @@ -98,6 +104,12 @@ pub struct ExtrinsicOpts { /// consumed. #[clap(long, parse(try_from_str = parse_balance))] storage_deposit_limit: Option, + /// Before submitting a transaction, do not dry-run it via RPC first. + #[clap(long)] + skip_dry_run: bool, + /// Before submitting a transaction, do not ask the user for confirmation. + #[clap(long)] + skip_confirm: bool, } impl ExtrinsicOpts { @@ -172,46 +184,30 @@ pub fn pair_signer(pair: sr25519::Pair) -> PairSigner { } const STORAGE_DEPOSIT_KEY: &str = "Storage Deposit"; -pub const EXEC_RESULT_MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1; +pub const MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1; /// Print to stdout the fields of the result of a `instantiate` or `call` dry-run via RPC. -pub fn display_contract_exec_result( +pub fn display_contract_exec_result( result: &ContractResult, ) -> Result<()> { let mut debug_message_lines = std::str::from_utf8(&result.debug_message) .context("Error decoding UTF8 debug message bytes")? .lines(); - name_value_println!( - "Gas Consumed", - format!("{:?}", result.gas_consumed), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); - name_value_println!( - "Gas Required", - format!("{:?}", result.gas_required), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); + name_value_println!("Gas Consumed", format!("{:?}", result.gas_consumed), WIDTH); + name_value_println!("Gas Required", format!("{:?}", result.gas_required), WIDTH); name_value_println!( STORAGE_DEPOSIT_KEY, format!("{:?}", result.storage_deposit), - EXEC_RESULT_MAX_KEY_COL_WIDTH + WIDTH ); // print debug messages aligned, only first line has key if let Some(debug_message) = debug_message_lines.next() { - name_value_println!( - "Debug Message", - format!("{}", debug_message), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); + name_value_println!("Debug Message", format!("{}", debug_message), WIDTH); } for debug_message in debug_message_lines { - name_value_println!( - "", - format!("{}", debug_message), - EXEC_RESULT_MAX_KEY_COL_WIDTH - ); + name_value_println!("", format!("{}", debug_message), WIDTH); } Ok(()) } @@ -253,7 +249,7 @@ async fn dry_run_error_details( let details = locked_metadata.error(error_data.pallet_index, error_data.error_index())?; format!( - "ModuleError: {}::{}: {:?}", + "{}::{}: {:?}", details.pallet(), details.error(), details.description() @@ -263,3 +259,45 @@ async fn dry_run_error_details( }; Ok(error) } + +/// Prompt the user to confirm transaction submission +fn prompt_confirm_tx(show_details: F) -> Result<()> { + println!( + "{} (skip with --skip-confirm)", + "Confirm transaction details:".bright_white().bold() + ); + show_details(); + print!( + "{} ({}/n): ", + "Submit?".bright_white().bold(), + "Y".bright_white().bold() + ); + + let mut buf = String::new(); + io::stdout().flush()?; + io::stdin().read_line(&mut buf)?; + match buf.trim().to_lowercase().as_str() { + // default is 'y' + "y" | "" => Ok(()), + "n" => Err(anyhow!("Transaction not submitted")), + c => Err(anyhow!("Expected either 'y' or 'n', got '{}'", c)), + } +} + +fn print_dry_running_status(msg: &str) { + println!( + "{:>width$} {} (skip with --skip-dry-run)", + "Dry-running".green().bold(), + msg.bright_white().bold(), + width = DEFAULT_KEY_COL_WIDTH + ); +} + +fn print_gas_required_success(gas: u64) { + println!( + "{:>width$} Gas required estimated at {}", + "Success!".green().bold(), + gas.to_string().bright_white(), + width = DEFAULT_KEY_COL_WIDTH + ); +} diff --git a/src/util.rs b/src/util.rs index 10652b523..b9e62d15a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -150,7 +150,7 @@ macro_rules! maybe_println { }; } -pub const DEFAULT_KEY_COL_WIDTH: usize = 13; +pub const DEFAULT_KEY_COL_WIDTH: usize = 12; /// Pretty print name value, name right aligned with colour. #[macro_export]