Skip to content

Commit

Permalink
Add flag --all to cargo contract info (#1319)
Browse files Browse the repository at this point in the history
* Add --all argument to info command

* Add integration-tests for contract info --all, and code cleanup

* Changelog with info --all support updated

* Added error when out of range in slice for pallet map key

* Typo fixed in error message

* Made the errors more readable

* Code cleanup for info call

* Simplified fetch_all_contracts function

* Changed usize conversion and run method to async in info cmd

* Code cleanup, typo in description fixed

* Variables renamed
  • Loading branch information
smiasojed committed Sep 12, 2023
1 parent 8309adb commit 5173489
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 68 deletions.
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.

0 comments on commit 5173489

Please sign in to comment.