1use alloy_json_abi::{ContractObject, InternalType, JsonAbi, Param, ToSolConfig};
2use alloy_primitives::{
3 Address,
4 map::{HashMap, HashSet},
5};
6use clap::Parser;
7use eyre::{Context, Result};
8use forge_fmt::FormatterConfig;
9use foundry_cli::{
10 json::print_json_object,
11 opts::EtherscanOpts,
12 utils::{LoadConfig, fetch_abi_from_etherscan},
13};
14use foundry_common::{
15 ContractsByArtifact,
16 compile::{PathOrContractInfo, ProjectCompiler, compile_abi_project},
17 find_target_path, fs, shell,
18};
19use foundry_config::load_config;
20use itertools::Itertools;
21use serde_json::Value;
22use std::{path::PathBuf, str::FromStr};
23
24#[derive(Clone, Debug, Parser)]
26pub struct InterfaceArgs {
27 contract: String,
33
34 #[arg(long, short)]
38 name: Option<String>,
39
40 #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
42 pragma: String,
43
44 #[arg(
48 short,
49 long,
50 value_hint = clap::ValueHint::FilePath,
51 value_name = "PATH",
52 )]
53 output: Option<PathBuf>,
54
55 #[arg(long)]
59 flatten: bool,
60
61 #[command(flatten)]
62 etherscan: EtherscanOpts,
63}
64
65impl InterfaceArgs {
66 pub async fn run(self) -> Result<()> {
67 let Self { contract, name, pragma, output, flatten, etherscan } = self;
68
69 let is_json_file = fs::read_to_string(&contract)
71 .is_ok_and(|content| serde_json::from_str::<Value>(&content).is_ok());
72 let abis = if is_json_file {
73 vec![(load_abi_from_file(&contract)?, name.unwrap_or_else(|| "Interface".to_owned()))]
74 } else if let Ok(address) = Address::from_str(&contract) {
75 fetch_abi_from_etherscan(address, ðerscan.load_config()?, true).await?
76 } else {
77 vec![load_abi_from_artifact(&contract)?]
78 };
79
80 let config = flatten.then(|| ToSolConfig::new().one_contract(true));
81 let mut json_abis = Vec::with_capacity(abis.len());
82 let mut sources = Vec::with_capacity(abis.len());
83 let multiple = abis.len() > 1;
84 let mut declarations = HashSet::default();
85 for (mut abi, mut name) in abis {
86 json_abis.push(serde_json::to_value(&abi)?);
87 abi.dedup();
88 if multiple {
89 let mut names = HashMap::<_, _>::default();
90 let unique = unique_declaration_name(&name, &mut declarations, Some(&abi));
91 names.insert(name, unique.clone());
92 name = unique;
93 visit_abi_types(&mut abi, &mut |ty| {
94 if let InternalType::Struct { contract: Some(contract), .. }
95 | InternalType::Enum { contract: Some(contract), .. }
96 | InternalType::Other { contract: Some(contract), .. } = ty
97 {
98 *contract = names
99 .entry(contract.clone())
100 .or_insert_with(|| {
101 if flatten {
102 contract.clone()
103 } else {
104 unique_declaration_name(contract, &mut declarations, None)
105 }
106 })
107 .clone();
108 }
109 });
110 }
111 let source = abi.to_sol(&name, config.clone());
112 sources.push(
113 match forge_fmt::format(&source, FormatterConfig::default()).into_result() {
114 Ok(formatted) => formatted,
115 Err(e) => {
116 sh_warn!("Failed to format interface for {name}: {e}")?;
117 source
118 }
119 },
120 );
121 }
122 let source = format!(
123 "// SPDX-License-Identifier: UNLICENSED\n\
124 pragma solidity {pragma};\n\n\
125 {}",
126 sources.iter().format("\n")
127 );
128
129 if let Some(loc) = output {
130 let res =
131 if shell::is_json() { serde_json::to_string_pretty(&json_abis)? } else { source };
132 if let Some(parent) = loc.parent() {
133 fs::create_dir_all(parent)?;
134 }
135 fs::write(&loc, res)?;
136 sh_status!("Saved interface at {}", loc.display())?;
137 } else if shell::is_json() {
138 print_json_object(json_abis)?;
139 } else {
140 sh_print!("{source}")?;
141 }
142 Ok(())
143 }
144}
145
146fn unique_declaration_name(
148 name: &str,
149 declarations: &mut HashSet<String>,
150 abi: Option<&JsonAbi>,
151) -> String {
152 let mut available = |candidate: &str| {
153 !abi.is_some_and(|abi| abi.functions.contains_key(candidate))
154 && declarations.insert(candidate.to_owned())
155 };
156 if available(name) {
157 return name.to_owned();
158 }
159 for suffix in 1.. {
160 let candidate = format!("{name}_{suffix}");
161 if available(&candidate) {
162 return candidate;
163 }
164 }
165 unreachable!()
166}
167
168fn visit_abi_types(abi: &mut JsonAbi, visit: &mut impl FnMut(&mut InternalType)) {
170 if let Some(constructor) = abi.constructor_mut() {
171 visit_param_types(&mut constructor.inputs, visit);
172 }
173 for function in abi.functions_mut() {
174 visit_param_types(&mut function.inputs, visit);
175 visit_param_types(&mut function.outputs, visit);
176 }
177 for error in abi.errors_mut() {
178 visit_param_types(&mut error.inputs, visit);
179 }
180 for event in abi.events_mut() {
181 for param in &mut event.inputs {
182 if let Some(ty) = &mut param.internal_type {
183 visit(ty);
184 }
185 visit_param_types(&mut param.components, visit);
186 }
187 }
188}
189
190fn visit_param_types(params: &mut [Param], visit: &mut impl FnMut(&mut InternalType)) {
191 for param in params {
192 if let Some(ty) = &mut param.internal_type {
193 visit(ty);
194 }
195 visit_param_types(&mut param.components, visit);
196 }
197}
198
199pub(crate) fn load_abi_from_file(path: &str) -> Result<JsonAbi> {
201 let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?;
202 let obj: ContractObject = serde_json::from_str(&file)?;
203 obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))
204}
205
206fn load_abi_from_artifact(path_or_contract: &str) -> Result<(JsonAbi, String)> {
208 let config = load_config()?;
209 let mut project = config.project()?;
210 project.no_artifacts = true;
211 let compiler = ProjectCompiler::new().quiet(true);
212
213 let contract = PathOrContractInfo::from_str(path_or_contract)?;
214 let target_path = find_target_path(&project, &contract)?;
215 let output = compile_abi_project(&mut project, compiler.files([target_path.clone()]))?;
216
217 let (abi, name) = ContractsByArtifact::from(output)
218 .find_abi_by_name_or_src_path(contract.name().unwrap_or(&target_path.to_string_lossy()))
219 .ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
220 Ok((abi, contract.name().unwrap_or(&name).to_string()))
221}