1use alloy_json_abi::{ContractObject, JsonAbi};
2use alloy_primitives::Address;
3use clap::Parser;
4use eyre::{Context, Result};
5use foundry_cli::{
6 opts::EtherscanOpts,
7 utils::{LoadConfig, fetch_abi_from_etherscan},
8};
9use foundry_common::{
10 ContractsByArtifact,
11 compile::{PathOrContractInfo, ProjectCompiler},
12 find_target_path, fs, shell,
13};
14use foundry_config::load_config;
15use itertools::Itertools;
16use serde_json::Value;
17use std::{
18 path::{Path, PathBuf},
19 str::FromStr,
20};
21
22#[derive(Clone, Debug, Parser)]
24pub struct InterfaceArgs {
25 contract: String,
30
31 #[arg(long, short)]
35 name: Option<String>,
36
37 #[arg(long, short, default_value = "^0.8.4", value_name = "VERSION")]
39 pragma: String,
40
41 #[arg(
45 short,
46 long,
47 value_hint = clap::ValueHint::FilePath,
48 value_name = "PATH",
49 )]
50 output: Option<PathBuf>,
51
52 #[command(flatten)]
53 etherscan: EtherscanOpts,
54}
55
56impl InterfaceArgs {
57 pub async fn run(self) -> Result<()> {
58 let Self { contract, name, pragma, output: output_location, etherscan } = self;
59
60 let abis = if Path::new(&contract).is_file()
62 && fs::read_to_string(&contract)
63 .ok()
64 .and_then(|content| serde_json::from_str::<Value>(&content).ok())
65 .is_some()
66 {
67 load_abi_from_file(&contract, name)?
68 } else {
69 match Address::from_str(&contract) {
70 Ok(address) => fetch_abi_from_etherscan(address, ðerscan.load_config()?).await?,
71 Err(_) => load_abi_from_artifact(&contract)?,
72 }
73 };
74
75 let interfaces = get_interfaces(abis)?;
77
78 let res = if shell::is_json() {
80 interfaces.iter().map(|iface| &iface.json_abi).format("\n").to_string()
82 } else {
83 format!(
85 "// SPDX-License-Identifier: UNLICENSED\n\
86 pragma solidity {pragma};\n\n\
87 {}",
88 interfaces.iter().map(|iface| &iface.source).format("\n")
89 )
90 };
91
92 if let Some(loc) = output_location {
93 if let Some(parent) = loc.parent() {
94 fs::create_dir_all(parent)?;
95 }
96 fs::write(&loc, res)?;
97 sh_println!("Saved interface at {}", loc.display())?;
98 } else {
99 sh_print!("{res}")?;
100 }
101
102 Ok(())
103 }
104}
105
106struct InterfaceSource {
107 json_abi: String,
108 source: String,
109}
110
111pub fn load_abi_from_file(path: &str, name: Option<String>) -> Result<Vec<(JsonAbi, String)>> {
113 let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?;
114 let obj: ContractObject = serde_json::from_str(&file)?;
115 let abi = obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))?;
116 let name = name.unwrap_or_else(|| "Interface".to_owned());
117 Ok(vec![(abi, name)])
118}
119
120fn load_abi_from_artifact(path_or_contract: &str) -> Result<Vec<(JsonAbi, String)>> {
122 let config = load_config()?;
123 let project = config.project()?;
124 let compiler = ProjectCompiler::new().quiet(true);
125
126 let contract = PathOrContractInfo::from_str(path_or_contract)?;
127
128 let target_path = find_target_path(&project, &contract)?;
129 let output = compiler.files([target_path.clone()]).compile(&project)?;
130
131 let contracts_by_artifact = ContractsByArtifact::from(output);
132
133 let maybe_abi = contracts_by_artifact
134 .find_abi_by_name_or_src_path(contract.name().unwrap_or(&target_path.to_string_lossy()));
135
136 let (abi, name) =
137 maybe_abi.as_ref().ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
138
139 Ok(vec![(abi.clone(), contract.name().unwrap_or(name).to_string())])
140}
141
142fn get_interfaces(abis: Vec<(JsonAbi, String)>) -> Result<Vec<InterfaceSource>> {
145 abis.into_iter()
146 .map(|(contract_abi, name)| {
147 let source = match foundry_cli::utils::abi_to_solidity(&contract_abi, &name) {
148 Ok(generated_source) => generated_source,
149 Err(e) => {
150 sh_warn!("Failed to format interface for {name}: {e}")?;
151 contract_abi.to_sol(&name, None)
152 }
153 };
154 Ok(InterfaceSource { json_abi: serde_json::to_string_pretty(&contract_abi)?, source })
155 })
156 .collect()
157}