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