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