cast/cmd/
interface.rs

1use alloy_json_abi::{ContractObject, JsonAbi};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::{Context, Result};
5use foundry_cli::{
6    opts::EtherscanOpts,
7    utils::{LoadConfig, fetch_abi_from_etherscan},
8};
9use foundry_common::{
10    ContractsByArtifact,
11    compile::{PathOrContractInfo, ProjectCompiler},
12    find_target_path, fs, shell,
13};
14use foundry_config::load_config;
15use itertools::Itertools;
16use serde_json::Value;
17use std::{
18    path::{Path, PathBuf},
19    str::FromStr,
20};
21
22/// CLI arguments for `cast interface`.
23#[derive(Clone, Debug, Parser)]
24pub struct InterfaceArgs {
25    /// The target contract, which can be one of:
26    /// - A file path to an ABI JSON file.
27    /// - A contract identifier in the form `<path>:<contractname>` or just `<contractname>`.
28    /// - An Ethereum address, for which the ABI will be fetched from Etherscan.
29    contract: String,
30
31    /// The name to use for the generated interface.
32    ///
33    /// Only relevant when retrieving the ABI from a file.
34    #[arg(long, short)]
35    name: Option<String>,
36
37    /// Solidity pragma version.
38    #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
39    pragma: String,
40
41    /// The path to the output file.
42    ///
43    /// If not specified, the interface will be output to stdout.
44    #[arg(
45        short,
46        long,
47        value_hint = clap::ValueHint::FilePath,
48        value_name = "PATH",
49    )]
50    output: Option<PathBuf>,
51
52    #[command(flatten)]
53    etherscan: EtherscanOpts,
54}
55
56impl InterfaceArgs {
57    pub async fn run(self) -> Result<()> {
58        let Self { contract, name, pragma, output: output_location, etherscan } = self;
59
60        // Determine if the target contract is an ABI file, a local contract or an Ethereum address.
61        let abis = if Path::new(&contract).is_file()
62            && fs::read_to_string(&contract)
63                .ok()
64                .and_then(|content| serde_json::from_str::<Value>(&content).ok())
65                .is_some()
66        {
67            load_abi_from_file(&contract, name)?
68        } else {
69            match Address::from_str(&contract) {
70                Ok(address) => fetch_abi_from_etherscan(address, &etherscan.load_config()?).await?,
71                Err(_) => load_abi_from_artifact(&contract)?,
72            }
73        };
74
75        // Retrieve interfaces from the array of ABIs.
76        let interfaces = get_interfaces(abis)?;
77
78        // Print result or write to file.
79        let res = if shell::is_json() {
80            // Format as JSON.
81            interfaces.iter().map(|iface| &iface.json_abi).format("\n").to_string()
82        } else {
83            // Format as Solidity.
84            format!(
85                "// SPDX-License-Identifier: UNLICENSED\n\
86                 pragma solidity {pragma};\n\n\
87                 {}",
88                interfaces.iter().map(|iface| &iface.source).format("\n")
89            )
90        };
91
92        if let Some(loc) = output_location {
93            if let Some(parent) = loc.parent() {
94                fs::create_dir_all(parent)?;
95            }
96            fs::write(&loc, res)?;
97            sh_println!("Saved interface at {}", loc.display())?;
98        } else {
99            sh_print!("{res}")?;
100        }
101
102        Ok(())
103    }
104}
105
106struct InterfaceSource {
107    json_abi: String,
108    source: String,
109}
110
111/// Load the ABI from a file.
112pub fn load_abi_from_file(path: &str, name: Option<String>) -> Result<Vec<(JsonAbi, String)>> {
113    let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?;
114    let obj: ContractObject = serde_json::from_str(&file)?;
115    let abi = obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))?;
116    let name = name.unwrap_or_else(|| "Interface".to_owned());
117    Ok(vec![(abi, name)])
118}
119
120/// Load the ABI from the artifact of a locally compiled contract.
121fn load_abi_from_artifact(path_or_contract: &str) -> Result<Vec<(JsonAbi, String)>> {
122    let config = load_config()?;
123    let project = config.project()?;
124    let compiler = ProjectCompiler::new().quiet(true);
125
126    let contract = PathOrContractInfo::from_str(path_or_contract)?;
127
128    let target_path = find_target_path(&project, &contract)?;
129    let output = compiler.files([target_path.clone()]).compile(&project)?;
130
131    let contracts_by_artifact = ContractsByArtifact::from(output);
132
133    let maybe_abi = contracts_by_artifact
134        .find_abi_by_name_or_src_path(contract.name().unwrap_or(&target_path.to_string_lossy()));
135
136    let (abi, name) =
137        maybe_abi.as_ref().ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
138
139    Ok(vec![(abi.clone(), contract.name().unwrap_or(name).to_string())])
140}
141
142/// Converts a vector of tuples containing the ABI and contract name into a vector of
143/// `InterfaceSource` objects.
144fn get_interfaces(abis: Vec<(JsonAbi, String)>) -> Result<Vec<InterfaceSource>> {
145    abis.into_iter()
146        .map(|(contract_abi, name)| {
147            let source = match foundry_cli::utils::abi_to_solidity(&contract_abi, &name) {
148                Ok(generated_source) => generated_source,
149                Err(e) => {
150                    sh_warn!("Failed to format interface for {name}: {e}")?;
151                    contract_abi.to_sol(&name, None)
152                }
153            };
154            Ok(InterfaceSource { json_abi: serde_json::to_string_pretty(&contract_abi)?, source })
155        })
156        .collect()
157}