use super::{types, util, Context}; use ethers_core::{ abi::{Function, FunctionExt, Param, StateMutability}, types::Selector, }; use anyhow::{anyhow, Context as _, Result}; use inflector::Inflector; use proc_macro2::{Literal, TokenStream}; use quote::quote; use rustc_hex::ToHex; use syn::Ident; /// Expands a context into a method struct containing all the generated bindings /// to the Solidity contract methods. impl Context { pub(crate) fn methods(&self) -> Result { let mut aliases = self.method_aliases.clone(); let functions = self .abi .functions() .map(|function| { let signature = function.abi_signature(); expand_function(function, aliases.remove(&signature)) .with_context(|| format!("error expanding function '{}'", signature)) }) .collect::>>()?; Ok(quote! { #( #functions )* }) } } #[allow(unused)] fn expand_function(function: &Function, alias: Option) -> Result { let name = alias.unwrap_or_else(|| util::safe_ident(&function.name.to_snake_case())); let selector = expand_selector(function.selector()); let input = expand_inputs(&function.inputs)?; let outputs = expand_fn_outputs(&function.outputs)?; let is_mutable = matches!( function.state_mutability, StateMutability::Nonpayable | StateMutability::Payable ); let result = if !is_mutable { quote! { ContractCall<'a, P, S, #outputs> } } else { quote! { ContractCall<'a, P, S, H256> } }; let arg = expand_inputs_call_arg(&function.inputs); let doc = util::expand_doc(&format!( "Calls the contract's `{}` (0x{}) function", function.name, function.selector().to_hex::() )); Ok(quote! { #doc pub fn #name(&self #input) -> #result { self.0.method_hash(#selector, #arg) .expect("method not found (this should never happen)") } }) } // converts the function params to name/type pairs pub(crate) fn expand_inputs(inputs: &[Param]) -> Result { let params = inputs .iter() .enumerate() .map(|(i, param)| { let name = util::expand_input_name(i, ¶m.name); let kind = types::expand(¶m.kind)?; Ok(quote! { #name: #kind }) }) .collect::>>()?; Ok(quote! { #( , #params )* }) } // packs the argument in a tuple to be used for the contract call pub(crate) fn expand_inputs_call_arg(inputs: &[Param]) -> TokenStream { let names = inputs .iter() .enumerate() .map(|(i, param)| util::expand_input_name(i, ¶m.name)); quote! { ( #( #names ,)* ) } } fn expand_fn_outputs(outputs: &[Param]) -> Result { match outputs.len() { 0 => Ok(quote! { () }), 1 => types::expand(&outputs[0].kind), _ => { let types = outputs .iter() .map(|param| types::expand(¶m.kind)) .collect::>>()?; Ok(quote! { (#( #types ),*) }) } } } fn expand_selector(selector: Selector) -> TokenStream { let bytes = selector.iter().copied().map(Literal::u8_unsuffixed); quote! { [#( #bytes ),*] } } #[cfg(test)] mod tests { use super::*; use ethers_core::abi::ParamType; #[test] fn expand_inputs_empty() { assert_quote!(expand_inputs(&[]).unwrap().to_string(), {},); } #[test] fn expand_inputs_() { assert_quote!( expand_inputs( &[ Param { name: "a".to_string(), kind: ParamType::Bool, }, Param { name: "b".to_string(), kind: ParamType::Address, }, ], ) .unwrap(), { , a: bool, b: Address }, ); } #[test] fn expand_fn_outputs_empty() { assert_quote!(expand_fn_outputs(&[],).unwrap(), { () }); } #[test] fn expand_fn_outputs_single() { assert_quote!( expand_fn_outputs(&[Param { name: "a".to_string(), kind: ParamType::Bool, }]) .unwrap(), { bool }, ); } #[test] fn expand_fn_outputs_muliple() { assert_quote!( expand_fn_outputs(&[ Param { name: "a".to_string(), kind: ParamType::Bool, }, Param { name: "b".to_string(), kind: ParamType::Address, }, ],) .unwrap(), { (bool, Address) }, ); } }