#![deny(unused_crate_dependencies)]
mod byte_str;
use anyhow::{
Context,
Result,
};
use semver::Version;
use serde::{
de,
Deserialize,
Serialize,
Serializer,
};
use serde_json::{
Map,
Value,
};
use std::{
fmt::{
Display,
Formatter,
Result as DisplayResult,
},
fs::File,
path::Path,
str::FromStr,
};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ContractMetadata {
pub source: Source,
pub contract: Contract,
pub image: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub user: Option<User>,
#[serde(flatten)]
pub abi: Map<String, Value>,
}
impl ContractMetadata {
pub fn new(
source: Source,
contract: Contract,
image: Option<String>,
user: Option<User>,
abi: Map<String, Value>,
) -> Self {
Self {
source,
contract,
image,
user,
abi,
}
}
pub fn remove_source_wasm_attribute(&mut self) {
self.source.wasm = None;
}
pub fn load<P>(metadata_path: P) -> Result<Self>
where
P: AsRef<Path>,
{
let path = metadata_path.as_ref();
let file = File::open(path)
.context(format!("Failed to open metadata file {}", path.display()))?;
serde_json::from_reader(file).context(format!(
"Failed to deserialize metadata file {}",
path.display()
))
}
}
#[derive(Clone, Copy, 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"
)]
pub [u8; 32],
);
impl From<[u8; 32]> for CodeHash {
fn from(value: [u8; 32]) -> Self {
CodeHash(value)
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Source {
pub hash: CodeHash,
pub language: SourceLanguage,
pub compiler: SourceCompiler,
#[serde(skip_serializing_if = "Option::is_none")]
pub wasm: Option<SourceWasm>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_info: Option<Map<String, Value>>,
}
impl Source {
pub fn new(
wasm: Option<SourceWasm>,
hash: CodeHash,
language: SourceLanguage,
compiler: SourceCompiler,
build_info: Option<Map<String, Value>>,
) -> Self {
Source {
hash,
language,
compiler,
wasm,
build_info,
}
}
}
#[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"
)]
pub Vec<u8>,
);
impl SourceWasm {
pub fn new(wasm: Vec<u8>) -> 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, "{byte:02x}").expect("failed writing to string");
}
write!(f, "")
}
}
#[derive(Clone, Debug)]
pub struct SourceLanguage {
pub language: Language,
pub version: Version,
}
impl SourceLanguage {
pub fn new(language: Language, version: Version) -> Self {
SourceLanguage { language, version }
}
}
impl Serialize for SourceLanguage {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SourceLanguage {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<Self, Self::Err> {
let mut parts = s.split_whitespace();
let language = parts
.next()
.ok_or_else(|| {
format!(
"SourceLanguage: Expected format '<language> <version>', got '{s}'"
)
})
.and_then(FromStr::from_str)?;
let version = parts
.next()
.ok_or_else(|| {
format!(
"SourceLanguage: Expected format '<language> <version>', got '{s}'"
)
})
.and_then(|v| {
<Version as FromStr>::from_str(v)
.map_err(|e| format!("Error parsing version {e}"))
})?;
Ok(Self { language, version })
}
}
#[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<Self, Self::Err> {
match s {
"ink!" => Ok(Self::Ink),
"Solidity" => Ok(Self::Solidity),
"AssemblyScript" => Ok(Self::AssemblyScript),
_ => Err(format!("Invalid language '{s}'")),
}
}
}
#[derive(Clone, Debug)]
pub struct SourceCompiler {
pub compiler: Compiler,
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<Self, Self::Err> {
let mut parts = s.split_whitespace();
let compiler = parts
.next()
.ok_or_else(|| {
format!(
"SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
)
})
.and_then(FromStr::from_str)?;
let version = parts
.next()
.ok_or_else(|| {
format!(
"SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
)
})
.and_then(|v| {
<Version as FromStr>::from_str(v)
.map_err(|e| format!("Error parsing version {e}"))
})?;
Ok(Self { compiler, version })
}
}
impl Serialize for SourceCompiler {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SourceCompiler {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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 }
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Compiler {
RustC,
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<Self, Self::Err> {
match s {
"rustc" => Ok(Self::RustC),
"solang" => Ok(Self::Solang),
_ => Err(format!("Invalid compiler '{s}'")),
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Contract {
pub name: String,
pub version: Version,
pub authors: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub repository: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub homepage: Option<Url>,
#[serde(skip_serializing_if = "Option::is_none")]
pub license: Option<String>,
}
impl Contract {
pub fn builder() -> ContractBuilder {
ContractBuilder::default()
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct User {
#[serde(flatten)]
pub json: Map<String, Value>,
}
impl User {
pub fn new(json: Map<String, Value>) -> Self {
User { json }
}
}
#[derive(Default)]
pub struct ContractBuilder {
name: Option<String>,
version: Option<Version>,
authors: Option<Vec<String>>,
description: Option<String>,
documentation: Option<Url>,
repository: Option<Url>,
homepage: Option<Url>,
license: Option<String>,
}
impl ContractBuilder {
pub fn name<S>(&mut self, name: S) -> &mut Self
where
S: AsRef<str>,
{
if self.name.is_some() {
panic!("name has already been set")
}
self.name = Some(name.as_ref().to_string());
self
}
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
}
pub fn authors<I, S>(&mut self, authors: I) -> &mut Self
where
I: IntoIterator<Item = S>,
S: AsRef<str>,
{
if self.authors.is_some() {
panic!("authors has already been set")
}
let authors = authors
.into_iter()
.map(|s| s.as_ref().to_string())
.collect::<Vec<_>>();
if authors.is_empty() {
panic!("must have at least one author")
}
self.authors = Some(authors);
self
}
pub fn description<S>(&mut self, description: S) -> &mut Self
where
S: AsRef<str>,
{
if self.description.is_some() {
panic!("description has already been set")
}
self.description = Some(description.as_ref().to_string());
self
}
pub fn documentation(&mut self, documentation: Url) -> &mut Self {
if self.documentation.is_some() {
panic!("documentation is already set")
}
self.documentation = Some(documentation);
self
}
pub fn repository(&mut self, repository: Url) -> &mut Self {
if self.repository.is_some() {
panic!("repository is already set")
}
self.repository = Some(repository);
self
}
pub fn homepage(&mut self, homepage: Url) -> &mut Self {
if self.homepage.is_some() {
panic!("homepage is already set")
}
self.homepage = Some(homepage);
self
}
pub fn license<S>(&mut self, license: S) -> &mut Self
where
S: AsRef<str>,
{
if self.license.is_some() {
panic!("license has already been set")
}
self.license = Some(license.as_ref().to_string());
self
}
pub fn build(&self) -> Result<Contract, String> {
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()
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build();
assert_eq!(
missing_name.unwrap_err(),
"Missing required non-default fields: name"
);
let missing_version = Contract::builder()
.name("incrementer")
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build();
assert_eq!(
missing_version.unwrap_err(),
"Missing required non-default fields: version"
);
let missing_authors = Contract::builder()
.name("incrementer")
.version(Version::new(2, 1, 0))
.build();
assert_eq!(
missing_authors.unwrap_err(),
"Missing required non-default fields: authors"
);
let missing_all = Contract::builder()
.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 build_info = json! {
{
"example_compiler_version": 42,
"example_settings": [],
"example_name": "increment"
}
}
.as_object()
.unwrap()
.clone();
let source = Source::new(
Some(wasm),
CodeHash([0u8; 32]),
language,
compiler,
Some(build_info),
);
let contract = Contract::builder()
.name("incrementer")
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.description("increment a value")
.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")
.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(String::from("paritytech/contracts-verifiable:3.0.1")),
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",
"build_info": {
"example_compiler_version": 42,
"example_settings": [],
"example_name": "increment"
}
},
"image": "paritytech/contracts-verifiable:3.0.1",
"contract": {
"name": "incrementer",
"version": "2.1.0",
"authors": [
"Parity Technologies <admin@parity.io>"
],
"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"
},
"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, None);
let contract = Contract::builder()
.name("incrementer")
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.build()
.unwrap();
let abi_json = json! {
{
"spec": {},
"storage": {},
"types": []
}
}
.as_object()
.unwrap()
.clone();
let metadata = ContractMetadata::new(source, contract, None, None, abi_json);
let json = serde_json::to_value(&metadata).unwrap();
let expected = json! {
{
"contract": {
"name": "incrementer",
"version": "2.1.0",
"authors": [
"Parity Technologies <admin@parity.io>"
],
},
"image": serde_json::Value::Null,
"source": {
"hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"language": "ink! 2.1.0",
"compiler": "rustc 1.46.0-nightly"
},
"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 build_info = json! {
{
"example_compiler_version": 42,
"example_settings": [],
"example_name": "increment",
}
}
.as_object()
.unwrap()
.clone();
let source = Source::new(
Some(wasm),
CodeHash([0u8; 32]),
language,
compiler,
Some(build_info),
);
let contract = Contract::builder()
.name("incrementer")
.version(Version::new(2, 1, 0))
.authors(vec!["Parity Technologies <admin@parity.io>".to_string()])
.description("increment a value")
.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")
.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, None, Some(user), abi_json);
let json = serde_json::to_value(&metadata).unwrap();
let decoded = serde_json::from_value::<ContractMetadata>(json);
assert!(decoded.is_ok())
}
}