//! Implementation of procedural macro for generating type-safe bindings to an //! ethereum smart contract. 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}; 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}; /// 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 { 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> = 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> { let contract = contract.into_builder()?; let ctx = Context::from_abigen(contract)?; Ok((ctx.expand()?, ctx)) } } impl Parse for Contracts { fn parse(input: ParseStream) -> ParseResult { let inner = input .parse_terminated::<_, Token![;]>(ContractArgs::spanned_parse)? .into_iter() .collect(); Ok(Self { inner }) } } /// Contract procedural macro arguments. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] pub(crate) struct ContractArgs { name: String, abi: String, parameters: Vec, } impl ContractArgs { fn into_builder(self) -> Result> { let mut builder = Abigen::new(&self.name, &self.abi)?; 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::EventDerives(derives) => derives .into_iter() .fold(builder, |builder, derive| builder.add_event_derive(derive)), }; } Ok(builder) } } impl ParseInner for ContractArgs { fn spanned_parse(input: ParseStream) -> ParseResult<(Span, Self)> { // read the contract name let name = input.parse::()?.to_string(); // skip the comma input.parse::()?; // 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::()?; 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::()?; } } } Ok(( span, ContractArgs { name, abi, parameters, }, )) } } /// A single procedural macro parameter. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] enum Parameter { Methods(Vec), EventDerives(Vec), } impl Parse for Parameter { fn parse(input: ParseStream) -> ParseResult { let name = input.call(Ident::parse_any)?; let param = match name.to_string().as_str() { "methods" => { let content; braced!(content in input); let methods = { 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(ParseError::new( method.span(), "duplicate method signature in `abigen!` macro invocation", )); } if !aliases.insert(method.alias.clone()) { return Err(ParseError::new( method.span(), "duplicate method alias in `abigen!` macro invocation", )); } methods.push(method.into_inner()) } methods }; Parameter::Methods(methods) } "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::EventDerives(derives) } _ => { return Err(ParseError::new( name.span(), format!("unexpected named parameter `{}`", name), )) } }; Ok(param) } } /// An explicitely named contract method. #[cfg_attr(test, derive(Debug, Eq, PartialEq))] struct Method { signature: String, alias: String, } impl Parse for Method { fn parse(input: ParseStream) -> ParseResult { let function = { let name = input.parse::()?.to_string(); 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| ParseError::new(ident.span(), err))?; Ok(Param { name: "".into(), kind, internal_type: None, }) }) .collect::>>()?; #[allow(deprecated)] Function { name, inputs, // NOTE: The output types and const-ness of the function do not // affect its signature. outputs: vec![], state_mutability: StateMutability::NonPayable, constant: false, } }; let signature = function.abi_signature(); input.parse::()?; let alias = { let ident = input.parse::()?; ident.to_string() }; Ok(Method { signature, alias }) } } #[cfg(test)] mod tests { use super::*; macro_rules! contract_args_result { ($($arg:tt)*) => {{ use syn::parse::Parser; as Parse>::parse .parse2(quote::quote! { $($arg)* }) }}; } macro_rules! contract_args { ($($arg:tt)*) => { contract_args_result!($($arg)*) .expect("failed to parse contract args") .into_inner() }; } macro_rules! contract_args_err { ($($arg:tt)*) => { contract_args_result!($($arg)*) .expect_err("expected parse contract args to error") }; } #[allow(unused)] fn method(signature: &str, alias: &str) -> Method { Method { signature: signature.into(), alias: alias.into(), } } fn parse_contracts(s: TokenStream2) -> Vec { use syn::parse::Parser; Contracts::parse .parse2(s) .unwrap() .inner .into_iter() .map(|(_, c)| c) .collect::>() } #[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"); assert_eq!(args.name, "TestContract"); assert_eq!(args.abi, "path/to/abi.json"); } #[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![], }, ); } #[test] fn parse_contract_args_with_parameters() { let args = contract_args!( TestContract, "abi.json", methods { myMethod(uint256, bool) as my_renamed_method; myOtherMethod() as my_other_renamed_method; }, 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()), Parameter::Methods(vec![ method("myMethod(uint256,bool)", "my_renamed_method"), method("myOtherMethod()", "my_other_renamed_method"), ]), Parameter::EventDerives(vec![ "Asdf".into(), "a :: B".into(), "a :: b :: c :: D".into() ]) ], }, ); } #[test] fn duplicate_method_rename_error() { contract_args_err!( "abi.json", methods { myMethod(uint256) as my_method_1; myMethod(uint256) as my_method_2; } ); contract_args_err!( "abi.json", methods { myMethod1(uint256) as my_method; myMethod2(uint256) as my_method; } ); } #[test] fn method_invalid_method_parameter_type() { contract_args_err!( "abi.json", methods { myMethod(invalid) as my_method; } ); } }