feat: add support for multiple contract definitions in abigen macro (#498)
* feat: support multiple contracts in abigen * fix: use correct events decl * fix: parsing and tests * test: add test * chore: update changelog
This commit is contained in:
parent
6d9b300b86
commit
ea8551da4c
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
|
|
||||||
|
- `abigen!` now supports multiple contracts [#498](https://github.com/gakonst/ethers-rs/pull/498)
|
||||||
- Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482)
|
- Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482)
|
||||||
- Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` trait and derive macro in ethers-derive-eip712 [#481](https://github.com/gakonst/ethers-rs/pull/481)
|
- Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` trait and derive macro in ethers-derive-eip712 [#481](https://github.com/gakonst/ethers-rs/pull/481)
|
||||||
|
|
||||||
|
|
|
@ -18,8 +18,48 @@ use serde::Deserialize;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use syn::Path;
|
use syn::Path;
|
||||||
|
|
||||||
|
/// The result of `Context::expand`
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ExpandedContract {
|
||||||
|
/// The name of the contract module
|
||||||
|
pub module: Ident,
|
||||||
|
/// The contract module's imports
|
||||||
|
pub imports: TokenStream,
|
||||||
|
/// Contract, Middle related implementations
|
||||||
|
pub contract: TokenStream,
|
||||||
|
/// All event impls of the contract
|
||||||
|
pub events: TokenStream,
|
||||||
|
/// The contract's internal structs
|
||||||
|
pub abi_structs: TokenStream,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExpandedContract {
|
||||||
|
/// Merges everything into a single module
|
||||||
|
pub fn into_tokens(self) -> TokenStream {
|
||||||
|
let ExpandedContract {
|
||||||
|
module,
|
||||||
|
imports,
|
||||||
|
contract,
|
||||||
|
events,
|
||||||
|
abi_structs,
|
||||||
|
} = self;
|
||||||
|
quote! {
|
||||||
|
// export all the created data types
|
||||||
|
pub use #module::*;
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
mod #module {
|
||||||
|
#imports
|
||||||
|
#contract
|
||||||
|
#events
|
||||||
|
#abi_structs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Internal shared context for generating smart contract bindings.
|
/// Internal shared context for generating smart contract bindings.
|
||||||
pub(crate) struct Context {
|
pub struct Context {
|
||||||
/// The ABI string pre-parsing.
|
/// The ABI string pre-parsing.
|
||||||
abi_str: Literal,
|
abi_str: Literal,
|
||||||
|
|
||||||
|
@ -49,12 +89,12 @@ pub(crate) struct Context {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Context {
|
impl Context {
|
||||||
pub(crate) fn expand(args: Abigen) -> Result<TokenStream> {
|
/// Expands the whole rust contract
|
||||||
let cx = Self::from_abigen(args)?;
|
pub fn expand(&self) -> Result<ExpandedContract> {
|
||||||
let name = &cx.contract_name;
|
let name = &self.contract_name;
|
||||||
let name_mod = util::ident(&format!(
|
let name_mod = util::ident(&format!(
|
||||||
"{}_mod",
|
"{}_mod",
|
||||||
cx.contract_name.to_string().to_lowercase()
|
self.contract_name.to_string().to_lowercase()
|
||||||
));
|
));
|
||||||
|
|
||||||
let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase()));
|
let abi_name = super::util::safe_ident(&format!("{}_ABI", name.to_string().to_uppercase()));
|
||||||
|
@ -63,31 +103,25 @@ impl Context {
|
||||||
let imports = common::imports(&name.to_string());
|
let imports = common::imports(&name.to_string());
|
||||||
|
|
||||||
// 1. Declare Contract struct
|
// 1. Declare Contract struct
|
||||||
let struct_decl = common::struct_declaration(&cx, &abi_name);
|
let struct_decl = common::struct_declaration(self, &abi_name);
|
||||||
|
|
||||||
// 2. Declare events structs & impl FromTokens for each event
|
// 2. Declare events structs & impl FromTokens for each event
|
||||||
let events_decl = cx.events_declaration()?;
|
let events_decl = self.events_declaration()?;
|
||||||
|
|
||||||
// 3. impl block for the event functions
|
// 3. impl block for the event functions
|
||||||
let contract_events = cx.event_methods()?;
|
let contract_events = self.event_methods()?;
|
||||||
|
|
||||||
// 4. impl block for the contract methods
|
// 4. impl block for the contract methods
|
||||||
let contract_methods = cx.methods()?;
|
let contract_methods = self.methods()?;
|
||||||
|
|
||||||
// 5. Declare the structs parsed from the human readable abi
|
// 5. Declare the structs parsed from the human readable abi
|
||||||
let abi_structs_decl = cx.abi_structs()?;
|
let abi_structs_decl = self.abi_structs()?;
|
||||||
|
|
||||||
let ethers_core = util::ethers_core_crate();
|
let ethers_core = util::ethers_core_crate();
|
||||||
let ethers_contract = util::ethers_contract_crate();
|
let ethers_contract = util::ethers_contract_crate();
|
||||||
let ethers_providers = util::ethers_providers_crate();
|
let ethers_providers = util::ethers_providers_crate();
|
||||||
|
|
||||||
Ok(quote! {
|
let contract = quote! {
|
||||||
// export all the created data types
|
|
||||||
pub use #name_mod::*;
|
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
mod #name_mod {
|
|
||||||
#imports
|
|
||||||
#struct_decl
|
#struct_decl
|
||||||
|
|
||||||
impl<'a, M: #ethers_providers::Middleware> #name<M> {
|
impl<'a, M: #ethers_providers::Middleware> #name<M> {
|
||||||
|
@ -105,16 +139,19 @@ impl Context {
|
||||||
|
|
||||||
#contract_events
|
#contract_events
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
#events_decl
|
Ok(ExpandedContract {
|
||||||
|
module: name_mod,
|
||||||
#abi_structs_decl
|
imports,
|
||||||
}
|
contract,
|
||||||
|
events: events_decl,
|
||||||
|
abi_structs: abi_structs_decl,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a context from the code generation arguments.
|
/// Create a context from the code generation arguments.
|
||||||
fn from_abigen(args: Abigen) -> Result<Self> {
|
pub fn from_abigen(args: Abigen) -> Result<Self> {
|
||||||
// get the actual ABI string
|
// get the actual ABI string
|
||||||
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
|
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
|
||||||
let mut abi_parser = AbiParser::default();
|
let mut abi_parser = AbiParser::default();
|
||||||
|
@ -202,4 +239,14 @@ impl Context {
|
||||||
event_aliases,
|
event_aliases,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The internal abi struct mapping table
|
||||||
|
pub fn internal_structs(&self) -> &InternalStructs {
|
||||||
|
&self.internal_structs
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The internal mutable abi struct mapping table
|
||||||
|
pub fn internal_structs_mut(&mut self) -> &mut InternalStructs {
|
||||||
|
&mut self.internal_structs
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,45 @@ impl Context {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// In the event of type conflicts this allows for removing a specific struct type.
|
||||||
|
pub fn remove_struct(&mut self, name: &str) {
|
||||||
|
if self.human_readable {
|
||||||
|
self.abi_parser.structs.remove(name);
|
||||||
|
} else {
|
||||||
|
self.internal_structs.structs.remove(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the type definition for the struct with the given name
|
||||||
|
pub fn struct_definition(&mut self, name: &str) -> Result<TokenStream> {
|
||||||
|
if self.human_readable {
|
||||||
|
self.generate_human_readable_struct(name)
|
||||||
|
} else {
|
||||||
|
self.generate_internal_struct(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generates the type definition for the name that matches the given identifier
|
||||||
|
fn generate_internal_struct(&self, id: &str) -> Result<TokenStream> {
|
||||||
|
let sol_struct = self
|
||||||
|
.internal_structs
|
||||||
|
.structs
|
||||||
|
.get(id)
|
||||||
|
.context("struct not found")?;
|
||||||
|
let struct_name = self
|
||||||
|
.internal_structs
|
||||||
|
.rust_type_names
|
||||||
|
.get(id)
|
||||||
|
.context(format!("No types found for {}", id))?;
|
||||||
|
let tuple = self
|
||||||
|
.internal_structs
|
||||||
|
.struct_tuples
|
||||||
|
.get(id)
|
||||||
|
.context(format!("No types found for {}", id))?
|
||||||
|
.clone();
|
||||||
|
self.expand_internal_struct(struct_name, sol_struct, tuple)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the `TokenStream` with all the internal structs extracted form the JSON ABI
|
/// Returns the `TokenStream` with all the internal structs extracted form the JSON ABI
|
||||||
fn gen_internal_structs(&self) -> Result<TokenStream> {
|
fn gen_internal_structs(&self) -> Result<TokenStream> {
|
||||||
let mut structs = TokenStream::new();
|
let mut structs = TokenStream::new();
|
||||||
|
@ -37,19 +76,7 @@ impl Context {
|
||||||
ids.sort();
|
ids.sort();
|
||||||
|
|
||||||
for id in ids {
|
for id in ids {
|
||||||
let sol_struct = &self.internal_structs.structs[id];
|
structs.extend(self.generate_internal_struct(id)?);
|
||||||
let struct_name = self
|
|
||||||
.internal_structs
|
|
||||||
.rust_type_names
|
|
||||||
.get(id)
|
|
||||||
.context(format!("No types found for {}", id))?;
|
|
||||||
let tuple = self
|
|
||||||
.internal_structs
|
|
||||||
.struct_tuples
|
|
||||||
.get(id)
|
|
||||||
.context(format!("No types found for {}", id))?
|
|
||||||
.clone();
|
|
||||||
structs.extend(self.expand_internal_struct(struct_name, sol_struct, tuple)?);
|
|
||||||
}
|
}
|
||||||
Ok(structs)
|
Ok(structs)
|
||||||
}
|
}
|
||||||
|
@ -113,75 +140,83 @@ impl Context {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn generate_human_readable_struct(&self, name: &str) -> Result<TokenStream> {
|
||||||
|
let sol_struct = self
|
||||||
|
.abi_parser
|
||||||
|
.structs
|
||||||
|
.get(name)
|
||||||
|
.context("struct not found")?;
|
||||||
|
let mut fields = Vec::with_capacity(sol_struct.fields().len());
|
||||||
|
let mut param_types = Vec::with_capacity(sol_struct.fields().len());
|
||||||
|
for field in sol_struct.fields() {
|
||||||
|
let field_name = util::ident(&field.name().to_snake_case());
|
||||||
|
match field.r#type() {
|
||||||
|
FieldType::Elementary(ty) => {
|
||||||
|
param_types.push(ty.clone());
|
||||||
|
let ty = types::expand(ty)?;
|
||||||
|
fields.push(quote! { pub #field_name: #ty });
|
||||||
|
}
|
||||||
|
FieldType::Struct(struct_ty) => {
|
||||||
|
let ty = expand_struct_type(struct_ty);
|
||||||
|
fields.push(quote! { pub #field_name: #ty });
|
||||||
|
|
||||||
|
let name = struct_ty.name();
|
||||||
|
let tuple = self
|
||||||
|
.abi_parser
|
||||||
|
.struct_tuples
|
||||||
|
.get(name)
|
||||||
|
.context(format!("No types found for {}", name))?
|
||||||
|
.clone();
|
||||||
|
let tuple = ParamType::Tuple(tuple);
|
||||||
|
|
||||||
|
param_types.push(struct_ty.as_param(tuple));
|
||||||
|
}
|
||||||
|
FieldType::Mapping(_) => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Mapping types in struct `{}` are not supported {:?}",
|
||||||
|
name,
|
||||||
|
field
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let abi_signature = format!(
|
||||||
|
"{}({})",
|
||||||
|
name,
|
||||||
|
param_types
|
||||||
|
.iter()
|
||||||
|
.map(|kind| kind.to_string())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(","),
|
||||||
|
);
|
||||||
|
|
||||||
|
let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature));
|
||||||
|
|
||||||
|
let name = util::ident(name);
|
||||||
|
|
||||||
|
// use the same derives as for events
|
||||||
|
let derives = &self.event_derives;
|
||||||
|
let derives = quote! {#(#derives),*};
|
||||||
|
|
||||||
|
let ethers_contract = util::ethers_contract_crate();
|
||||||
|
|
||||||
|
Ok(quote! {
|
||||||
|
#abi_signature_doc
|
||||||
|
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #derives)]
|
||||||
|
pub struct #name {
|
||||||
|
#( #fields ),*
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Expand all structs parsed from the human readable ABI
|
/// Expand all structs parsed from the human readable ABI
|
||||||
fn gen_human_readable_structs(&self) -> Result<TokenStream> {
|
fn gen_human_readable_structs(&self) -> Result<TokenStream> {
|
||||||
let mut structs = TokenStream::new();
|
let mut structs = TokenStream::new();
|
||||||
let mut names: Vec<_> = self.abi_parser.structs.keys().collect();
|
let mut names: Vec<_> = self.abi_parser.structs.keys().collect();
|
||||||
names.sort();
|
names.sort();
|
||||||
for name in names {
|
for name in names {
|
||||||
let sol_struct = &self.abi_parser.structs[name];
|
structs.extend(self.generate_human_readable_struct(name)?);
|
||||||
let mut fields = Vec::with_capacity(sol_struct.fields().len());
|
|
||||||
let mut param_types = Vec::with_capacity(sol_struct.fields().len());
|
|
||||||
for field in sol_struct.fields() {
|
|
||||||
let field_name = util::ident(&field.name().to_snake_case());
|
|
||||||
match field.r#type() {
|
|
||||||
FieldType::Elementary(ty) => {
|
|
||||||
param_types.push(ty.clone());
|
|
||||||
let ty = types::expand(ty)?;
|
|
||||||
fields.push(quote! { pub #field_name: #ty });
|
|
||||||
}
|
|
||||||
FieldType::Struct(struct_ty) => {
|
|
||||||
let ty = expand_struct_type(struct_ty);
|
|
||||||
fields.push(quote! { pub #field_name: #ty });
|
|
||||||
|
|
||||||
let name = struct_ty.name();
|
|
||||||
let tuple = self
|
|
||||||
.abi_parser
|
|
||||||
.struct_tuples
|
|
||||||
.get(name)
|
|
||||||
.context(format!("No types found for {}", name))?
|
|
||||||
.clone();
|
|
||||||
let tuple = ParamType::Tuple(tuple);
|
|
||||||
|
|
||||||
param_types.push(struct_ty.as_param(tuple));
|
|
||||||
}
|
|
||||||
FieldType::Mapping(_) => {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Mapping types in struct `{}` are not supported {:?}",
|
|
||||||
name,
|
|
||||||
field
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let abi_signature = format!(
|
|
||||||
"{}({})",
|
|
||||||
name,
|
|
||||||
param_types
|
|
||||||
.iter()
|
|
||||||
.map(|kind| kind.to_string())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(","),
|
|
||||||
);
|
|
||||||
|
|
||||||
let abi_signature_doc = util::expand_doc(&format!("`{}`", abi_signature));
|
|
||||||
|
|
||||||
let name = util::ident(name);
|
|
||||||
|
|
||||||
// use the same derives as for events
|
|
||||||
let derives = &self.event_derives;
|
|
||||||
let derives = quote! {#(#derives),*};
|
|
||||||
|
|
||||||
let ethers_contract = util::ethers_contract_crate();
|
|
||||||
|
|
||||||
structs.extend(quote! {
|
|
||||||
#abi_signature_doc
|
|
||||||
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #derives)]
|
|
||||||
pub struct #name {
|
|
||||||
#( #fields ),*
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
Ok(structs)
|
Ok(structs)
|
||||||
}
|
}
|
||||||
|
@ -209,6 +244,7 @@ pub struct InternalStructs {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InternalStructs {
|
impl InternalStructs {
|
||||||
|
/// Creates a new instance with a filled type mapping table based on the abi
|
||||||
pub fn new(abi: RawAbi) -> Self {
|
pub fn new(abi: RawAbi) -> Self {
|
||||||
let mut top_level_internal_types = HashMap::new();
|
let mut top_level_internal_types = HashMap::new();
|
||||||
let mut function_params = HashMap::new();
|
let mut function_params = HashMap::new();
|
||||||
|
@ -285,6 +321,11 @@ impl InternalStructs {
|
||||||
.and_then(|id| self.rust_type_names.get(id))
|
.and_then(|id| self.rust_type_names.get(id))
|
||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the mapping table of abi `internal type identifier -> rust type`
|
||||||
|
pub fn rust_type_names(&self) -> &HashMap<String, String> {
|
||||||
|
&self.rust_type_names
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This will determine the name of the rust type and will make sure that possible collisions are resolved by adjusting the actual Rust name of the structure, e.g. `LibraryA.Point` and `LibraryB.Point` to `LibraryAPoint` and `LibraryBPoint`.
|
/// This will determine the name of the rust type and will make sure that possible collisions are resolved by adjusting the actual Rust name of the structure, e.g. `LibraryA.Point` and `LibraryB.Point` to `LibraryAPoint` and `LibraryBPoint`.
|
||||||
|
|
|
@ -10,7 +10,8 @@
|
||||||
#[path = "test/macros.rs"]
|
#[path = "test/macros.rs"]
|
||||||
mod test_macros;
|
mod test_macros;
|
||||||
|
|
||||||
mod contract;
|
/// Contains types to generate rust bindings for solidity contracts
|
||||||
|
pub mod contract;
|
||||||
use contract::Context;
|
use contract::Context;
|
||||||
|
|
||||||
pub mod rawabi;
|
pub mod rawabi;
|
||||||
|
@ -126,7 +127,7 @@ impl Abigen {
|
||||||
/// Generates the contract bindings.
|
/// Generates the contract bindings.
|
||||||
pub fn generate(self) -> Result<ContractBindings> {
|
pub fn generate(self) -> Result<ContractBindings> {
|
||||||
let rustfmt = self.rustfmt;
|
let rustfmt = self.rustfmt;
|
||||||
let tokens = Context::expand(self)?;
|
let tokens = Context::from_abigen(self)?.expand()?.into_tokens();
|
||||||
Ok(ContractBindings { tokens, rustfmt })
|
Ok(ContractBindings { tokens, rustfmt })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,16 +5,96 @@ use crate::spanned::{ParseInner, Spanned};
|
||||||
use ethers_contract_abigen::Abigen;
|
use ethers_contract_abigen::Abigen;
|
||||||
use ethers_core::abi::{Function, FunctionExt, Param, StateMutability};
|
use ethers_core::abi::{Function, FunctionExt, Param, StateMutability};
|
||||||
|
|
||||||
|
use ethers_contract_abigen::contract::{Context, ExpandedContract};
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use quote::ToTokens;
|
use quote::{quote, ToTokens};
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||||
use syn::{braced, parenthesized, Ident, LitStr, Path, Token};
|
use syn::{braced, parenthesized, Ident, LitStr, Path, Token};
|
||||||
|
|
||||||
pub(crate) fn expand(args: ContractArgs) -> Result<TokenStream2, Box<dyn Error>> {
|
/// A series of `ContractArgs` separated by `;`
|
||||||
Ok(args.into_builder()?.generate()?.into_tokens())
|
#[cfg_attr(test, derive(Debug))]
|
||||||
|
pub(crate) struct Contracts {
|
||||||
|
inner: Vec<(Span, ContractArgs)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
for (span, contract) in self.inner {
|
||||||
|
let contract = Self::expand_contract(contract)
|
||||||
|
.map_err(|err| syn::Error::new(span, err.to_string()))?;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for Contracts {
|
||||||
|
fn parse(input: ParseStream) -> ParseResult<Self> {
|
||||||
|
let inner = input
|
||||||
|
.parse_terminated::<_, Token![;]>(ContractArgs::spanned_parse)?
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
Ok(Self { inner })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Contract procedural macro arguments.
|
/// Contract procedural macro arguments.
|
||||||
|
@ -62,14 +142,27 @@ impl ParseInner for ContractArgs {
|
||||||
(literal.span(), literal.value())
|
(literal.span(), literal.value())
|
||||||
};
|
};
|
||||||
|
|
||||||
if !input.is_empty() {
|
let mut parameters = Vec::new();
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if lookahead.peek(Token![,]) {
|
||||||
input.parse::<Token![,]>()?;
|
input.parse::<Token![,]>()?;
|
||||||
}
|
|
||||||
|
|
||||||
let parameters = input
|
loop {
|
||||||
.parse_terminated::<_, Token![,]>(Parameter::parse)?
|
if input.is_empty() {
|
||||||
.into_iter()
|
break;
|
||||||
.collect();
|
}
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if lookahead.peek(Token![;]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let param = Parameter::parse(input)?;
|
||||||
|
parameters.push(param);
|
||||||
|
let lookahead = input.lookahead1();
|
||||||
|
if lookahead.peek(Token![,]) {
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
span,
|
span,
|
||||||
|
@ -232,6 +325,121 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_contracts(s: TokenStream2) -> Vec<ContractArgs> {
|
||||||
|
use syn::parse::Parser;
|
||||||
|
Contracts::parse
|
||||||
|
.parse2(s)
|
||||||
|
.unwrap()
|
||||||
|
.inner
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, c)| c)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multi_contract_args_events() {
|
||||||
|
let args = parse_contracts(quote::quote! {
|
||||||
|
TestContract,
|
||||||
|
"path/to/abi.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
|
||||||
|
TestContract2,
|
||||||
|
"other.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract".to_string(),
|
||||||
|
abi: "path/to/abi.json".to_string(),
|
||||||
|
parameters: vec![Parameter::EventDerives(vec![
|
||||||
|
"serde :: Deserialize".into(),
|
||||||
|
"serde :: Serialize".into(),
|
||||||
|
])],
|
||||||
|
},
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract2".to_string(),
|
||||||
|
abi: "other.json".to_string(),
|
||||||
|
parameters: vec![Parameter::EventDerives(vec![
|
||||||
|
"serde :: Deserialize".into(),
|
||||||
|
"serde :: Serialize".into(),
|
||||||
|
])],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn parse_multi_contract_args_methods() {
|
||||||
|
let args = parse_contracts(quote::quote! {
|
||||||
|
TestContract,
|
||||||
|
"path/to/abi.json",
|
||||||
|
methods {
|
||||||
|
myMethod(uint256, bool) as my_renamed_method;
|
||||||
|
myOtherMethod() as my_other_renamed_method;
|
||||||
|
}
|
||||||
|
;
|
||||||
|
|
||||||
|
TestContract2,
|
||||||
|
"other.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract".to_string(),
|
||||||
|
abi: "path/to/abi.json".to_string(),
|
||||||
|
parameters: vec![Parameter::Methods(vec![
|
||||||
|
method("myMethod(uint256,bool)", "my_renamed_method"),
|
||||||
|
method("myOtherMethod()", "my_other_renamed_method"),
|
||||||
|
])],
|
||||||
|
},
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract2".to_string(),
|
||||||
|
abi: "other.json".to_string(),
|
||||||
|
parameters: vec![Parameter::EventDerives(vec![
|
||||||
|
"serde :: Deserialize".into(),
|
||||||
|
"serde :: Serialize".into(),
|
||||||
|
])],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_multi_contract_args() {
|
||||||
|
let args = parse_contracts(quote::quote! {
|
||||||
|
TestContract,
|
||||||
|
"path/to/abi.json",;
|
||||||
|
|
||||||
|
TestContract2,
|
||||||
|
"other.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
args,
|
||||||
|
vec![
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract".to_string(),
|
||||||
|
abi: "path/to/abi.json".to_string(),
|
||||||
|
parameters: vec![],
|
||||||
|
},
|
||||||
|
ContractArgs {
|
||||||
|
name: "TestContract2".to_string(),
|
||||||
|
abi: "other.json".to_string(),
|
||||||
|
parameters: vec![Parameter::EventDerives(vec![
|
||||||
|
"serde :: Deserialize".into(),
|
||||||
|
"serde :: Serialize".into(),
|
||||||
|
])],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_contract_args() {
|
fn parse_contract_args() {
|
||||||
let args = contract_args!(TestContract, "path/to/abi.json");
|
let args = contract_args!(TestContract, "path/to/abi.json");
|
||||||
|
|
|
@ -12,16 +12,15 @@ use syn::{
|
||||||
GenericArgument, Lit, Meta, NestedMeta, PathArguments, Type,
|
GenericArgument, Lit, Meta, NestedMeta, PathArguments, Type,
|
||||||
};
|
};
|
||||||
|
|
||||||
use abigen::{expand, ContractArgs};
|
use abigen::Contracts;
|
||||||
use ethers_core::abi::{param_type::Reader, AbiParser, Event, EventExt, EventParam, ParamType};
|
use ethers_core::abi::{param_type::Reader, AbiParser, Event, EventExt, EventParam, ParamType};
|
||||||
use hex::FromHex;
|
use hex::FromHex;
|
||||||
use spanned::Spanned;
|
|
||||||
|
|
||||||
mod abigen;
|
mod abigen;
|
||||||
mod spanned;
|
mod spanned;
|
||||||
|
|
||||||
/// Proc macro to generate type-safe bindings to a contract. This macro accepts
|
/// Proc macro to generate type-safe bindings to a contract(s). This macro accepts
|
||||||
/// an Ethereum contract ABI or a path. Note that this path is rooted in
|
/// one or more Ethereum contract ABI or a path. Note that this path is rooted in
|
||||||
/// the crate's root `CARGO_MANIFEST_DIR`.
|
/// the crate's root `CARGO_MANIFEST_DIR`.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
|
@ -55,6 +54,8 @@ mod spanned;
|
||||||
/// - `event_derives`: A list of additional derives that should be added to
|
/// - `event_derives`: A list of additional derives that should be added to
|
||||||
/// contract event structs and enums.
|
/// contract event structs and enums.
|
||||||
///
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// abigen!(
|
/// abigen!(
|
||||||
/// MyContract,
|
/// MyContract,
|
||||||
|
@ -65,13 +66,32 @@ mod spanned;
|
||||||
/// event_derives (serde::Deserialize, serde::Serialize),
|
/// event_derives (serde::Deserialize, serde::Serialize),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// `abigen!` supports multiple abigen definitions separated by a semicolon `;`
|
||||||
|
/// This is useful if the contracts use ABIEncoderV2 structs. In which case `abigen!` bundles all type duplicates so that all rust contracts also use the same rust types.
|
||||||
|
///
|
||||||
|
/// # Example Multiple contracts
|
||||||
|
/// ```ignore
|
||||||
|
/// abigen!(
|
||||||
|
/// MyContract,
|
||||||
|
/// "path/to/MyContract.json",
|
||||||
|
/// methods {
|
||||||
|
/// myMethod(uint256,bool) as my_renamed_method;
|
||||||
|
/// },
|
||||||
|
/// event_derives (serde::Deserialize, serde::Serialize);
|
||||||
|
///
|
||||||
|
/// MyOtherContract,
|
||||||
|
/// "path/to/MyOtherContract.json",
|
||||||
|
/// event_derives (serde::Deserialize, serde::Serialize);
|
||||||
|
/// );
|
||||||
|
/// ```
|
||||||
#[proc_macro]
|
#[proc_macro]
|
||||||
pub fn abigen(input: TokenStream) -> TokenStream {
|
pub fn abigen(input: TokenStream) -> TokenStream {
|
||||||
let args = parse_macro_input!(input as Spanned<ContractArgs>);
|
let contracts = parse_macro_input!(input as Contracts);
|
||||||
|
|
||||||
let span = args.span();
|
contracts
|
||||||
expand(args.into_inner())
|
.expand()
|
||||||
.unwrap_or_else(|e| Error::new(span, format!("{:?}", e)).to_compile_error())
|
.unwrap_or_else(|err| err.to_compile_error())
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
//! Test cases to validate the `abigen!` macro
|
//! Test cases to validate the `abigen!` macro
|
||||||
use ethers_contract::{abigen, EthEvent};
|
use ethers_contract::{abigen, EthEvent};
|
||||||
use ethers_core::abi::{Address, Tokenizable};
|
use ethers_core::abi::{Address, Tokenizable};
|
||||||
|
use ethers_core::types::U256;
|
||||||
use ethers_providers::Provider;
|
use ethers_providers::Provider;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -21,6 +22,33 @@ fn can_gen_human_readable() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_gen_human_readable_multiple() {
|
||||||
|
abigen!(
|
||||||
|
SimpleContract1,
|
||||||
|
r#"[
|
||||||
|
event ValueChanged1(address indexed author, string oldValue, string newValue)
|
||||||
|
]"#,
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
|
||||||
|
SimpleContract2,
|
||||||
|
r#"[
|
||||||
|
event ValueChanged2(address indexed author, string oldValue, string newValue)
|
||||||
|
]"#,
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize)
|
||||||
|
);
|
||||||
|
assert_eq!("ValueChanged1", ValueChanged1Filter::name());
|
||||||
|
assert_eq!(
|
||||||
|
"ValueChanged1(address,string,string)",
|
||||||
|
ValueChanged1Filter::abi_signature()
|
||||||
|
);
|
||||||
|
assert_eq!("ValueChanged2", ValueChanged2Filter::name());
|
||||||
|
assert_eq!(
|
||||||
|
"ValueChanged2(address,string,string)",
|
||||||
|
ValueChanged2Filter::abi_signature()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_gen_structs_readable() {
|
fn can_gen_structs_readable() {
|
||||||
abigen!(
|
abigen!(
|
||||||
|
@ -77,6 +105,57 @@ fn can_generate_internal_structs() {
|
||||||
assert_tokenizeable::<G2Point>();
|
assert_tokenizeable::<G2Point>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_generate_internal_structs_multiple() {
|
||||||
|
// NOTE: nesting here is necessary due to how tests are structured...
|
||||||
|
use contract::*;
|
||||||
|
mod contract {
|
||||||
|
use super::*;
|
||||||
|
abigen!(
|
||||||
|
VerifierContract,
|
||||||
|
"ethers-contract/tests/solidity-contracts/verifier_abi.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
|
||||||
|
MyOtherVerifierContract,
|
||||||
|
"ethers-contract/tests/solidity-contracts/verifier_abi.json",
|
||||||
|
event_derives(serde::Deserialize, serde::Serialize);
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_tokenizeable::<VerifyingKey>();
|
||||||
|
assert_tokenizeable::<G1Point>();
|
||||||
|
assert_tokenizeable::<G2Point>();
|
||||||
|
|
||||||
|
let (provider, _) = Provider::mocked();
|
||||||
|
let client = Arc::new(provider);
|
||||||
|
|
||||||
|
let g1 = G1Point {
|
||||||
|
x: U256::zero(),
|
||||||
|
y: U256::zero(),
|
||||||
|
};
|
||||||
|
let g2 = G2Point {
|
||||||
|
x: [U256::zero(), U256::zero()],
|
||||||
|
y: [U256::zero(), U256::zero()],
|
||||||
|
};
|
||||||
|
let vk = VerifyingKey {
|
||||||
|
alfa_1: g1.clone(),
|
||||||
|
beta_2: g2.clone(),
|
||||||
|
gamma_2: g2.clone(),
|
||||||
|
delta_2: g2.clone(),
|
||||||
|
ic: vec![g1.clone()],
|
||||||
|
};
|
||||||
|
let proof = Proof {
|
||||||
|
a: g1.clone(),
|
||||||
|
b: g2,
|
||||||
|
c: g1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// ensure both contracts use the same types
|
||||||
|
let contract = VerifierContract::new(Address::zero(), client.clone());
|
||||||
|
let _ = contract.verify(vec![], proof.clone(), vk.clone());
|
||||||
|
let contract = MyOtherVerifierContract::new(Address::zero(), client);
|
||||||
|
let _ = contract.verify(vec![], proof, vk);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn can_gen_human_readable_with_structs() {
|
fn can_gen_human_readable_with_structs() {
|
||||||
abigen!(
|
abigen!(
|
||||||
|
|
Loading…
Reference in New Issue