diff --git a/CHANGELOG.md b/CHANGELOG.md index ae8600339..1654b08bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Adds workflow for publishing docker images for the verifiable builds - [#1267](https://github.com/paritytech/cargo-contract/pull/1267) - 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/) ## [4.0.0-alpha] diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index f1740e47b..009a34d1d 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -24,9 +24,13 @@ use anyhow::{ }; use contract_extrinsics::{ fetch_contract_info, + fetch_wasm_code, ErrorVariant, }; -use std::fmt::Debug; +use std::{ + fmt::Debug, + io::Write, +}; use subxt::{ Config, OnlineClient, @@ -50,6 +54,9 @@ pub struct InfoCommand { /// Export the instantiate output in JSON format. #[clap(name = "output-json", long)] output_json: bool, + /// Display the contract's Wasm bytecode. + #[clap(name = "binary", long)] + binary: bool, } impl InfoCommand { @@ -67,10 +74,46 @@ impl InfoCommand { match info_result { Some(info_to_json) => { - if self.output_json { - println!("{}", info_to_json.to_json()?); - } else { - basic_display_format_contract_info(&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(()) } diff --git a/crates/extrinsics/src/integration_tests.rs b/crates/extrinsics/src/integration_tests.rs index 660c728f3..3b9e65c1a 100644 --- a/crates/extrinsics/src/integration_tests.rs +++ b/crates/extrinsics/src/integration_tests.rs @@ -22,6 +22,7 @@ use crate::{ UploadCommandBuilder, }; use anyhow::Result; +use contract_build::code_hash; use predicates::prelude::*; use std::{ ffi::OsStr, @@ -358,7 +359,7 @@ async fn build_upload_instantiate_info() { let contract_account = extract_contract_address(stdout); assert_eq!(48, contract_account.len(), "{stdout:?}"); - cargo_contract(project_path.as_path()) + let output = cargo_contract(project_path.as_path()) .arg("info") .args(["--contract", contract_account]) .output() @@ -366,7 +367,7 @@ async fn build_upload_instantiate_info() { let stderr = str::from_utf8(&output.stderr).unwrap(); assert!(output.status.success(), "getting info failed: {stderr}"); - cargo_contract(project_path.as_path()) + let output = cargo_contract(project_path.as_path()) .arg("info") .args(["--contract", contract_account]) .arg("--output-json") @@ -378,6 +379,32 @@ async fn build_upload_instantiate_info() { "getting info as JSON format failed: {stderr}" ); + let output = cargo_contract(project_path.as_path()) + .arg("info") + .args(["--contract", contract_account]) + .arg("--binary") + .output() + .expect("failed to execute process"); + let stderr = str::from_utf8(&output.stderr).unwrap(); + assert!( + output.status.success(), + "getting Wasm code failed: {stderr}" + ); + + // construct the contract file path + let contract_wasm = project_path.join("target/ink/flipper.wasm"); + + let code = std::fs::read(contract_wasm).expect("contract Wasm file not found"); + assert_eq!(code_hash(&code), code_hash(&output.stdout)); + + cargo_contract(project_path.as_path()) + .arg("info") + .args(["--contract", contract_account]) + .arg("--output-json") + .arg("--binary") + .assert() + .stdout(predicate::str::contains(r#""wasm": "0x"#)); + // prevent the node_process from being dropped and killed let _ = node_process; } diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 0901c5b33..06f28832d 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -43,7 +43,7 @@ use jsonrpsee::{ }; use std::path::PathBuf; -use crate::runtime_api::api::{self,}; +use crate::runtime_api::api; use contract_build::{ CrateMetadata, DEFAULT_KEY_COL_WIDTH, @@ -363,6 +363,21 @@ 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>> { + let pristine_code_address = api::storage().contracts().pristine_code(hash); + + let pristine_bytes = client + .storage() + .at_latest() + .await? + .fetch(&pristine_code_address) + .await? + .map(|v| v.0); + + Ok(pristine_bytes) +} + /// Copy of `pallet_contracts_primitives::StorageDeposit` which implements `Serialize`, /// required for json output. #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, serde::Serialize)] diff --git a/docs/info.md b/docs/info.md index 4712c84ec..00a278d63 100644 --- a/docs/info.md +++ b/docs/info.md @@ -18,3 +18,4 @@ cargo contract info \ *Optional* - `--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.