feat(abigen): support shared type during multiabigen (#959)
* refactor: move type deduplication to abigen crate * refactor: separate expansion process * feat: support shared type during multiabigen * test: add type deduplication test * chore: rustfmt * typos * chore(clippy): make clippy happy * chore: add anonymous field
This commit is contained in:
parent
72da5913d2
commit
f5ef8149e5
|
@ -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<String, MethodAlias>,
|
||||
|
@ -91,9 +94,9 @@ pub struct Context {
|
|||
impl Context {
|
||||
/// Expands the whole rust contract
|
||||
pub fn expand(&self) -> Result<ExpandedContract> {
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Path>) -> Result<Self> {
|
||||
let name = path
|
||||
|
@ -153,8 +154,15 @@ impl Abigen {
|
|||
pub fn generate(self) -> Result<ContractBindings> {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<Item = Abigen>) -> Result<Self> {
|
||||
let contracts = abigens.into_iter().map(|abigen| abigen.expand()).collect::<Result<_>>()?;
|
||||
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<String, Vec<usize>> = 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<usize>,
|
||||
/// all type definitions of types that are shared by multiple contracts
|
||||
shared_types: Vec<TokenStream>,
|
||||
}
|
||||
|
||||
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<MultiBindings> {
|
||||
let bindings = self
|
||||
.abigens
|
||||
.into_iter()
|
||||
.map(|v| v.generate())
|
||||
.collect::<Result<Vec<_>>>()?
|
||||
.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<Path>, 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<str>,
|
||||
version: impl AsRef<str>,
|
||||
lib: impl AsRef<Path>,
|
||||
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<str>,
|
||||
version: impl AsRef<str>,
|
||||
crate_path: impl AsRef<Path>,
|
||||
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<Path>,
|
||||
single_file: bool,
|
||||
) -> Result<()> {
|
||||
self.into_inner(single_file).ensure_consistent_module(module, single_file)
|
||||
}
|
||||
}
|
||||
|
||||
struct MultiBindingsInner {
|
||||
/// Abigen objects to be written
|
||||
bindings: BTreeMap<String, ContractBindings>,
|
||||
/// contains the content of the shared types if any
|
||||
shared_types: Option<ContractBindings>,
|
||||
}
|
||||
|
||||
// deref allows for inspection without modification
|
||||
impl std::ops::Deref for MultiBindings {
|
||||
impl std::ops::Deref for MultiBindingsInner {
|
||||
type Target = BTreeMap<String, ContractBindings>;
|
||||
|
||||
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<str>,
|
||||
|
@ -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<Vec<u8>> {
|
||||
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<Path>, single_file: bool) -> Result<()> {
|
||||
fn write_to_module(self, module: impl AsRef<Path>, 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<str>,
|
||||
version: impl AsRef<str>,
|
||||
|
@ -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<str>,
|
||||
version: impl AsRef<str>,
|
||||
|
@ -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<Path>,
|
||||
single_file: bool,
|
||||
) -> Result<()> {
|
||||
fn ensure_consistent_module(self, module: impl AsRef<Path>, 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"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,6 +84,9 @@ pub struct Item {
|
|||
pub name: Option<String>,
|
||||
#[serde(default)]
|
||||
pub outputs: Vec<Component>,
|
||||
// required to satisfy solidity events
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub anonymous: Option<bool>,
|
||||
}
|
||||
|
||||
/// 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<Component>,
|
||||
/// Indexed flag. for solidity events
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub indexed: Option<bool>,
|
||||
}
|
||||
|
||||
#[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::<RawAbi>(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::<RawAbi>(s).unwrap();
|
||||
let abi = serde_json::from_str::<Abi>(s).unwrap();
|
||||
let de = serde_json::to_string(&raw).unwrap();
|
||||
assert_eq!(abi, serde_json::from_str::<Abi>(&de).unwrap());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<TokenStream2, syn::Error> {
|
||||
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<String, Vec<usize>> = 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<dyn Error>> {
|
||||
let contract = contract.into_builder()?;
|
||||
let ctx = Context::from_abigen(contract)?;
|
||||
Ok((ctx.expand()?, ctx))
|
||||
Ok(contract.into_builder()?.expand()?)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue