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

Add flag --all to cargo contract info #1319

Merged
merged 11 commits into from
Sep 12, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Detect `INK_STATIC_BUFFER_SIZE` env var - [#1310](https://github.com/paritytech/cargo-contract/pull/1310)
- Add `verify` command - [#1306](https://github.com/paritytech/cargo-contract/pull/1306)
- Add `--binary` flag for `info` command - [#1311](https://github.com/paritytech/cargo-contract/pull/1311/)
- Add `--all` flag for `info` command - [#1319](https://github.com/paritytech/cargo-contract/pull/1319)

## [4.0.0-alpha]

Expand Down
142 changes: 78 additions & 64 deletions crates/cargo-contract/src/cmd/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@

use super::{
basic_display_format_contract_info,
display_all_contracts,
DefaultConfig,
};
use anyhow::{
anyhow,
Result,
};
use contract_extrinsics::{
fetch_all_contracts,
fetch_contract_info,
fetch_wasm_code,
ErrorVariant,
Expand All @@ -35,14 +37,18 @@ use subxt::{
Config,
OnlineClient,
};
use tokio::runtime::Runtime;

#[derive(Debug, clap::Args)]
#[clap(name = "info", about = "Get infos from a contract")]
pub struct InfoCommand {
/// The address of the contract to display info of.
#[clap(name = "contract", long, env = "CONTRACT")]
contract: <DefaultConfig as Config>::AccountId,
#[clap(
name = "contract",
long,
env = "CONTRACT",
required_unless_present = "all"
)]
contract: Option<<DefaultConfig as Config>::AccountId>,
/// Websockets url of a substrate node.
#[clap(
name = "url",
Expand All @@ -55,76 +61,84 @@ pub struct InfoCommand {
#[clap(name = "output-json", long)]
output_json: bool,
/// Display the contract's Wasm bytecode.
#[clap(name = "binary", long)]
#[clap(name = "binary", long, conflicts_with = "all")]
binary: bool,
/// Display all contracts addresses
#[clap(name = "all", long)]
all: bool,
}

impl InfoCommand {
pub fn run(&self) -> Result<(), ErrorVariant> {
tracing::debug!(
"Getting contract information for AccountId {:?}",
self.contract
);
pub async fn run(&self) -> Result<(), ErrorVariant> {
let client = OnlineClient::<DefaultConfig>::from_url(&self.url).await?;

Runtime::new()?.block_on(async {
let url = self.url.clone();
let client = OnlineClient::<DefaultConfig>::from_url(url).await?;
// All flag applied
if self.all {
// 1000 is max allowed value
const MAX_COUNT: u32 = 1000;
let mut count_from = None;
let mut contracts = Vec::new();
loop {
let len = contracts.len();
contracts.append(
&mut fetch_all_contracts(&client, MAX_COUNT, count_from).await?,
);
if contracts.len() < len + MAX_COUNT as usize {
break
}
count_from = contracts.last();
}

let info_result = fetch_contract_info(&self.contract, &client).await?;
if self.output_json {
let contracts_json = serde_json::json!({
"contracts": contracts
});
println!("{}", serde_json::to_string_pretty(&contracts_json)?);
} else {
display_all_contracts(&contracts)
}
Ok(())
} else {
// Contract arg shall be always present in this case, it is enforced by
// clap configuration
let contract = self
.contract
.as_ref()
.expect("Contract argument was not provided");

match info_result {
Some(info_to_json) => {
match (self.output_json, self.binary) {
(true, false) => println!("{}", info_to_json.to_json()?),
(false, false) => {
basic_display_format_contract_info(&info_to_json)
}
// Binary flag applied
(_, true) => {
let wasm_code =
fetch_wasm_code(*info_to_json.code_hash(), &client)
.await?;
match (wasm_code, self.output_json) {
(Some(code), false) => {
std::io::stdout()
.write_all(&code)
.expect("Writing to stdout failed")
}
(Some(code), true) => {
let wasm = serde_json::json!({
"wasm": format!("0x{}", hex::encode(code))
});
println!(
"{}",
serde_json::to_string_pretty(&wasm).map_err(
|err| {
anyhow!(
"JSON serialization failed: {}",
err
)
}
)?
);
}
(None, _) => {
return Err(anyhow!(
"Contract wasm code was not found"
)
.into())
}
}
}
}
Ok(())
}
None => {
Err(anyhow!(
let info_to_json =
fetch_contract_info(contract, &client)
.await?
.ok_or(anyhow!(
"No contract information was found for account id {}",
self.contract
)
.into())
contract
))?;

// Binary flag applied
if self.binary {
let wasm_code = fetch_wasm_code(&client, info_to_json.code_hash())
.await?
.ok_or(anyhow!(
"Contract wasm code was not found for account id {}",
contract
))?;

if self.output_json {
let wasm = serde_json::json!({
"wasm": format!("0x{}", hex::encode(wasm_code))
});
println!("{}", serde_json::to_string_pretty(&wasm)?);
} else {
std::io::stdout()
.write_all(&wasm_code)
.expect("Writing to stdout failed")
}
} else if self.output_json {
println!("{}", info_to_json.to_json()?)
} else {
basic_display_format_contract_info(&info_to_json)
}
})
Ok(())
}
}
}
12 changes: 11 additions & 1 deletion crates/cargo-contract/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ use std::io::{
self,
Write,
};
pub use subxt::PolkadotConfig as DefaultConfig;
pub use subxt::{
Config,
PolkadotConfig as DefaultConfig,
};

/// Arguments required for creating and sending an extrinsic to a substrate node.
#[derive(Clone, Debug, clap::Args)]
Expand Down Expand Up @@ -230,3 +233,10 @@ pub fn basic_display_format_contract_info(info: &ContractInfo) {
MAX_KEY_COL_WIDTH
);
}

/// Display all contracts addresses in a formatted way
pub fn display_all_contracts(contracts: &[<DefaultConfig as Config>::AccountId]) {
contracts
.iter()
.for_each(|e: &<DefaultConfig as Config>::AccountId| println!("{}", e))
}
4 changes: 3 additions & 1 deletion crates/cargo-contract/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ fn exec(cmd: Command) -> Result<()> {
.map_err(|err| map_extrinsic_err(err, remove.output_json()))
})
}
Command::Info(info) => info.run().map_err(format_err),
Command::Info(info) => {
runtime.block_on(async { info.run().await.map_err(format_err) })
}
Command::Verify(verify) => {
let result = verify.run().map_err(format_err)?;

Expand Down
6 changes: 6 additions & 0 deletions crates/extrinsics/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ impl From<std::io::Error> for ErrorVariant {
}
}

impl From<serde_json::Error> for ErrorVariant {
fn from(error: serde_json::Error) -> Self {
Self::Generic(GenericError::from_message(format!("{error:?}")))
}
}

#[derive(serde::Serialize)]
pub struct ModuleError {
pub pallet: String,
Expand Down
14 changes: 14 additions & 0 deletions crates/extrinsics/src/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,20 @@ async fn build_upload_instantiate_info() {
.assert()
.stdout(predicate::str::contains(r#""wasm": "0x"#));

let output = cargo_contract(project_path.as_path())
.arg("info")
.arg("--all")
.output()
.expect("failed to execute process");
let stdout = str::from_utf8(&output.stdout).unwrap();
let stderr = str::from_utf8(&output.stderr).unwrap();
assert!(
output.status.success(),
"getting all contracts failed: {stderr}"
);

assert_eq!(stdout.trim_end(), contract_account, "{stdout:?}");

// prevent the node_process from being dropped and killed
let _ = node_process;
}
Expand Down
53 changes: 51 additions & 2 deletions crates/extrinsics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ use scale::{
Decode,
Encode,
};
use sp_core::Bytes;
use sp_core::{
hashing,
Bytes,
};
use subxt::{
blocks,
config,
Expand Down Expand Up @@ -364,7 +367,10 @@ impl ContractInfo {
}

/// Fetch the contract wasm code from the storage using the provided client and code hash.
pub async fn fetch_wasm_code(hash: CodeHash, client: &Client) -> Result<Option<Vec<u8>>> {
pub async fn fetch_wasm_code(
client: &Client,
hash: &CodeHash,
) -> Result<Option<Vec<u8>>> {
let pristine_code_address = api::storage().contracts().pristine_code(hash);

let pristine_bytes = client
Expand All @@ -378,6 +384,49 @@ pub async fn fetch_wasm_code(hash: CodeHash, client: &Client) -> Result<Option<V
Ok(pristine_bytes)
}

/// Parse a contract account address from a storage key. Returns error if a key is
/// malformated.
fn parse_contract_account_address(
storage_contract_account_key: &[u8],
storage_contract_root_key_len: usize,
) -> Result<AccountId32> {
// storage_contract_account_key is a concatenation of contract_info_of root key and
// Twox64Concat(AccountId)
let mut account = storage_contract_account_key
.get(storage_contract_root_key_len + 8..)
.ok_or(anyhow!("Unexpected storage key size"))?;
AccountId32::decode(&mut account)
.map_err(|err| anyhow!("AccountId deserialization error: {}", err))
}

/// Fetch all contract addresses from the storage using the provided client and count of
/// requested elements starting from an optional address
pub async fn fetch_all_contracts(
client: &Client,
count: u32,
count_from: Option<&AccountId32>,
) -> Result<Vec<AccountId32>> {
let key = api::storage()
.contracts()
.contract_info_of_root()
.to_root_bytes();
let start_key = count_from
.map(|e| [key.clone(), hashing::twox_64(&e.0).to_vec(), e.0.to_vec()].concat());
let keys = client
.storage()
.at_latest()
.await?
.fetch_keys(key.as_ref(), count, start_key.as_deref())
.await?;

let contracts = keys
.into_iter()
.map(|e| parse_contract_account_address(&e.0, key.len()))
.collect::<Result<_, _>>()?;

Ok(contracts)
}

/// Copy of `pallet_contracts_primitives::StorageDeposit` which implements `Serialize`,
/// required for json output.
#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, serde::Serialize)]
Expand Down
1 change: 1 addition & 0 deletions docs/info.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ cargo contract info \
- `--url` the url of the rpc endpoint you want to specify - by default `ws://localhost:9944`.
- `--output-json` to export the output as JSON.
- `--binary` outputs Wasm code as a binary blob. If used in combination with `--output-json`, outputs Wasm code as JSON object with hex string.
- `--all` outputs all contracts addresses. It can not be used together with `--binary` flag.