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