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