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