Skip to main content

contract_build/
metadata.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
17use std::{
18    fs,
19    path::{
20        Path,
21        PathBuf,
22    },
23};
24
25use anyhow::Result;
26use colored::Colorize;
27use contract_metadata::{
28    Compiler,
29    Contract,
30    ContractMetadata,
31    Language,
32    Source,
33    SourceCompiler,
34    SourceContractBinary,
35    SourceLanguage,
36    User,
37};
38use ink_metadata::InkProject;
39use semver::Version;
40use serde::{
41    Deserialize,
42    Serialize,
43};
44use url::Url;
45
46use crate::{
47    BuildMode,
48    Features,
49    Lto,
50    Network,
51    Profile,
52    UnstableFlags,
53    Verbosity,
54    code_hash,
55    crate_metadata::CrateMetadata,
56    solidity_metadata::{
57        self,
58        SolidityContractMetadata,
59        SolidityMetadataArtifacts,
60    },
61    util,
62    verbose_eprintln,
63    workspace::{
64        ManifestPath,
65        Workspace,
66    },
67};
68
69/// Artifacts resulting from metadata generation.
70#[derive(serde::Serialize, serde::Deserialize)]
71pub enum MetadataArtifacts {
72    /// Artifacts resulting from ink! metadata generation.
73    Ink(InkMetadataArtifacts),
74    /// Artifacts resulting from Solidity compatible metadata generation.
75    Solidity(SolidityMetadataArtifacts),
76}
77
78impl MetadataArtifacts {
79    /// Returns true if all metadata files exist.
80    pub(crate) fn exists(&self) -> bool {
81        match self {
82            MetadataArtifacts::Ink(ink_metadata_artifacts) => {
83                ink_metadata_artifacts.dest_metadata.exists()
84                    && ink_metadata_artifacts.dest_bundle.exists()
85            }
86            MetadataArtifacts::Solidity(solidity_metadata_artifacts) => {
87                solidity_metadata_artifacts.dest_abi.exists()
88                    && solidity_metadata_artifacts.dest_metadata.exists()
89            }
90        }
91    }
92}
93
94/// Artifacts resulting from ink! metadata generation.
95#[derive(serde::Serialize, serde::Deserialize)]
96pub struct InkMetadataArtifacts {
97    /// Path to the resulting metadata file.
98    pub dest_metadata: PathBuf,
99    /// Path to the bundled file.
100    pub dest_bundle: PathBuf,
101}
102
103/// Result of generating the extended contract project metadata
104struct ExtendedMetadataResult {
105    source: Source,
106    contract: Contract,
107    user: Option<User>,
108}
109
110/// Information about the settings used to build a particular ink! contract.
111///
112/// Note that this should be an optional part of the metadata since it may not necessarily
113/// translate to other languages (e.g. ask!, Solidity).
114#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
115pub struct BuildInfo {
116    /// The Rust toolchain used to build the contract.
117    pub rust_toolchain: String,
118    /// The version of `cargo-contract` used to build the contract.
119    pub cargo_contract_version: Version,
120    /// The type of build that was used when building the contract.
121    pub build_mode: BuildMode,
122}
123
124impl TryFrom<BuildInfo> for serde_json::Map<String, serde_json::Value> {
125    type Error = serde_json::Error;
126
127    fn try_from(build_info: BuildInfo) -> Result<Self, Self::Error> {
128        let tmp = serde_json::to_string(&build_info)?;
129        serde_json::from_str(&tmp)
130    }
131}
132
133/// Multi ABI metadata from by ink! codegen.
134#[derive(Debug, Serialize, Deserialize)]
135pub struct CodegenMetadata {
136    ink: Option<InkProject>,
137    solidity: Option<ink_metadata::sol::ContractMetadata>,
138}
139
140/// Generates a file with metadata describing the ABI of the smart contract.
141///
142/// It does so by generating and invoking a temporary workspace member.
143#[allow(clippy::too_many_arguments)]
144pub fn execute(
145    crate_metadata: &CrateMetadata,
146    final_contract_binary: &Path,
147    metadata_artifacts: &MetadataArtifacts,
148    features: &Features,
149    network: Network,
150    verbosity: Verbosity,
151    unstable_options: &UnstableFlags,
152    build_info: BuildInfo,
153) -> Result<()> {
154    // build the extended contract project metadata
155    let ExtendedMetadataResult {
156        source,
157        contract,
158        user,
159    } = extended_metadata(crate_metadata, final_contract_binary, build_info)?;
160
161    let generate_metadata = |manifest_path: &ManifestPath| -> Result<()> {
162        verbose_eprintln!(
163            verbosity,
164            " {} {}",
165            "[==]".bold(),
166            "Generating metadata".bright_green().bold(),
167        );
168        let target_dir = crate_metadata
169            .target_directory
170            .to_string_lossy()
171            .to_string();
172        let mut args = vec![
173            "--package".to_owned(),
174            "metadata-gen".to_owned(),
175            manifest_path.cargo_arg()?,
176            "--target-dir".to_owned(),
177            target_dir,
178            "--release".to_owned(),
179        ];
180        network.append_to_args(&mut args);
181        features.append_to_args(&mut args);
182
183        #[cfg(windows)]
184        let link_dead_code = "";
185
186        #[cfg(not(windows))]
187        let link_dead_code = "\x1f-Clink-dead-code";
188
189        let mut abi_cfg = String::new();
190        if let Some(abi) = crate_metadata.abi {
191            abi_cfg.push('\x1f');
192            abi_cfg.push_str(&abi.cargo_encoded_rustflag());
193        }
194
195        let cmd = util::cargo_cmd(
196            "run",
197            args,
198            crate_metadata.manifest_path.directory(),
199            verbosity,
200            vec![(
201                "CARGO_ENCODED_RUSTFLAGS",
202                Some(format!("--cap-lints=allow{link_dead_code}{abi_cfg}")),
203            )],
204        );
205        let output = cmd.stdout_capture().run()?;
206        let codegen_meta: CodegenMetadata = serde_json::from_slice(&output.stdout)?;
207        match metadata_artifacts {
208            MetadataArtifacts::Ink(ink_metadata_artifacts) => {
209                let ink_project = codegen_meta
210                    .ink
211                    .ok_or_else(|| anyhow::anyhow!("Expected ink! metadata"))?;
212                let ink_meta = match serde_json::to_value(&ink_project)? {
213                    serde_json::Value::Object(meta) => meta,
214                    _ => anyhow::bail!("Expected ink! metadata object"),
215                };
216                let metadata =
217                    ContractMetadata::new(source, contract, None, user, ink_meta);
218
219                write_metadata(ink_metadata_artifacts, metadata, &verbosity, false)?;
220            }
221            MetadataArtifacts::Solidity(solidity_metadata_artifacts) => {
222                let sol_meta = codegen_meta.solidity.ok_or_else(|| {
223                    anyhow::anyhow!("Expected Solidity compatibility metadata")
224                })?;
225                let sol_abi = solidity_metadata::generate_abi(&sol_meta)?;
226                let metadata = solidity_metadata::generate_metadata(
227                    &sol_meta,
228                    sol_abi,
229                    source,
230                    contract,
231                    crate_metadata,
232                    None,
233                )?;
234
235                write_solidity_metadata(
236                    solidity_metadata_artifacts,
237                    metadata,
238                    &verbosity,
239                    false,
240                )?;
241            }
242        }
243
244        Ok(())
245    };
246
247    if unstable_options.original_manifest {
248        generate_metadata(&crate_metadata.manifest_path)?;
249    } else {
250        Workspace::new(&crate_metadata.cargo_meta, &crate_metadata.root_package.id)?
251            .with_root_package_manifest(|manifest| {
252                manifest
253                    .with_added_crate_type("rlib")?
254                    .with_profile_release_defaults(Profile {
255                        lto: Some(Lto::Thin),
256                        ..Profile::default()
257                    })?
258                    .with_merged_workspace_dependencies(crate_metadata)?
259                    .with_empty_workspace();
260                Ok(())
261            })?
262            .with_metadata_gen_package()?
263            .using_temp(generate_metadata)?;
264    }
265
266    Ok(())
267}
268
269pub fn write_metadata(
270    metadata_artifacts: &InkMetadataArtifacts,
271    metadata: ContractMetadata,
272    verbosity: &Verbosity,
273    overwrite: bool,
274) -> Result<()> {
275    {
276        let mut metadata = metadata.clone();
277        metadata.remove_source_contract_binary_attribute();
278        let contents = serde_json::to_string_pretty(&metadata)?;
279        fs::write(&metadata_artifacts.dest_metadata, contents)?;
280    }
281
282    if overwrite {
283        verbose_eprintln!(
284            verbosity,
285            " {} {}",
286            "[==]".bold(),
287            "Updating paths".bright_cyan().bold()
288        );
289    } else {
290        verbose_eprintln!(
291            verbosity,
292            " {} {}",
293            "[==]".bold(),
294            "Generating bundle".bright_green().bold()
295        );
296    }
297    let contents = serde_json::to_string(&metadata)?;
298    fs::write(&metadata_artifacts.dest_bundle, contents)?;
299
300    Ok(())
301}
302
303/// Writes Solidity compatible ABI and metadata files.
304pub fn write_solidity_metadata(
305    metadata_artifacts: &SolidityMetadataArtifacts,
306    metadata: SolidityContractMetadata,
307    verbosity: &Verbosity,
308    overwrite: bool,
309) -> Result<()> {
310    if overwrite {
311        verbose_eprintln!(
312            verbosity,
313            " {} {}",
314            "[==]".bold(),
315            "Updating Solidity compatible metadata".bright_cyan().bold()
316        );
317    } else {
318        verbose_eprintln!(
319            verbosity,
320            " {} {}",
321            "[==]".bold(),
322            "Generating Solidity compatible metadata"
323                .bright_green()
324                .bold()
325        );
326    }
327
328    // Writes Solidity ABI file.
329    solidity_metadata::write_abi(&metadata.output.abi, &metadata_artifacts.dest_abi)?;
330
331    // Writes Solidity Metadata file.
332    solidity_metadata::write_metadata(&metadata, &metadata_artifacts.dest_metadata)?;
333
334    Ok(())
335}
336
337/// Generate the extended contract project metadata
338fn extended_metadata(
339    crate_metadata: &CrateMetadata,
340    final_contract_binary: &Path,
341    build_info: BuildInfo,
342) -> Result<ExtendedMetadataResult> {
343    let contract_package = &crate_metadata.root_package;
344    let ink_version = &crate_metadata.ink_version;
345    let rust_version = Version::parse(&rustc_version::version()?.to_string())?;
346    let contract_name = contract_package.name.clone();
347    let contract_version = Version::parse(&contract_package.version.to_string())?;
348    let contract_authors = contract_package.authors.clone();
349    // optional
350    let description = contract_package.description.clone();
351    let documentation = crate_metadata.documentation.clone();
352    let repository = contract_package
353        .repository
354        .as_ref()
355        .map(|repo| Url::parse(repo))
356        .transpose()?;
357    let homepage = crate_metadata.homepage.clone();
358    let license = contract_package.license.clone();
359    let source = {
360        let lang = SourceLanguage::new(Language::Ink, ink_version.clone());
361        let compiler = SourceCompiler::new(Compiler::RustC, rust_version);
362        let contract_binary = fs::read(final_contract_binary)?;
363        let hash = code_hash(contract_binary.as_slice());
364        Source::new(
365            Some(SourceContractBinary::new(contract_binary)),
366            hash.into(),
367            lang,
368            compiler,
369            Some(build_info.try_into()?),
370        )
371    };
372
373    // Required contract fields
374    let mut builder = Contract::builder();
375    builder
376        .name(contract_name)
377        .version(contract_version)
378        .authors(contract_authors);
379
380    if let Some(description) = description {
381        builder.description(description);
382    }
383
384    if let Some(documentation) = documentation {
385        builder.documentation(documentation);
386    }
387
388    if let Some(repository) = repository {
389        builder.repository(repository);
390    }
391
392    if let Some(homepage) = homepage {
393        builder.homepage(homepage);
394    }
395
396    if let Some(license) = license {
397        builder.license(license);
398    }
399
400    let contract = builder.build().map_err(|err| {
401        anyhow::anyhow!("Invalid contract metadata builder state: {err}")
402    })?;
403
404    // user defined metadata
405    let user = crate_metadata.user.clone().map(User::new);
406
407    Ok(ExtendedMetadataResult {
408        source,
409        contract,
410        user,
411    })
412}