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("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 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!("{: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 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    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    /// todo can be removed
442    /// The solang compiler.
443    Solang,
444}
445
446impl Display for Compiler {
447    fn fmt(&self, f: &mut Formatter<'_>) -> DisplayResult {
448        match self {
449            Self::RustC => write!(f, "rustc"),
450            Self::Solang => write!(f, "solang"),
451        }
452    }
453}
454
455impl FromStr for Compiler {
456    type Err = String;
457
458    fn from_str(s: &str) -> Result<Self, Self::Err> {
459        match s {
460            "rustc" => Ok(Self::RustC),
461            "solang" => Ok(Self::Solang),
462            _ => Err(format!("Invalid compiler '{s}'")),
463        }
464    }
465}
466
467/// Metadata about a smart contract.
468#[derive(Clone, Debug, Deserialize, Serialize)]
469pub struct Contract {
470    /// The name of the smart contract.
471    pub name: String,
472    /// The version of the smart contract.
473    pub version: Version,
474    /// The authors of the smart contract.
475    pub authors: Vec<String>,
476    /// The description of the smart contract.
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub description: Option<String>,
479    /// Link to the documentation of the smart contract.
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub documentation: Option<Url>,
482    /// Link to the code repository of the smart contract.
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub repository: Option<Url>,
485    /// Link to the homepage of the smart contract.
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub homepage: Option<Url>,
488    /// The license of the smart contract.
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub license: Option<String>,
491}
492
493impl Contract {
494    pub fn builder() -> ContractBuilder {
495        ContractBuilder::default()
496    }
497}
498
499/// Additional user defined metadata, can be any valid json.
500#[derive(Clone, Debug, Deserialize, Serialize)]
501pub struct User {
502    /// Raw json of user defined metadata.
503    #[serde(flatten)]
504    pub json: Map<String, Value>,
505}
506
507impl User {
508    /// Constructs new user metadata.
509    pub fn new(json: Map<String, Value>) -> Self {
510        User { json }
511    }
512}
513
514/// Builder for contract metadata
515#[derive(Default)]
516pub struct ContractBuilder {
517    name: Option<String>,
518    version: Option<Version>,
519    authors: Option<Vec<String>>,
520    description: Option<String>,
521    documentation: Option<Url>,
522    repository: Option<Url>,
523    homepage: Option<Url>,
524    license: Option<String>,
525}
526
527impl ContractBuilder {
528    /// Set the contract name (required)
529    pub fn name<S>(&mut self, name: S) -> &mut Self
530    where
531        S: AsRef<str>,
532    {
533        if self.name.is_some() {
534            panic!("name has already been set")
535        }
536        self.name = Some(name.as_ref().to_string());
537        self
538    }
539
540    /// Set the contract version (required)
541    pub fn version(&mut self, version: Version) -> &mut Self {
542        if self.version.is_some() {
543            panic!("version has already been set")
544        }
545        self.version = Some(version);
546        self
547    }
548
549    /// Set the contract version (required)
550    pub fn authors<I, S>(&mut self, authors: I) -> &mut Self
551    where
552        I: IntoIterator<Item = S>,
553        S: AsRef<str>,
554    {
555        if self.authors.is_some() {
556            panic!("authors has already been set")
557        }
558
559        let authors = authors
560            .into_iter()
561            .map(|s| s.as_ref().to_string())
562            .collect::<Vec<_>>();
563
564        if authors.is_empty() {
565            panic!("must have at least one author")
566        }
567
568        self.authors = Some(authors);
569        self
570    }
571
572    /// Set the contract description (optional)
573    pub fn description<S>(&mut self, description: S) -> &mut Self
574    where
575        S: AsRef<str>,
576    {
577        if self.description.is_some() {
578            panic!("description has already been set")
579        }
580        self.description = Some(description.as_ref().to_string());
581        self
582    }
583
584    /// Set the contract documentation url (optional)
585    pub fn documentation(&mut self, documentation: Url) -> &mut Self {
586        if self.documentation.is_some() {
587            panic!("documentation is already set")
588        }
589        self.documentation = Some(documentation);
590        self
591    }
592
593    /// Set the contract repository url (optional)
594    pub fn repository(&mut self, repository: Url) -> &mut Self {
595        if self.repository.is_some() {
596            panic!("repository is already set")
597        }
598        self.repository = Some(repository);
599        self
600    }
601
602    /// Set the contract homepage url (optional)
603    pub fn homepage(&mut self, homepage: Url) -> &mut Self {
604        if self.homepage.is_some() {
605            panic!("homepage is already set")
606        }
607        self.homepage = Some(homepage);
608        self
609    }
610
611    /// Set the contract license (optional)
612    pub fn license<S>(&mut self, license: S) -> &mut Self
613    where
614        S: AsRef<str>,
615    {
616        if self.license.is_some() {
617            panic!("license has already been set")
618        }
619        self.license = Some(license.as_ref().to_string());
620        self
621    }
622
623    /// Finalize construction of the [`Contract`] metadata.
624    ///
625    /// Returns an `Err` if any required fields missing.
626    pub fn build(&self) -> Result<Contract, String> {
627        let mut required = Vec::new();
628
629        if let (Some(name), Some(version), Some(authors)) =
630            (&self.name, &self.version, &self.authors)
631        {
632            Ok(Contract {
633                name: name.to_string(),
634                version: version.clone(),
635                authors: authors.to_vec(),
636                description: self.description.clone(),
637                documentation: self.documentation.clone(),
638                repository: self.repository.clone(),
639                homepage: self.homepage.clone(),
640                license: self.license.clone(),
641            })
642        } else {
643            if self.name.is_none() {
644                required.push("name");
645            }
646            if self.version.is_none() {
647                required.push("version")
648            }
649            if self.authors.is_none() {
650                required.push("authors")
651            }
652            Err(format!(
653                "Missing required non-default fields: {}",
654                required.join(", ")
655            ))
656        }
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use super::*;
663    use pretty_assertions::assert_eq;
664    use serde_json::json;
665
666    #[test]
667    fn builder_fails_with_missing_required_fields() {
668        let missing_name = Contract::builder()
669            // .name("incrementer".to_string())
670            .version(Version::new(2, 1, 0))
671            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
672            .build();
673
674        assert_eq!(
675            missing_name.unwrap_err(),
676            "Missing required non-default fields: name"
677        );
678
679        let missing_version = Contract::builder()
680            .name("incrementer")
681            // .version(Version::new(2, 1, 0))
682            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
683            .build();
684
685        assert_eq!(
686            missing_version.unwrap_err(),
687            "Missing required non-default fields: version"
688        );
689
690        let missing_authors = Contract::builder()
691            .name("incrementer")
692            .version(Version::new(2, 1, 0))
693            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
694            .build();
695
696        assert_eq!(
697            missing_authors.unwrap_err(),
698            "Missing required non-default fields: authors"
699        );
700
701        let missing_all = Contract::builder()
702            // .name("incrementer".to_string())
703            // .version(Version::new(2, 1, 0))
704            // .authors(vec!["Use Ink <ink@use.ink>".to_string()])
705            .build();
706
707        assert_eq!(
708            missing_all.unwrap_err(),
709            "Missing required non-default fields: name, version, authors"
710        );
711    }
712
713    #[test]
714    fn json_with_optional_fields() {
715        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
716        let compiler = SourceCompiler::new(
717            Compiler::RustC,
718            Version::parse("1.46.0-nightly").unwrap(),
719        );
720        let contract_binary = SourceContractBinary::new(vec![0u8, 1u8, 2u8]);
721        let build_info = json! {
722            {
723                "example_compiler_version": 42,
724                "example_settings": [],
725                "example_name": "increment"
726            }
727        }
728        .as_object()
729        .unwrap()
730        .clone();
731
732        let source = Source::new(
733            Some(contract_binary),
734            CodeHash([0u8; 32]),
735            language,
736            compiler,
737            Some(build_info),
738        );
739
740        let contract = Contract::builder()
741            .name("incrementer")
742            .version(Version::new(2, 1, 0))
743            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
744            .description("increment a value")
745            .documentation(Url::parse("http://docs.rs/").unwrap())
746            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
747            .homepage(Url::parse("http://example.com/").unwrap())
748            .license("Apache-2.0")
749            .build()
750            .unwrap();
751
752        let user_json = json! {
753            {
754                "more-user-provided-fields": [
755                  "and",
756                  "their",
757                  "values"
758                ],
759                "some-user-provided-field": "and-its-value"
760            }
761        };
762        let user = User::new(user_json.as_object().unwrap().clone());
763        let abi_json = json! {
764            {
765                "spec": {},
766                "storage": {},
767                "types": []
768            }
769        }
770        .as_object()
771        .unwrap()
772        .clone();
773
774        let metadata = ContractMetadata::new(
775            source,
776            contract,
777            Some(String::from("paritytech/contracts-verifiable:3.0.1")),
778            Some(user),
779            abi_json,
780        );
781        let json = serde_json::to_value(&metadata).unwrap();
782
783        let expected = json! {
784            {
785                "source": {
786                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
787                    "language": "ink! 2.1.0",
788                    "compiler": "rustc 1.46.0-nightly",
789                    "contract_binary": "0x000102",
790                    "build_info": {
791                        "example_compiler_version": 42,
792                        "example_settings": [],
793                        "example_name": "increment"
794                    }
795                },
796                "image": "paritytech/contracts-verifiable:3.0.1",
797                "contract": {
798                    "name": "incrementer",
799                    "version": "2.1.0",
800                    "authors": [
801                      "Use Ink <ink@use.ink>"
802                    ],
803                    "description": "increment a value",
804                    "documentation": "http://docs.rs/",
805                    "repository": "http://github.com/use-ink/ink/",
806                    "homepage": "http://example.com/",
807                    "license": "Apache-2.0",
808                },
809                "user": {
810                    "more-user-provided-fields": [
811                      "and",
812                      "their",
813                      "values"
814                    ],
815                    "some-user-provided-field": "and-its-value"
816                },
817                // these fields are part of the flattened raw json for the contract ABI
818                "spec": {},
819                "storage": {},
820                "types": []
821            }
822        };
823
824        assert_eq!(json, expected);
825    }
826
827    #[test]
828    fn json_excludes_optional_fields() {
829        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
830        let compiler = SourceCompiler::new(
831            Compiler::RustC,
832            Version::parse("1.46.0-nightly").unwrap(),
833        );
834        let source = Source::new(None, CodeHash([0u8; 32]), language, compiler, None);
835        let contract = Contract::builder()
836            .name("incrementer")
837            .version(Version::new(2, 1, 0))
838            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
839            .build()
840            .unwrap();
841        let abi_json = json! {
842            {
843                "spec": {},
844                "storage": {},
845                "types": []
846            }
847        }
848        .as_object()
849        .unwrap()
850        .clone();
851
852        let metadata = ContractMetadata::new(source, contract, None, None, abi_json);
853        let json = serde_json::to_value(&metadata).unwrap();
854
855        let expected = json! {
856            {
857                "contract": {
858                    "name": "incrementer",
859                    "version": "2.1.0",
860                    "authors": [
861                      "Use Ink <ink@use.ink>"
862                    ],
863                },
864                "image": serde_json::Value::Null,
865                "source": {
866                    "hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
867                    "language": "ink! 2.1.0",
868                    "compiler": "rustc 1.46.0-nightly"
869                },
870                // these fields are part of the flattened raw json for the contract ABI
871                "spec": {},
872                "storage": {},
873                "types": []
874            }
875        };
876
877        assert_eq!(json, expected);
878    }
879
880    #[test]
881    fn decoding_works() {
882        let language = SourceLanguage::new(Language::Ink, Version::new(2, 1, 0));
883        let compiler = SourceCompiler::new(
884            Compiler::RustC,
885            Version::parse("1.46.0-nightly").unwrap(),
886        );
887        let contract_binary = SourceContractBinary::new(vec![0u8, 1u8, 2u8]);
888        let build_info = json! {
889            {
890                "example_compiler_version": 42,
891                "example_settings": [],
892                "example_name": "increment",
893            }
894        }
895        .as_object()
896        .unwrap()
897        .clone();
898
899        let source = Source::new(
900            Some(contract_binary),
901            CodeHash([0u8; 32]),
902            language,
903            compiler,
904            Some(build_info),
905        );
906        let contract = Contract::builder()
907            .name("incrementer")
908            .version(Version::new(2, 1, 0))
909            .authors(vec!["Use Ink <ink@use.ink>".to_string()])
910            .description("increment a value")
911            .documentation(Url::parse("http://docs.rs/").unwrap())
912            .repository(Url::parse("http://github.com/use-ink/ink/").unwrap())
913            .homepage(Url::parse("http://example.com/").unwrap())
914            .license("Apache-2.0")
915            .build()
916            .unwrap();
917
918        let user_json = json! {
919            {
920                "more-user-provided-fields": [
921                  "and",
922                  "their",
923                  "values"
924                ],
925                "some-user-provided-field": "and-its-value"
926            }
927        };
928        let user = User::new(user_json.as_object().unwrap().clone());
929        let abi_json = json! {
930            {
931                "spec": {},
932                "storage": {},
933                "types": []
934            }
935        }
936        .as_object()
937        .unwrap()
938        .clone();
939
940        let metadata =
941            ContractMetadata::new(source, contract, None, Some(user), abi_json);
942        let json = serde_json::to_value(&metadata).unwrap();
943
944        let decoded = serde_json::from_value::<ContractMetadata>(json);
945        assert!(decoded.is_ok())
946    }
947}