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#[derive(Clone, Debug, Parser)]
25pub struct InterfaceArgs {
26 contract: String,
31
32 #[arg(long, short)]
36 name: Option<String>,
37
38 #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
40 pragma: String,
41
42 #[arg(
46 short,
47 long,
48 value_hint = clap::ValueHint::FilePath,
49 value_name = "PATH",
50 )]
51 output: Option<PathBuf>,
52
53 #[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 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, ðerscan.load_config()?).await?,
78 Err(_) => load_abi_from_artifact(&contract)?,
79 }
80 };
81
82 let config = flatten.then(|| ToSolConfig::new().one_contract(true));
84
85 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
133pub 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
142fn 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
164fn 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}