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 contract_binary = SourceContractBinary::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(contract_binary),
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("useink/contracts-verifiable:6.0.0-beta");
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    Deserialize,
79    Serialize,
80    Serializer,
81    de,
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 binary.
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_contract_binary_attribute(&mut self) {
136        self.source.contract_binary = 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
154    /// `cargo-contract` 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 contract 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!("{b:x?}"))
189            .collect::<Vec<String>>()
190            .join("");
191        f.write_fmt(format_args!("0x{raw_string}"))
192    }
193}
194
195/// Information about the contract's binary (for PolkaVM).
196#[derive(Clone, Debug, Deserialize, Serialize)]
197pub struct Source {
198    /// The hash of the contract's binary.
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 binary of the contract (compiled for PolkaVM).
205    /// Used to optionally bundle the code with the metadata.
206    #[serde(skip_serializing_if = "Option::is_none")]
207    pub contract_binary: Option<SourceContractBinary>,
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        contract_binary: Option<SourceContractBinary>,
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            contract_binary,
229            build_info,
230        }
231    }
232}
233
234/// The binary of the compiled smart contract (compiled for PolkaVM).
235#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
236pub struct SourceContractBinary(
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 contract's binary.
242    pub Vec<u8>,
243);
244
245impl SourceContractBinary {
246    /// Constructs a new `SourceContractBytecode`.
247    pub fn new(contract_binary: Vec<u8>) -> Self {
248        SourceContractBinary(contract_binary)
249    }
250}
251
252impl Display for SourceContractBinary {
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}
340
341impl Display for Language {
342    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
343        match self {
344            Self::Ink => write!(f, "ink!"),
345            Self::Solidity => write!(f, "Solidity"),
346        }
347    }
348}
349
350impl FromStr for Language {
351    type Err = String;
352
353    fn from_str(s: &str) -> Result<Self, Self::Err> {
354        match s {
355            "ink!" => Ok(Self::Ink),
356            "Solidity" => Ok(Self::Solidity),
357            _ => Err(format!("Invalid language '{s}'")),
358        }
359    }
360}
361
362/// A compiler used to compile a smart contract.
363#[derive(Clone, Debug)]
364pub struct SourceCompiler {
365    /// The compiler used to compile the smart contract.
366    pub compiler: Compiler,
367    /// The version of the compiler used to compile the smart contract.
368    pub version: Version,
369}
370
371impl Display for SourceCompiler {
372    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
373        write!(f, "{} {}", self.compiler, self.version)
374    }
375}
376
377impl FromStr for SourceCompiler {
378    type Err = String;
379
380    fn from_str(s: &str) -> Result<Self, Self::Err> {
381        let mut parts = s.split_whitespace();
382
383        let compiler = parts
384            .next()
385            .ok_or_else(|| {
386                format!(
387                    "SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
388                )
389            })
390            .and_then(FromStr::from_str)?;
391
392        let version = parts
393            .next()
394            .ok_or_else(|| {
395                format!(
396                    "SourceCompiler: Expected format '<compiler> <version>', got '{s}'"
397                )
398            })
399            .and_then(|v| {
400                <Version as FromStr>::from_str(v)
401                    .map_err(|e| format!("Error parsing version {e}"))
402            })?;
403
404        Ok(Self { compiler, version })
405    }
406}
407
408impl Serialize for SourceCompiler {
409    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
410    where
411        S: Serializer,
412    {
413        serializer.serialize_str(&self.to_string())
414    }
415}
416
417impl<'de> Deserialize<'de> for SourceCompiler {
418    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
419    where
420        D: de::Deserializer<'de>,
421    {
422        let s = String::deserialize(deserializer)?;
423        FromStr::from_str(&s).map_err(de::Error::custom)
424    }
425}
426
427impl SourceCompiler {
428    pub fn new(compiler: Compiler, version: Version) -> Self {
429        SourceCompiler { compiler, version }
430    }
431}
432
433/// Compilers used to compile a smart contract.
434#[derive(Clone, Debug, Deserialize, Serialize)]
435pub enum Compiler {
436    /// The rust compiler.
437    RustC,
438    /// todo can be removed
439    /// The solang compiler.
440    Solang,
441}
442
443impl Display for Compiler {
444    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
445        match self {
446            Self::RustC => write!(f, "rustc"),
447            Self::Solang => write!(f, "solang"),
448        }
449    }
450}
451
452impl FromStr for Compiler {
453    type Err = String;
454
455    fn from_str(s: &str) -> Result<Self, Self::Err> {
456        match s {
457            "rustc" => Ok(Self::RustC),
458            "solang" => Ok(Self::Solang),
459            _ => Err(format!("Invalid compiler '{s}'")),
460        }
461    }
462}
463
464/// Metadata about a smart contract.
465#[derive(Clone, Debug, Deserialize, Serialize)]
466pub struct Contract {
467    /// The name of the smart contract.
468    pub name: String,
469    /// The version of the smart contract.
470    pub version: Version,
471    /// The authors of the smart contract.
472    pub authors: Vec<String>,
473    /// The description of the smart contract.
474    #[serde(skip_serializing_if = "Option::is_none")]
475    pub description: Option<String>,
476    /// Link to the documentation of the smart contract.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub documentation: Option<Url>,
479    /// Link to the code repository of the smart contract.
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub repository: Option<Url>,
482    /// Link to the homepage of the smart contract.
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub homepage: Option<Url>,
485    /// The license of the smart contract.
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub license: Option<String>,
488}
489
490impl Contract {
491    pub fn builder() -> ContractBuilder {
492        ContractBuilder::default()
493    }
494}
495
496/// Additional user defined metadata, can be any valid json.
497#[derive(Clone, Debug, Deserialize, Serialize)]
498pub struct User {
499    /// Raw json of user defined metadata.
500    #[serde(flatten)]
501    pub json: Map<String, Value>,
502}
503
504impl User {
505    /// Constructs new user metadata.
506    pub fn new(json: Map<String, Value>) -> Self {
507        User { json }
508    }
509}
510
511/// Builder for contract metadata
512#[derive(Default)]
513pub struct ContractBuilder {
514    name: Option<String>,
515    version: Option<Version>,
516    authors: Option<Vec<String>>,
517    description: Option<String>,
518    documentation: Option<Url>,
519    repository: Option<Url>,
520    homepage: Option<Url>,
521    license: Option<String>,
522}
523
524impl ContractBuilder {
525    /// Set the contract name (required)
526    pub fn name<S>(&mut self, name: S) -> &mut Self
527    where
528        S: AsRef<str>,
529    {
530        if self.name.is_some() {
531            panic!("name has already been set")
532        }
533        self.name = Some(name.as_ref().to_string());
534        self
535    }
536
537    /// Set the contract version (required)
538    pub fn version(&mut self, version: Version) -> &mut Self {
539        if self.version.is_some() {
540            panic!("version has already been set")
541        }
542        self.version = Some(version);
543        self
544    }
545
546    /// Set the contract version (required)
547    pub fn authors<I, S>(&mut self, authors: I) -> &mut Self
548    where
549        I: IntoIterator<Item = S>,
550        S: AsRef<str>,
551    {
552        if self.authors.is_some() {
553            panic!("authors has already been set")
554        }
555
556        let authors = authors
557            .into_iter()
558            .map(|s| s.as_ref().to_string())
559            .collect::<Vec<_>>();
560
561        if authors.is_empty() {
562            panic!("must have at least one author")
563        }
564
565        self.authors = Some(authors);
566        self
567    }
568
569    /// Set the contract description (optional)
570    pub fn description<S>(&mut self, description: S) -> &mut Self
571    where
572        S: AsRef<str>,
573    {
574        if self.description.is_some() {
575            panic!("description has already been set")
576        }
577        self.description = Some(description.as_ref().to_string());
578        self
579    }
580
581    /// Set the contract documentation url (optional)
582    pub fn documentation(&mut self, documentation: Url) -> &mut Self {
583        if self.documentation.is_some() {
584            panic!("documentation is already set")
585        }
586        self.documentation = Some(documentation);
587        self
588    }
589
590    /// Set the contract repository url (optional)
591    pub fn repository(&mut self, repository: Url) -> &mut Self {
592        if self.repository.is_some() {
593            panic!("repository is already set")
594        }
595        self.repository = Some(repository);
596        self
597    }
598
599    /// Set the contract homepage url (optional)
600    pub fn homepage(&mut self, homepage: Url) -> &mut Self {
601        if self.homepage.is_some() {
602            panic!("homepage is already set")
603        }
604        self.homepage = Some(homepage);
605        self
606    }
607
608    /// Set the contract license (optional)
609    pub fn license<S>(&mut self, license: S) -> &mut Self
610    where
611        S: AsRef<str>,
612    {
613        if self.license.is_some() {
614            panic!("license has already been set")
615        }
616        self.license = Some(license.as_ref().to_string());
617        self
618    }
619
620    /// Finalize construction of the [`Contract`] metadata.
621    ///
622    /// Returns an `Err` if any required fields missing.
623    pub fn build(&self) -> Result<Contract, String> {
624        let mut required = Vec::new();
625
626        if let (Some(name), Some(version), Some(authors)) =
627            (&self.name, &self.version, &self.authors)
628        {
629            Ok(Contract {
630                name: name.to_string(),
631                version: version.clone(),
632                authors: authors.to_vec(),
633                description: self.description.clone(),
634                documentation: self.documentation.clone(),
635                repository: self.repository.clone(),
636                homepage: self.homepage.clone(),
637                license: self.license.clone(),
638            })
639        } else {
640            if self.name.is_none() {
641                required.push("name");
642            }
643            if self.version.is_none() {
644                required.push("version")
645            }
646            if self.authors.is_none() {
647                required.push("authors")
648            }
649            Err(format!(
650                "Missing required non-default fields: {}",
651                required.join(", ")
652            ))
653        }
654    }
655}
656
657#[cfg(test)]
658mod tests {
659    use super::*;
660    use pretty_assertions::assert_eq;
661    use serde_json::json;
662
663    #[test]
664    fn builder_fails_with_missing_required_fields() {
665        let missing_name = Contract::builder()
666            // .name("incrementer".to_string())
667            .version(Version::new(2, 1, 0))
668            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
669            .build();
670
671        assert_eq!(
672            missing_name.unwrap_err(),
673            "Missing required non-default fields: name"
674        );
675
676        let missing_version = Contract::builder()
677            .name("incrementer")
678            // .version(Version::new(2, 1, 0))
679            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
680            .build();
681
682        assert_eq!(
683            missing_version.unwrap_err(),
684            "Missing required non-default fields: version"
685        );
686
687        let missing_authors = Contract::builder()
688            .name("incrementer")
689            .version(Version::new(2, 1, 0))
690            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
691            .build();
692
693        assert_eq!(
694            missing_authors.unwrap_err(),
695            "Missing required non-default fields: authors"
696        );
697
698        let missing_all = Contract::builder()
699            // .name("incrementer".to_string())
700            // .version(Version::new(2, 1, 0))
701            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
702            .build();
703
704        assert_eq!(
705            missing_all.unwrap_err(),
706            "Missing required non-default fields: name, version, authors"
707        );
708    }
709
710    #[test]
711    fn json_with_optional_fields() {
712        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
713        let compiler = SourceCompiler::new(
714            Compiler::RustC,
715            Version::parse("1.46.0-nightly").unwrap(),
716        );
717        let contract_binary = SourceContractBinary::new(vec![0u8, 1u8, 2u8]);
718        let build_info = json! {
719            {
720                "example_compiler_version": 42,
721                "example_settings": [],
722                "example_name": "increment"
723            }
724        }
725        .as_object()
726        .unwrap()
727        .clone();
728
729        let source = Source::new(
730            Some(contract_binary),
731            CodeHash([0u8; 32]),
732            language,
733            compiler,
734            Some(build_info),
735        );
736
737        let contract = Contract::builder()
738            .name("incrementer")
739            .version(Version::new(2, 1, 0))
740            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
741            .description("increment a value")
742            .documentation(Url::parse("http://docs.rs/").unwrap())
743            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
744            .homepage(Url::parse("http://example.com/").unwrap())
745            .license("Apache-2.0")
746            .build()
747            .unwrap();
748
749        let user_json = json! {
750            {
751                "more-user-provided-fields": [
752                  "and",
753                  "their",
754                  "values"
755                ],
756                "some-user-provided-field": "and-its-value"
757            }
758        };
759        let user = User::new(user_json.as_object().unwrap().clone());
760        let abi_json = json! {
761            {
762                "spec": {},
763                "storage": {},
764                "types": []
765            }
766        }
767        .as_object()
768        .unwrap()
769        .clone();
770
771        let metadata = ContractMetadata::new(
772            source,
773            contract,
774            Some(String::from("useink/contracts-verifiable:6.0.0-beta")),
775            Some(user),
776            abi_json,
777        );
778        let json = serde_json::to_value(&metadata).unwrap();
779
780        let expected = json! {
781            {
782                "source": {
783                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
784                    "language": "ink! 2.1.0",
785                    "compiler": "rustc 1.46.0-nightly",
786                    "contract_binary": "0x000102",
787                    "build_info": {
788                        "example_compiler_version": 42,
789                        "example_settings": [],
790                        "example_name": "increment"
791                    }
792                },
793                "image": "useink/contracts-verifiable:6.0.0-beta",
794                "contract": {
795                    "name": "incrementer",
796                    "version": "2.1.0",
797                    "authors": [
798                      "Use Ink <ink@use.ink>"
799                    ],
800                    "description": "increment a value",
801                    "documentation": "http://docs.rs/",
802                    "repository": "http://github.com/use-ink/ink/",
803                    "homepage": "http://example.com/",
804                    "license": "Apache-2.0",
805                },
806                "user": {
807                    "more-user-provided-fields": [
808                      "and",
809                      "their",
810                      "values"
811                    ],
812                    "some-user-provided-field": "and-its-value"
813                },
814                // these fields are part of the flattened raw json for the contract ABI
815                "spec": {},
816                "storage": {},
817                "types": []
818            }
819        };
820
821        assert_eq!(json, expected);
822    }
823
824    #[test]
825    fn json_excludes_optional_fields() {
826        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
827        let compiler = SourceCompiler::new(
828            Compiler::RustC,
829            Version::parse("1.46.0-nightly").unwrap(),
830        );
831        let source = Source::new(None, CodeHash([0u8; 32]), language, compiler, None);
832        let contract = Contract::builder()
833            .name("incrementer")
834            .version(Version::new(2, 1, 0))
835            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
836            .build()
837            .unwrap();
838        let abi_json = json! {
839            {
840                "spec": {},
841                "storage": {},
842                "types": []
843            }
844        }
845        .as_object()
846        .unwrap()
847        .clone();
848
849        let metadata = ContractMetadata::new(source, contract, None, None, abi_json);
850        let json = serde_json::to_value(&metadata).unwrap();
851
852        let expected = json! {
853            {
854                "contract": {
855                    "name": "incrementer",
856                    "version": "2.1.0",
857                    "authors": [
858                      "Use Ink <ink@use.ink>"
859                    ],
860                },
861                "image": serde_json::Value::Null,
862                "source": {
863                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
864                    "language": "ink! 2.1.0",
865                    "compiler": "rustc 1.46.0-nightly"
866                },
867                // these fields are part of the flattened raw json for the contract ABI
868                "spec": {},
869                "storage": {},
870                "types": []
871            }
872        };
873
874        assert_eq!(json, expected);
875    }
876
877    #[test]
878    fn decoding_works() {
879        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
880        let compiler = SourceCompiler::new(
881            Compiler::RustC,
882            Version::parse("1.46.0-nightly").unwrap(),
883        );
884        let contract_binary = SourceContractBinary::new(vec![0u8, 1u8, 2u8]);
885        let build_info = json! {
886            {
887                "example_compiler_version": 42,
888                "example_settings": [],
889                "example_name": "increment",
890            }
891        }
892        .as_object()
893        .unwrap()
894        .clone();
895
896        let source = Source::new(
897            Some(contract_binary),
898            CodeHash([0u8; 32]),
899            language,
900            compiler,
901            Some(build_info),
902        );
903        let contract = Contract::builder()
904            .name("incrementer")
905            .version(Version::new(2, 1, 0))
906            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
907            .description("increment a value")
908            .documentation(Url::parse("http://docs.rs/").unwrap())
909            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
910            .homepage(Url::parse("http://example.com/").unwrap())
911            .license("Apache-2.0")
912            .build()
913            .unwrap();
914
915        let user_json = json! {
916            {
917                "more-user-provided-fields": [
918                  "and",
919                  "their",
920                  "values"
921                ],
922                "some-user-provided-field": "and-its-value"
923            }
924        };
925        let user = User::new(user_json.as_object().unwrap().clone());
926        let abi_json = json! {
927            {
928                "spec": {},
929                "storage": {},
930                "types": []
931            }
932        }
933        .as_object()
934        .unwrap()
935        .clone();
936
937        let metadata =
938            ContractMetadata::new(source, contract, None, Some(user), abi_json);
939        let json = serde_json::to_value(&metadata).unwrap();
940
941        let decoded = serde_json::from_value::<ContractMetadata>(json);
942        assert!(decoded.is_ok())
943    }
944}