From c9a7b4acafed1c71f605bd727edd166e9bcd8bd2 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Mon, 13 Mar 2023 20:49:32 +0100 Subject: [PATCH] refactor(abigen): keep and use parsed spans (#2247) * order * refactor(abigen): keep and use spans * chore: use getters instead of making fields public * fix: tests * docs: update abigen documentation * chore: clippy --- Cargo.lock | 2 - .../ethers-contract-abigen/Cargo.toml | 2 +- .../ethers-contract-abigen/src/contract.rs | 19 +- .../ethers-contract-abigen/src/lib.rs | 135 ++++-- .../ethers-contract-abigen/src/multi.rs | 2 +- .../ethers-contract-derive/Cargo.toml | 2 - .../ethers-contract-derive/src/abigen.rs | 415 ++++++++---------- .../ethers-contract-derive/src/lib.rs | 75 ++-- 8 files changed, 345 insertions(+), 307 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 573c7dcb..91a208e1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1382,11 +1382,9 @@ version = "2.0.0" dependencies = [ "ethers-contract-abigen", "ethers-core", - "eyre", "hex", "proc-macro2", "quote", - "serde_json", "syn", ] diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index fcc9cce2..ba752d62 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -19,7 +19,7 @@ ethers-etherscan = { version = "^2.0.0", path = "../../ethers-etherscan", defaul proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0.12", default-features = false, features = ["full"] } +syn = { version = "1.0.12", default-features = false, features = ["full", "extra-traits"] } prettyplease = "0.1.23" Inflector = "0.11" diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index a9ca8bf6..d831e9e6 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -120,7 +120,7 @@ pub struct Context { } impl Context { - /// Expands the whole rust contract + /// Generates the tokens. pub fn expand(&self) -> Result { let name = &self.contract_ident; let name_mod = util::ident(&util::safe_module_name(&self.contract_name)); @@ -232,8 +232,6 @@ impl Context { } }; - 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 // method will be re-defined. @@ -281,27 +279,20 @@ impl Context { ); } - let extra_derives = args - .derives - .iter() - .map(|derive| syn::parse_str::(derive)) - .collect::, _>>() - .wrap_err("failed to parse event derives")?; - - Ok(Context { + Ok(Self { abi, human_readable, abi_str: Literal::string(&abi_str), abi_parser, internal_structs, - contract_ident, - contract_name: args.contract_name, + contract_name: args.contract_name.to_string(), + contract_ident: args.contract_name, contract_bytecode, contract_deployed_bytecode, method_aliases, error_aliases: Default::default(), - extra_derives, event_aliases, + extra_derives: args.derives, }) } diff --git a/ethers-contract/ethers-contract-abigen/src/lib.rs b/ethers-contract/ethers-contract-abigen/src/lib.rs index 984a11cc..8ea9a972 100644 --- a/ethers-contract/ethers-contract-abigen/src/lib.rs +++ b/ethers-contract/ethers-contract-abigen/src/lib.rs @@ -36,7 +36,7 @@ pub use ethers_core::types::Address; use contract::{Context, ExpandedContract}; use eyre::{Context as _, Result}; -use proc_macro2::TokenStream; +use proc_macro2::{Ident, TokenStream}; use quote::ToTokens; use std::{collections::HashMap, fmt, fs, io, path::Path}; @@ -57,11 +57,10 @@ use std::{collections::HashMap, fmt, fs, io, path::Path}; /// which exports an `ERC20Token` struct, along with all its events. /// /// ```no_run -/// # use ethers_contract_abigen::Abigen; -/// # fn foo() -> Result<(), Box> { +/// use ethers_contract_abigen::Abigen; +/// /// Abigen::new("ERC20Token", "./abi.json")?.generate()?.write_to_file("token.rs")?; -/// # Ok(()) -/// # } +/// # Ok::<_, Box>(()) #[derive(Clone, Debug)] #[must_use = "Abigen does nothing unless you generate or expand it."] pub struct Abigen { @@ -69,31 +68,35 @@ pub struct Abigen { abi_source: Source, /// The contract's name to use for the generated type. - contract_name: String, - - /// Manually specified contract method aliases. - method_aliases: HashMap, - - /// Manually specified `derive` macros added to all structs and enums. - derives: Vec, + contract_name: Ident, /// Whether to format the generated bindings using [`prettyplease`]. format: bool, + /// Manually specified contract method aliases. + method_aliases: HashMap, + /// Manually specified event name aliases. event_aliases: HashMap, /// Manually specified error name aliases. error_aliases: HashMap, + + /// Manually specified `derive` macros added to all structs and enums. + derives: Vec, } impl Abigen { - /// Creates a new builder with the given [ABI Source][Source]. - pub fn new, S: AsRef>(contract_name: T, abi_source: S) -> Result { - let abi_source = abi_source.as_ref().parse()?; + /// Creates a new builder with the given contract name and ABI source strings. + /// + /// # Errors + /// + /// If `contract_name` could not be parsed as a valid [Ident], or if `abi_source` could not be + /// parsed as a valid [Source]. + pub fn new, S: AsRef>(contract_name: T, abi_source: S) -> Result { Ok(Self { - abi_source, - contract_name: contract_name.into(), + abi_source: abi_source.as_ref().parse()?, + contract_name: syn::parse_str(contract_name.as_ref())?, format: true, method_aliases: Default::default(), derives: Default::default(), @@ -102,6 +105,19 @@ impl Abigen { }) } + /// Creates a new builder with the given contract name [Ident] and [ABI source][Source]. + pub fn new_raw(contract_name: Ident, abi_source: Source) -> Self { + Self { + contract_name, + abi_source, + format: true, + method_aliases: Default::default(), + derives: Default::default(), + event_aliases: Default::default(), + error_aliases: Default::default(), + } + } + /// Attempts to load a new builder from an ABI JSON file at the specific path. pub fn from_file(path: impl AsRef) -> Result { let path = dunce::canonicalize(path).wrap_err("File does not exist")?; @@ -155,6 +171,20 @@ impl Abigen { self } + #[deprecated = "Use add_derive instead"] + #[doc(hidden)] + pub fn add_event_derive>(self, derive: S) -> Result { + self.add_derive(derive) + } + + /// Add a custom derive to the derives for all structs and enums. + /// + /// For example, this makes it possible to derive serde::Serialize and serde::Deserialize. + pub fn add_derive>(mut self, derive: S) -> Result { + self.derives.push(syn::parse_str(derive.as_ref())?); + Ok(self) + } + #[deprecated = "Use format instead"] #[doc(hidden)] pub fn rustfmt(mut self, rustfmt: bool) -> Self { @@ -171,25 +201,10 @@ impl Abigen { self } - #[deprecated = "Use add_derive instead"] - #[doc(hidden)] - pub fn add_event_derive>(mut self, derive: S) -> Self { - self.derives.push(derive.into()); - self - } - - /// Add a custom derive to the derives for all structs and enums. - /// - /// For example, this makes it possible to derive serde::Serialize and serde::Deserialize. - pub fn add_derive>(mut self, derive: S) -> Self { - self.derives.push(derive.into()); - self - } - /// Generates the contract bindings. pub fn generate(self) -> Result { let format = self.format; - let name = self.contract_name.clone(); + let name = self.contract_name.to_string(); let (expanded, _) = self.expand()?; Ok(ContractBindings { tokens: expanded.into_tokens(), format, name }) } @@ -202,7 +217,59 @@ impl Abigen { } } -/// Type-safe contract bindings generated by `Abigen`. +impl Abigen { + /// Returns a reference to the contract's ABI source. + pub fn source(&self) -> &Source { + &self.abi_source + } + + /// Returns a mutable reference to the contract's ABI source. + pub fn source_mut(&mut self) -> &mut Source { + &mut self.abi_source + } + + /// Returns a reference to the contract's name. + pub fn name(&self) -> &Ident { + &self.contract_name + } + + /// Returns a mutable reference to the contract's name. + pub fn name_mut(&mut self) -> &mut Ident { + &mut self.contract_name + } + + /// Returns a reference to the contract's method aliases. + pub fn method_aliases(&self) -> &HashMap { + &self.method_aliases + } + + /// Returns a mutable reference to the contract's method aliases. + pub fn method_aliases_mut(&mut self) -> &mut HashMap { + &mut self.method_aliases + } + + /// Returns a reference to the contract's event aliases. + pub fn event_aliases(&self) -> &HashMap { + &self.event_aliases + } + + /// Returns a mutable reference to the contract's event aliases. + pub fn error_aliases_mut(&mut self) -> &mut HashMap { + &mut self.error_aliases + } + + /// Returns a reference to the contract's derives. + pub fn derives(&self) -> &Vec { + &self.derives + } + + /// Returns a mutable reference to the contract's derives. + pub fn derives_mut(&mut self) -> &mut Vec { + &mut self.derives + } +} + +/// Type-safe contract bindings generated by [Abigen]. /// /// This type can be either written to file or converted to a token stream for a procedural macro. #[derive(Clone)] diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs index c56f7dff..b51bc3a0 100644 --- a/ethers-contract/ethers-contract-abigen/src/multi.rs +++ b/ethers-contract/ethers-contract-abigen/src/multi.rs @@ -129,7 +129,7 @@ impl MultiAbigen { /// Removes all `Abigen` items that should not be included based on the given filter pub fn apply_filter(&mut self, filter: &ContractFilter) { - self.abigens.retain(|abi| filter.is_match(&abi.contract_name)) + self.abigens.retain(|abi| filter.is_match(abi.contract_name.to_string())) } /// Add another Abigen to the module or lib diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index f255ba79..4e87a4cf 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -24,9 +24,7 @@ proc-macro2 = "1.0" quote = "1.0" syn = "1.0.12" -serde_json = "1.0.53" hex = { version = "0.4.3", default-features = false, features = ["std"] } -eyre = "0.6" [package.metadata.docs.rs] all-features = true diff --git a/ethers-contract/ethers-contract-derive/src/abigen.rs b/ethers-contract/ethers-contract-derive/src/abigen.rs index 500fa630..18cb471c 100644 --- a/ethers-contract/ethers-contract-derive/src/abigen.rs +++ b/ethers-contract/ethers-contract-derive/src/abigen.rs @@ -1,57 +1,47 @@ //! Implementation of procedural macro for generating type-safe bindings to an Ethereum smart //! contract. -use crate::spanned::{ParseInner, Spanned}; -use ethers_contract_abigen::{ - contract::{Context, ExpandedContract}, - multi::MultiExpansion, - Abigen, -}; -use ethers_core::abi::{Function, FunctionExt, Param, StateMutability}; -use eyre::Result; -use proc_macro2::{Span, TokenStream as TokenStream2}; -use quote::ToTokens; +use crate::spanned::Spanned; +use ethers_contract_abigen::{multi::MultiExpansion, Abigen}; +use proc_macro2::TokenStream; use std::collections::HashSet; use syn::{ braced, ext::IdentExt, parenthesized, - parse::{Error, Parse, ParseStream, Result as ParseResult}, + parse::{Error, Parse, ParseStream, Result}, + punctuated::Punctuated, Ident, LitStr, Path, Token, }; /// A series of `ContractArgs` separated by `;` #[derive(Clone, Debug)] pub(crate) struct Contracts { - pub(crate) inner: Vec<(Span, ContractArgs)>, + pub(crate) inner: Vec, } impl Contracts { - pub(crate) fn expand(self) -> Result { + pub(crate) fn expand(self) -> Result { 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| Error::new(span, err.to_string()))?; + for contract in self.inner { + let span = contract.abi.span(); + let contract = contract + .into_builder() + .and_then(|a| a.expand().map_err(|e| Error::new(span, e)))?; expansions.push(contract); } // expand all contract expansions Ok(MultiExpansion::new(expansions).expand_inplace()) } - - fn expand_contract(contract: ContractArgs) -> Result<(ExpandedContract, Context)> { - contract.into_builder()?.expand() - } } impl Parse for Contracts { - fn parse(input: ParseStream) -> ParseResult { - let inner = input - .parse_terminated::<_, Token![;]>(ContractArgs::spanned_parse)? - .into_iter() - .collect(); + fn parse(input: ParseStream) -> Result { + let inner = + input.parse_terminated::<_, Token![;]>(ContractArgs::parse)?.into_iter().collect(); Ok(Self { inner }) } } @@ -59,71 +49,62 @@ impl Parse for Contracts { /// Contract procedural macro arguments. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct ContractArgs { - name: String, - abi: String, - parameters: Vec, + name: Ident, + abi: LitStr, + parameters: Punctuated, } impl ContractArgs { fn into_builder(self) -> Result { - let mut builder = Abigen::new(&self.name, &self.abi)?; + // use the name's ident + let contract_name = self.name; + let abi = self.abi.value(); + let abi_source = abi.parse().map_err(|e| Error::new(self.abi.span(), e))?; + let mut builder = Abigen::new_raw(contract_name, abi_source); - for parameter in self.parameters.into_iter() { - builder = match parameter { - Parameter::Methods(methods) => methods - .into_iter() - .fold(builder, |builder, m| builder.add_method_alias(m.signature, m.alias)), - Parameter::Derives(derives) => { - derives.into_iter().fold(builder, |builder, derive| builder.add_derive(derive)) - } - }; + for parameter in self.parameters { + match parameter { + Parameter::Methods(methods) => builder + .method_aliases_mut() + .extend(methods.into_iter().map(|m| (m.signature, m.alias.to_string()))), + Parameter::Derives(derives) => builder.derives_mut().extend(derives), + } } Ok(builder) } } -impl ParseInner for ContractArgs { - fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> { - // read the contract name - let name = input.parse::()?.to_string(); +impl Parse for ContractArgs { + fn parse(input: ParseStream) -> Result { + // name + let name = input.parse::()?; - // skip the comma input.parse::()?; + // abi // TODO(nlordell): Due to limitation with the proc-macro Span API, we // can't currently get a path the the file where we were called from; // therefore, the path will always be rooted on the cargo manifest // directory. Eventually we can use the `Span::source_file` API to // have a better experience. - let (span, abi) = { - let literal = input.parse::()?; - (literal.span(), literal.value()) - }; - - let mut parameters = Vec::new(); - let lookahead = input.lookahead1(); - if lookahead.peek(Token![,]) { - input.parse::()?; + let abi = input.parse::()?; + // optional parameters + let mut parameters = Punctuated::default(); + if input.parse::().is_ok() { loop { - if input.is_empty() { + if input.is_empty() || input.peek(Token![;]) { 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::()?; + parameters.push_value(input.parse()?); + if let Ok(comma) = input.parse() { + parameters.push_punct(comma); } } } - Ok((span, ContractArgs { name, abi, parameters })) + Ok(ContractArgs { name, abi, parameters }) } } @@ -131,60 +112,40 @@ impl ParseInner for ContractArgs { #[derive(Clone, Debug, PartialEq, Eq)] enum Parameter { Methods(Vec), - Derives(Vec), + Derives(Punctuated), } impl Parse for Parameter { - fn parse(input: ParseStream) -> ParseResult { - let name = input.call(Ident::parse_any)?; - let param = match name.to_string().as_str() { + fn parse(input: ParseStream) -> Result { + let name = Ident::parse_any(input)?; + match name.to_string().as_str() { "methods" => { let content; braced!(content in input); - let methods = { - let parsed = - content.parse_terminated::<_, Token![;]>(Spanned::::parse)?; + let parsed = content.parse_terminated::<_, Token![;]>(Spanned::::parse)?; - let mut methods = Vec::with_capacity(parsed.len()); - let mut signatures = HashSet::new(); - let mut aliases = HashSet::new(); - for method in parsed { - if !signatures.insert(method.signature.clone()) { - return Err(Error::new( - method.span(), - "duplicate method signature in `abigen!` macro invocation", - )) - } - if !aliases.insert(method.alias.clone()) { - return Err(Error::new( - method.span(), - "duplicate method alias in `abigen!` macro invocation", - )) - } - methods.push(method.into_inner()) + let mut methods = Vec::with_capacity(parsed.len()); + let mut signatures = HashSet::new(); + let mut aliases = HashSet::new(); + for method in parsed { + if !signatures.insert(method.signature.clone()) { + return Err(Error::new(method.span(), "duplicate method signature")) } - - methods - }; - - Parameter::Methods(methods) + if !aliases.insert(method.alias.clone()) { + return Err(Error::new(method.alias.span(), "duplicate method alias")) + } + methods.push(method.into_inner()); + } + Ok(Parameter::Methods(methods)) } "derives" | "event_derives" => { let content; parenthesized!(content in input); - let derives = content - .parse_terminated::<_, Token![,]>(Path::parse)? - .into_iter() - .map(|path| path.to_token_stream().to_string()) - .collect(); - Parameter::Derives(derives) + let derives = content.parse_terminated::<_, Token![,]>(Path::parse)?; + Ok(Parameter::Derives(derives)) } - _ => { - return Err(Error::new(name.span(), format!("unexpected named parameter `{name}`"))) - } - }; - - Ok(param) + _ => Err(Error::new(name.span(), "unexpected named parameter")), + } } } @@ -192,44 +153,40 @@ impl Parse for Parameter { #[derive(Clone, Debug, PartialEq, Eq)] struct Method { signature: String, - alias: String, + alias: Ident, } impl Parse for Method { - fn parse(input: ParseStream) -> ParseResult { - let function = { - let name = input.parse::()?.to_string(); + fn parse(input: ParseStream) -> Result { + // `{name}({params.join(",")})` + let mut signature = String::with_capacity(64); - let content; - parenthesized!(content in input); - let inputs = content - .parse_terminated::<_, Token![,]>(Ident::parse)? - .iter() - .map(|ident| { - let kind = serde_json::from_value(serde_json::json!(&ident.to_string())) - .map_err(|err| Error::new(ident.span(), err))?; - Ok(Param { name: "".into(), kind, internal_type: None }) - }) - .collect::>>()?; + // function name + let name = input.parse::()?; + signature.push_str(&name.to_string()); - #[allow(deprecated)] - Function { - name, - inputs, + // function params + let content; + parenthesized!(content in input); + let params = content.parse_terminated::<_, Token![,]>(Ident::parse)?; + let last_i = params.len().saturating_sub(1); - // NOTE: The output types and const-ness of the function do not - // affect its signature. - outputs: vec![], - state_mutability: StateMutability::NonPayable, - constant: None, + signature.push('('); + for (i, param) in params.into_iter().enumerate() { + let s = param.to_string(); + // validate + ethers_core::abi::ethabi::param_type::Reader::read(&s) + .map_err(|e| Error::new(param.span(), e))?; + signature.push_str(&s); + if i < last_i { + signature.push(','); } - }; - let signature = function.abi_signature(); + } + signature.push(')'); + input.parse::()?; - let alias = { - let ident = input.parse::()?; - ident.to_string() - }; + + let alias = input.parse()?; Ok(Method { signature, alias }) } @@ -238,43 +195,80 @@ impl Parse for Method { #[cfg(test)] mod tests { use super::*; + use proc_macro2::Span; + use quote::quote; + use syn::parse::Parser; macro_rules! contract_args_result { - ($($arg:tt)*) => {{ - use syn::parse::Parser; - as Parse>::parse - .parse2(quote::quote! { $($arg)* }) + ($($tt:tt)+) => {{ + Parser::parse2(Contracts::parse, quote!($($tt)+)) }}; } macro_rules! contract_args { - ($($arg:tt)*) => { - contract_args_result!($($arg)*) + ($($tt:tt)*) => { + contract_args_result!($($tt)*) .expect("failed to parse contract args") - .into_inner() + .inner }; } macro_rules! contract_args_err { - ($($arg:tt)*) => { - contract_args_result!($($arg)*) + ($($tt:tt)*) => { + contract_args_result!($($tt)*) .expect_err("expected parse contract args to error") }; } - #[allow(unused)] fn method(signature: &str, alias: &str) -> Method { - Method { signature: signature.into(), alias: alias.into() } + Method { signature: signature.into(), alias: ident(alias) } } - fn parse_contracts(s: TokenStream2) -> Vec { - use syn::parse::Parser; - Contracts::parse.parse2(s).unwrap().inner.into_iter().map(|(_, c)| c).collect::>() + // Note: AST structs implement PartialEq by comparing the string repr, so the span is ignored. + fn arg( + name: &str, + abi: &str, + parameters: impl IntoIterator, + trailing: bool, + ) -> ContractArgs { + ContractArgs { + name: ident(name), + abi: lit_str(abi), + parameters: params(parameters, trailing), + } + } + + fn ident(s: &str) -> Ident { + Ident::new(s, Span::call_site()) + } + + fn lit_str(s: &str) -> LitStr { + LitStr::new(s, Span::call_site()) + } + + fn params( + v: impl IntoIterator, + trailing: bool, + ) -> Punctuated { + let mut punct: Punctuated = v.into_iter().collect(); + if trailing { + punct.push_punct(Default::default()); + } + punct + } + + fn derives<'a>(v: impl IntoIterator, trailing: bool) -> Parameter { + let mut derives: Punctuated<_, _> = + v.into_iter().map(|s| syn::parse_str::(s).unwrap()).collect(); + if trailing { + derives.push_punct(Default::default()); + } + Parameter::Derives(derives) } #[test] fn parse_multi_contract_args_events() { - let args = parse_contracts(quote::quote! { + let args = contract_args! { TestContract, "path/to/abi.json", event_derives(serde::Deserialize, serde::Serialize); @@ -282,96 +276,85 @@ mod tests { 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::Derives(vec![ - "serde :: Deserialize".into(), - "serde :: Serialize".into(), - ])], - }, - ContractArgs { - name: "TestContract2".to_string(), - abi: "other.json".to_string(), - parameters: vec![Parameter::Derives(vec![ - "serde :: Deserialize".into(), - "serde :: Serialize".into(), - ])], - }, + arg( + "TestContract", + "path/to/abi.json", + [derives(["serde::Deserialize", "serde::Serialize"], false)], + false + ), + arg( + "TestContract2", + "other.json", + [derives(["serde::Deserialize", "serde::Serialize"], false)], + false + ), ] ); } + #[test] fn parse_multi_contract_args_methods() { - let args = parse_contracts(quote::quote! { + let args = contract_args! { TestContract, "path/to/abi.json", - methods { + 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![ + arg( + "TestContract", + "path/to/abi.json", + [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::Derives(vec![ - "serde :: Deserialize".into(), - "serde :: Serialize".into(), - ])], - }, + false + ), + arg( + "TestContract2", + "other.json", + [derives(["serde::Deserialize", "serde::Serialize"], false)], + false + ), ] ); } #[test] fn parse_multi_contract_args() { - let args = parse_contracts(quote::quote! { + let args = contract_args! { TestContract, "path/to/abi.json",; TestContract2, "other.json", - event_derives(serde::Deserialize, serde::Serialize); - }); + 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::Derives(vec![ - "serde :: Deserialize".into(), - "serde :: Serialize".into(), - ])], - }, + arg("TestContract", "path/to/abi.json", [], false), + arg( + "TestContract2", + "other.json", + [derives(["serde::Deserialize", "serde::Serialize"], true)], + false + ), ] ); } @@ -379,21 +362,13 @@ mod tests { #[test] fn parse_contract_args() { let args = contract_args!(TestContract, "path/to/abi.json"); - assert_eq!(args.name, "TestContract"); - assert_eq!(args.abi, "path/to/abi.json"); + assert_eq!(*args.first().unwrap(), arg("TestContract", "path/to/abi.json", [], false)); } #[test] fn parse_contract_args_with_defaults() { let args = contract_args!(TestContract, "[{}]"); - assert_eq!( - args, - ContractArgs { - name: "TestContract".to_string(), - abi: "[{}]".to_string(), - parameters: vec![], - }, - ); + assert_eq!(*args.first().unwrap(), arg("TestContract", "[{}]", [], false)); } #[test] @@ -408,23 +383,19 @@ mod tests { event_derives (Asdf, a::B, a::b::c::D) ); assert_eq!( - args, - ContractArgs { - name: "TestContract".to_string(), - abi: "abi.json".to_string(), - parameters: vec![ - // Parameter::Contract("Contract".into()), + *args.first().unwrap(), + arg( + "TestContract", + "abi.json", + [ Parameter::Methods(vec![ method("myMethod(uint256,bool)", "my_renamed_method"), method("myOtherMethod()", "my_other_renamed_method"), ]), - Parameter::Derives(vec![ - "Asdf".into(), - "a :: B".into(), - "a :: b :: c :: D".into() - ]) + derives(["Asdf", "a::B", "a::b::c::D"], false) ], - }, + false + ) ); } diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index 3000c354..c41de57e 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -1,13 +1,12 @@ -//! Implementation of procedural macro for generating type-safe bindings to an -//! ethereum smart contract. +//! Procedural macros for generating type-safe bindings to an Ethereum smart contract. + #![deny(missing_docs, unsafe_code, unused_crate_dependencies)] #![deny(rustdoc::broken_intra_doc_links)] +use abigen::Contracts; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; -use abigen::Contracts; - pub(crate) mod abi_ty; mod abigen; mod call; @@ -19,18 +18,42 @@ mod event; mod spanned; pub(crate) mod utils; -/// 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 relative paths are -/// rooted in the crate's root `CARGO_MANIFEST_DIR`. -/// Environment variable interpolation is supported via `$` prefix, like -/// `"$CARGO_MANIFEST_DIR/contracts/c.json"` +/// Generates type-safe bindings to an Ethereum smart contract from its ABI. +/// +/// All the accepted ABI sources are listed in the examples below and in [Source]. +/// +/// Note: +/// - relative paths are rooted in the crate's root (`CARGO_MANIFEST_DIR`). +/// - Environment variable interpolation is supported via `$` prefix, like +/// `"$CARGO_MANIFEST_DIR/contracts/c.json"` +/// - Etherscan rate-limits requests to their API. To avoid this, set the `ETHERSCAN_API_KEY` +/// environment variable. +/// +/// Additionally, this macro accepts additional parameters to configure some aspects of the code +/// generation: +/// - `methods`: A list of mappings from method signatures to method names allowing methods names to +/// be explicitely set for contract methods. This also provides a workaround for generating code +/// for contracts with multiple methods with the same name. +/// - `derives`: A list of additional derive macros that are added to all the generated structs and +/// enums, after the default ones which are ([when applicable][tuple_derive_ref]): +/// * [PartialEq] +/// * [Eq] +/// * [Debug] +/// * [Default] +/// * [Hash] +/// +/// [Source]: ethers_contract_abigen::Source +/// [tuple_derive_ref]: https://doc.rust-lang.org/stable/std/primitive.tuple.html#trait-implementations-1 /// /// # Examples /// +/// All the possible ABI sources: +/// /// ```ignore -/// # use ethers_contract_derive::abigen; +/// use ethers_contract_derive::abigen; +/// /// // ABI Path -/// abigen!(MyContract, "MyContract.json"); +/// abigen!(MyContract, "./MyContractABI.json"); /// /// // HTTP(S) source /// abigen!(MyContract, "https://my.domain.local/path/to/contract.json"); @@ -50,19 +73,7 @@ pub(crate) mod utils; /// ]"); /// ``` /// -/// Note that Etherscan rate-limits requests to their API, to avoid this an -/// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use -/// that API key when retrieving the contract ABI. -/// -/// Currently, the proc macro accepts additional parameters to configure some -/// aspects of the code generation. Specifically it accepts: -/// - `methods`: A list of mappings from method signatures to method names allowing methods names to -/// be explicitely set for contract methods. This also provides a workaround for generating code -/// for contracts with multiple methods with the same name. -/// - `event_derives`: A list of additional derives that should be added to contract event structs -/// and enums. -/// -/// # Example +/// Specify additional parameters: /// /// ```ignore /// abigen!( @@ -71,7 +82,7 @@ pub(crate) mod utils; /// methods { /// myMethod(uint256,bool) as my_renamed_method; /// }, -/// event_derives (serde::Deserialize, serde::Serialize), +/// derives(serde::Deserialize, serde::Serialize), /// ); /// ``` /// @@ -83,7 +94,6 @@ pub(crate) mod utils; /// `abigen!` bundles all type duplicates so that all rust contracts also use /// the same rust types. /// -/// # Example Multiple contracts /// ```ignore /// abigen!( /// MyContract, @@ -91,18 +101,21 @@ pub(crate) mod utils; /// methods { /// myMethod(uint256,bool) as my_renamed_method; /// }, -/// event_derives (serde::Deserialize, serde::Serialize); +/// derives(serde::Deserialize, serde::Serialize); /// /// MyOtherContract, /// "path/to/MyOtherContract.json", -/// event_derives (serde::Deserialize, serde::Serialize); +/// derives(serde::Deserialize, serde::Serialize); /// ); /// ``` #[proc_macro] pub fn abigen(input: TokenStream) -> TokenStream { let contracts = parse_macro_input!(input as Contracts); - - contracts.expand().unwrap_or_else(|err| err.to_compile_error()).into() + match contracts.expand() { + Ok(tokens) => tokens, + Err(err) => err.to_compile_error(), + } + .into() } /// Derives the `AbiType` and all `Tokenizable` traits for the labeled type. @@ -146,7 +159,7 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream { #[proc_macro_derive(EthAbiCodec)] pub fn derive_abi_codec(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - TokenStream::from(codec::derive_codec_impl(&input)) + codec::derive_codec_impl(&input).into() } /// Derives `fmt::Display` trait and generates a convenient format for all the