contract_metadata/
lib.rs

1// Copyright (C) Use Ink (UK) Ltd.
2// This file is part of cargo-contract.
3//
4// cargo-contract is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// cargo-contract is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with cargo-contract.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Type definitions for creating and serializing metadata for smart contracts targeting
18//! Substrate's contracts pallet.
19//!
20//! # Example
21//!
22//! ```
23//! # use contract_metadata::*;
24//! # use semver::Version;
25//! # use url::Url;
26//! # use serde_json::{Map, Value};
27//!
28//! let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
29//! let compiler =
30//!     SourceCompiler::new(Compiler::RustC, Version::parse("1.46.0-nightly").unwrap());
31//! let wasm = SourceWasm::new(vec![0u8]);
32//! // Optional information about how the contract was build
33//! let build_info: Map<String, Value> = Map::new();
34//! let source = Source::new(
35//!     Some(wasm),
36//!     CodeHash([0u8; 32]),
37//!     language,
38//!     compiler,
39//!     Some(build_info),
40//! );
41//! let contract = Contract::builder()
42//!     .name("incrementer".to_string())
43//!     .version(Version::new(2, 1, 0))
44//!     .authors(vec!["Use Ink <ink@use.ink>".to_string()])
45//!     .description("increment a value".to_string())
46//!     .documentation(Url::parse("http://docs.rs/").unwrap())
47//!     .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
48//!     .homepage(Url::parse("http://example.com/").unwrap())
49//!     .license("Apache-2.0".to_string())
50//!     .build()
51//!     .unwrap();
52//! // user defined raw json
53//! let user_json: Map<String, Value> = Map::new();
54//! let user = User::new(user_json);
55//! // contract abi raw json generated by contract compilation
56//! let abi_json: Map<String, Value> = Map::new();
57//! // image name and tag used for the verifiable build.
58//! let image = String::from("paritytech/contracts-verifiable:3.0.1");
59//!
60//! let metadata =
61//!     ContractMetadata::new(source, contract, Some(image), Some(user), abi_json);
62//!
63//! // serialize to json
64//! let json = serde_json::to_value(&metadata).unwrap();
65//! ```
66
67#![deny(unused_crate_dependencies)]
68
69pub mod byte_str;
70pub mod compatibility;
71
72use anyhow::{
73    Context,
74    Result,
75};
76use semver::Version;
77use serde::{
78    de,
79    Deserialize,
80    Serialize,
81    Serializer,
82};
83use serde_json::{
84    Map,
85    Value,
86};
87use std::{
88    fmt::{
89        Display,
90        Formatter,
91        Result as DisplayResult,
92    },
93    fs::File,
94    path::Path,
95    str::FromStr,
96};
97use url::Url;
98
99/// Smart contract metadata.
100#[derive(Clone, Debug, Deserialize, Serialize)]
101pub struct ContractMetadata {
102    /// Information about the contract's Wasm code.
103    pub source: Source,
104    /// Metadata about the contract.
105    pub contract: Contract,
106    /// If the contract is meant to be verifiable,
107    /// then the Docker image is specified.
108    pub image: Option<String>,
109    /// Additional user-defined metadata.
110    #[serde(skip_serializing_if = "Option::is_none")]
111    pub user: Option<User>,
112    /// Raw JSON of the contract's abi metadata, generated during contract compilation.
113    #[serde(flatten)]
114    pub abi: Map<String, Value>,
115}
116
117impl ContractMetadata {
118    /// Construct new contract metadata.
119    pub fn new(
120        source: Source,
121        contract: Contract,
122        image: Option<String>,
123        user: Option<User>,
124        abi: Map<String, Value>,
125    ) -> Self {
126        Self {
127            source,
128            contract,
129            image,
130            user,
131            abi,
132        }
133    }
134
135    pub fn remove_source_wasm_attribute(&mut self) {
136        self.source.wasm = None;
137    }
138
139    /// Reads the file and tries to parse it as instance of `ContractMetadata`.
140    pub fn load<P>(metadata_path: P) -> Result<Self>
141    where
142        P: AsRef<Path>,
143    {
144        let path = metadata_path.as_ref();
145        let file = File::open(path)
146            .context(format!("Failed to open metadata file {}", path.display()))?;
147        serde_json::from_reader(file).context(format!(
148            "Failed to deserialize metadata file {}",
149            path.display()
150        ))
151    }
152
153    /// Checks whether the contract's ink! version is compatible with the cargo-contract
154    /// binary
155    pub fn check_ink_compatibility(&self) -> Result<()> {
156        if let Language::Ink = self.source.language.language {
157            compatibility::check_contract_ink_compatibility(
158                &self.source.language.version,
159                None,
160            )?;
161        }
162        Ok(())
163    }
164}
165
166/// Representation of the Wasm code hash.
167#[derive(Clone, Copy, Debug, Eq, PartialEq, Deserialize, Serialize)]
168pub struct CodeHash(
169    #[serde(
170        serialize_with = "byte_str::serialize_as_byte_str",
171        deserialize_with = "byte_str::deserialize_from_byte_str_array"
172    )]
173    /// The raw bytes of the hash.
174    pub [u8; 32],
175);
176
177impl From<[u8; 32]> for CodeHash {
178    fn from(value: [u8; 32]) -> Self {
179        CodeHash(value)
180    }
181}
182
183impl Display for CodeHash {
184    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
185        let raw_string = self
186            .0
187            .iter()
188            .map(|b| format!("{:x?}", b))
189            .collect::<Vec<String>>()
190            .join("");
191        f.write_fmt(format_args!("0x{}", raw_string))
192    }
193}
194
195/// Information about the contract's Wasm code.
196#[derive(Clone, Debug, Deserialize, Serialize)]
197pub struct Source {
198    /// The hash of the contract's Wasm code.
199    pub hash: CodeHash,
200    /// The language used to write the contract.
201    pub language: SourceLanguage,
202    /// The compiler used to compile the contract.
203    pub compiler: SourceCompiler,
204    /// The actual Wasm code of the contract, for optionally bundling the code
205    /// with the metadata.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub wasm: Option<SourceWasm>,
208    /// Extra information about the environment in which the contract was built.
209    ///
210    /// Useful for producing deterministic builds.
211    #[serde(skip_serializing_if = "Option::is_none")]
212    pub build_info: Option<Map<String, Value>>,
213}
214
215impl Source {
216    /// Constructs a new InkProjectSource.
217    pub fn new(
218        wasm: Option<SourceWasm>,
219        hash: CodeHash,
220        language: SourceLanguage,
221        compiler: SourceCompiler,
222        build_info: Option<Map<String, Value>>,
223    ) -> Self {
224        Source {
225            hash,
226            language,
227            compiler,
228            wasm,
229            build_info,
230        }
231    }
232}
233
234/// The bytes of the compiled Wasm smart contract.
235#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
236pub struct SourceWasm(
237    #[serde(
238        serialize_with = "byte_str::serialize_as_byte_str",
239        deserialize_with = "byte_str::deserialize_from_byte_str"
240    )]
241    /// The raw bytes of the Wasm code.
242    pub Vec<u8>,
243);
244
245impl SourceWasm {
246    /// Constructs a new `SourceWasm`.
247    pub fn new(wasm: Vec<u8>) -> Self {
248        SourceWasm(wasm)
249    }
250}
251
252impl Display for SourceWasm {
253    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
254        write!(f, "0x").expect("failed writing to string");
255        for byte in &self.0 {
256            write!(f, "{byte:02x}").expect("failed writing to string");
257        }
258        write!(f, "")
259    }
260}
261
262/// The language and version in which a smart contract is written.
263#[derive(Clone, Debug)]
264pub struct SourceLanguage {
265    /// The language used to write the contract.
266    pub language: Language,
267    /// The version of the language used to write the contract.
268    pub version: Version,
269}
270
271impl SourceLanguage {
272    /// Constructs a new SourceLanguage.
273    pub fn new(language: Language, version: Version) -> Self {
274        SourceLanguage { language, version }
275    }
276}
277
278impl Serialize for SourceLanguage {
279    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
280    where
281        S: Serializer,
282    {
283        serializer.serialize_str(&self.to_string())
284    }
285}
286
287impl<'de> Deserialize<'de> for SourceLanguage {
288    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
289    where
290        D: de::Deserializer<'de>,
291    {
292        let s = String::deserialize(deserializer)?;
293        FromStr::from_str(&s).map_err(de::Error::custom)
294    }
295}
296
297impl Display for SourceLanguage {
298    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
299        write!(f, "{} {}", self.language, self.version)
300    }
301}
302
303impl FromStr for SourceLanguage {
304    type Err = String;
305
306    fn from_str(s: &str) -> Result<Self, Self::Err> {
307        let mut parts = s.split_whitespace();
308
309        let language = parts
310            .next()
311            .ok_or_else(|| {
312                format!(
313                    "SourceLanguage: Expected format '<language> <version>', got '{s}'"
314                )
315            })
316            .and_then(FromStr::from_str)?;
317
318        let version = parts
319            .next()
320            .ok_or_else(|| {
321                format!(
322                    "SourceLanguage: Expected format '<language> <version>', got '{s}'"
323                )
324            })
325            .and_then(|v| {
326                <Version as FromStr>::from_str(v)
327                    .map_err(|e| format!("Error parsing version {e}"))
328            })?;
329
330        Ok(Self { language, version })
331    }
332}
333
334/// The language in which the smart contract is written.
335#[derive(Clone, Debug)]
336pub enum Language {
337    Ink,
338    Solidity,
339    AssemblyScript,
340}
341
342impl Display for Language {
343    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
344        match self {
345            Self::Ink => write!(f, "ink!"),
346            Self::Solidity => write!(f, "Solidity"),
347            Self::AssemblyScript => write!(f, "AssemblyScript"),
348        }
349    }
350}
351
352impl FromStr for Language {
353    type Err = String;
354
355    fn from_str(s: &str) -> Result<Self, Self::Err> {
356        match s {
357            "ink!" => Ok(Self::Ink),
358            "Solidity" => Ok(Self::Solidity),
359            "AssemblyScript" => Ok(Self::AssemblyScript),
360            _ => Err(format!("Invalid language '{s}'")),
361        }
362    }
363}
364
365/// A compiler used to compile a smart contract.
366#[derive(Clone, Debug)]
367pub struct SourceCompiler {
368    /// The compiler used to compile the smart contract.
369    pub compiler: Compiler,
370    /// The version of the compiler used to compile the smart contract.
371    pub version: Version,
372}
373
374impl Display for SourceCompiler {
375    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
376        write!(f, "{} {}", self.compiler, self.version)
377    }
378}
379
380impl FromStr for SourceCompiler {
381    type Err = String;
382
383    fn from_str(s: &str) -> Result<Self, Self::Err> {
384        let mut parts = s.split_whitespace();
385
386        let compiler = parts
387            .next()
388            .ok_or_else(|| {
389                format!(
390                    "SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
391                )
392            })
393            .and_then(FromStr::from_str)?;
394
395        let version = parts
396            .next()
397            .ok_or_else(|| {
398                format!(
399                    "SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
400                )
401            })
402            .and_then(|v| {
403                <Version as FromStr>::from_str(v)
404                    .map_err(|e| format!("Error parsing version {e}"))
405            })?;
406
407        Ok(Self { compiler, version })
408    }
409}
410
411impl Serialize for SourceCompiler {
412    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
413    where
414        S: Serializer,
415    {
416        serializer.serialize_str(&self.to_string())
417    }
418}
419
420impl<'de> Deserialize<'de> for SourceCompiler {
421    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
422    where
423        D: de::Deserializer<'de>,
424    {
425        let s = String::deserialize(deserializer)?;
426        FromStr::from_str(&s).map_err(de::Error::custom)
427    }
428}
429
430impl SourceCompiler {
431    pub fn new(compiler: Compiler, version: Version) -> Self {
432        SourceCompiler { compiler, version }
433    }
434}
435
436/// Compilers used to compile a smart contract.
437#[derive(Clone, Debug, Deserialize, Serialize)]
438pub enum Compiler {
439    /// The rust compiler.
440    RustC,
441    /// The solang compiler.
442    Solang,
443}
444
445impl Display for Compiler {
446    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
447        match self {
448            Self::RustC => write!(f, "rustc"),
449            Self::Solang => write!(f, "solang"),
450        }
451    }
452}
453
454impl FromStr for Compiler {
455    type Err = String;
456
457    fn from_str(s: &str) -> Result<Self, Self::Err> {
458        match s {
459            "rustc" => Ok(Self::RustC),
460            "solang" => Ok(Self::Solang),
461            _ => Err(format!("Invalid compiler '{s}'")),
462        }
463    }
464}
465
466/// Metadata about a smart contract.
467#[derive(Clone, Debug, Deserialize, Serialize)]
468pub struct Contract {
469    /// The name of the smart contract.
470    pub name: String,
471    /// The version of the smart contract.
472    pub version: Version,
473    /// The authors of the smart contract.
474    pub authors: Vec<String>,
475    /// The description of the smart contract.
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub description: Option<String>,
478    /// Link to the documentation of the smart contract.
479    #[serde(skip_serializing_if = "Option::is_none")]
480    pub documentation: Option<Url>,
481    /// Link to the code repository of the smart contract.
482    #[serde(skip_serializing_if = "Option::is_none")]
483    pub repository: Option<Url>,
484    /// Link to the homepage of the smart contract.
485    #[serde(skip_serializing_if = "Option::is_none")]
486    pub homepage: Option<Url>,
487    /// The license of the smart contract.
488    #[serde(skip_serializing_if = "Option::is_none")]
489    pub license: Option<String>,
490}
491
492impl Contract {
493    pub fn builder() -> ContractBuilder {
494        ContractBuilder::default()
495    }
496}
497
498/// Additional user defined metadata, can be any valid json.
499#[derive(Clone, Debug, Deserialize, Serialize)]
500pub struct User {
501    /// Raw json of user defined metadata.
502    #[serde(flatten)]
503    pub json: Map<String, Value>,
504}
505
506impl User {
507    /// Constructs new user metadata.
508    pub fn new(json: Map<String, Value>) -> Self {
509        User { json }
510    }
511}
512
513/// Builder for contract metadata
514#[derive(Default)]
515pub struct ContractBuilder {
516    name: Option<String>,
517    version: Option<Version>,
518    authors: Option<Vec<String>>,
519    description: Option<String>,
520    documentation: Option<Url>,
521    repository: Option<Url>,
522    homepage: Option<Url>,
523    license: Option<String>,
524}
525
526impl ContractBuilder {
527    /// Set the contract name (required)
528    pub fn name<S>(&mut self, name: S) -> &mut Self
529    where
530        S: AsRef<str>,
531    {
532        if self.name.is_some() {
533            panic!("name has already been set")
534        }
535        self.name = Some(name.as_ref().to_string());
536        self
537    }
538
539    /// Set the contract version (required)
540    pub fn version(&mut self, version: Version) -> &mut Self {
541        if self.version.is_some() {
542            panic!("version has already been set")
543        }
544        self.version = Some(version);
545        self
546    }
547
548    /// Set the contract version (required)
549    pub fn authors<I, S>(&mut self, authors: I) -> &mut Self
550    where
551        I: IntoIterator<Item = S>,
552        S: AsRef<str>,
553    {
554        if self.authors.is_some() {
555            panic!("authors has already been set")
556        }
557
558        let authors = authors
559            .into_iter()
560            .map(|s| s.as_ref().to_string())
561            .collect::<Vec<_>>();
562
563        if authors.is_empty() {
564            panic!("must have at least one author")
565        }
566
567        self.authors = Some(authors);
568        self
569    }
570
571    /// Set the contract description (optional)
572    pub fn description<S>(&mut self, description: S) -> &mut Self
573    where
574        S: AsRef<str>,
575    {
576        if self.description.is_some() {
577            panic!("description has already been set")
578        }
579        self.description = Some(description.as_ref().to_string());
580        self
581    }
582
583    /// Set the contract documentation url (optional)
584    pub fn documentation(&mut self, documentation: Url) -> &mut Self {
585        if self.documentation.is_some() {
586            panic!("documentation is already set")
587        }
588        self.documentation = Some(documentation);
589        self
590    }
591
592    /// Set the contract repository url (optional)
593    pub fn repository(&mut self, repository: Url) -> &mut Self {
594        if self.repository.is_some() {
595            panic!("repository is already set")
596        }
597        self.repository = Some(repository);
598        self
599    }
600
601    /// Set the contract homepage url (optional)
602    pub fn homepage(&mut self, homepage: Url) -> &mut Self {
603        if self.homepage.is_some() {
604            panic!("homepage is already set")
605        }
606        self.homepage = Some(homepage);
607        self
608    }
609
610    /// Set the contract license (optional)
611    pub fn license<S>(&mut self, license: S) -> &mut Self
612    where
613        S: AsRef<str>,
614    {
615        if self.license.is_some() {
616            panic!("license has already been set")
617        }
618        self.license = Some(license.as_ref().to_string());
619        self
620    }
621
622    /// Finalize construction of the [`ContractMetadata`].
623    ///
624    /// Returns an `Err` if any required fields missing.
625    pub fn build(&self) -> Result<Contract, String> {
626        let mut required = Vec::new();
627
628        if let (Some(name), Some(version), Some(authors)) =
629            (&self.name, &self.version, &self.authors)
630        {
631            Ok(Contract {
632                name: name.to_string(),
633                version: version.clone(),
634                authors: authors.to_vec(),
635                description: self.description.clone(),
636                documentation: self.documentation.clone(),
637                repository: self.repository.clone(),
638                homepage: self.homepage.clone(),
639                license: self.license.clone(),
640            })
641        } else {
642            if self.name.is_none() {
643                required.push("name");
644            }
645            if self.version.is_none() {
646                required.push("version")
647            }
648            if self.authors.is_none() {
649                required.push("authors")
650            }
651            Err(format!(
652                "Missing required non-default fields: {}",
653                required.join(", ")
654            ))
655        }
656    }
657}
658
659#[cfg(test)]
660mod tests {
661    use super::*;
662    use pretty_assertions::assert_eq;
663    use serde_json::json;
664
665    #[test]
666    fn builder_fails_with_missing_required_fields() {
667        let missing_name = Contract::builder()
668            // .name("incrementer".to_string())
669            .version(Version::new(2, 1, 0))
670            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
671            .build();
672
673        assert_eq!(
674            missing_name.unwrap_err(),
675            "Missing required non-default fields: name"
676        );
677
678        let missing_version = Contract::builder()
679            .name("incrementer")
680            // .version(Version::new(2, 1, 0))
681            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
682            .build();
683
684        assert_eq!(
685            missing_version.unwrap_err(),
686            "Missing required non-default fields: version"
687        );
688
689        let missing_authors = Contract::builder()
690            .name("incrementer")
691            .version(Version::new(2, 1, 0))
692            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
693            .build();
694
695        assert_eq!(
696            missing_authors.unwrap_err(),
697            "Missing required non-default fields: authors"
698        );
699
700        let missing_all = Contract::builder()
701            // .name("incrementer".to_string())
702            // .version(Version::new(2, 1, 0))
703            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
704            .build();
705
706        assert_eq!(
707            missing_all.unwrap_err(),
708            "Missing required non-default fields: name, version, authors"
709        );
710    }
711
712    #[test]
713    fn json_with_optional_fields() {
714        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
715        let compiler = SourceCompiler::new(
716            Compiler::RustC,
717            Version::parse("1.46.0-nightly").unwrap(),
718        );
719        let wasm = SourceWasm::new(vec![0u8, 1u8, 2u8]);
720        let build_info = json! {
721            {
722                "example_compiler_version": 42,
723                "example_settings": [],
724                "example_name": "increment"
725            }
726        }
727        .as_object()
728        .unwrap()
729        .clone();
730
731        let source = Source::new(
732            Some(wasm),
733            CodeHash([0u8; 32]),
734            language,
735            compiler,
736            Some(build_info),
737        );
738
739        let contract = Contract::builder()
740            .name("incrementer")
741            .version(Version::new(2, 1, 0))
742            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
743            .description("increment a value")
744            .documentation(Url::parse("http://docs.rs/").unwrap())
745            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
746            .homepage(Url::parse("http://example.com/").unwrap())
747            .license("Apache-2.0")
748            .build()
749            .unwrap();
750
751        let user_json = json! {
752            {
753                "more-user-provided-fields": [
754                  "and",
755                  "their",
756                  "values"
757                ],
758                "some-user-provided-field": "and-its-value"
759            }
760        };
761        let user = User::new(user_json.as_object().unwrap().clone());
762        let abi_json = json! {
763            {
764                "spec": {},
765                "storage": {},
766                "types": []
767            }
768        }
769        .as_object()
770        .unwrap()
771        .clone();
772
773        let metadata = ContractMetadata::new(
774            source,
775            contract,
776            Some(String::from("paritytech/contracts-verifiable:3.0.1")),
777            Some(user),
778            abi_json,
779        );
780        let json = serde_json::to_value(&metadata).unwrap();
781
782        let expected = json! {
783            {
784                "source": {
785                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
786                    "language": "ink! 2.1.0",
787                    "compiler": "rustc 1.46.0-nightly",
788                    "wasm": "0x000102",
789                    "build_info": {
790                        "example_compiler_version": 42,
791                        "example_settings": [],
792                        "example_name": "increment"
793                    }
794                },
795                "image": "paritytech/contracts-verifiable:3.0.1",
796                "contract": {
797                    "name": "incrementer",
798                    "version": "2.1.0",
799                    "authors": [
800                      "Use Ink <ink@use.ink>"
801                    ],
802                    "description": "increment a value",
803                    "documentation": "http://docs.rs/",
804                    "repository": "http://github.com/use-ink/ink/",
805                    "homepage": "http://example.com/",
806                    "license": "Apache-2.0",
807                },
808                "user": {
809                    "more-user-provided-fields": [
810                      "and",
811                      "their",
812                      "values"
813                    ],
814                    "some-user-provided-field": "and-its-value"
815                },
816                // these fields are part of the flattened raw json for the contract ABI
817                "spec": {},
818                "storage": {},
819                "types": []
820            }
821        };
822
823        assert_eq!(json, expected);
824    }
825
826    #[test]
827    fn json_excludes_optional_fields() {
828        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
829        let compiler = SourceCompiler::new(
830            Compiler::RustC,
831            Version::parse("1.46.0-nightly").unwrap(),
832        );
833        let source = Source::new(None, CodeHash([0u8; 32]), language, compiler, None);
834        let contract = Contract::builder()
835            .name("incrementer")
836            .version(Version::new(2, 1, 0))
837            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
838            .build()
839            .unwrap();
840        let abi_json = json! {
841            {
842                "spec": {},
843                "storage": {},
844                "types": []
845            }
846        }
847        .as_object()
848        .unwrap()
849        .clone();
850
851        let metadata = ContractMetadata::new(source, contract, None, None, abi_json);
852        let json = serde_json::to_value(&metadata).unwrap();
853
854        let expected = json! {
855            {
856                "contract": {
857                    "name": "incrementer",
858                    "version": "2.1.0",
859                    "authors": [
860                      "Use Ink <ink@use.ink>"
861                    ],
862                },
863                "image": serde_json::Value::Null,
864                "source": {
865                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
866                    "language": "ink! 2.1.0",
867                    "compiler": "rustc 1.46.0-nightly"
868                },
869                // these fields are part of the flattened raw json for the contract ABI
870                "spec": {},
871                "storage": {},
872                "types": []
873            }
874        };
875
876        assert_eq!(json, expected);
877    }
878
879    #[test]
880    fn decoding_works() {
881        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
882        let compiler = SourceCompiler::new(
883            Compiler::RustC,
884            Version::parse("1.46.0-nightly").unwrap(),
885        );
886        let wasm = SourceWasm::new(vec![0u8, 1u8, 2u8]);
887        let build_info = json! {
888            {
889                "example_compiler_version": 42,
890                "example_settings": [],
891                "example_name": "increment",
892            }
893        }
894        .as_object()
895        .unwrap()
896        .clone();
897
898        let source = Source::new(
899            Some(wasm),
900            CodeHash([0u8; 32]),
901            language,
902            compiler,
903            Some(build_info),
904        );
905        let contract = Contract::builder()
906            .name("incrementer")
907            .version(Version::new(2, 1, 0))
908            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
909            .description("increment a value")
910            .documentation(Url::parse("http://docs.rs/").unwrap())
911            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
912            .homepage(Url::parse("http://example.com/").unwrap())
913            .license("Apache-2.0")
914            .build()
915            .unwrap();
916
917        let user_json = json! {
918            {
919                "more-user-provided-fields": [
920                  "and",
921                  "their",
922                  "values"
923                ],
924                "some-user-provided-field": "and-its-value"
925            }
926        };
927        let user = User::new(user_json.as_object().unwrap().clone());
928        let abi_json = json! {
929            {
930                "spec": {},
931                "storage": {},
932                "types": []
933            }
934        }
935        .as_object()
936        .unwrap()
937        .clone();
938
939        let metadata =
940            ContractMetadata::new(source, contract, None, Some(user), abi_json);
941        let json = serde_json::to_value(&metadata).unwrap();
942
943        let decoded = serde_json::from_value::<ContractMetadata>(json);
944        assert!(decoded.is_ok())
945    }
946}