mod config;
mod prod_chains;
pub mod account;
pub mod build;
pub mod call;
pub mod decode;
pub mod encode;
pub mod info;
pub mod instantiate;
pub mod lint;
pub mod remove;
pub mod rpc;
pub mod schema;
pub mod storage;
pub mod test_cmd;
pub mod upload;
pub mod verify;
pub(crate) use self::{
account::AccountCommand,
build::{
BuildCommand,
CheckCommand,
},
call::CallCommand,
decode::DecodeCommand,
info::{
ExtendedContractInfo,
InfoCommand,
},
instantiate::InstantiateCommand,
lint::LintCommand,
prod_chains::ProductionChain,
remove::RemoveCommand,
rpc::RpcCommand,
schema::{
GenerateSchemaCommand,
VerifySchemaCommand,
},
storage::StorageCommand,
test_cmd::TestCommand,
upload::UploadCommand,
verify::VerifyCommand,
};
use crate::{
PathBuf,
Weight,
anyhow,
};
use anyhow::Result;
use colored::Colorize;
use contract_build::{
DEFAULT_KEY_COL_WIDTH,
Verbosity,
VerbosityFlags,
name_value_println,
};
pub(crate) use contract_extrinsics::ErrorVariant;
use contract_extrinsics::{
BalanceVariant,
MapAccountCommandBuilder,
MapAccountExec,
TokenMetadata,
pallet_revive_primitives::ContractResult,
};
use crate::cmd::config::SignerConfig;
use ink_env::Environment;
use std::{
fmt::{
Debug,
Display,
},
io::{
self,
Write,
},
str::FromStr,
};
use subxt::{
config::{
DefaultExtrinsicParams,
ExtrinsicParams,
},
ext::scale_decode::IntoVisitor,
utils::H160,
};
#[derive(Clone, Debug, clap::Args)]
pub struct CLIExtrinsicOpts {
#[clap(value_parser, conflicts_with = "manifest_path")]
file: Option<PathBuf>,
#[clap(long, value_parser)]
manifest_path: Option<PathBuf>,
#[clap(name = "suri", long, short)]
suri: String,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(short('x'), long)]
execute: bool,
#[clap(long)]
storage_deposit_limit: Option<String>,
#[clap(long)]
skip_dry_run: bool,
#[clap(short('y'), long)]
skip_confirm: bool,
#[clap(flatten)]
chain_cli_opts: CLIChainOpts,
}
impl CLIExtrinsicOpts {
pub fn verbosity(&self) -> Result<Verbosity> {
TryFrom::try_from(&self.verbosity)
}
}
#[derive(Clone, Debug, clap::Args)]
pub struct CLIChainOpts {
#[clap(
name = "url",
long,
value_parser,
default_value = "ws://localhost:9944"
)]
url: url::Url,
#[clap(name = "config", long, default_value = "Polkadot")]
config: String,
#[clap(name = "chain", long, conflicts_with_all = ["url", "config"])]
chain: Option<ProductionChain>,
}
impl CLIChainOpts {
pub fn chain(&self) -> Chain {
if let Some(chain) = &self.chain {
Chain::Production(chain.clone())
} else if let Some(prod) = ProductionChain::from_parts(&self.url, &self.config) {
Chain::Production(prod)
} else {
Chain::Custom(self.url.clone(), self.config.clone())
}
}
}
#[derive(Debug)]
pub enum Chain {
Production(ProductionChain),
Custom(url::Url, String),
}
impl Chain {
pub fn url(&self) -> url::Url {
match self {
Chain::Production(prod) => prod.url(),
Chain::Custom(url, _) => url.clone(),
}
}
pub fn config(&self) -> &str {
match self {
Chain::Production(prod) => prod.config(),
Chain::Custom(_, config) => config,
}
}
pub fn production(&self) -> Option<&ProductionChain> {
if let Chain::Production(prod) = self {
return Some(prod)
}
None
}
}
const STORAGE_DEPOSIT_KEY: &str = "Storage Total Deposit";
pub const MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1;
pub fn display_contract_exec_result<R, const WIDTH: usize, Balance>(
result: &ContractResult<R, Balance>,
) -> Result<()>
where
Balance: Debug,
{
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),
WIDTH
);
Ok(())
}
pub fn display_dry_run_result_warning(command: &str) {
println!("Your {} call {} been executed.", command, "has not".bold());
println!(
"To submit the transaction and execute the call on chain, add {} flag to the command.",
"-x/--execute".bold()
);
}
pub fn prompt_confirm_tx<F: FnOnce()>(show_details: F) -> Result<()> {
println!(
"{} (skip with --skip-confirm or -y)",
"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() {
"y" | "" => Ok(()),
"n" => Err(anyhow!("Transaction not submitted")),
c => Err(anyhow!("Expected either 'y' or 'n', got '{c}'")),
}
}
fn prompt_confirm_mapping<F: FnOnce()>(show_details: F) -> Result<()> {
println!(
"{} (skip with --skip-confirm or -y)",
"Confirm transaction details:".bright_white().bold()
);
show_details();
print!(
"{} ({}/n): ",
"The account you're submitting from is not yet mapped. Map?"
.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() {
"y" | "" => Ok(()),
"n" => Err(anyhow!("Mapping not intended")),
c => Err(anyhow!("Expected either 'y' or 'n', got '{c}'")),
}
}
async fn offer_map_account_if_needed<C: subxt::Config + Environment + SignerConfig<C>>(
extrinsic_opts: contract_extrinsics::ExtrinsicOpts<
C,
C,
<C as SignerConfig<C>>::Signer,
>,
) -> Result<(), contract_extrinsics::ErrorVariant>
where
<C as SignerConfig<C>>::Signer: subxt::tx::Signer<C> + Clone + FromStr,
<C::ExtrinsicParams as ExtrinsicParams<C>>::Params:
From<<DefaultExtrinsicParams<C> as ExtrinsicParams<C>>::Params>,
{
let map_exec: MapAccountExec<C, C, _> =
MapAccountCommandBuilder::new(extrinsic_opts).done().await?;
let result = map_exec.map_account_dry_run().await;
if let Ok(partial_fee_estimation) = result {
let reply = prompt_confirm_mapping(|| {
name_value_println!(
"Estimated fee",
format!("{:?}", partial_fee_estimation),
DEFAULT_KEY_COL_WIDTH
);
});
if reply.is_ok() {
let res = map_exec.map_account().await?;
name_value_println!(
"Address",
format!("{:?}", res.address),
DEFAULT_KEY_COL_WIDTH
);
}
}
Ok(())
}
pub 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
);
}
pub fn print_gas_required_success(gas: Weight) {
println!(
"{:>width$} Gas required estimated at {}",
"Success!".green().bold(),
gas.to_string().bright_white(),
width = DEFAULT_KEY_COL_WIDTH
);
}
pub fn basic_display_format_extended_contract_info<AccountId, Balance>(
contract_addr: &H160,
info: &ExtendedContractInfo<AccountId, Balance>,
) where
AccountId: Display + serde::Serialize + Debug + IntoVisitor + Clone,
Balance: Debug + serde::Serialize + IntoVisitor + Copy,
{
name_value_println!(
"Contract Address",
format!("{:?}", contract_addr),
MAX_KEY_COL_WIDTH
);
println!();
name_value_println!("TrieId", info.trie_id, MAX_KEY_COL_WIDTH);
name_value_println!(
"Code Hash",
format!("{:?}", info.code_hash),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Storage Bytes",
format!("{:?}", info.storage_bytes),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Storage Items",
format!("{:?}", info.storage_items),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Storage Byte Deposit",
format!("{:?}", info.storage_byte_deposit),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Storage Item Deposit",
format!("{:?}", info.storage_item_deposit),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Storage Base Deposit",
format!("{:?}", info.storage_base_deposit),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Immutable Data Length",
format!("{:?}", info.immutable_data_len),
MAX_KEY_COL_WIDTH
);
name_value_println!(
STORAGE_DEPOSIT_KEY,
format!("{:?}", info.storage_total_deposit),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Source Language",
format!("{}", info.source_language),
MAX_KEY_COL_WIDTH
);
println!();
name_value_println!(
"Code Owner",
format!("{}", info.code_info.owner),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Code Deposit",
format!("{:?}", info.code_info.deposit),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Code Refcount",
format!("{}", info.code_info.refcount),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Code Length",
format!("{}", info.code_info.code_len),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Code Type",
format!("{:?}", info.code_info.code_type),
MAX_KEY_COL_WIDTH
);
name_value_println!(
"Code Behaviour Version",
format!("{}", info.code_info.behaviour_version),
MAX_KEY_COL_WIDTH
);
}
pub fn display_all_contracts(contracts: &[H160]) {
contracts.iter().for_each(|e: &H160| println!("{e:?}"))
}
pub fn parse_balance<Balance: FromStr + From<u128> + Clone>(
balance: &str,
token_metadata: &TokenMetadata,
) -> Result<Balance> {
BalanceVariant::from_str(balance)
.map_err(|e| anyhow!("Balance parsing failed: {e}"))
.and_then(|bv| bv.denominate_balance(token_metadata))
}
pub fn parse_account<AccountId: FromStr>(account: &str) -> Result<AccountId>
where
<AccountId as FromStr>::Err: Display,
{
AccountId::from_str(account)
.map_err(|e| anyhow::anyhow!("Account address parsing failed: {e}"))
}
pub fn parse_addr(addr: &str) -> Result<H160> {
let bytes = contract_build::util::decode_hex(addr)?;
if bytes.len() != 20 {
anyhow::bail!("H160 must be 20 bytes in length, but is {:?}", bytes.len())
}
Ok(H160::from_slice(&bytes[..]))
}
pub fn parse_code_hash<Hash>(input: &str) -> Result<Hash>
where
Hash: From<[u8; 32]>,
{
let bytes = contract_build::util::decode_hex(input)?;
if bytes.len() != 32 {
anyhow::bail!("Code hash must be 32 bytes in length")
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr.into())
}
pub fn prompt_confirm_unverifiable_upload(chain: &str) -> Result<()> {
println!("{}", "Confirm upload:".bright_white().bold());
let warning = format!(
"Warning: You are about to upload unverifiable code to {chain} mainnet.\n\
A third party won't be able to confirm that your uploaded contract binary blob \
matches a particular contract source code.\n\n\
You can use `cargo contract build --verifiable` to make the contract verifiable.\n\
See https://use.ink/basics/contract-verification for more info."
)
.bold()
.yellow();
print!("{warning}");
println!(
"{} ({}): ",
"\nContinue?".bright_white().bold(),
"y/N".bright_white().bold()
);
let mut buf = String::new();
io::stdout().flush()?;
io::stdin().read_line(&mut buf)?;
match buf.trim().to_lowercase().as_str() {
"y" => Ok(()),
"n" | "" => Err(anyhow!("Upload cancelled!")),
c => Err(anyhow!("Expected either 'y' or 'n', got '{c}'")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use subxt::{
SubstrateConfig,
config::HashFor,
};
#[test]
fn parse_code_hash_works() {
assert!(
parse_code_hash::<HashFor<SubstrateConfig>>(
"0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
)
.is_ok()
);
assert!(
parse_code_hash::<HashFor<SubstrateConfig>>(
"d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
)
.is_ok()
)
}
#[test]
fn parse_incorrect_len_code_hash_fails() {
assert!(
parse_code_hash::<HashFor<SubstrateConfig>>(
"d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da2"
)
.is_err()
)
}
#[test]
fn parse_bad_format_code_hash_fails() {
assert!(
parse_code_hash::<HashFor<SubstrateConfig>>(
"x43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"
)
.is_err()
)
}
}