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())
+ }
}