cast/cmd/
interface.rs

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