diff --git a/src/cmd/decode.rs b/src/cmd/decode.rs new file mode 100644 index 000000000..b0e94f7e8 --- /dev/null +++ b/src/cmd/decode.rs @@ -0,0 +1,67 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use crate::{ + cmd::extrinsics::{load_metadata, ContractMessageTranscoder}, + util::decode_hex, + DEFAULT_KEY_COL_WIDTH, +}; +use anyhow::{Context, Result}; +use colored::Colorize as _; + +#[derive(Debug, Clone, clap::Args)] +#[clap(name = "decode", about = "Decode input_data for a contract")] +pub struct DecodeCommand { + /// Type of data + #[clap(arg_enum, short, long)] + r#type: DataType, + /// The data to decode + #[clap(short, long)] + data: String, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, clap::ArgEnum)] +enum DataType { + Event, + Message, + Constructor, +} + +impl DecodeCommand { + pub fn run(&self) -> Result<()> { + let (_, contract_metadata) = load_metadata(None)?; + let transcoder = ContractMessageTranscoder::new(&contract_metadata); + + const ERR_MSG: &str = "Failed to decode specified data as a hex value"; + let decoded_data = match self.r#type { + DataType::Event => transcoder + .decode_contract_event(&mut &decode_hex(&self.data).context(ERR_MSG)?[..])?, + DataType::Message => transcoder + .decode_contract_message(&mut &decode_hex(&self.data).context(ERR_MSG)?[..])?, + DataType::Constructor => transcoder + .decode_contract_constructor(&mut &decode_hex(&self.data).context(ERR_MSG)?[..])?, + }; + + println!( + "{:>width$} {}", + "Decoded data:".bright_green().bold(), + decoded_data, + width = DEFAULT_KEY_COL_WIDTH + ); + + Ok(()) + } +} diff --git a/src/cmd/extrinsics/call.rs b/src/cmd/extrinsics/call.rs index 70bd194f1..e6d255509 100644 --- a/src/cmd/extrinsics/call.rs +++ b/src/cmd/extrinsics/call.rs @@ -56,6 +56,8 @@ impl CallCommand { let (_, contract_metadata) = load_metadata(self.extrinsic_opts.manifest_path.as_ref())?; let transcoder = ContractMessageTranscoder::new(&contract_metadata); let call_data = transcoder.encode(&self.message, &self.args)?; + log::debug!("message data: {:?}", hex::encode(&call_data)); + let signer = super::pair_signer(self.extrinsic_opts.signer()?); async_std::task::block_on(async { diff --git a/src/cmd/extrinsics/events.rs b/src/cmd/extrinsics/events.rs index 845e3cc69..98031c9bc 100644 --- a/src/cmd/extrinsics/events.rs +++ b/src/cmd/extrinsics/events.rs @@ -66,6 +66,7 @@ pub fn display_events( if ::is_event(&event.pallet, &event.variant) && field.name() == Some(&"data".to_string()) { + log::debug!("event data: {:?}", hex::encode(&event_data)); let contract_event = transcoder.decode_contract_event(event_data)?; maybe_println!( verbosity, diff --git a/src/cmd/extrinsics/mod.rs b/src/cmd/extrinsics/mod.rs index 8a544772b..158a7a14c 100644 --- a/src/cmd/extrinsics/mod.rs +++ b/src/cmd/extrinsics/mod.rs @@ -28,7 +28,7 @@ mod integration_tests; use anyhow::{anyhow, Context, Result}; use std::{fs::File, path::PathBuf}; -use self::{events::display_events, transcode::ContractMessageTranscoder}; +use self::events::display_events; use crate::{ crate_metadata::CrateMetadata, name_value_println, workspace::ManifestPath, Verbosity, VerbosityFlags, @@ -37,6 +37,7 @@ use pallet_contracts_primitives::ContractResult; use sp_core::{crypto::Pair, sr25519}; use subxt::{Config, DefaultConfig}; +pub use self::transcode::ContractMessageTranscoder; pub use call::CallCommand; pub use instantiate::InstantiateCommand; pub use runtime_api::api::{DispatchError as RuntimeDispatchError, Event as RuntimeEvent}; diff --git a/src/cmd/extrinsics/transcode/mod.rs b/src/cmd/extrinsics/transcode/mod.rs index 4c12c2f4e..421b2e585 100644 --- a/src/cmd/extrinsics/transcode/mod.rs +++ b/src/cmd/extrinsics/transcode/mod.rs @@ -175,7 +175,6 @@ impl<'a> ContractMessageTranscoder<'a> { // data is an encoded `Vec` so is prepended with its length `Compact`, which we // ignore because the structure of the event data is known for decoding. let _len = >::decode(data)?; - let variant_index = data.read_byte()?; let event_spec = self .metadata @@ -203,6 +202,60 @@ impl<'a> ContractMessageTranscoder<'a> { Ok(Value::Map(map)) } + pub fn decode_contract_message(&self, data: &mut &[u8]) -> Result { + let mut msg_selector = [0u8; 4]; + data.read(&mut msg_selector)?; + let msg_spec = self + .messages() + .find(|x| msg_selector == x.selector().to_bytes()) + .ok_or_else(|| { + anyhow::anyhow!( + "Message with selector {} not found in contract metadata", + hex::encode(&msg_selector) + ) + })?; + log::debug!("decoding contract message '{}'", msg_spec.label()); + + let mut args = Vec::new(); + for arg in msg_spec.args() { + let name = arg.label().to_string(); + let value = self.transcoder.decode(arg.ty().ty().id(), data)?; + args.push((Value::String(name), value)); + } + + let name = msg_spec.label().to_string(); + let map = Map::new(Some(&name), args.into_iter().collect()); + + Ok(Value::Map(map)) + } + + pub fn decode_contract_constructor(&self, data: &mut &[u8]) -> Result { + let mut msg_selector = [0u8; 4]; + data.read(&mut msg_selector)?; + let msg_spec = self + .constructors() + .find(|x| msg_selector == x.selector().to_bytes()) + .ok_or_else(|| { + anyhow::anyhow!( + "Constructor with selector {} not found in contract metadata", + hex::encode(&msg_selector) + ) + })?; + log::debug!("decoding contract constructor '{}'", msg_spec.label()); + + let mut args = Vec::new(); + for arg in msg_spec.args() { + let name = arg.label().to_string(); + let value = self.transcoder.decode(arg.ty().ty().id(), data)?; + args.push((Value::String(name), value)); + } + + let name = msg_spec.label().to_string(); + let map = Map::new(Some(&name), args.into_iter().collect()); + + Ok(Value::Map(map)) + } + pub fn decode_return(&self, name: &str, data: &mut &[u8]) -> Result { let msg_spec = self .find_message_spec(name) @@ -391,4 +444,15 @@ mod tests { Ok(()) } + + #[test] + fn decode_contract_message() -> Result<()> { + let metadata = generate_metadata(); + let transcoder = ContractMessageTranscoder::new(&metadata); + + let encoded_bytes = hex::decode("633aa551").unwrap(); + let _ = transcoder.decode_contract_message(&mut &encoded_bytes[..])?; + + Ok(()) + } } diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index aadc8619e..afee32e88 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -15,12 +15,14 @@ // along with cargo-contract. If not, see . pub mod build; +pub mod decode; pub mod metadata; pub mod new; pub mod test; pub(crate) use self::{ build::{BuildCommand, CheckCommand}, + decode::DecodeCommand, test::TestCommand, }; mod extrinsics; diff --git a/src/main.rs b/src/main.rs index 848a75ac3..5711fce6a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,8 +22,8 @@ mod workspace; use self::{ cmd::{ - metadata::MetadataResult, BuildCommand, CallCommand, CheckCommand, InstantiateCommand, - TestCommand, UploadCommand, + metadata::MetadataResult, BuildCommand, CallCommand, CheckCommand, DecodeCommand, + InstantiateCommand, TestCommand, UploadCommand, }, util::DEFAULT_KEY_COL_WIDTH, workspace::ManifestPath, @@ -447,6 +447,9 @@ enum Command { /// Call a contract #[clap(name = "call")] Call(CallCommand), + /// Decode a contract input data + #[clap(name = "decode")] + Decode(DecodeCommand), } fn main() { @@ -504,6 +507,7 @@ fn exec(cmd: Command) -> Result<()> { Command::Upload(upload) => upload.run(), Command::Instantiate(instantiate) => instantiate.run(), Command::Call(call) => call.run(), + Command::Decode(decode) => decode.run(), } } diff --git a/tests/decode.rs b/tests/decode.rs new file mode 100644 index 000000000..970458d66 --- /dev/null +++ b/tests/decode.rs @@ -0,0 +1,134 @@ +// Copyright 2018-2020 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use std::path::Path; + +/// Create a `cargo contract` command +fn cargo_contract>(path: P) -> assert_cmd::Command { + let mut cmd = assert_cmd::Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); + cmd.current_dir(path).arg("contract"); + cmd +} + +#[test] +fn decode_works() { + // given + let contract = r#" + #![cfg_attr(not(feature = "std"), no_std)] + + use ink_lang as ink; + + #[ink::contract] + mod switcher { + #[ink(event)] + pub struct Switched { + new_value: bool, + } + + #[ink(storage)] + pub struct Switcher { + value: bool, + } + + impl Switcher { + #[ink(constructor, selector = 0xBABEBABE)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + + #[ink(message, selector = 0xBABEBABE)] + pub fn switch(&mut self, value: bool) { + self.value = value; + } + } + }"#; + + let tmp_dir = tempfile::Builder::new() + .prefix("cargo-contract.cli.test.") + .tempdir() + .expect("temporary directory creation failed"); + + // cargo contract new decode_test + cargo_contract(tmp_dir.path()) + .arg("new") + .arg("switcher") + .assert() + .success(); + + let project_dir = tmp_dir.path().to_path_buf().join("switcher"); + + let lib = project_dir.join("lib.rs"); + std::fs::write(&lib, contract).expect("Failed to write contract lib.rs"); + + assert_cmd::Command::new("rustup") + .arg("override") + .arg("set") + .arg("nightly") + .assert() + .success(); + + log::info!("Building contract in {}", project_dir.to_string_lossy()); + cargo_contract(&project_dir) + .arg("build") + .arg("--skip-linting") + .assert() + .success(); + + let msg_data: &str = "babebabe01"; + let msg_decoded: &str = r#"switch { value: true }"#; + + // then + // message data is being decoded properly + cargo_contract(&project_dir) + .arg("decode") + .arg("--data") + .arg(msg_data) + .arg("-t") + .arg("message") + .assert() + .success() + .stdout(predicates::str::contains(msg_decoded)); + + let event_data: &str = "080001"; + let event_decoded: &str = r#"Switched { new_value: true }"#; + + // and + // event data is being decoded properly + cargo_contract(&project_dir) + .arg("decode") + .arg("--data") + .arg(event_data) + .arg("-t") + .arg("event") + .assert() + .success() + .stdout(predicates::str::contains(event_decoded)); + + let constructor_data: &str = "babebabe00"; + let constructor_decoded: &str = r#"new { init_value: false }"#; + + // and + // event data is being decoded properly + cargo_contract(&project_dir) + .arg("decode") + .arg("-d") + .arg(constructor_data) + .arg("-t") + .arg("constructor") + .assert() + .success() + .stdout(predicates::str::contains(constructor_decoded)); +}