mod balance;
mod call;
mod error;
mod events;
mod instantiate;
mod runtime_api;
mod upload;
#[cfg(test)]
#[cfg(feature = "integration-tests")]
mod integration_tests;
use anyhow::{
anyhow,
Context,
Ok,
Result,
};
use colored::Colorize;
use jsonrpsee::{
core::client::ClientT,
rpc_params,
ws_client::WsClientBuilder,
};
use std::{
io::{
self,
Write,
},
path::PathBuf,
};
use crate::DEFAULT_KEY_COL_WIDTH;
use contract_build::{
name_value_println,
CrateMetadata,
Verbosity,
VerbosityFlags,
};
use pallet_contracts_primitives::ContractResult;
use scale::{
Decode,
Encode,
};
use sp_core::{
crypto::Pair,
sr25519,
Bytes,
};
use sp_weights::Weight;
use subxt::{
blocks,
config,
tx,
Config,
OnlineClient,
};
use std::{
option::Option,
path::Path,
};
pub use balance::{
BalanceVariant,
TokenMetadata,
};
pub use call::CallCommand;
use contract_build::metadata::METADATA_FILE;
use contract_metadata::ContractMetadata;
pub use contract_transcode::ContractMessageTranscoder;
pub use error::ErrorVariant;
pub use instantiate::InstantiateCommand;
pub use subxt::PolkadotConfig as DefaultConfig;
pub use upload::UploadCommand;
type Balance = u128;
type CodeHash = <DefaultConfig as Config>::Hash;
type PairSigner = tx::PairSigner<DefaultConfig, sr25519::Pair>;
type Client = OnlineClient<DefaultConfig>;
#[derive(Clone, Debug, clap::Args)]
pub struct ExtrinsicOpts {
#[clap(value_parser, conflicts_with = "manifest_path")]
file: Option<PathBuf>,
#[clap(long, value_parser)]
manifest_path: Option<PathBuf>,
#[clap(
name = "url",
long,
value_parser,
default_value = "ws://localhost:9944"
)]
url: url::Url,
#[clap(name = "suri", long, short)]
suri: String,
#[clap(name = "password", long, short)]
password: Option<String>,
#[clap(flatten)]
verbosity: VerbosityFlags,
#[clap(long)]
dry_run: bool,
#[clap(long)]
storage_deposit_limit: Option<BalanceVariant>,
#[clap(long)]
skip_dry_run: bool,
#[clap(long)]
skip_confirm: bool,
}
impl ExtrinsicOpts {
pub fn contract_artifacts(&self) -> Result<ContractArtifacts> {
let artifact_path = match (self.manifest_path.as_ref(), self.file.as_ref()) {
(manifest_path, None) => {
let crate_metadata = CrateMetadata::from_manifest_path(manifest_path)?;
if crate_metadata.contract_bundle_path().exists() {
crate_metadata.contract_bundle_path()
} else if crate_metadata.metadata_path().exists() {
crate_metadata.metadata_path()
} else {
anyhow::bail!(
"Failed to find any contract artifacts in target directory. \n\
Run `cargo contract build --release` to generate the artifacts."
)
}
}
(None, Some(artifact_file)) => artifact_file.clone(),
(Some(_), Some(_)) => {
anyhow::bail!("conflicting options: --manifest-path and --file")
}
};
ContractArtifacts::from_artifact_path(artifact_path.as_path())
}
pub fn signer(&self) -> Result<sr25519::Pair> {
Pair::from_string(&self.suri, self.password.as_ref().map(String::as_ref))
.map_err(|_| anyhow::anyhow!("Secret string error"))
}
pub fn verbosity(&self) -> Result<Verbosity> {
TryFrom::try_from(&self.verbosity)
}
pub fn url_to_string(&self) -> String {
let mut res = self.url.to_string();
match (self.url.port(), self.url.port_or_known_default()) {
(None, Some(port)) => {
res.insert_str(res.len() - 1, &format!(":{port}"));
res
}
_ => res,
}
}
pub fn storage_deposit_limit(
&self,
token_metadata: &TokenMetadata,
) -> Result<Option<scale::Compact<Balance>>> {
Ok(self
.storage_deposit_limit
.as_ref()
.map(|bv| bv.denominate_balance(token_metadata))
.transpose()?
.map(Into::into))
}
}
#[derive(Debug)]
pub struct ContractArtifacts {
artifacts_path: PathBuf,
metadata_path: PathBuf,
metadata: Option<ContractMetadata>,
pub code: Option<WasmCode>,
}
impl ContractArtifacts {
pub fn from_artifact_path(path: &Path) -> Result<Self> {
tracing::debug!("Loading contracts artifacts from `{}`", path.display());
let (metadata_path, metadata, code) =
match path.extension().and_then(|ext| ext.to_str()) {
Some("contract") | Some("json") => {
let metadata = ContractMetadata::load(path)?;
let code = metadata.clone().source.wasm.map(|wasm| WasmCode(wasm.0));
(PathBuf::from(path), Some(metadata), code)
}
Some("wasm") => {
let code = Some(WasmCode(std::fs::read(path)?));
let dir = path.parent().map_or_else(PathBuf::new, PathBuf::from);
let metadata_path = dir.join(METADATA_FILE);
if !metadata_path.exists() {
(metadata_path, None, code)
} else {
let metadata = ContractMetadata::load(&metadata_path)?;
(metadata_path, Some(metadata), code)
}
}
Some(ext) => anyhow::bail!(
"Invalid artifact extension {ext}, expected `.contract`, `.json` or `.wasm`"
),
None => {
anyhow::bail!(
"Artifact path has no extension, expected `.contract`, `.json`, or `.wasm`"
)
}
};
Ok(Self {
artifacts_path: path.into(),
metadata_path,
metadata,
code,
})
}
pub fn artifact_path(&self) -> &Path {
self.artifacts_path.as_path()
}
pub fn metadata(&self) -> Result<ContractMetadata> {
self.metadata.clone().ok_or_else(|| {
anyhow!(
"No contract metadata found. Expected file {}",
self.metadata_path.as_path().display()
)
})
}
pub fn code_hash(&self) -> Result<[u8; 32]> {
let metadata = self.metadata()?;
Ok(metadata.source.hash.0)
}
pub fn contract_transcoder(&self) -> Result<ContractMessageTranscoder> {
let metadata = self.metadata()?;
ContractMessageTranscoder::try_from(metadata)
.context("Failed to deserialize ink project metadata from contract metadata")
}
}
#[derive(Debug)]
pub struct WasmCode(Vec<u8>);
impl WasmCode {
pub fn code_hash(&self) -> [u8; 32] {
contract_build::code_hash(&self.0)
}
}
pub fn pair_signer(pair: sr25519::Pair) -> PairSigner {
PairSigner::new(pair)
}
const STORAGE_DEPOSIT_KEY: &str = "Storage Deposit";
pub const MAX_KEY_COL_WIDTH: usize = STORAGE_DEPOSIT_KEY.len() + 1;
pub fn display_contract_exec_result<R, const WIDTH: usize>(
result: &ContractResult<R, Balance>,
) -> 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), WIDTH);
name_value_println!("Gas Required", format!("{:?}", result.gas_required), WIDTH);
name_value_println!(
STORAGE_DEPOSIT_KEY,
format!("{:?}", result.storage_deposit),
WIDTH
);
if let Some(debug_message) = debug_message_lines.next() {
name_value_println!("Debug Message", format!("{debug_message}"), WIDTH);
}
for debug_message in debug_message_lines {
name_value_println!("", format!("{debug_message}"), WIDTH);
}
Ok(())
}
pub fn display_contract_exec_result_debug<R, const WIDTH: usize>(
result: &ContractResult<R, Balance>,
) -> Result<()> {
let mut debug_message_lines = std::str::from_utf8(&result.debug_message)
.context("Error decoding UTF8 debug message bytes")?
.lines();
if let Some(debug_message) = debug_message_lines.next() {
name_value_println!("Debug Message", format!("{debug_message}"), WIDTH);
}
for debug_message in debug_message_lines {
name_value_println!("", format!("{debug_message}"), WIDTH);
}
Ok(())
}
async fn submit_extrinsic<T, Call, Signer>(
client: &OnlineClient<T>,
call: &Call,
signer: &Signer,
) -> core::result::Result<blocks::ExtrinsicEvents<T>, subxt::Error>
where
T: Config,
Call: tx::TxPayload,
Signer: tx::Signer<T>,
<T::ExtrinsicParams as config::ExtrinsicParams<T::Index, T::Hash>>::OtherParams:
Default,
{
client
.tx()
.sign_and_submit_then_watch_default(call, signer)
.await?
.wait_for_in_block()
.await?
.wait_for_success()
.await
}
async fn state_call<A: Encode, R: Decode>(url: &str, func: &str, args: A) -> Result<R> {
let cli = WsClientBuilder::default().build(&url).await?;
let params = rpc_params![func, Bytes(args.encode())];
let bytes: Bytes = cli.request("state_call", params).await?;
Ok(R::decode(&mut bytes.as_ref())?)
}
fn prompt_confirm_tx<F: FnOnce()>(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() {
"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: Weight) {
println!(
"{:>width$} Gas required estimated at {}",
"Success!".green().bold(),
gas.to_string().bright_white(),
width = DEFAULT_KEY_COL_WIDTH
);
}
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, serde::Serialize)]
pub enum StorageDeposit {
Refund(Balance),
Charge(Balance),
}
impl From<&pallet_contracts_primitives::StorageDeposit<Balance>> for StorageDeposit {
fn from(deposit: &pallet_contracts_primitives::StorageDeposit<Balance>) -> Self {
match deposit {
pallet_contracts_primitives::StorageDeposit::Refund(balance) => {
Self::Refund(*balance)
}
pallet_contracts_primitives::StorageDeposit::Charge(balance) => {
Self::Charge(*balance)
}
}
}
}