Skip to main content

cast/cmd/
interface.rs

1use alloy_json_abi::{ContractObject, JsonAbi, ToSolConfig};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::{Context, Result};
5use forge_fmt::FormatterConfig;
6use foundry_cli::{
7    json::print_json_object,
8    opts::EtherscanOpts,
9    utils::{LoadConfig, fetch_abi_from_etherscan},
10};
11use foundry_common::{
12    ContractsByArtifact,
13    compile::{PathOrContractInfo, ProjectCompiler, compile_abi_project},
14    find_target_path, fs, shell,
15};
16use foundry_config::load_config;
17use itertools::Itertools;
18use serde_json::Value;
19use std::{path::PathBuf, str::FromStr};
20
21/// CLI arguments for `cast interface`.
22#[derive(Clone, Debug, Parser)]
23pub struct InterfaceArgs {
24    /// The target contract, which can be one of:
25    /// - A file path to an ABI JSON file.
26    /// - A contract identifier in the form `<path>:<contractname>` or just `<contractname>`.
27    /// - An Ethereum address, for which the ABI will be fetched from Etherscan.
28    contract: String,
29
30    /// The name to use for the generated interface.
31    ///
32    /// Only relevant when retrieving the ABI from a file.
33    #[arg(long, short)]
34    name: Option<String>,
35
36    /// Solidity pragma version.
37    #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
38    pragma: String,
39
40    /// The path to the output file.
41    ///
42    /// If not specified, the interface will be output to stdout.
43    #[arg(
44        short,
45        long,
46        value_hint = clap::ValueHint::FilePath,
47        value_name = "PATH",
48    )]
49    output: Option<PathBuf>,
50
51    /// If set, generate all types in a single interface, inlining any inherited or library types.
52    ///
53    /// This can fail if there are structs with the same name in different interfaces.
54    #[arg(long)]
55    flatten: bool,
56
57    #[command(flatten)]
58    etherscan: EtherscanOpts,
59}
60
61impl InterfaceArgs {
62    pub async fn run(self) -> Result<()> {
63        let Self { contract, name, pragma, output, flatten, etherscan } = self;
64
65        // The target is an ABI file, an Ethereum address, or a local contract.
66        let is_json_file = fs::read_to_string(&contract)
67            .is_ok_and(|content| serde_json::from_str::<Value>(&content).is_ok());
68        let abis = if is_json_file {
69            vec![(load_abi_from_file(&contract)?, name.unwrap_or_else(|| "Interface".to_owned()))]
70        } else if let Ok(address) = Address::from_str(&contract) {
71            fetch_abi_from_etherscan(address, &etherscan.load_config()?).await?
72        } else {
73            vec![load_abi_from_artifact(&contract)?]
74        };
75
76        let config = flatten.then(|| ToSolConfig::new().one_contract(true));
77        let mut json_abis = Vec::with_capacity(abis.len());
78        let mut sources = Vec::with_capacity(abis.len());
79        for (abi, name) in &abis {
80            let source = abi.to_sol(name, config.clone());
81            sources.push(
82                match forge_fmt::format(&source, FormatterConfig::default()).into_result() {
83                    Ok(formatted) => formatted,
84                    Err(e) => {
85                        sh_warn!("Failed to format interface for {name}: {e}")?;
86                        source
87                    }
88                },
89            );
90            json_abis.push(serde_json::to_value(abi)?);
91        }
92        let source = format!(
93            "// SPDX-License-Identifier: UNLICENSED\n\
94             pragma solidity {pragma};\n\n\
95             {}",
96            sources.iter().format("\n")
97        );
98
99        if let Some(loc) = output {
100            let res =
101                if shell::is_json() { serde_json::to_string_pretty(&json_abis)? } else { source };
102            if let Some(parent) = loc.parent() {
103                fs::create_dir_all(parent)?;
104            }
105            fs::write(&loc, res)?;
106            sh_status!("Saved interface at {}", loc.display())?;
107        } else if shell::is_json() {
108            print_json_object(json_abis)?;
109        } else {
110            sh_print!("{source}")?;
111        }
112        Ok(())
113    }
114}
115
116/// Load the ABI from a file.
117pub(crate) fn load_abi_from_file(path: &str) -> Result<JsonAbi> {
118    let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?;
119    let obj: ContractObject = serde_json::from_str(&file)?;
120    obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))
121}
122
123/// Load the ABI and name from the artifact of a locally compiled contract.
124fn load_abi_from_artifact(path_or_contract: &str) -> Result<(JsonAbi, String)> {
125    let config = load_config()?;
126    let mut project = config.project()?;
127    project.no_artifacts = true;
128    let compiler = ProjectCompiler::new().quiet(true);
129
130    let contract = PathOrContractInfo::from_str(path_or_contract)?;
131    let target_path = find_target_path(&project, &contract)?;
132    let output = compile_abi_project(&mut project, compiler.files([target_path.clone()]))?;
133
134    let (abi, name) = ContractsByArtifact::from(output)
135        .find_abi_by_name_or_src_path(contract.name().unwrap_or(&target_path.to_string_lossy()))
136        .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
137    Ok((abi, contract.name().unwrap_or(&name).to_string()))
138}