// Copyright 2018-2022 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 . //! Type definitions for creating and serializing metadata for smart contracts targeting //! Substrate's contracts pallet. //! //! # Example //! //! ``` //! # use contract_metadata::*; //! # use semver::Version; //! # use url::Url; //! # use serde_json::{Map, Value}; //! //! 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]); //! 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(); //! // user defined raw json //! let user_json: Map = Map::new(); //! let user = User::new(user_json); //! // contract abi raw json generated by contract compilation //! let abi_json: Map = Map::new(); //! //! let metadata = ContractMetadata::new(source, contract, Some(user), abi_json); //! //! // serialize to json //! let json = serde_json::to_value(&metadata).unwrap(); //! ``` mod byte_str; use semver::Version; 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, Deserialize, Serialize)] pub struct ContractMetadata { /// 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")] pub user: Option, /// Raw JSON of the contract's abi metadata, generated during contract compilation. #[serde(flatten)] pub abi: Map, } impl ContractMetadata { /// Construct new contract metadata. pub fn new( source: Source, contract: Contract, user: Option, abi: Map, ) -> Self { Self { source, contract, user, abi, } } pub fn remove_source_wasm_attribute(&mut self) { self.source.wasm = None; } } /// Representation of the Wasm code hash. #[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 { /// 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")] pub wasm: Option, } impl Source { /// Constructs a new InkProjectSource. pub fn new( wasm: Option, hash: CodeHash, language: SourceLanguage, compiler: SourceCompiler, ) -> Self { Source { hash, language, compiler, wasm, } } } /// The bytes of the compiled Wasm smart contract. #[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 Display for SourceWasm { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { write!(f, "0x").expect("failed writing to string"); for byte in &self.0 { write!(f, "{:02x}", byte).expect("failed writing to string"); } write!(f, "") } } /// The language and version in which a smart contract is written. #[derive(Clone, Debug)] pub struct SourceLanguage { /// 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 { /// Constructs a new SourceLanguage. pub fn new(language: Language, version: Version) -> Self { SourceLanguage { language, version } } } impl Serialize for SourceLanguage { fn serialize(&self, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&self.to_string()) } } 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 { Ink, Solidity, AssemblyScript, } impl Display for Language { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { match self { Self::Ink => write!(f, "ink!"), Self::Solidity => write!(f, "Solidity"), Self::AssemblyScript => write!(f, "AssemblyScript"), } } } 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 { /// 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 { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { write!(f, "{} {}", self.compiler, self.version) } } 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 S: Serializer, { serializer.serialize_str(&self.to_string()) } } 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 } } } /// Compilers used to compile a smart contract. #[derive(Clone, Debug, Deserialize, Serialize)] pub enum Compiler { /// The rust compiler. RustC, /// The solang compiler. Solang, } impl Display for Compiler { fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult { match self { Self::RustC => write!(f, "rustc"), Self::Solang => write!(f, "solang"), } } } 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, Deserialize, Serialize)] pub struct Contract { /// 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")] pub description: Option, /// Link to the documentation of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] pub documentation: Option, /// Link to the code repository of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] pub repository: Option, /// Link to the homepage of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] pub homepage: Option, /// The license of the smart contract. #[serde(skip_serializing_if = "Option::is_none")] pub license: Option, } impl Contract { pub fn builder() -> ContractBuilder { ContractBuilder::default() } } /// Additional user defined metadata, can be any valid json. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct User { /// Raw json of user defined metadata. #[serde(flatten)] pub json: Map, } impl User { /// Constructs new user metadata. pub fn new(json: Map) -> Self { User { json } } } /// Builder for contract metadata #[derive(Default)] pub struct ContractBuilder { name: Option, version: Option, authors: Option>, description: Option, documentation: Option, repository: Option, homepage: Option, license: Option, } impl ContractBuilder { /// Set the contract name (required) pub fn name(&mut self, name: S) -> &mut Self where S: AsRef, { if self.name.is_some() { panic!("name has already been set") } self.name = Some(name.as_ref().to_string()); self } /// Set the contract version (required) pub fn version(&mut self, version: Version) -> &mut Self { if self.version.is_some() { panic!("version has already been set") } self.version = Some(version); self } /// Set the contract version (required) pub fn authors(&mut self, authors: I) -> &mut Self where I: IntoIterator, S: AsRef, { if self.authors.is_some() { panic!("authors has already been set") } let authors = authors .into_iter() .map(|s| s.as_ref().to_string()) .collect::>(); if authors.is_empty() { panic!("must have at least one author") } self.authors = Some(authors); self } /// Set the contract description (optional) pub fn description(&mut self, description: S) -> &mut Self where S: AsRef, { if self.description.is_some() { panic!("description has already been set") } self.description = Some(description.as_ref().to_string()); self } /// Set the contract documentation url (optional) pub fn documentation(&mut self, documentation: Url) -> &mut Self { if self.documentation.is_some() { panic!("documentation is already set") } self.documentation = Some(documentation); self } /// Set the contract repository url (optional) pub fn repository(&mut self, repository: Url) -> &mut Self { if self.repository.is_some() { panic!("repository is already set") } self.repository = Some(repository); self } /// Set the contract homepage url (optional) pub fn homepage(&mut self, homepage: Url) -> &mut Self { if self.homepage.is_some() { panic!("homepage is already set") } self.homepage = Some(homepage); self } /// Set the contract license (optional) pub fn license(&mut self, license: S) -> &mut Self where S: AsRef, { if self.license.is_some() { panic!("license has already been set") } self.license = Some(license.as_ref().to_string()); self } /// Finalize construction of the [`ContractMetadata`]. /// /// Returns an `Err` if any required fields missing. pub fn build(&self) -> Result { let mut required = Vec::new(); if let (Some(name), Some(version), Some(authors)) = (&self.name, &self.version, &self.authors) { Ok(Contract { name: name.to_string(), version: version.clone(), authors: authors.to_vec(), description: self.description.clone(), documentation: self.documentation.clone(), repository: self.repository.clone(), homepage: self.homepage.clone(), license: self.license.clone(), }) } else { if self.name.is_none() { required.push("name"); } if self.version.is_none() { required.push("version") } if self.authors.is_none() { required.push("authors") } Err(format!( "Missing required non-default fields: {}", required.join(", ") )) } } } #[cfg(test)] mod tests { use super::*; use pretty_assertions::assert_eq; use serde_json::json; #[test] fn builder_fails_with_missing_required_fields() { let missing_name = Contract::builder() // .name("incrementer".to_string()) .version(Version::new(2, 1, 0)) .authors(vec!["Parity Technologies ".to_string()]) .build(); assert_eq!( missing_name.unwrap_err(), "Missing required non-default fields: name" ); let missing_version = Contract::builder() .name("incrementer".to_string()) // .version(Version::new(2, 1, 0)) .authors(vec!["Parity Technologies ".to_string()]) .build(); assert_eq!( missing_version.unwrap_err(), "Missing required non-default fields: version" ); let missing_authors = Contract::builder() .name("incrementer".to_string()) .version(Version::new(2, 1, 0)) // .authors(vec!["Parity Technologies ".to_string()]) .build(); assert_eq!( missing_authors.unwrap_err(), "Missing required non-default fields: authors" ); let missing_all = Contract::builder() // .name("incrementer".to_string()) // .version(Version::new(2, 1, 0)) // .authors(vec!["Parity Technologies ".to_string()]) .build(); assert_eq!( missing_all.unwrap_err(), "Missing required non-default fields: name, version, authors" ); } #[test] fn json_with_optional_fields() { 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 expected = json! { { "source": { "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "language": "ink! 2.1.0", "compiler": "rustc 1.46.0-nightly", "wasm": "0x000102" }, "contract": { "name": "incrementer", "version": "2.1.0", "authors": [ "Parity Technologies " ], "description": "increment a value", "documentation": "http://docs.rs/", "repository": "http://github.com/paritytech/ink/", "homepage": "http://example.com/", "license": "Apache-2.0", }, "user": { "more-user-provided-fields": [ "and", "their", "values" ], "some-user-provided-field": "and-its-value" }, // these fields are part of the flattened raw json for the contract ABI "spec": {}, "storage": {}, "types": [] } }; assert_eq!(json, expected); } #[test] fn json_excludes_optional_fields() { 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 source = Source::new(None, 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()]) .build() .unwrap(); let abi_json = json! { { "spec": {}, "storage": {}, "types": [] } } .as_object() .unwrap() .clone(); let metadata = ContractMetadata::new(source, contract, None, abi_json); let json = serde_json::to_value(&metadata).unwrap(); let expected = json! { { "contract": { "name": "incrementer", "version": "2.1.0", "authors": [ "Parity Technologies " ], }, "source": { "hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "language": "ink! 2.1.0", "compiler": "rustc 1.46.0-nightly" }, // these fields are part of the flattened raw json for the contract ABI "spec": {}, "storage": {}, "types": [] } }; 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()) } }