Skip to content

Commit fbdd40d

Browse files
leovctmattsse
andauthored
feat(cast interface): allow retrieving abi from contract name (#8585)
* feat(`cast interface`): allow retrieving abi from contract name * fix: cast tests * test: add test that fetches weth interface from etherscan * Revert "fix: cast tests" This reverts commit c0ec3e9. * fix: cast tests on macos --------- Co-authored-by: Matthias Seitz <[email protected]>
1 parent e9c8bf5 commit fbdd40d

File tree

3 files changed

+140
-70
lines changed

3 files changed

+140
-70
lines changed

crates/cast/bin/cmd/interface.rs

Lines changed: 94 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
1-
use alloy_chains::Chain;
2-
use alloy_json_abi::ContractObject;
1+
use alloy_json_abi::{ContractObject, JsonAbi};
32
use alloy_primitives::Address;
43
use clap::Parser;
54
use eyre::{Context, Result};
65
use foundry_block_explorers::Client;
76
use foundry_cli::opts::EtherscanOpts;
8-
use foundry_common::fs;
9-
use foundry_config::Config;
7+
use foundry_common::{compile::ProjectCompiler, fs};
8+
use foundry_compilers::{info::ContractInfo, utils::canonicalize};
9+
use foundry_config::{find_project_root_path, load_config_with_root, Config};
1010
use itertools::Itertools;
11-
use std::path::{Path, PathBuf};
11+
use serde_json::Value;
12+
use std::{
13+
path::{Path, PathBuf},
14+
str::FromStr,
15+
};
1216

1317
/// CLI arguments for `cast interface`.
1418
#[derive(Clone, Debug, Parser)]
1519
pub struct InterfaceArgs {
16-
/// The contract address, or the path to an ABI file.
17-
///
18-
/// If an address is specified, then the ABI is fetched from Etherscan.
19-
path_or_address: String,
20+
/// The target contract, which can be one of:
21+
/// - A file path to an ABI JSON file.
22+
/// - A contract identifier in the form `<path>:<contractname>` or just `<contractname>`.
23+
/// - An Ethereum address, for which the ABI will be fetched from Etherscan.
24+
contract: String,
2025

2126
/// The name to use for the generated interface.
27+
///
28+
/// Only relevant when retrieving the ABI from a file.
2229
#[arg(long, short)]
2330
name: Option<String>,
2431

@@ -47,61 +54,32 @@ pub struct InterfaceArgs {
4754

4855
impl InterfaceArgs {
4956
pub async fn run(self) -> Result<()> {
50-
let Self { path_or_address, name, pragma, output: output_location, etherscan, json } = self;
51-
let source = if Path::new(&path_or_address).exists() {
52-
AbiPath::Local { path: path_or_address, name }
57+
let Self { contract, name, pragma, output: output_location, etherscan, json } = self;
58+
59+
// Determine if the target contract is an ABI file, a local contract or an Ethereum address.
60+
let abis = if Path::new(&contract).is_file() &&
61+
fs::read_to_string(&contract)
62+
.ok()
63+
.and_then(|content| serde_json::from_str::<Value>(&content).ok())
64+
.is_some()
65+
{
66+
load_abi_from_file(&contract, name)?
5367
} else {
54-
let config = Config::from(&etherscan);
55-
let chain = config.chain.unwrap_or_default();
56-
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
57-
AbiPath::Etherscan {
58-
chain,
59-
api_key,
60-
address: path_or_address.parse().wrap_err("invalid path or address")?,
68+
match Address::from_str(&contract) {
69+
Ok(address) => fetch_abi_from_etherscan(address, &etherscan).await?,
70+
Err(_) => load_abi_from_artifact(&contract)?,
6171
}
6272
};
6373

64-
let items = match source {
65-
AbiPath::Local { path, name } => {
66-
let file = std::fs::read_to_string(&path).wrap_err("unable to read abi file")?;
67-
let obj: ContractObject = serde_json::from_str(&file)?;
68-
let abi =
69-
obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))?;
70-
let name = name.unwrap_or_else(|| "Interface".to_owned());
71-
vec![(abi, name)]
72-
}
73-
AbiPath::Etherscan { address, chain, api_key } => {
74-
let client = Client::new(chain, api_key)?;
75-
let source = client.contract_source_code(address).await?;
76-
source
77-
.items
78-
.into_iter()
79-
.map(|item| Ok((item.abi()?, item.contract_name)))
80-
.collect::<Result<Vec<_>>>()?
81-
}
82-
};
74+
// Retrieve interfaces from the array of ABIs.
75+
let interfaces = get_interfaces(abis)?;
8376

84-
let interfaces = items
85-
.into_iter()
86-
.map(|(contract_abi, name)| {
87-
let source = match foundry_cli::utils::abi_to_solidity(&contract_abi, &name) {
88-
Ok(generated_source) => generated_source,
89-
Err(e) => {
90-
warn!("Failed to format interface for {name}: {e}");
91-
contract_abi.to_sol(&name, None)
92-
}
93-
};
94-
Ok(InterfaceSource {
95-
json_abi: serde_json::to_string_pretty(&contract_abi)?,
96-
source,
97-
})
98-
})
99-
.collect::<Result<Vec<InterfaceSource>>>()?;
100-
101-
// put it all together
77+
// Print result or write to file.
10278
let res = if json {
79+
// Format as JSON.
10380
interfaces.iter().map(|iface| &iface.json_abi).format("\n").to_string()
10481
} else {
82+
// Format as Solidity.
10583
format!(
10684
"// SPDX-License-Identifier: UNLICENSED\n\
10785
pragma solidity {pragma};\n\n\
@@ -110,7 +88,6 @@ impl InterfaceArgs {
11088
)
11189
};
11290

113-
// print or write to file
11491
if let Some(loc) = output_location {
11592
if let Some(parent) = loc.parent() {
11693
fs::create_dir_all(parent)?;
@@ -120,6 +97,7 @@ impl InterfaceArgs {
12097
} else {
12198
print!("{res}");
12299
}
100+
123101
Ok(())
124102
}
125103
}
@@ -129,9 +107,63 @@ struct InterfaceSource {
129107
source: String,
130108
}
131109

132-
// Local is a path to the directory containing the ABI files
133-
// In case of etherscan, ABI is fetched from the address on the chain
134-
enum AbiPath {
135-
Local { path: String, name: Option<String> },
136-
Etherscan { address: Address, chain: Chain, api_key: String },
110+
/// Load the ABI from a file.
111+
fn load_abi_from_file(path: &str, name: Option<String>) -> Result<Vec<(JsonAbi, String)>> {
112+
let file = std::fs::read_to_string(path).wrap_err("unable to read abi file")?;
113+
let obj: ContractObject = serde_json::from_str(&file)?;
114+
let abi = obj.abi.ok_or_else(|| eyre::eyre!("could not find ABI in file {path}"))?;
115+
let name = name.unwrap_or_else(|| "Interface".to_owned());
116+
Ok(vec![(abi, name)])
117+
}
118+
119+
/// Load the ABI from the artifact of a locally compiled contract.
120+
fn load_abi_from_artifact(path_or_contract: &str) -> Result<Vec<(JsonAbi, String)>> {
121+
let root = find_project_root_path(None)?;
122+
let config = load_config_with_root(Some(root));
123+
let project = config.project()?;
124+
let compiler = ProjectCompiler::new().quiet(true);
125+
126+
let contract = ContractInfo::new(path_or_contract);
127+
let target_path = if let Some(path) = &contract.path {
128+
canonicalize(project.root().join(path))?
129+
} else {
130+
project.find_contract_path(&contract.name)?
131+
};
132+
let mut output = compiler.files([target_path.clone()]).compile(&project)?;
133+
134+
let artifact = output.remove(&target_path, &contract.name).ok_or_else(|| {
135+
eyre::eyre!("Could not find artifact `{contract}` in the compiled artifacts")
136+
})?;
137+
let abi = artifact.abi.as_ref().ok_or_else(|| eyre::eyre!("Failed to fetch lossless ABI"))?;
138+
Ok(vec![(abi.clone(), contract.name)])
139+
}
140+
141+
/// Fetches the ABI of a contract from Etherscan.
142+
async fn fetch_abi_from_etherscan(
143+
address: Address,
144+
etherscan: &EtherscanOpts,
145+
) -> Result<Vec<(JsonAbi, String)>> {
146+
let config = Config::from(etherscan);
147+
let chain = config.chain.unwrap_or_default();
148+
let api_key = config.get_etherscan_api_key(Some(chain)).unwrap_or_default();
149+
let client = Client::new(chain, api_key)?;
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+
154+
/// Converts a vector of tuples containing the ABI and contract name into a vector of
155+
/// `InterfaceSource` objects.
156+
fn 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+
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()
137169
}

crates/cast/bin/cmd/logs.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -391,29 +391,32 @@ mod tests {
391391
)
392392
.err()
393393
.unwrap()
394-
.to_string();
394+
.to_string()
395+
.to_lowercase();
395396

396-
assert_eq!(err, "parser error:\n1234\n^\nInvalid string length");
397+
assert_eq!(err, "parser error:\n1234\n^\ninvalid string length");
397398
}
398399

399400
#[test]
400401
fn test_build_filter_with_invalid_sig_or_topic() {
401402
let err = build_filter(None, None, None, Some("asdasdasd".to_string()), vec![])
402403
.err()
403404
.unwrap()
404-
.to_string();
405+
.to_string()
406+
.to_lowercase();
405407

406-
assert_eq!(err, "Odd number of digits");
408+
assert_eq!(err, "odd number of digits");
407409
}
408410

409411
#[test]
410412
fn test_build_filter_with_invalid_sig_or_topic_hex() {
411413
let err = build_filter(None, None, None, Some(ADDRESS.to_string()), vec![])
412414
.err()
413415
.unwrap()
414-
.to_string();
416+
.to_string()
417+
.to_lowercase();
415418

416-
assert_eq!(err, "Invalid string length");
419+
assert_eq!(err, "invalid string length");
417420
}
418421

419422
#[test]
@@ -427,8 +430,9 @@ mod tests {
427430
)
428431
.err()
429432
.unwrap()
430-
.to_string();
433+
.to_string()
434+
.to_lowercase();
431435

432-
assert_eq!(err, "Invalid string length");
436+
assert_eq!(err, "invalid string length");
433437
}
434438
}

crates/cast/tests/cli/main.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -910,6 +910,40 @@ interface Interface {
910910
assert_eq!(output.trim(), s);
911911
});
912912

913+
// tests that fetches WETH interface from etherscan
914+
// <https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2>
915+
casttest!(fetch_weth_interface_from_etherscan, |_prj, cmd| {
916+
let weth_address = "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2";
917+
let api_key = "ZUB97R31KSYX7NYVW6224Q6EYY6U56H591";
918+
cmd.args(["interface", "--etherscan-api-key", api_key, weth_address]);
919+
let output = cmd.stdout_lossy();
920+
921+
let weth_interface = r#"// SPDX-License-Identifier: UNLICENSED
922+
pragma solidity ^0.8.4;
923+
924+
interface WETH9 {
925+
event Approval(address indexed src, address indexed guy, uint256 wad);
926+
event Deposit(address indexed dst, uint256 wad);
927+
event Transfer(address indexed src, address indexed dst, uint256 wad);
928+
event Withdrawal(address indexed src, uint256 wad);
929+
930+
fallback() external payable;
931+
932+
function allowance(address, address) external view returns (uint256);
933+
function approve(address guy, uint256 wad) external returns (bool);
934+
function balanceOf(address) external view returns (uint256);
935+
function decimals() external view returns (uint8);
936+
function deposit() external payable;
937+
function name() external view returns (string memory);
938+
function symbol() external view returns (string memory);
939+
function totalSupply() external view returns (uint256);
940+
function transfer(address dst, uint256 wad) external returns (bool);
941+
function transferFrom(address src, address dst, uint256 wad) external returns (bool);
942+
function withdraw(uint256 wad) external;
943+
}"#;
944+
assert_eq!(output.trim(), weth_interface);
945+
});
946+
913947
const ENS_NAME: &str = "emo.eth";
914948
const ENS_NAMEHASH: B256 =
915949
b256!("0a21aaf2f6414aa664deb341d1114351fdb023cad07bf53b28e57c26db681910");

0 commit comments

Comments
 (0)