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#[derive(Clone, Debug, Parser)]
23pub struct InterfaceArgs {
24 contract: String,
29
30 #[arg(long, short)]
34 name: Option<String>,
35
36 #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
38 pragma: String,
39
40 #[arg(
44 short,
45 long,
46 value_hint = clap::ValueHint::FilePath,
47 value_name = "PATH",
48 )]
49 output: Option<PathBuf>,
50
51 #[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 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, ðerscan.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
116pub(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
123fn 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}