contract_build/
solidity_metadata.rs

1// Copyright (C) ink! contributors.
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
17mod abi;
18mod natspec;
19
20use std::{
21    collections::HashMap,
22    fs::{
23        self,
24        File,
25    },
26    path::{
27        Path,
28        PathBuf,
29    },
30};
31
32use alloy_json_abi::JsonAbi;
33use anyhow::{
34    Context,
35    Result,
36};
37use cargo_metadata::TargetKind;
38use contract_metadata::{
39    CodeHash,
40    Contract,
41    Source,
42};
43use serde::{
44    Deserialize,
45    Serialize,
46};
47use serde_json::{
48    Map,
49    Value,
50};
51
52use self::natspec::{
53    DevDoc,
54    UserDoc,
55};
56use crate::{
57    CrateMetadata,
58    code_hash,
59};
60
61// Re-exports ABI utilities.
62pub use self::abi::{
63    abi_path,
64    generate_abi,
65    write_abi,
66};
67
68/// Artifacts resulting from Solidity compatible metadata generation.
69#[derive(serde::Serialize, serde::Deserialize)]
70pub struct SolidityMetadataArtifacts {
71    /// Path to the resulting ABI file.
72    pub dest_abi: PathBuf,
73    /// Path to the resulting metadata file.
74    pub dest_metadata: PathBuf,
75}
76
77/// Solidity compatible smart contract metadata.
78///
79/// Ref: <https://docs.soliditylang.org/en/latest/metadata.html>
80#[derive(Clone, Debug, Deserialize, Serialize)]
81pub struct SolidityContractMetadata {
82    /// Details about the compiler.
83    pub compiler: Compiler,
84    /// Source code language
85    pub language: String,
86    /// Generated information about the contract.
87    pub output: Output,
88    /// Compiler settings.
89    pub settings: Settings,
90    /// Compilation source files/source units, keys are file paths.
91    pub sources: HashMap<String, SourceFile>,
92    /// The version of the metadata format.
93    pub version: u8,
94}
95
96/// Details about the compiler.
97#[derive(Clone, Debug, Deserialize, Serialize)]
98pub struct Compiler {
99    /// Hash of the compiler binary which produced this output.
100    #[serde(skip_serializing_if = "Option::is_none")]
101    #[serde(rename = "keccak256")]
102    pub hash: Option<CodeHash>,
103    /// Version of the compiler.
104    pub version: String,
105}
106
107/// Generated information about the contract.
108#[derive(Clone, Debug, Deserialize, Serialize)]
109pub struct Output {
110    /// ABI definition of the contract.
111    /// Ref: <https://docs.soliditylang.org/en/latest/abi-spec.html#json>
112    pub abi: JsonAbi,
113    /// NatSpec developer documentation of the contract.
114    /// Ref: <https://docs.soliditylang.org/en/latest/natspec-format.html#developer-documentation>
115    #[serde(rename = "devdoc")]
116    pub dev_doc: DevDoc,
117    /// NatSpec user documentation of the contract.
118    /// Ref: <https://docs.soliditylang.org/en/latest/natspec-format.html#user-documentation>
119    #[serde(rename = "userdoc")]
120    pub user_doc: UserDoc,
121}
122
123/// Compiler settings.
124///
125/// **NOTE:** The Solidity metadata spec for this is very Solidity specific.
126/// We include build info instead and namespace it under an "ink" key.
127
128#[derive(Clone, Debug, Deserialize, Serialize)]
129pub struct Settings {
130    /// Extra Information about the contract and build environment.
131    pub ink: InkSettings,
132}
133
134/// Extra Information about the contract and build environment.
135#[derive(Clone, Debug, Deserialize, Serialize)]
136pub struct InkSettings {
137    /// The hash of the contract's binary.
138    pub hash: CodeHash,
139    /// If the contract is meant to be verifiable,
140    /// then the Docker image is specified.
141    pub image: Option<String>,
142    /// Extra information about the environment in which the contract was built.
143    ///
144    /// Useful for producing deterministic builds.
145    ///
146    /// Equivalent to `source.build_info` in ink! metadata spec.
147    /// Ref: <https://use.ink/basics/metadata/#source>
148    pub build_info: Option<Map<String, Value>>,
149}
150
151/// Compilation source files/source units, keys are file paths.
152#[derive(Clone, Debug, Deserialize, Serialize)]
153pub struct SourceFile {
154    /// Contents of the source file.
155    pub content: String,
156    /// Hash of the source file.
157    #[serde(rename = "keccak256")]
158    pub hash: CodeHash,
159    /// SPDX license identifier.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub license: Option<String>,
162}
163
164impl SourceFile {
165    /// Creates a source file.
166    pub fn new(content: String, license: Option<String>) -> Self {
167        let hash = code_hash(content.as_bytes());
168        Self {
169            hash: CodeHash::from(hash),
170            content,
171            license,
172        }
173    }
174}
175
176/// Generates a contract metadata file compatible with the Solidity metadata specification
177/// for the ink! smart contract.
178///
179/// Ref: <https://docs.soliditylang.org/en/latest/metadata.html>
180pub fn generate_metadata(
181    ink_project: &ink_metadata::sol::ContractMetadata,
182    abi: JsonAbi,
183    source: Source,
184    contract: Contract,
185    crate_metadata: &CrateMetadata,
186    image: Option<String>,
187) -> Result<SolidityContractMetadata> {
188    let sources = source_files(crate_metadata)?;
189    let (dev_doc, user_doc) = natspec::generate_natspec(ink_project, contract)?;
190    let metadata = SolidityContractMetadata {
191        compiler: Compiler {
192            hash: None,
193            version: source.compiler.to_string(),
194        },
195        language: source.language.to_string(),
196        output: Output {
197            abi,
198            dev_doc,
199            user_doc,
200        },
201        sources,
202        settings: Settings {
203            ink: InkSettings {
204                hash: source.hash,
205                image,
206                build_info: source.build_info,
207            },
208        },
209        version: 1,
210    };
211
212    Ok(metadata)
213}
214
215/// Get the path of the Solidity compatible contract metadata file.
216pub fn metadata_path(crate_metadata: &CrateMetadata) -> PathBuf {
217    let metadata_file = format!("{}.json", crate_metadata.contract_artifact_name);
218    crate_metadata.artifact_directory.join(metadata_file)
219}
220
221/// Writes a Solidity compatible metadata file.
222///
223/// Ref: <https://docs.soliditylang.org/en/latest/metadata.html>
224pub fn write_metadata<P>(metadata: &SolidityContractMetadata, path: P) -> Result<()>
225where
226    P: AsRef<Path>,
227{
228    let json = serde_json::to_string(metadata)?;
229    fs::write(path, json)?;
230
231    Ok(())
232}
233
234/// Reads the file and tries to parse it as instance of [`SolidityContractMetadata`].
235pub fn load_metadata<P>(metadata_path: P) -> Result<SolidityContractMetadata>
236where
237    P: AsRef<Path>,
238{
239    let path = metadata_path.as_ref();
240    let file = File::open(path)
241        .context(format!("Failed to open metadata file {}", path.display()))?;
242    serde_json::from_reader(file).context(format!(
243        "Failed to deserialize metadata file {}",
244        path.display()
245    ))
246}
247
248/// Returns compilation source file content, keys are relative file paths.
249fn source_files(crate_metadata: &CrateMetadata) -> Result<HashMap<String, SourceFile>> {
250    let mut source_files = HashMap::new();
251
252    // Adds `Cargo.toml` source.
253    let manifest_path = &crate_metadata.manifest_path;
254    let project_dir = manifest_path.absolute_directory()?;
255    let manifest_path_buf = PathBuf::from(manifest_path.clone());
256    let manifest_key = manifest_path_buf
257        .strip_prefix(&project_dir)
258        .unwrap_or_else(|_| &manifest_path_buf)
259        .to_string_lossy()
260        .into_owned();
261    let manifest_content = fs::read_to_string(&manifest_path_buf)?;
262    source_files.insert(
263        manifest_key,
264        SourceFile::new(
265            manifest_content,
266            crate_metadata.root_package.license.clone(),
267        ),
268    );
269
270    // Adds `lib.rs` source.
271    let lib_src_path = &crate_metadata
272        .root_package
273        .targets
274        .iter()
275        .find_map(|target| {
276            (target.kind == [TargetKind::Lib]).then_some(target.src_path.clone())
277        })
278        .context("Couldn't find `lib.rs` path")?;
279    let lib_src_content = fs::read_to_string(lib_src_path)?;
280    let lib_src_key = lib_src_path
281        .strip_prefix(&project_dir)
282        .unwrap_or_else(|_| lib_src_path)
283        .to_string();
284    source_files.insert(
285        lib_src_key,
286        SourceFile::new(lib_src_content, crate_metadata.root_package.license.clone()),
287    );
288
289    Ok(source_files)
290}