diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 4a667e06..c9652f3d 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -76,7 +76,10 @@ pub struct Context { human_readable: bool, /// The contract name as an identifier. - contract_name: Ident, + contract_ident: Ident, + + /// The contract name as string + contract_name: String, /// Manually specified method aliases. method_aliases: BTreeMap, @@ -91,9 +94,9 @@ pub struct Context { impl Context { /// Expands the whole rust contract pub fn expand(&self) -> Result { - let name = &self.contract_name; + let name = &self.contract_ident; let name_mod = - util::ident(&format!("{}_mod", self.contract_name.to_string().to_lowercase())); + util::ident(&format!("{}_mod", self.contract_ident.to_string().to_lowercase())); let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase())); @@ -190,7 +193,7 @@ impl Context { .unwrap_or_default() }; - let contract_name = util::ident(&args.contract_name); + let contract_ident = util::ident(&args.contract_name); // NOTE: We only check for duplicate signatures here, since if there are // duplicate aliases, the compiler will produce a warning because a @@ -226,13 +229,19 @@ impl Context { abi_str: Literal::string(&abi_str), abi_parser, internal_structs, - contract_name, + contract_ident, + contract_name: args.contract_name, method_aliases, event_derives, event_aliases, }) } + /// The initial name fo the contract + pub(crate) fn contract_name(&self) -> &str { + &self.contract_name + } + /// The internal abi struct mapping table pub fn internal_structs(&self) -> &InternalStructs { &self.internal_structs diff --git a/ethers-contract/ethers-contract-abigen/src/contract/common.rs b/ethers-contract/ethers-contract-abigen/src/contract/common.rs index 05d787ff..07a008e7 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/common.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/common.rs @@ -31,7 +31,7 @@ pub(crate) fn imports(name: &str) -> TokenStream { /// Generates the static `Abi` constants and the contract struct pub(crate) fn struct_declaration(cx: &Context, abi_name: &proc_macro2::Ident) -> TokenStream { - let name = &cx.contract_name; + let name = &cx.contract_ident; let abi = &cx.abi_str; let ethers_core = ethers_core_crate(); diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index c0d29bc2..96946af8 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -102,7 +102,7 @@ impl Context { /// The name ident of the events enum fn expand_event_enum_name(&self) -> Ident { - util::ident(&format!("{}Events", self.contract_name)) + util::ident(&format!("{}Events", self.contract_ident)) } /// Expands the `events` function that bundles all declared events of this contract diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index 42e34c28..212ebdb8 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -161,7 +161,7 @@ impl Context { /// The name ident of the calls enum fn expand_calls_enum_name(&self) -> Ident { - util::ident(&format!("{}Calls", self.contract_name)) + util::ident(&format!("{}Calls", self.contract_ident)) } /// Expands to the `name : type` pairs of the function's inputs diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index 11e439e7..ac83f68d 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -26,6 +26,7 @@ pub use ethers_core::types::Address; pub use source::Source; pub use util::parse_address; +use crate::contract::ExpandedContract; use eyre::Result; use inflector::Inflector; use proc_macro2::TokenStream; @@ -87,7 +88,7 @@ impl Abigen { }) } - /// Attemtps to load a new builder from an ABI JSON file at the specific + /// Attempts to load a new builder from an ABI JSON file at the specific /// path. pub fn from_file(path: impl AsRef) -> Result { let name = path @@ -153,8 +154,15 @@ impl Abigen { pub fn generate(self) -> Result { let rustfmt = self.rustfmt; let name = self.contract_name.clone(); - let tokens = Context::from_abigen(self)?.expand()?.into_tokens(); - Ok(ContractBindings { tokens, rustfmt, name }) + let (expanded, _) = self.expand()?; + Ok(ContractBindings { tokens: expanded.into_tokens(), rustfmt, name }) + } + + /// Expands the `Abigen` and returns the [`ExpandedContract`] that holds all tokens and the + /// [`Context`] that holds the state used during expansion. + pub fn expand(self) -> Result<(ExpandedContract, Context)> { + let ctx = Context::from_abigen(self)?; + Ok((ctx.expand()?, ctx)) } } diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs index ad72c151..a77b71a8 100644 --- a/ethers-contract/ethers-contract-abigen/src/multi.rs +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -2,9 +2,190 @@ use eyre::Result; use inflector::Inflector; -use std::{collections::BTreeMap, fs, io::Write, path::Path}; +use proc_macro2::TokenStream; +use quote::quote; +use std::{ + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, + fs, + io::Write, + path::Path, +}; -use crate::{util, Abigen, ContractBindings}; +use crate::{util, Abigen, Context, ContractBindings, ExpandedContract}; + +/// Represents a collection of [`Abigen::expand()`] +pub struct MultiExpansion { + // all expanded contracts collection from [`Abigen::expand()`] + contracts: Vec<(ExpandedContract, Context)>, +} + +impl MultiExpansion { + /// Create a new instance that wraps the given `contracts` + pub fn new(contracts: Vec<(ExpandedContract, Context)>) -> Self { + Self { contracts } + } + + /// Create a new instance by expanding all `Abigen` elements the given iterator yields + pub fn from_abigen(abigens: impl IntoIterator) -> Result { + let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::>()?; + Ok(Self::new(contracts)) + } + + /// Expands all contracts into a single `TokenStream` + /// + /// This will deduplicate types into a separate `mod __shared_types` module, if any. + pub fn expand_inplace(self) -> TokenStream { + self.expand().expand_inplace() + } + + /// Expands all contracts into separated [`TokenStream`]s + /// + /// If there was type deduplication, this returns a list of [`TokenStream`] containing the type + /// definitions of all shared types. + pub fn expand(self) -> MultiExpansionResult { + let mut expansions = self.contracts; + let mut shared_types = Vec::new(); + // this keeps track of those contracts that need to be updated after a struct was + // extracted from the contract's module and moved to the shared module + let mut dirty_contracts = HashSet::new(); + + // merge all types if more than 1 contract + if expansions.len() > 1 { + // check for type conflicts across all contracts + let mut conflicts: HashMap> = HashMap::new(); + for (idx, (_, ctx)) in expansions.iter().enumerate() { + for type_identifier in ctx.internal_structs().rust_type_names().keys() { + conflicts + .entry(type_identifier.clone()) + .or_insert_with(|| Vec::with_capacity(1)) + .push(idx); + } + } + + // resolve type conflicts + for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) { + // extract the shared type once + shared_types.push( + expansions[contracts[0]] + .1 + .struct_definition(id) + .expect("struct def succeeded previously"), + ); + + // remove the shared type from the contract's bindings + for contract in contracts.iter().copied() { + expansions[contract].1.remove_struct(id); + dirty_contracts.insert(contract); + } + } + + // regenerate all struct definitions that were hit + for contract in dirty_contracts.iter().copied() { + let (expanded, ctx) = &mut expansions[contract]; + expanded.abi_structs = ctx.abi_structs().expect("struct def succeeded previously"); + } + } + + MultiExpansionResult { contracts: expansions, dirty_contracts, shared_types } + } +} + +/// Represents an intermediary result of [`MultiExpansion::expand()`] +pub struct MultiExpansionResult { + contracts: Vec<(ExpandedContract, Context)>, + /// contains the indices of contracts with structs that need to be updated + dirty_contracts: HashSet, + /// all type definitions of types that are shared by multiple contracts + shared_types: Vec, +} + +impl MultiExpansionResult { + /// Expands all contracts into a single [`TokenStream`] + pub fn expand_inplace(mut self) -> TokenStream { + let mut tokens = TokenStream::new(); + + let shared_types_module = quote! {__shared_types}; + // the import path to the shared types + let shared_path = quote!( + pub use super::#shared_types_module::*; + ); + self.add_shared_import_path(shared_path); + + let Self { contracts, shared_types, .. } = self; + + tokens.extend(quote! { + pub mod #shared_types_module { + #( #shared_types )* + } + }); + + tokens.extend(contracts.into_iter().map(|(exp, _)| exp.into_tokens())); + + tokens + } + + /// Sets the path to the shared types module according to the value of `single_file` + /// + /// If `single_file` then it's expected that types will be written to `shared_types.rs` + fn set_shared_import_path(&mut self, single_file: bool) { + let shared_path = if single_file { + quote!( + pub use super::__shared_types::*; + ) + } else { + quote!( + pub use super::super::shared_types::*; + ) + }; + self.add_shared_import_path(shared_path); + } + + /// adds the `shared` import path to every `dirty` contract + fn add_shared_import_path(&mut self, shared: TokenStream) { + for contract in self.dirty_contracts.iter().copied() { + let (expanded, ..) = &mut self.contracts[contract]; + expanded.imports.extend(shared.clone()); + } + } + + /// Converts this result into [`MultiBindingsInner`] + fn into_bindings(mut self, single_file: bool, rustfmt: bool) -> MultiBindingsInner { + self.set_shared_import_path(single_file); + let Self { contracts, shared_types, .. } = self; + let bindings = contracts + .into_iter() + .map(|(expanded, ctx)| ContractBindings { + tokens: expanded.into_tokens(), + rustfmt, + name: ctx.contract_name().to_string(), + }) + .map(|v| (v.name.clone(), v)) + .collect(); + + let shared_types = if !shared_types.is_empty() { + let shared_types = if single_file { + quote! { + pub mod __shared_types { + #( #shared_types )* + } + } + } else { + quote! { + #( #shared_types )* + } + }; + Some(ContractBindings { + tokens: shared_types, + rustfmt, + name: "shared_types".to_string(), + }) + } else { + None + }; + + MultiBindingsInner { bindings, shared_types } + } +} /// Collects Abigen structs for a series of contracts, pending generation of /// the contract bindings. @@ -86,16 +267,11 @@ impl MultiAbigen { /// Build the contract bindings and prepare for writing pub fn build(self) -> Result { - let bindings = self - .abigens - .into_iter() - .map(|v| v.generate()) - .collect::>>()? - .into_iter() - .map(|v| (v.name.clone(), v)) - .collect(); - - Ok(MultiBindings { bindings }) + let rustfmt = self.abigens.iter().any(|gen| gen.rustfmt); + Ok(MultiBindings { + expansion: MultiExpansion::from_abigen(self.abigens)?.expand(), + rustfmt, + }) } } @@ -126,12 +302,188 @@ impl MultiAbigen { /// * CI will fail if the generated code is out of date (if `abigen!` or the contract's ABI itself /// changed) pub struct MultiBindings { + expansion: MultiExpansionResult, + rustfmt: bool, +} + +impl MultiBindings { + fn into_inner(self, single_file: bool) -> MultiBindingsInner { + self.expansion.into_bindings(single_file, self.rustfmt) + } + + /// Generates all the bindings and writes them to the given module + /// + /// # Example + /// + /// Read all json abi files from the `./abi` directory + /// ```text + /// abi + /// ├── ERC20.json + /// ├── Contract1.json + /// ├── Contract2.json + /// ... + /// ``` + /// + /// and write them to the `./src/contracts` location as + /// + /// ```text + /// src/contracts + /// ├── mod.rs + /// ├── er20.rs + /// ├── contract1.rs + /// ├── contract2.rs + /// ... + /// ``` + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.write_to_module("./src/contracts", false).unwrap(); + /// ``` + pub fn write_to_module(self, module: impl AsRef, single_file: bool) -> Result<()> { + self.into_inner(single_file).write_to_module(module, single_file) + } + + /// Generates all the bindings and writes a library crate containing them + /// to the provided path + /// + /// # Example + /// + /// Read all json abi files from the `./abi` directory + /// ```text + /// abi + /// ├── ERC20.json + /// ├── Contract1.json + /// ├── Contract2.json + /// ├── Contract3/ + /// ├── Contract3.json + /// ... + /// ``` + /// + /// and write them to the `./bindings` location as + /// + /// ```text + /// bindings + /// ├── Cargo.toml + /// ├── src/ + /// ├── lib.rs + /// ├── er20.rs + /// ├── contract1.rs + /// ├── contract2.rs + /// ... + /// ``` + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.write_to_crate( + /// "my-crate", "0.0.5", "./bindings", false + /// ).unwrap(); + /// ``` + pub fn write_to_crate( + self, + name: impl AsRef, + version: impl AsRef, + lib: impl AsRef, + single_file: bool, + ) -> Result<()> { + self.into_inner(single_file).write_to_crate(name, version, lib, single_file) + } + + /// This ensures that the already generated bindings crate matches the + /// output of a fresh new run. Run this in a rust test, to get notified in + /// CI if the newly generated bindings deviate from the already generated + /// ones, and it's time to generate them again. This could happen if the + /// ABI of a contract or the output that `ethers` generates changed. + /// + /// If this functions is run within a test during CI and fails, then it's + /// time to update all bindings. + /// + /// # Returns + /// + /// `Ok(())` if the freshly generated bindings match with the + /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report` + /// with more information. + /// + /// # Example + /// + /// Check that the generated files are up to date + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// #[test] + /// fn generated_bindings_are_fresh() { + /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); + /// let abi_dir = project_root.join("abi"); + /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.ensure_consistent_crate( + /// "my-crate", "0.0.1", project_root.join("src/contracts"), false + /// ).expect("inconsistent bindings"); + /// } + /// ``` + pub fn ensure_consistent_crate( + self, + name: impl AsRef, + version: impl AsRef, + crate_path: impl AsRef, + single_file: bool, + ) -> Result<()> { + self.into_inner(single_file).ensure_consistent_crate(name, version, crate_path, single_file) + } + + /// This ensures that the already generated bindings module matches the + /// output of a fresh new run. Run this in a rust test, to get notified in + /// CI if the newly generated bindings deviate from the already generated + /// ones, and it's time to generate them again. This could happen if the + /// ABI of a contract or the output that `ethers` generates changed. + /// + /// If this functions is run within a test during CI and fails, then it's + /// time to update all bindings. + /// + /// # Returns + /// + /// `Ok(())` if the freshly generated bindings match with the + /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report` + /// with more information. + /// + /// # Example + /// + /// Check that the generated files are up to date + /// + /// ```no_run + /// # use ethers_contract_abigen::MultiAbigen; + /// #[test] + /// fn generated_bindings_are_fresh() { + /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); + /// let abi_dir = project_root.join("abi"); + /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); + /// let bindings = gen.build().unwrap(); + /// bindings.ensure_consistent_module( + /// project_root.join("src/contracts"), false + /// ).expect("inconsistent bindings"); + /// } + /// ``` + pub fn ensure_consistent_module( + self, + module: impl AsRef, + single_file: bool, + ) -> Result<()> { + self.into_inner(single_file).ensure_consistent_module(module, single_file) + } +} + +struct MultiBindingsInner { /// Abigen objects to be written bindings: BTreeMap, + /// contains the content of the shared types if any + shared_types: Option, } // deref allows for inspection without modification -impl std::ops::Deref for MultiBindings { +impl std::ops::Deref for MultiBindingsInner { type Target = BTreeMap; fn deref(&self) -> &Self::Target { @@ -139,8 +491,8 @@ impl std::ops::Deref for MultiBindings { } } -impl MultiBindings { - /// Generat the contents of the `Cargo.toml` file for a lib +impl MultiBindingsInner { + /// Generate the contents of the `Cargo.toml` file for a lib fn generate_cargo_toml( &self, name: impl AsRef, @@ -185,34 +537,14 @@ serde_json = "1.0.79" Ok(()) } - /// Generate the shared prefix of the `lib.rs` or `mod.rs` - fn generate_prefix( - &self, - mut buf: impl Write, - is_crate: bool, - single_file: bool, - ) -> Result<()> { - writeln!(buf, "#![allow(clippy::all)]")?; - writeln!( - buf, - "//! This {} contains abigen! generated bindings for solidity contracts.", - if is_crate { "lib" } else { "module" } - )?; - writeln!(buf, "//! This is autogenerated code.")?; - writeln!(buf, "//! Do not manually edit these files.")?; - writeln!( - buf, - "//! {} may be overwritten by the codegen system at any time.", - if single_file && !is_crate { "This file" } else { "These files" } - )?; - - Ok(()) - } - /// Append module declarations to the `lib.rs` or `mod.rs` fn append_module_names(&self, mut buf: impl Write) -> Result<()> { - // sorting here not necessary, as btreemap keys are ordered - for module in self.bindings.keys().map(|name| format!("pub mod {};", name.to_snake_case())) + let mut mod_names: BTreeSet<_> = self.bindings.keys().collect(); + if let Some(ref shared) = self.shared_types { + mod_names.insert(&shared.name); + } + + for module in mod_names.into_iter().map(|name| format!("pub mod {};", name.to_snake_case())) { writeln!(buf, "{}", module)?; } @@ -223,14 +555,17 @@ serde_json = "1.0.79" /// Generate the contents of `lib.rs` or `mod.rs` fn generate_super_contents(&self, is_crate: bool, single_file: bool) -> Result> { let mut contents = vec![]; - self.generate_prefix(&mut contents, is_crate, single_file)?; + generate_prefix(&mut contents, is_crate, single_file)?; - if !single_file { - self.append_module_names(&mut contents)?; - } else { + if single_file { + if let Some(ref shared) = self.shared_types { + shared.write(&mut contents)?; + } for binding in self.bindings.values() { binding.write(&mut contents)?; } + } else { + self.append_module_names(&mut contents)?; } Ok(contents) @@ -246,43 +581,16 @@ serde_json = "1.0.79" /// Write all contract bindings to their respective files fn write_bindings(&self, path: &Path) -> Result<()> { + if let Some(ref shared) = self.shared_types { + shared.write_module_in_dir(path)?; + } for binding in self.bindings.values() { binding.write_module_in_dir(path)?; } Ok(()) } - /// Generates all the bindings and writes them to the given module - /// - /// # Example - /// - /// Read all json abi files from the `./abi` directory - /// ```text - /// abi - /// ├── ERC20.json - /// ├── Contract1.json - /// ├── Contract2.json - /// ... - /// ``` - /// - /// and write them to the `./src/contracts` location as - /// - /// ```text - /// src/contracts - /// ├── mod.rs - /// ├── er20.rs - /// ├── contract1.rs - /// ├── contract2.rs - /// ... - /// ``` - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); - /// let bindings = gen.build().unwrap(); - /// bindings.write_to_module("./src/contracts", false).unwrap(); - /// ``` - pub fn write_to_module(self, module: impl AsRef, single_file: bool) -> Result<()> { + fn write_to_module(self, module: impl AsRef, single_file: bool) -> Result<()> { let module = module.as_ref(); fs::create_dir_all(module)?; @@ -294,44 +602,7 @@ serde_json = "1.0.79" Ok(()) } - /// Generates all the bindings and writes a library crate containing them - /// to the provided path - /// - /// # Example - /// - /// Read all json abi files from the `./abi` directory - /// ```text - /// abi - /// ├── ERC20.json - /// ├── Contract1.json - /// ├── Contract2.json - /// ├── Contract3/ - /// ├── Contract3.json - /// ... - /// ``` - /// - /// and write them to the `./bindings` location as - /// - /// ```text - /// bindings - /// ├── Cargo.toml - /// ├── src/ - /// ├── lib.rs - /// ├── er20.rs - /// ├── contract1.rs - /// ├── contract2.rs - /// ... - /// ``` - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// let gen = MultiAbigen::from_json_files("./abi").unwrap(); - /// let bindings = gen.build().unwrap(); - /// bindings.write_to_crate( - /// "my-crate", "0.0.5", "./bindings", false - /// ).unwrap(); - /// ``` - pub fn write_to_crate( + fn write_to_crate( self, name: impl AsRef, version: impl AsRef, @@ -379,39 +650,7 @@ serde_json = "1.0.79" Ok(()) } - /// This ensures that the already generated bindings crate matches the - /// output of a fresh new run. Run this in a rust test, to get notified in - /// CI if the newly generated bindings deviate from the already generated - /// ones, and it's time to generate them again. This could happen if the - /// ABI of a contract or the output that `ethers` generates changed. - /// - /// If this functions is run within a test during CI and fails, then it's - /// time to update all bindings. - /// - /// # Returns - /// - /// `Ok(())` if the freshly generated bindings match with the - /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report` - /// with more information. - /// - /// # Example - /// - /// Check that the generated files are up to date - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// #[test] - /// fn generated_bindings_are_fresh() { - /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); - /// let abi_dir = project_root.join("abi"); - /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); - /// let bindings = gen.build().unwrap(); - /// bindings.ensure_consistent_crate( - /// "my-crate", "0.0.1", project_root.join("src/contracts"), false - /// ).expect("inconsistent bindings"); - /// } - /// ``` - pub fn ensure_consistent_crate( + fn ensure_consistent_crate( self, name: impl AsRef, version: impl AsRef, @@ -428,48 +667,30 @@ serde_json = "1.0.79" Ok(()) } - /// This ensures that the already generated bindings module matches the - /// output of a fresh new run. Run this in a rust test, to get notified in - /// CI if the newly generated bindings deviate from the already generated - /// ones, and it's time to generate them again. This could happen if the - /// ABI of a contract or the output that `ethers` generates changed. - /// - /// If this functions is run within a test during CI and fails, then it's - /// time to update all bindings. - /// - /// # Returns - /// - /// `Ok(())` if the freshly generated bindings match with the - /// existing bindings. Otherwise an `Err(_)` containing an `eyre::Report` - /// with more information. - /// - /// # Example - /// - /// Check that the generated files are up to date - /// - /// ```no_run - /// # use ethers_contract_abigen::MultiAbigen; - /// #[test] - /// fn generated_bindings_are_fresh() { - /// let project_root = std::path::Path::new(&env!("CARGO_MANIFEST_DIR")); - /// let abi_dir = project_root.join("abi"); - /// let gen = MultiAbigen::from_json_files(&abi_dir).unwrap(); - /// let bindings = gen.build().unwrap(); - /// bindings.ensure_consistent_module( - /// project_root.join("src/contracts"), false - /// ).expect("inconsistent bindings"); - /// } - /// ``` - pub fn ensure_consistent_module( - self, - module: impl AsRef, - single_file: bool, - ) -> Result<()> { + fn ensure_consistent_module(self, module: impl AsRef, single_file: bool) -> Result<()> { self.ensure_consistent_bindings(module, false, single_file)?; Ok(()) } } +/// Generate the shared prefix of the `lib.rs` or `mod.rs` +fn generate_prefix(mut buf: impl Write, is_crate: bool, single_file: bool) -> Result<()> { + writeln!(buf, "#![allow(clippy::all)]")?; + writeln!( + buf, + "//! This {} contains abigen! generated bindings for solidity contracts.", + if is_crate { "lib" } else { "module" } + )?; + writeln!(buf, "//! This is autogenerated code.")?; + writeln!(buf, "//! Do not manually edit these files.")?; + writeln!( + buf, + "//! {} may be overwritten by the codegen system at any time.", + if single_file && !is_crate { "This file" } else { "These files" } + )?; + Ok(()) +} + fn check_file_in_dir(dir: &Path, file_name: &str, expected_contents: &[u8]) -> Result<()> { eyre::ensure!(dir.is_dir(), "Not a directory: {}", dir.display()); @@ -493,6 +714,7 @@ fn check_binding_in_dir(dir: &Path, binding: &ContractBindings) -> Result<()> { mod tests { use super::*; + use ethers_solc::project_util::TempProject; use std::{panic, path::PathBuf}; struct Context { @@ -754,4 +976,82 @@ mod tests { assert!(result, "Inconsistent bindings wrongly approved"); }) } + + #[test] + fn can_deduplicate_types() { + let tmp = TempProject::dapptools().unwrap(); + + tmp.add_source( + "Greeter", + r#" +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +struct Inner { + bool a; +} + +struct Stuff { + Inner inner; +} + +contract Greeter1 { + + function greet(Stuff calldata stuff) public pure returns (Stuff memory) { + return stuff; + } +} + +contract Greeter2 { + + function greet(Stuff calldata stuff) public pure returns (Stuff memory) { + return stuff; + } +} +"#, + ) + .unwrap(); + + let _ = tmp.compile().unwrap(); + + let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap(); + let bindings = gen.build().unwrap(); + let single_file_dir = tmp.root().join("single_bindings"); + bindings.write_to_module(&single_file_dir, true).unwrap(); + + let single_file_mod = single_file_dir.join("mod.rs"); + assert!(single_file_mod.exists()); + let content = fs::read_to_string(&single_file_mod).unwrap(); + assert!(content.contains("mod __shared_types")); + assert!(content.contains("pub struct Inner")); + assert!(content.contains("pub struct Stuff")); + + // multiple files + let gen = MultiAbigen::from_json_files(tmp.artifacts_path()).unwrap(); + let bindings = gen.build().unwrap(); + let multi_file_dir = tmp.root().join("multi_bindings"); + bindings.write_to_module(&multi_file_dir, false).unwrap(); + let multi_file_mod = multi_file_dir.join("mod.rs"); + assert!(multi_file_mod.exists()); + let content = fs::read_to_string(&multi_file_mod).unwrap(); + assert!(content.contains("pub mod shared_types")); + + let greeter1 = multi_file_dir.join("greeter_1.rs"); + assert!(greeter1.exists()); + let content = fs::read_to_string(&greeter1).unwrap(); + assert!(!content.contains("pub struct Inner")); + assert!(!content.contains("pub struct Stuff")); + + let greeter2 = multi_file_dir.join("greeter_2.rs"); + assert!(greeter2.exists()); + let content = fs::read_to_string(&greeter2).unwrap(); + assert!(!content.contains("pub struct Inner")); + assert!(!content.contains("pub struct Stuff")); + + let shared_types = multi_file_dir.join("shared_types.rs"); + assert!(shared_types.exists()); + let content = fs::read_to_string(&shared_types).unwrap(); + assert!(content.contains("pub struct Inner")); + assert!(content.contains("pub struct Stuff")); + } } diff --git a/ethers-contract/ethers-contract-abigen/src/rawabi.rs b/ethers-contract/ethers-contract-abigen/src/rawabi.rs index 3d1bd713..782e4d35 100644 --- a/ethers-contract/ethers-contract-abigen/src/rawabi.rs +++ b/ethers-contract/ethers-contract-abigen/src/rawabi.rs @@ -84,6 +84,9 @@ pub struct Item { pub name: Option, #[serde(default)] pub outputs: Vec, + // required to satisfy solidity events + #[serde(default, skip_serializing_if = "Option::is_none")] + pub anonymous: Option, } /// Either an input/output or a nested component of an input/output @@ -97,11 +100,15 @@ pub struct Component { pub type_field: String, #[serde(default)] pub components: Vec, + /// Indexed flag. for solidity events + #[serde(default, skip_serializing_if = "Option::is_none")] + pub indexed: Option, } #[cfg(test)] mod tests { use super::*; + use ethers_core::abi::Abi; #[test] fn can_parse_raw_abi() { @@ -123,4 +130,14 @@ mod tests { let s = r#"[{"type":"function","name":"greet","inputs":[{"internalType":"struct Greeter.Stuff","name":"stuff","type":"tuple","components":[{"type":"bool"}]}],"outputs":[{"internalType":"struct Greeter.Stuff","name":"","type":"tuple","components":[{"type":"bool"}]}],"stateMutability":"view"}]"#; let _ = serde_json::from_str::(s).unwrap(); } + + #[test] + fn can_ethabi_round_trip() { + let s = r#"[{"anonymous":false,"inputs":[{"indexed":true,"internalType":"uint64","name":"number","type":"uint64"}],"name":"MyEvent","type":"event"},{"inputs":[],"name":"greet","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#; + + let raw = serde_json::from_str::(s).unwrap(); + let abi = serde_json::from_str::(s).unwrap(); + let de = serde_json::to_string(&raw).unwrap(); + assert_eq!(abi, serde_json::from_str::(&de).unwrap()); + } } diff --git a/ethers-contract/ethers-contract-derive/src/abigen.rs b/ethers-contract/ethers-contract-derive/src/abigen.rs index 86b31a5e..f1c47e3b 100644 --- a/ethers-contract/ethers-contract-derive/src/abigen.rs +++ b/ethers-contract/ethers-contract-derive/src/abigen.rs @@ -5,13 +5,13 @@ use crate::spanned::{ParseInner, Spanned}; use ethers_contract_abigen::Abigen; use ethers_core::abi::{Function, FunctionExt, Param, StateMutability}; -use ethers_contract_abigen::contract::{Context, ExpandedContract}; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::{quote, ToTokens}; -use std::{ - collections::{HashMap, HashSet}, - error::Error, +use ethers_contract_abigen::{ + contract::{Context, ExpandedContract}, + multi::MultiExpansion, }; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::ToTokens; +use std::{collections::HashSet, error::Error}; use syn::{ braced, ext::IdentExt, @@ -28,7 +28,6 @@ pub(crate) struct Contracts { impl Contracts { pub(crate) fn expand(self) -> Result { - let mut tokens = TokenStream2::new(); let mut expansions = Vec::with_capacity(self.inner.len()); // expand all contracts @@ -38,56 +37,14 @@ impl Contracts { expansions.push(contract); } - // merge all types if more than 1 contract - if expansions.len() > 1 { - // check for type conflicts - let mut conflicts: HashMap> = HashMap::new(); - for (idx, (_, ctx)) in expansions.iter().enumerate() { - for type_identifier in ctx.internal_structs().rust_type_names().keys() { - conflicts - .entry(type_identifier.clone()) - .or_insert_with(|| Vec::with_capacity(1)) - .push(idx); - } - } - - let mut shared_types = TokenStream2::new(); - let shared_types_mdoule = quote!(__shared_types); - let mut dirty = HashSet::new(); - // resolve type conflicts - for (id, contracts) in conflicts.iter().filter(|(_, c)| c.len() > 1) { - // extract the shared type once - shared_types.extend(expansions[contracts[0]].1.struct_definition(id).unwrap()); - // remove the shared type - for contract in contracts.iter().copied() { - expansions[contract].1.remove_struct(id); - dirty.insert(contract); - } - } - - // regenerate all struct definitions that were hit and adjust imports - for contract in dirty { - let (expanded, ctx) = &mut expansions[contract]; - expanded.abi_structs = ctx.abi_structs().unwrap(); - expanded.imports.extend(quote!( pub use super::#shared_types_mdoule::*;)); - } - tokens.extend(quote! { - pub mod #shared_types_mdoule { - #shared_types - } - }); - } - - tokens.extend(expansions.into_iter().map(|(exp, _)| exp.into_tokens())); - Ok(tokens) + // expand all contract expansions + Ok(MultiExpansion::new(expansions).expand_inplace()) } fn expand_contract( contract: ContractArgs, ) -> Result<(ExpandedContract, Context), Box> { - let contract = contract.into_builder()?; - let ctx = Context::from_abigen(contract)?; - Ok((ctx.expand()?, ctx)) + Ok(contract.into_builder()?.expand()?) } }