diff --git a/CHANGELOG.md b/CHANGELOG.md index 8928c2459502d3ad98cfe5e613a492acab406a0f..5d560459a84f999fc982f748da45d5cd5334a0b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Use of `-Clinker-plugin-lto` flag(reduces the size of the contract) if `lto` is enabled - [#358](https://github.com/paritytech/cargo-contract/pull/358) +- Deserialize metadata - [#368](https://github.com/paritytech/cargo-contract/pull/368) ### Added - Disabled overflow checks in the `cargo contract new` template - [#372](https://github.com/paritytech/cargo-contract/pull/372) diff --git a/Cargo.lock b/Cargo.lock index cd307a83753ae3f317aada8236a65d301c5c1bb5..7a58f1bac6dc50ad79d1892c291952ce7e6f8924 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -640,6 +640,7 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contract-metadata" version = "0.4.0" dependencies = [ + "impl-serde", "pretty_assertions", "semver", "serde", diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 1e2126df890a4d8e18e8e5643e8cfc12ee731258..f5c7e486240da97d20db897196e8c80ad994d493 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -17,6 +17,7 @@ include = ["Cargo.toml", "*.rs", "LICENSE"] path = "lib.rs" [dependencies] +impl-serde = "0.3.2" semver = { version = "1.0.4", features = ["serde"] } serde = { version = "1.0.130", default-features = false, features = ["derive"] } serde_json = "1.0.71" diff --git a/metadata/byte_str.rs b/metadata/byte_str.rs new file mode 100644 index 0000000000000000000000000000000000000000..7ebff356b568f1484724676325037d0312519d3d --- /dev/null +++ b/metadata/byte_str.rs @@ -0,0 +1,97 @@ +// Copyright 2018-2021 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 impl_serde::serialize as serde_hex; + +/// Serializes the given bytes as byte string. +pub fn serialize_as_byte_str(bytes: &[u8], serializer: S) -> Result +where + S: serde::Serializer, +{ + if bytes.is_empty() { + // Return empty string without prepended `0x`. + return serializer.serialize_str(""); + } + serde_hex::serialize(bytes, serializer) +} + +/// Deserializes the given hex string with optional `0x` prefix. +pub fn deserialize_from_byte_str<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + struct Visitor; + + impl<'b> serde::de::Visitor<'b> for Visitor { + type Value = Vec; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "hex string with optional 0x prefix") + } + + fn visit_str(self, v: &str) -> Result { + let result = from_hex(v); + result.map_err(E::custom) + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + } + + deserializer.deserialize_str(Visitor) +} + +/// Deserializes the given hex string with optional `0x` prefix. +pub fn deserialize_from_byte_str_array<'de, D>(deserializer: D) -> Result<[u8; 32], D::Error> +where + D: serde::Deserializer<'de>, +{ + struct Visitor; + + impl<'b> serde::de::Visitor<'b> for Visitor { + type Value = [u8; 32]; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(formatter, "hex string with optional 0x prefix") + } + + fn visit_str(self, v: &str) -> Result { + let result = from_hex(v).map_err(E::custom)?; + if result.len() != 32 { + Err(E::custom("Expected exactly 32 bytes")) + } else { + let mut arr = [0u8; 32]; + arr.copy_from_slice(&result[..]); + Ok(arr) + } + } + + fn visit_string(self, v: String) -> Result { + self.visit_str(&v) + } + } + + deserializer.deserialize_str(Visitor) +} + +fn from_hex(v: &str) -> Result, serde_hex::FromHexError> { + if v.starts_with("0x") { + serde_hex::from_hex(v) + } else { + serde_hex::from_hex(&format!("0x{}", v)) + } +} diff --git a/metadata/lib.rs b/metadata/lib.rs index 929c6d57d420cfd16f252338796a4cdc42b3d914..776c55ee033232f58befba32399fccf13d2a75d0 100644 --- a/metadata/lib.rs +++ b/metadata/lib.rs @@ -52,22 +52,30 @@ //! let json = serde_json::to_value(&metadata).unwrap(); //! ``` -use core::fmt::{Display, Formatter, Result as DisplayResult, Write}; +mod byte_str; + use semver::Version; -use serde::{Serialize, Serializer}; +use serde::{de, Deserialize, Serialize, Serializer}; use serde_json::{Map, Value}; +use std::{ + fmt::{Display, Formatter, Result as DisplayResult}, + str::FromStr, +}; use url::Url; /// Smart contract metadata. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct ContractMetadata { - source: Source, - contract: Contract, + /// Information about the contract's Wasm code. + pub source: Source, + /// Metadata about the contract. + pub contract: Contract, + /// Additional user-defined metadata. #[serde(skip_serializing_if = "Option::is_none")] - user: Option, - /// Raw JSON of the contract abi metadata, generated during contract compilation. + pub user: Option, + /// Raw JSON of the contract's abi metadata, generated during contract compilation. #[serde(flatten)] - abi: Map, + pub abi: Map, } impl ContractMetadata { @@ -92,25 +100,29 @@ impl ContractMetadata { } /// Representation of the Wasm code hash. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct CodeHash(pub [u8; 32]); - -impl Serialize for CodeHash { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serialize_as_byte_str(&self.0[..], serializer) - } -} - -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)] +pub struct CodeHash( + #[serde( + serialize_with = "byte_str::serialize_as_byte_str", + deserialize_with = "byte_str::deserialize_from_byte_str_array" + )] + /// The raw bytes of the hash. + pub [u8; 32], +); + +/// Information about the contract's Wasm code. +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Source { - hash: CodeHash, - language: SourceLanguage, - compiler: SourceCompiler, + /// The hash of the contract's Wasm code. + pub hash: CodeHash, + /// The language used to write the contract. + pub language: SourceLanguage, + /// The compiler used to compile the contract. + pub compiler: SourceCompiler, + /// The actual Wasm code of the contract, for optionally bundling the code + /// with the metadata. #[serde(skip_serializing_if = "Option::is_none")] - wasm: Option, + pub wasm: Option, } impl Source { @@ -131,31 +143,27 @@ impl Source { } /// The bytes of the compiled Wasm smart contract. -#[derive(Clone, Debug)] -pub struct SourceWasm { - wasm: Vec, -} +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct SourceWasm( + #[serde( + serialize_with = "byte_str::serialize_as_byte_str", + deserialize_with = "byte_str::deserialize_from_byte_str" + )] + /// The raw bytes of the Wasm code. + pub Vec, +); impl SourceWasm { /// Constructs a new `SourceWasm`. pub fn new(wasm: Vec) -> Self { - SourceWasm { wasm } - } -} - -impl Serialize for SourceWasm { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serialize_as_byte_str(&self.wasm[..], serializer) + SourceWasm(wasm) } } impl Display for SourceWasm { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { write!(f, "0x").expect("failed writing to string"); - for byte in &self.wasm { + for byte in &self.0 { write!(f, "{:02x}", byte).expect("failed writing to string"); } write!(f, "") @@ -165,8 +173,10 @@ impl Display for SourceWasm { /// The language and version in which a smart contract is written. #[derive(Clone, Debug)] pub struct SourceLanguage { - language: Language, - version: Version, + /// The language used to write the contract. + pub language: Language, + /// The version of the language used to write the contract. + pub version: Version, } impl SourceLanguage { @@ -185,12 +195,55 @@ impl Serialize for SourceLanguage { } } +impl<'de> Deserialize<'de> for SourceLanguage { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + impl Display for SourceLanguage { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { write!(f, "{} {}", self.language, self.version) } } +impl FromStr for SourceLanguage { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut parts = s.split_whitespace(); + + let language = parts + .next() + .ok_or_else(|| { + format!( + "SourceLanguage: Expected format ' ', got '{}'", + s + ) + }) + .and_then(FromStr::from_str)?; + + let version = parts + .next() + .ok_or_else(|| { + format!( + "SourceLanguage: Expected format ' ', got '{}'", + s + ) + }) + .and_then(|v| { + ::from_str(v) + .map_err(|e| format!("Error parsing version {}", e)) + })?; + + Ok(Self { language, version }) + } +} + /// The language in which the smart contract is written. #[derive(Clone, Debug)] pub enum Language { @@ -209,11 +262,26 @@ impl Display for Language { } } +impl FromStr for Language { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "ink!" => Ok(Self::Ink), + "Solidity" => Ok(Self::Solidity), + "AssemblyScript" => Ok(Self::AssemblyScript), + _ => Err(format!("Invalid language '{}'", s)), + } + } +} + /// A compiler used to compile a smart contract. #[derive(Clone, Debug)] pub struct SourceCompiler { - compiler: Compiler, - version: Version, + /// The compiler used to compile the smart contract. + pub compiler: Compiler, + /// The version of the compiler used to compile the smart contract. + pub version: Version, } impl Display for SourceCompiler { @@ -222,6 +290,39 @@ impl Display for SourceCompiler { } } +impl FromStr for SourceCompiler { + type Err = String; + + fn from_str(s: &str) -> Result { + let mut parts = s.split_whitespace(); + + let compiler = parts + .next() + .ok_or_else(|| { + format!( + "SourceCompiler: Expected format ' ', got '{}'", + s + ) + }) + .and_then(FromStr::from_str)?; + + let version = parts + .next() + .ok_or_else(|| { + format!( + "SourceCompiler: Expected format ' ', got '{}'", + s + ) + }) + .and_then(|v| { + ::from_str(v) + .map_err(|e| format!("Error parsing version {}", e)) + })?; + + Ok(Self { compiler, version }) + } +} + impl Serialize for SourceCompiler { fn serialize(&self, serializer: S) -> Result where @@ -231,6 +332,16 @@ impl Serialize for SourceCompiler { } } +impl<'de> Deserialize<'de> for SourceCompiler { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + impl SourceCompiler { pub fn new(compiler: Compiler, version: Version) -> Self { SourceCompiler { compiler, version } @@ -238,9 +349,11 @@ impl SourceCompiler { } /// Compilers used to compile a smart contract. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub enum Compiler { + /// The rust compiler. RustC, + /// The solang compiler. Solang, } @@ -253,22 +366,42 @@ impl Display for Compiler { } } +impl FromStr for Compiler { + type Err = String; + + fn from_str(s: &str) -> Result { + match s { + "rustc" => Ok(Self::RustC), + "solang" => Ok(Self::Solang), + _ => Err(format!("Invalid compiler '{}'", s)), + } + } +} + /// Metadata about a smart contract. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct Contract { - name: String, - version: Version, - authors: Vec, + /// The name of the smart contract. + pub name: String, + /// The version of the smart contract. + pub version: Version, + /// The authors of the smart contract. + pub authors: Vec, + /// The description of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] - description: Option, + pub description: Option, + /// Link to the documentation of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] - documentation: Option, + pub documentation: Option, + /// Link to the code repository of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] - repository: Option, + pub repository: Option, + /// Link to the homepage of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] - homepage: Option, + pub homepage: Option, + /// The license of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] - license: Option, + pub license: Option, } impl Contract { @@ -278,10 +411,11 @@ impl Contract { } /// Additional user defined metadata, can be any valid json. -#[derive(Clone, Debug, Serialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] pub struct User { + /// Raw json of user defined metadata. #[serde(flatten)] - json: Map, + pub json: Map, } impl User { @@ -437,23 +571,6 @@ impl ContractBuilder { } } -/// Serializes the given bytes as byte string. -fn serialize_as_byte_str(bytes: &[u8], serializer: S) -> Result -where - S: serde::Serializer, -{ - if bytes.is_empty() { - // Return empty string without prepended `0x`. - return serializer.serialize_str(""); - } - let mut hex = String::with_capacity(bytes.len() * 2 + 2); - write!(hex, "0x").expect("failed writing to string"); - for byte in bytes { - write!(hex, "{:02x}", byte).expect("failed writing to string"); - } - serializer.serialize_str(&hex) -} - #[cfg(test)] mod tests { use super::*; @@ -638,4 +755,52 @@ mod tests { assert_eq!(json, expected); } + + #[test] + fn decoding_works() { + let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0)); + let compiler = + SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap()); + let wasm = SourceWasm::new(vec![0u8, 1u8, 2u8]); + let source = Source::new(Some(wasm), CodeHash([0u8; 32]), language, compiler); + let contract = Contract::builder() + .name("incrementer".to_string()) + .version(Version::new(2, 1, 0)) + .authors(vec!["Parity Technologies ".to_string()]) + .description("increment a value".to_string()) + .documentation(Url::parse("http://docs.rs/").unwrap()) + .repository(Url::parse("http://github.com/paritytech/ink/").unwrap()) + .homepage(Url::parse("http://example.com/").unwrap()) + .license("Apache-2.0".to_string()) + .build() + .unwrap(); + + let user_json = json! { + { + "more-user-provided-fields": [ + "and", + "their", + "values" + ], + "some-user-provided-field": "and-its-value" + } + }; + let user = User::new(user_json.as_object().unwrap().clone()); + let abi_json = json! { + { + "spec": {}, + "storage": {}, + "types": [] + } + } + .as_object() + .unwrap() + .clone(); + + let metadata = ContractMetadata::new(source, contract, Some(user), abi_json); + let json = serde_json::to_value(&metadata).unwrap(); + + let decoded = serde_json::from_value::(json); + assert!(decoded.is_ok()) + } }