1- use alloy_chains:: Chain ;
2- use alloy_json_abi:: ContractObject ;
1+ use alloy_json_abi:: { ContractObject , JsonAbi } ;
32use alloy_primitives:: Address ;
43use clap:: Parser ;
54use eyre:: { Context , Result } ;
65use foundry_block_explorers:: Client ;
76use 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 } ;
1010use 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 ) ]
1519pub 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
4855impl 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}
0 commit comments