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:
Matthias Seitz 2021-10-11 16:18:09 +02:00 committed by GitHub
parent 6d9b300b86
commit ea8551da4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 515 additions and 118 deletions

View File

@ -4,6 +4,7 @@
### 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)
- 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)

View File

@ -18,8 +18,48 @@ use serde::Deserialize;
use std::collections::BTreeMap;
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.
pub(crate) struct Context {
pub struct Context {
/// The ABI string pre-parsing.
abi_str: Literal,
@ -49,12 +89,12 @@ pub(crate) struct Context {
}
impl Context {
pub(crate) fn expand(args: Abigen) -> Result<TokenStream> {
let cx = Self::from_abigen(args)?;
let name = &cx.contract_name;
/// Expands the whole rust contract
pub fn expand(&self) -> Result<ExpandedContract> {
let name = &self.contract_name;
let name_mod = util::ident(&format!(
"{}_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()));
@ -63,31 +103,25 @@ impl Context {
let imports = common::imports(&name.to_string());
// 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
let events_decl = cx.events_declaration()?;
let events_decl = self.events_declaration()?;
// 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
let contract_methods = cx.methods()?;
let contract_methods = self.methods()?;
// 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_contract = util::ethers_contract_crate();
let ethers_providers = util::ethers_providers_crate();
Ok(quote! {
// export all the created data types
pub use #name_mod::*;
#[allow(clippy::too_many_arguments)]
mod #name_mod {
#imports
let contract = quote! {
#struct_decl
impl<'a, M: #ethers_providers::Middleware> #name<M> {
@ -105,16 +139,19 @@ impl Context {
#contract_events
}
};
#events_decl
#abi_structs_decl
}
Ok(ExpandedContract {
module: name_mod,
imports,
contract,
events: events_decl,
abi_structs: abi_structs_decl,
})
}
/// 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
let abi_str = args.abi_source.get().context("failed to get ABI JSON")?;
let mut abi_parser = AbiParser::default();
@ -202,4 +239,14 @@ impl Context {
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
}
}

View File

@ -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
fn gen_internal_structs(&self) -> Result<TokenStream> {
let mut structs = TokenStream::new();
@ -37,19 +76,7 @@ impl Context {
ids.sort();
for id in ids {
let sol_struct = &self.internal_structs.structs[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)?);
structs.extend(self.generate_internal_struct(id)?);
}
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
fn gen_human_readable_structs(&self) -> Result<TokenStream> {
let mut structs = TokenStream::new();
let mut names: Vec<_> = self.abi_parser.structs.keys().collect();
names.sort();
for name in names {
let sol_struct = &self.abi_parser.structs[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 ),*
}
});
structs.extend(self.generate_human_readable_struct(name)?);
}
Ok(structs)
}
@ -209,6 +244,7 @@ pub struct InternalStructs {
}
impl InternalStructs {
/// Creates a new instance with a filled type mapping table based on the abi
pub fn new(abi: RawAbi) -> Self {
let mut top_level_internal_types = HashMap::new();
let mut function_params = HashMap::new();
@ -285,6 +321,11 @@ impl InternalStructs {
.and_then(|id| self.rust_type_names.get(id))
.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`.

View File

@ -10,7 +10,8 @@
#[path = "test/macros.rs"]
mod test_macros;
mod contract;
/// Contains types to generate rust bindings for solidity contracts
pub mod contract;
use contract::Context;
pub mod rawabi;
@ -126,7 +127,7 @@ impl Abigen {
/// Generates the contract bindings.
pub fn generate(self) -> Result<ContractBindings> {
let rustfmt = self.rustfmt;
let tokens = Context::expand(self)?;
let tokens = Context::from_abigen(self)?.expand()?.into_tokens();
Ok(ContractBindings { tokens, rustfmt })
}
}

View File

@ -5,16 +5,96 @@ 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::ToTokens;
use std::collections::HashSet;
use quote::{quote, ToTokens};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use syn::ext::IdentExt;
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
use syn::{braced, parenthesized, Ident, LitStr, Path, Token};
pub(crate) fn expand(args: ContractArgs) -> Result<TokenStream2, Box<dyn Error>> {
Ok(args.into_builder()?.generate()?.into_tokens())
/// A series of `ContractArgs` separated by `;`
#[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.
@ -62,14 +142,27 @@ impl ParseInner for ContractArgs {
(literal.span(), literal.value())
};
if !input.is_empty() {
let mut parameters = Vec::new();
let lookahead = input.lookahead1();
if lookahead.peek(Token![,]) {
input.parse::<Token![,]>()?;
}
let parameters = input
.parse_terminated::<_, Token![,]>(Parameter::parse)?
.into_iter()
.collect();
loop {
if input.is_empty() {
break;
}
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((
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]
fn parse_contract_args() {
let args = contract_args!(TestContract, "path/to/abi.json");

View File

@ -12,16 +12,15 @@ use syn::{
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 hex::FromHex;
use spanned::Spanned;
mod abigen;
mod spanned;
/// Proc macro to generate type-safe bindings to a contract. This macro accepts
/// an Ethereum contract ABI or a path. Note that this path is rooted in
/// Proc macro to generate type-safe bindings to a contract(s). This macro accepts
/// one or more Ethereum contract ABI or a path. Note that this path is rooted in
/// the crate's root `CARGO_MANIFEST_DIR`.
///
/// # Examples
@ -55,6 +54,8 @@ mod spanned;
/// - `event_derives`: A list of additional derives that should be added to
/// contract event structs and enums.
///
/// # Example
///
/// ```ignore
/// abigen!(
/// MyContract,
@ -65,13 +66,32 @@ mod spanned;
/// 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]
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();
expand(args.into_inner())
.unwrap_or_else(|e| Error::new(span, format!("{:?}", e)).to_compile_error())
contracts
.expand()
.unwrap_or_else(|err| err.to_compile_error())
.into()
}

View File

@ -2,6 +2,7 @@
//! Test cases to validate the `abigen!` macro
use ethers_contract::{abigen, EthEvent};
use ethers_core::abi::{Address, Tokenizable};
use ethers_core::types::U256;
use ethers_providers::Provider;
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]
fn can_gen_structs_readable() {
abigen!(
@ -77,6 +105,57 @@ fn can_generate_internal_structs() {
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]
fn can_gen_human_readable_with_structs() {
abigen!(