From 341c7a817f13dfa9aac4bd6ddc7af5b7607e8e14 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 12 Jan 2023 11:30:02 +0000 Subject: [PATCH] Extrinsics: allow specifying contract artifact directly (#893) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP introduce ContractArtifacts * WIP introduce ContractArtifacts * Use ContractArtifacts in call and upload * Extract function for constructing contract event data field * WIP refactoring instantiate * WIP fixing up CodeHas usage, fmt * Complete instantiate refactoring to use code artifacts * Attempt to use contract bundle artifact by default * Fix test compilation * CLIPPY * Remove `wasm_path`, put file option first * Fix conflicting * Update docs * Update docs/extrinsics.md Co-authored-by: Michael Müller * Update crates/build/src/crate_metadata.rs Co-authored-by: Michael Müller * Bump assert_cmd from 2.0.7 to 2.0.8 Bumps [assert_cmd](https://github.com/assert-rs/assert_cmd) from 2.0.7 to 2.0.8. - [Release notes](https://github.com/assert-rs/assert_cmd/releases) - [Changelog](https://github.com/assert-rs/assert_cmd/blob/master/CHANGELOG.md) - [Commits](https://github.com/assert-rs/assert_cmd/compare/v2.0.7...v2.0.8) --- updated-dependencies: - dependency-name: assert_cmd dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Bump regex from 1.7.0 to 1.7.1 Bumps [regex](https://github.com/rust-lang/regex) from 1.7.0 to 1.7.1. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.7.0...1.7.1) --- updated-dependencies: - dependency-name: regex dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * dependabot: ignore substrate deps (#902) * dependabot: ignore substrate deps * Move ignore section and use pattern * Fix update-types * Review suggestions * Fix Signed-off-by: dependabot[bot] Co-authored-by: Michael Müller Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- crates/build/src/crate_metadata.rs | 7 + crates/build/src/lib.rs | 12 ++ crates/build/src/metadata.rs | 33 ++--- crates/build/src/tests.rs | 4 +- .../cargo-contract/src/cmd/extrinsics/call.rs | 10 +- .../src/cmd/extrinsics/events.rs | 59 +++++--- .../src/cmd/extrinsics/instantiate.rs | 129 +++++------------ .../cargo-contract/src/cmd/extrinsics/mod.rs | 135 +++++++++++++++++- .../src/cmd/extrinsics/upload.rs | 70 ++++----- crates/metadata/src/lib.rs | 8 +- crates/transcode/src/lib.rs | 1 + docs/extrinsics.md | 11 ++ 12 files changed, 284 insertions(+), 195 deletions(-) diff --git a/crates/build/src/crate_metadata.rs b/crates/build/src/crate_metadata.rs index 12f10caad..2e025d61f 100644 --- a/crates/build/src/crate_metadata.rs +++ b/crates/build/src/crate_metadata.rs @@ -137,6 +137,13 @@ impl CrateMetadata { pub fn metadata_path(&self) -> PathBuf { self.target_directory.join(METADATA_FILE) } + + /// Get the path of the contract bundle, containing metadata + code. + pub fn contract_bundle_path(&self) -> PathBuf { + let target_directory = self.target_directory.clone(); + let fname_bundle = format!("{}.contract", self.contract_artifact_name); + target_directory.join(fname_bundle) + } } /// Get the result of `cargo metadata`, together with the root package id. diff --git a/crates/build/src/lib.rs b/crates/build/src/lib.rs index d13b9efeb..b6e16ef5d 100644 --- a/crates/build/src/lib.rs +++ b/crates/build/src/lib.rs @@ -732,6 +732,18 @@ pub fn execute(args: ExecuteArgs) -> Result { }) } +/// Returns the blake2 hash of the code slice. +pub fn code_hash(code: &[u8]) -> [u8; 32] { + use blake2::digest::{ + consts::U32, + Digest as _, + }; + let mut blake2 = blake2::Blake2b::::new(); + blake2.update(code); + let result = blake2.finalize(); + result.into() +} + /// Testing individual functions where the build itself is not actually invoked. See [`tests`] for /// all tests which invoke the `build` command. #[cfg(test)] diff --git a/crates/build/src/metadata.rs b/crates/build/src/metadata.rs index 88013cc04..93ec2dc99 100644 --- a/crates/build/src/metadata.rs +++ b/crates/build/src/metadata.rs @@ -15,6 +15,7 @@ // along with cargo-contract. If not, see . use crate::{ + code_hash, crate_metadata::CrateMetadata, maybe_println, util, @@ -31,13 +32,8 @@ use crate::{ }; use anyhow::Result; -use blake2::digest::{ - consts::U32, - Digest as _, -}; use colored::Colorize; use contract_metadata::{ - CodeHash, Compiler, Contract, ContractMetadata, @@ -58,7 +54,7 @@ use std::{ }; use url::Url; -const METADATA_FILE: &str = "metadata.json"; +pub const METADATA_FILE: &str = "metadata.json"; /// Metadata generation result. #[derive(serde::Serialize)] @@ -122,11 +118,8 @@ pub(crate) fn execute( unstable_options: &UnstableFlags, build_info: BuildInfo, ) -> Result { - let target_directory = crate_metadata.target_directory.clone(); - let out_path_metadata = target_directory.join(METADATA_FILE); - - let fname_bundle = format!("{}.contract", crate_metadata.contract_artifact_name); - let out_path_bundle = target_directory.join(fname_bundle); + let out_path_metadata = crate_metadata.metadata_path(); + let out_path_bundle = crate_metadata.contract_bundle_path(); // build the extended contract project metadata let ExtendedMetadataResult { @@ -142,8 +135,10 @@ pub(crate) fn execute( format!("{}", build_steps).bold(), "Generating metadata".bright_green().bold() ); - let target_dir_arg = - format!("--target-dir={}", target_directory.to_string_lossy()); + let target_dir_arg = format!( + "--target-dir={}", + crate_metadata.target_directory.to_string_lossy() + ); let stdout = util::invoke_cargo( "run", [ @@ -230,10 +225,10 @@ fn extended_metadata( let lang = SourceLanguage::new(Language::Ink, ink_version.clone()); let compiler = SourceCompiler::new(Compiler::RustC, rust_version); let wasm = fs::read(final_contract_wasm)?; - let hash = blake2_hash(wasm.as_slice()); + let hash = code_hash(wasm.as_slice()); Source::new( Some(SourceWasm::new(wasm)), - hash, + hash.into(), lang, compiler, Some(build_info.try_into()?), @@ -280,11 +275,3 @@ fn extended_metadata( user, }) } - -/// Returns the blake2 hash of the submitted slice. -pub fn blake2_hash(code: &[u8]) -> CodeHash { - let mut blake2 = blake2::Blake2b::::new(); - blake2.update(code); - let result = blake2.finalize(); - CodeHash(result.into()) -} diff --git a/crates/build/src/tests.rs b/crates/build/src/tests.rs index 19cd54a75..de0fb2241 100644 --- a/crates/build/src/tests.rs +++ b/crates/build/src/tests.rs @@ -497,7 +497,7 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { // calculate wasm hash let fs_wasm = fs::read(&crate_metadata.dest_wasm)?; - let expected_hash = crate::metadata::blake2_hash(&fs_wasm[..]); + let expected_hash = crate::code_hash(&fs_wasm[..]); let expected_wasm = build_byte_str(&fs_wasm); let expected_language = @@ -514,7 +514,7 @@ fn generates_metadata(manifest_path: &ManifestPath) -> Result<()> { serde_json::Value::Array(vec!["and".into(), "their".into(), "values".into()]), ); - assert_eq!(build_byte_str(&expected_hash.0[..]), hash.as_str().unwrap()); + assert_eq!(build_byte_str(&expected_hash[..]), hash.as_str().unwrap()); assert_eq!(expected_wasm, wasm.as_str().unwrap()); assert_eq!(expected_language, language.as_str().unwrap()); assert_eq!(expected_compiler, compiler.as_str().unwrap()); diff --git a/crates/cargo-contract/src/cmd/extrinsics/call.rs b/crates/cargo-contract/src/cmd/extrinsics/call.rs index c103e9196..4555f1aa1 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/call.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/call.rs @@ -24,7 +24,6 @@ use super::{ BalanceVariant, Client, ContractMessageTranscoder, - CrateMetadata, DefaultConfig, ExtrinsicOpts, PairSigner, @@ -95,10 +94,9 @@ impl CallCommand { } pub fn run(&self) -> Result<(), ErrorVariant> { - let crate_metadata = CrateMetadata::from_manifest_path( - self.extrinsic_opts.manifest_path.as_ref(), - )?; - let transcoder = ContractMessageTranscoder::load(crate_metadata.metadata_path())?; + let artifacts = self.extrinsic_opts.contract_artifacts()?; + let transcoder = artifacts.contract_transcoder()?; + let call_data = transcoder.encode(&self.message, &self.args)?; tracing::debug!("Message data: {:?}", hex::encode(&call_data)); @@ -218,7 +216,7 @@ impl CallCommand { let result = submit_extrinsic(client, &call, signer).await?; let display_events = - DisplayEvents::from_events(&result, transcoder, &client.metadata())?; + DisplayEvents::from_events(&result, Some(transcoder), &client.metadata())?; let output = if self.output_json { display_events.to_json()? diff --git a/crates/cargo-contract/src/cmd/extrinsics/events.rs b/crates/cargo-contract/src/cmd/extrinsics/events.rs index 5120eab64..fbd354a9f 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/events.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/events.rs @@ -25,16 +25,21 @@ use colored::Colorize as _; use contract_build::Verbosity; use contract_transcode::{ ContractMessageTranscoder, + Hex, TranscoderBuilder, Value, }; use anyhow::Result; -use std::fmt::Write; +use std::{ + fmt::Write, + str::FromStr, +}; use subxt::{ self, blocks::ExtrinsicEvents, events::StaticEvent, + metadata::EventFieldMetadata, }; /// Field that represent data of an event from invoking a contract extrinsic. @@ -78,7 +83,7 @@ impl DisplayEvents { /// Parses events and returns an object which can be serialised pub fn from_events( result: &ExtrinsicEvents, - transcoder: &ContractMessageTranscoder, + transcoder: Option<&ContractMessageTranscoder>, subxt_metadata: &subxt::Metadata, ) -> Result { let mut events: Vec = vec![]; @@ -111,22 +116,12 @@ impl DisplayEvents { ) && field_metadata.name() == Some("data") { tracing::debug!("event data: {:?}", hex::encode(&event_data)); - match transcoder.decode_contract_event(event_data) { - Ok(contract_event) => { - let field = Field::new( - String::from("data"), - contract_event, - field_metadata.type_name().map(|s| s.to_string()), - ); - event_entry.fields.push(field); - } - Err(err) => { - tracing::warn!( - "Decoding contract event failed: {:?}. It might have come from another contract.", - err - ) - } - } + let field = contract_event_data_field( + transcoder, + field_metadata, + event_data, + )?; + event_entry.fields.push(field); } else { let field_name = field_metadata .name() @@ -208,3 +203,31 @@ impl DisplayEvents { Ok(serde_json::to_string_pretty(self)?) } } + +/// Construct the contract event data field, attempting to decode the event using the +/// [`ContractMessageTranscoder`] if available. +fn contract_event_data_field( + transcoder: Option<&ContractMessageTranscoder>, + field_metadata: &EventFieldMetadata, + event_data: &mut &[u8], +) -> Result { + let event_value = if let Some(transcoder) = transcoder { + match transcoder.decode_contract_event(event_data) { + Ok(contract_event) => contract_event, + Err(err) => { + tracing::warn!( + "Decoding contract event failed: {:?}. It might have come from another contract.", + err + ); + Value::Hex(Hex::from_str(&hex::encode(&event_data))?) + } + } + } else { + Value::Hex(Hex::from_str(&hex::encode(event_data))?) + }; + Ok(Field::new( + String::from("data"), + event_value, + field_metadata.type_name().map(|s| s.to_string()), + )) +} diff --git a/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs b/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs index aa301500e..a04188aea 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/instantiate.rs @@ -25,7 +25,6 @@ use super::{ Client, CodeHash, ContractMessageTranscoder, - CrateMetadata, DefaultConfig, ExtrinsicOpts, PairSigner, @@ -43,7 +42,6 @@ use crate::{ }; use anyhow::{ anyhow, - Context, Result, }; use contract_build::{ @@ -60,13 +58,6 @@ use sp_core::{ Bytes, }; use sp_weights::Weight; -use std::{ - fs, - path::{ - Path, - PathBuf, - }, -}; use subxt::{ blocks::ExtrinsicEvents, Config, @@ -75,16 +66,6 @@ use subxt::{ #[derive(Debug, clap::Args)] pub struct InstantiateCommand { - /// Path to Wasm contract code, defaults to `./target/ink/.wasm`. - /// Use to instantiate contracts which have not yet been uploaded. - /// If the contract has already been uploaded use `--code-hash` instead. - #[clap(value_parser)] - wasm_path: Option, - /// The hash of the smart contract code already uploaded to the chain. - /// If the contract has not already been uploaded use `--wasm-path` or run the `upload` command - /// first. - #[clap(long, value_parser = parse_code_hash)] - code_hash: Option<::Hash>, /// The name of the contract constructor to call #[clap(name = "constructor", long, default_value = "new")] constructor: String, @@ -113,17 +94,6 @@ pub struct InstantiateCommand { output_json: bool, } -/// Parse a hex encoded 32 byte hash. Returns error if not exactly 32 bytes. -fn parse_code_hash(input: &str) -> Result<::Hash> { - let bytes = decode_hex(input)?; - if bytes.len() != 32 { - anyhow::bail!("Code hash should be 32 bytes in length") - } - let mut arr = [0u8; 32]; - arr.copy_from_slice(&bytes); - Ok(arr.into()) -} - /// Parse hex encoded bytes. fn parse_hex_bytes(input: &str) -> Result { let bytes = decode_hex(input)?; @@ -140,36 +110,18 @@ impl InstantiateCommand { /// Creates an extrinsic with the `Contracts::instantiate` Call, submits via RPC, then waits for /// the `ContractsEvent::Instantiated` event. pub fn run(&self) -> Result<(), ErrorVariant> { - let crate_metadata = CrateMetadata::from_manifest_path( - self.extrinsic_opts.manifest_path.as_ref(), - )?; - let transcoder = ContractMessageTranscoder::load(crate_metadata.metadata_path())?; + let artifacts = self.extrinsic_opts.contract_artifacts()?; + let transcoder = artifacts.contract_transcoder()?; let data = transcoder.encode(&self.constructor, &self.args)?; let signer = super::pair_signer(self.extrinsic_opts.signer()?); let url = self.extrinsic_opts.url_to_string(); let verbosity = self.extrinsic_opts.verbosity()?; - - fn load_code(wasm_path: &Path) -> Result { - tracing::debug!("Contract code path: {}", wasm_path.display()); - let code = fs::read(wasm_path) - .context(format!("Failed to read from {}", wasm_path.display()))?; - Ok(Code::Upload(code)) - } - - let code = match (self.wasm_path.as_ref(), self.code_hash.as_ref()) { - (Some(_), Some(_)) => { - Err(anyhow!( - "Specify either `--wasm-path` or `--code-hash` but not both" - )) - } - (Some(wasm_path), None) => load_code(wasm_path), - (None, None) => { - // default to the target contract wasm in the current project, - // inferred via the crate metadata. - load_code(&crate_metadata.dest_wasm) - } - (None, Some(code_hash)) => Ok(Code::Existing(*code_hash)), - }?; + let code = if let Some(code) = artifacts.code { + Code::Upload(code.0) + } else { + let code_hash = artifacts.code_hash()?; + Code::Existing(code_hash.into()) + }; let salt = self.salt.clone().map(|s| s.0).unwrap_or_default(); async_std::task::block_on(async move { @@ -189,6 +141,7 @@ impl InstantiateCommand { .as_ref() .map(|bv| bv.denominate_balance(&token_metadata)) .transpose()?, + code, data, salt, }; @@ -204,7 +157,7 @@ impl InstantiateCommand { output_json: self.output_json, }; - exec.exec(code, self.extrinsic_opts.dry_run).await + exec.exec(self.extrinsic_opts.dry_run).await }) } } @@ -216,6 +169,7 @@ struct InstantiateArgs { gas_limit: Option, proof_size: Option, storage_deposit_limit: Option, + code: Code, data: Vec, salt: Vec, } @@ -238,10 +192,10 @@ pub struct Exec { } impl Exec { - async fn exec(&self, code: Code, dry_run: bool) -> Result<(), ErrorVariant> { + async fn exec(&self, dry_run: bool) -> Result<(), ErrorVariant> { tracing::debug!("instantiate data {:?}", self.args.data); if dry_run { - let result = self.instantiate_dry_run(code).await?; + let result = self.instantiate_dry_run().await?; match result.result { Ok(ref ret_val) => { let dry_run_result = InstantiateDryRunResult { @@ -277,23 +231,24 @@ impl Exec { } } } else { - match code { + let gas_limit = self.pre_submit_dry_run_gas_estimate().await?; + match self.args.code.clone() { Code::Upload(code) => { - self.instantiate_with_code(code).await?; + self.instantiate_with_code(code, gas_limit).await?; } Code::Existing(code_hash) => { - self.instantiate(code_hash).await?; + self.instantiate(code_hash, gas_limit).await?; } } Ok(()) } } - async fn instantiate_with_code(&self, code: Vec) -> Result<(), ErrorVariant> { - let gas_limit = self - .pre_submit_dry_run_gas_estimate(Code::Upload(code.clone())) - .await?; - + async fn instantiate_with_code( + &self, + code: Vec, + gas_limit: Weight, + ) -> Result<(), ErrorVariant> { if !self.opts.skip_confirm { prompt_confirm_tx(|| self.print_default_instantiate_preview(gas_limit))?; } @@ -323,11 +278,11 @@ impl Exec { .await } - async fn instantiate(&self, code_hash: CodeHash) -> Result<(), ErrorVariant> { - let gas_limit = self - .pre_submit_dry_run_gas_estimate(Code::Existing(code_hash)) - .await?; - + async fn instantiate( + &self, + code_hash: CodeHash, + gas_limit: Weight, + ) -> Result<(), ErrorVariant> { if !self.opts.skip_confirm { prompt_confirm_tx(|| { self.print_default_instantiate_preview(gas_limit); @@ -368,7 +323,7 @@ impl Exec { ) -> Result<(), ErrorVariant> { let events = DisplayEvents::from_events( result, - &self.transcoder, + Some(&self.transcoder), &self.client.metadata(), )?; let contract_address = contract_address.to_ss58check(); @@ -398,7 +353,6 @@ impl Exec { async fn instantiate_dry_run( &self, - code: Code, ) -> Result::AccountId, Balance>> { let storage_deposit_limit = self.args.storage_deposit_limit; @@ -407,7 +361,7 @@ impl Exec { value: self.args.value, gas_limit: None, storage_deposit_limit, - code, + code: self.args.code.clone(), data: self.args.data.clone(), salt: self.args.salt.clone(), }; @@ -415,7 +369,7 @@ impl Exec { } /// Dry run the instantiation before tx submission. Returns the gas required estimate. - async fn pre_submit_dry_run_gas_estimate(&self, code: Code) -> Result { + async fn pre_submit_dry_run_gas_estimate(&self) -> Result { if self.opts.skip_dry_run { return match (self.args.gas_limit, self.args.proof_size) { (Some(ref_time), Some(proof_size)) => Ok(Weight::from_parts(ref_time, proof_size)), @@ -429,7 +383,7 @@ impl Exec { if !self.output_json { super::print_dry_running_status(&self.args.constructor); } - let instantiate_result = self.instantiate_dry_run(code).await?; + let instantiate_result = self.instantiate_dry_run().await?; match instantiate_result.result { Ok(_) => { if !self.output_json { @@ -529,29 +483,10 @@ struct InstantiateRequest { } /// Reference to an existing code hash or a new Wasm module. -#[derive(Encode)] +#[derive(Clone, Encode)] enum Code { /// A Wasm module as raw bytes. Upload(Vec), /// The code hash of an on-chain Wasm blob. Existing(::Hash), } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_code_hash_works() { - // with 0x prefix - assert!(parse_code_hash( - "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) - .is_ok()); - // without 0x prefix - assert!(parse_code_hash( - "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" - ) - .is_ok()) - } -} diff --git a/crates/cargo-contract/src/cmd/extrinsics/mod.rs b/crates/cargo-contract/src/cmd/extrinsics/mod.rs index 116ec7745..6470b570a 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/mod.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/mod.rs @@ -71,13 +71,18 @@ use subxt::{ OnlineClient, }; -use std::option::Option; +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; @@ -92,6 +97,10 @@ type Client = OnlineClient; /// Arguments required for creating and sending an extrinsic to a substrate node. #[derive(Clone, Debug, clap::Args)] pub struct ExtrinsicOpts { + /// Path to a contract build artifact file: a raw `.wasm` file, a `.contract` bundle, + /// or a `.json` metadata file. + #[clap(value_parser, conflicts_with = "manifest_path")] + file: Option, /// Path to the `Cargo.toml` of the contract. #[clap(long, value_parser)] manifest_path: Option, @@ -127,8 +136,34 @@ pub struct ExtrinsicOpts { } impl ExtrinsicOpts { + /// Load contract artifacts. + pub fn contract_artifacts(&self) -> Result { + 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()) + } + + /// Returns the signer for contract extrinsics. pub fn signer(&self) -> Result { - sr25519::Pair::from_string(&self.suri, self.password.as_ref().map(String::as_ref)) + Pair::from_string(&self.suri, self.password.as_ref().map(String::as_ref)) .map_err(|_| anyhow::anyhow!("Secret string error")) } @@ -163,6 +198,102 @@ impl ExtrinsicOpts { } } +/// Contract artifacts for use with extrinsic commands. +#[derive(Debug)] +pub struct ContractArtifacts { + /// The original artifact path + artifacts_path: PathBuf, + /// The expected path of the file containing the contract metadata. + metadata_path: PathBuf, + /// The deserialized contract metadata if the expected metadata file exists. + metadata: Option, + /// The Wasm code of the contract if available. + pub code: Option, +} + +impl ContractArtifacts { + /// Given a contract artifact path, load the contract code and metadata where possible. + pub fn from_artifact_path(path: &Path) -> Result { + 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, + }) + } + + /// Get the path of the artifact file used to load the artifacts. + pub fn artifact_path(&self) -> &Path { + self.artifacts_path.as_path() + } + + /// Get contract metadata, if available. + /// + /// ## Errors + /// - No contract metadata could be found. + /// - Invalid contract metadata. + pub fn metadata(&self) -> Result { + self.metadata.clone().ok_or_else(|| { + anyhow!( + "No contract metadata found. Expected file {}", + self.metadata_path.as_path().display() + ) + }) + } + + /// Get the code hash from the contract metadata. + pub fn code_hash(&self) -> Result<[u8; 32]> { + let metadata = self.metadata()?; + Ok(metadata.source.hash.0) + } + + /// Construct a [`ContractMessageTranscoder`] from contract metadata. + pub fn contract_transcoder(&self) -> Result { + let metadata = self.metadata()?; + ContractMessageTranscoder::try_from(metadata) + .context("Failed to deserialize ink project metadata from contract metadata") + } +} + +/// The Wasm code of a contract. +#[derive(Debug)] +pub struct WasmCode(Vec); + +impl WasmCode { + /// The hash of the contract code: uniquely identifies the contract code on-chain. + pub fn code_hash(&self) -> [u8; 32] { + contract_build::code_hash(&self.0) + } +} + /// Create a new [`PairSigner`] from the given [`sr25519::Pair`]. pub fn pair_signer(pair: sr25519::Pair) -> PairSigner { PairSigner::new(pair) diff --git a/crates/cargo-contract/src/cmd/extrinsics/upload.rs b/crates/cargo-contract/src/cmd/extrinsics/upload.rs index 7e93b73c6..79f410c34 100644 --- a/crates/cargo-contract/src/cmd/extrinsics/upload.rs +++ b/crates/cargo-contract/src/cmd/extrinsics/upload.rs @@ -15,14 +15,15 @@ // along with cargo-contract. If not, see . use super::{ - runtime_api::api, + runtime_api::api::{ + self, + runtime_types::pallet_contracts::wasm::Determinism, + }, state_call, submit_extrinsic, Balance, Client, CodeHash, - ContractMessageTranscoder, - CrateMetadata, DefaultConfig, ExtrinsicOpts, PairSigner, @@ -32,32 +33,22 @@ use crate::{ cmd::extrinsics::{ events::DisplayEvents, ErrorVariant, + WasmCode, }, name_value_println, }; -use anyhow::{ - Context, - Result, -}; +use anyhow::Result; use pallet_contracts_primitives::CodeUploadResult; use scale::Encode; -use std::{ - fmt::Debug, - path::PathBuf, -}; +use std::fmt::Debug; use subxt::{ Config, OnlineClient, }; -use super::runtime_api::api::runtime_types::pallet_contracts::wasm::Determinism; - #[derive(Debug, clap::Args)] #[clap(name = "upload", about = "Upload a contract's code")] pub struct UploadCommand { - /// Path to Wasm contract code, defaults to `./target/ink/.wasm`. - #[clap(value_parser)] - wasm_path: Option, #[clap(flatten)] extrinsic_opts: ExtrinsicOpts, /// Export the call output in JSON format. @@ -71,27 +62,17 @@ impl UploadCommand { } pub fn run(&self) -> Result<(), ErrorVariant> { - let crate_metadata = CrateMetadata::from_manifest_path( - self.extrinsic_opts.manifest_path.as_ref(), - )?; - let contract_metadata = - contract_metadata::ContractMetadata::load(&crate_metadata.metadata_path())?; - let code_hash = contract_metadata.source.hash; - let transcoder = - ContractMessageTranscoder::try_from(contract_metadata).context(format!( - "Failed to deserialize ink project metadata from contract metadata {}", - crate_metadata.metadata_path().display() - ))?; + let artifacts = self.extrinsic_opts.contract_artifacts()?; let signer = super::pair_signer(self.extrinsic_opts.signer()?); - let wasm_path = match &self.wasm_path { - Some(wasm_path) => wasm_path.clone(), - None => crate_metadata.dest_wasm, - }; - - tracing::debug!("Contract code path: {}", wasm_path.display()); - let code = std::fs::read(&wasm_path) - .context(format!("Failed to read from {}", wasm_path.display()))?; + let artifacts_path = artifacts.artifact_path().to_path_buf(); + let code = artifacts.code.ok_or_else(|| { + anyhow::anyhow!( + "Contract code not found from artifact file {}", + artifacts_path.display() + ) + })?; + let code_hash = code.code_hash(); async_std::task::block_on(async { let url = self.extrinsic_opts.url_to_string(); @@ -122,9 +103,8 @@ impl UploadCommand { } } Ok(()) - } else if let Some(code_stored) = self - .upload_code(&client, code, &signer, &transcoder) - .await? + } else if let Some(code_stored) = + self.upload_code(&client, code, &signer).await? { let upload_result = UploadResult { code_hash: format!("{:?}", code_stored.code_hash), @@ -137,8 +117,7 @@ impl UploadCommand { Ok(()) } else { Err(anyhow::anyhow!( - "This contract has already been uploaded with code hash: {:?}", - code_hash + "This contract has already been uploaded with code hash: {code_hash:?}" ) .into()) } @@ -147,7 +126,7 @@ impl UploadCommand { async fn upload_code_rpc( &self, - code: Vec, + code: WasmCode, client: &Client, signer: &PairSigner, ) -> Result> { @@ -161,7 +140,7 @@ impl UploadCommand { .transpose()?; let call_request = CodeUploadRequest { origin: signer.account_id().clone(), - code, + code: code.0, storage_deposit_limit, determinism: Determinism::Deterministic, }; @@ -171,22 +150,21 @@ impl UploadCommand { async fn upload_code( &self, client: &Client, - code: Vec, + code: WasmCode, signer: &PairSigner, - transcoder: &ContractMessageTranscoder, ) -> Result, ErrorVariant> { let token_metadata = TokenMetadata::query(client).await?; let storage_deposit_limit = self.extrinsic_opts.storage_deposit_limit(&token_metadata)?; let call = super::runtime_api::api::tx().contracts().upload_code( - code, + code.0, storage_deposit_limit, Determinism::Deterministic, ); let result = submit_extrinsic(client, &call, signer).await?; let display_events = - DisplayEvents::from_events(&result, transcoder, &client.metadata())?; + DisplayEvents::from_events(&result, None, &client.metadata())?; let output = if self.output_json { display_events.to_json()? diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index 9943f0c7d..93810111d 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -121,7 +121,7 @@ impl ContractMetadata { } /// Reads the file and tries to parse it as instance of `ContractMetadata`. - pub fn load

(metadata_path: &P) -> Result + pub fn load

(metadata_path: P) -> Result where P: AsRef, { @@ -146,6 +146,12 @@ pub struct CodeHash( pub [u8; 32], ); +impl From<[u8; 32]> for CodeHash { + fn from(value: [u8; 32]) -> Self { + CodeHash(value) + } +} + /// Information about the contract's Wasm code. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Source { diff --git a/crates/transcode/src/lib.rs b/crates/transcode/src/lib.rs index 033d1efab..05b70f880 100644 --- a/crates/transcode/src/lib.rs +++ b/crates/transcode/src/lib.rs @@ -105,6 +105,7 @@ mod util; pub use self::{ scon::{ + Hex, Map, Tuple, Value, diff --git a/docs/extrinsics.md b/docs/extrinsics.md index 3a4ce0ba6..b1935487a 100644 --- a/docs/extrinsics.md +++ b/docs/extrinsics.md @@ -97,6 +97,17 @@ cargo contract call \ - `--message` the name of the contract message to invoke. - `--args` accepts a space separated list of values, encoded in order as the arguments of the message to invoke. +## Specifying the contract artifact + +The above examples assume the working directory is the contract source code where the `Cargo.toml` file is located. +This is used to determine the location of the contract artifacts. Alternatively, there is an optional positional +argument to each of the extrinsic commands which allows specifying the contract artifact file directly. E.g. + + +`cargo upload ../path/to/mycontract.wasm` +`cargo instantiate ../path/to/mycontract.contract` +`cargo call ..path/to/metadata.json` +