diff --git a/contract/src/lib.rs b/contract/src/lib.rs index 2e9be6db7..aaaa64874 100644 --- a/contract/src/lib.rs +++ b/contract/src/lib.rs @@ -20,4 +20,34 @@ macro_rules! use_contract { struct _Dummy; } }; + + (@inner $module:ident, $path: expr, ($($signature:expr => $alias:expr),*)) => { + #[allow(dead_code)] + #[allow(missing_docs)] + #[allow(unused_imports)] + #[allow(unused_mut)] + #[allow(unused_variables)] + pub mod $module { + #[derive(ethabi_derive::EthabiContract)] + #[ethabi_contract_options(path = $path)] + $(#[ethabi_function_options(signature = $signature, alias = $alias)])* + struct _Dummy; + } + }; + + // match for the extra comma in the last parameter: + // use_contract!{erc721, "../res/ERC721.abi", + // "safeTransferFrom(address,address,uint256,bytes)" => "safe_transfer_with_data", + // } + ($module:ident, $path: expr, $($signature:expr => $alias:expr,)*) => { + use_contract!(@inner $module, $path, ($($signature => $alias),*)); + }; + + // match for: + // use_contract!{erc721, "../res/ERC721.abi", + // "safeTransferFrom(address,address,uint256,bytes)" => "safe_transfer_with_data" + // } + ($module:ident, $path: expr, $($signature:expr => $alias:expr),*) => { + use_contract!(@inner $module, $path, ($($signature => $alias),*)); + }; } diff --git a/derive/src/contract.rs b/derive/src/contract.rs index ef51f237e..66479214f 100644 --- a/derive/src/contract.rs +++ b/derive/src/contract.rs @@ -10,7 +10,12 @@ use ethabi; use proc_macro2::TokenStream; use quote::quote; -use crate::{constructor::Constructor, event::Event, function::Function}; +use crate::{ + constructor::Constructor, + event::Event, + function::Function, + options::ContractOptions +}; /// Structure used to generate rust interface for solidity contract. pub struct Contract { @@ -30,6 +35,29 @@ impl<'a> From<&'a ethabi::Contract> for Contract { } impl Contract { + pub fn new(c: ðabi::Contract, options: Option) -> Self { + + let functions: Vec = match options { + Some(contract_options) => { + c.functions() + .map(|function| { + let mut func = Function::from(function); + if let Some(fn_options) = contract_options.functions.get(&func.signature) { + func.module_name = fn_options.alias.to_string(); + } + func + }).collect() + }, + None => c.functions().map(Into::into).collect() + }; + + Self { + constructor: c.constructor.as_ref().map(Into::into), + functions, + events: c.events().map(Into::into).collect(), + } + } + /// Generates rust interface for a contract. pub fn generate(&self) -> TokenStream { let constructor = self.constructor.as_ref().map(Constructor::generate); diff --git a/derive/src/function.rs b/derive/src/function.rs index 9bcd78fb2..456847c44 100644 --- a/derive/src/function.rs +++ b/derive/src/function.rs @@ -57,7 +57,13 @@ struct Outputs { /// Structure used to generate contract's function interface. pub struct Function { /// Function name. - name: String, + pub name: String, + /// Function module name. ex: safe_transfer_from + pub module_name: String, + /// Function signature. ex: safeTransferFrom(address,address,uint256) + pub signature: String, + /// Function short signature. ex: 0x42842e0e + pub short_signature: [u8; 4], /// Function input params. inputs: Inputs, /// Function output params. @@ -124,8 +130,12 @@ impl<'a> From<&'a ethabi::Function> for Function { } }; + Function { name: f.name.clone(), + module_name: f.name.clone().to_snake_case(), + signature: f.signature().split(":").collect::>()[0].to_string(), + short_signature: f.short_signature(), inputs: Inputs { tokenize, template_params, recreate_quote: to_ethabi_param_vec(&f.inputs) }, outputs: Outputs { implementation: output_implementation, @@ -141,7 +151,7 @@ impl Function { /// Generates the interface for contract's function. pub fn generate(&self) -> TokenStream { let name = &self.name; - let module_name = syn::Ident::new(&self.name.to_snake_case(), Span::call_site()); + let module_name = syn::Ident::new(&self.module_name, Span::call_site()); let tokenize = &self.inputs.tokenize; let declarations: &Vec<_> = &self.inputs.template_params.iter().map(|i| &i.declaration).collect(); let definitions: &Vec<_> = &self.inputs.template_params.iter().map(|i| &i.definition).collect(); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 3f3b54007..9ece54526 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -14,17 +14,19 @@ mod constructor; mod contract; mod event; mod function; +mod options; use ethabi::{Contract, Param, ParamType, Result}; use heck::SnakeCase; use quote::quote; +use options::ContractOptions; use std::path::PathBuf; use std::{env, fs}; use syn::export::Span; const ERROR_MSG: &str = "`derive(EthabiContract)` failed"; -#[proc_macro_derive(EthabiContract, attributes(ethabi_contract_options))] +#[proc_macro_derive(EthabiContract, attributes(ethabi_contract_options,ethabi_function_options))] pub fn ethabi_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let ast = syn::parse(input).expect(ERROR_MSG); let gen = impl_ethabi_derive(&ast).expect(ERROR_MSG); @@ -32,48 +34,15 @@ pub fn ethabi_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream } fn impl_ethabi_derive(ast: &syn::DeriveInput) -> Result { - let options = get_options(&ast.attrs, "ethabi_contract_options")?; - let path = get_option(&options, "path")?; - let normalized_path = normalize_path(&path)?; + let contract_options = ContractOptions::from_attrs(ast.attrs.as_slice())?; + let normalized_path = normalize_path(&contract_options.path)?; let source_file = fs::File::open(&normalized_path) .map_err(|_| format!("Cannot load contract abi from `{}`", normalized_path.display()))?; let contract = Contract::load(source_file)?; - let c = contract::Contract::from(&contract); + let c = contract::Contract::new(&contract, Some(contract_options)); Ok(c.generate()) } -fn get_options(attrs: &[syn::Attribute], name: &str) -> Result> { - let options = attrs.iter().flat_map(syn::Attribute::parse_meta).find(|meta| meta.path().is_ident(name)); - - match options { - Some(syn::Meta::List(list)) => Ok(list.nested.into_iter().collect()), - _ => Err("Unexpected meta item".into()), - } -} - -fn get_option(options: &[syn::NestedMeta], name: &str) -> Result { - let item = options - .iter() - .flat_map(|nested| match *nested { - syn::NestedMeta::Meta(ref meta) => Some(meta), - _ => None, - }) - .find(|meta| meta.path().is_ident(name)) - .ok_or_else(|| format!("Expected to find option {}", name))?; - - str_value_of_meta_item(item, name) -} - -fn str_value_of_meta_item(item: &syn::Meta, name: &str) -> Result { - if let syn::Meta::NameValue(ref name_value) = *item { - if let syn::Lit::Str(ref value) = name_value.lit { - return Ok(value.value()); - } - } - - Err(format!(r#"`{}` must be in the form `#[{}="something"]`"#, name, name).into()) -} - fn normalize_path(relative_path: &str) -> Result { // workaround for https://github.com/rust-lang/rust/issues/43860 let cargo_toml_directory = env::var("CARGO_MANIFEST_DIR").map_err(|_| "Cannot find manifest file")?; diff --git a/derive/src/options.rs b/derive/src/options.rs new file mode 100644 index 000000000..7fd44be1d --- /dev/null +++ b/derive/src/options.rs @@ -0,0 +1,81 @@ +use syn::Attribute; +use ethabi::Result; +use std::collections::HashMap; + +pub struct FunctionOptions { + pub signature: String, + pub alias: String, +} + +pub struct ContractOptions { + pub path: String, + pub functions: HashMap, +} + +impl ContractOptions { + pub fn from_attrs(attrs: &[Attribute]) -> Result { + let options = get_options(attrs, "ethabi_contract_options")?; + let path = get_option(&options, "path")?; + let functions = get_function_options(attrs)? + .into_iter() + .fold(HashMap::new(), |mut map, option| { + map.entry(option.signature.to_string()).or_insert(option); + map + }); + Ok(Self { + path, + functions, + }) + } +} + +fn get_function_options(attrs: &[syn::Attribute]) -> Result> { + attrs + .iter() + .flat_map(syn::Attribute::parse_meta) + .filter(|meta| meta.path().is_ident("ethabi_function_options")) + .filter_map(|meta| -> Option> { + match meta { + syn::Meta::List(list) => Some(list.nested.into_iter().collect()), + _ => None, + } + }) + .map(|nested_meta| -> Result { + let signature = get_option(&nested_meta, "signature")?; + let alias = get_option(&nested_meta, "alias")?; + Ok(FunctionOptions { signature, alias }) + }) + .collect() +} + +fn get_options(attrs: &[syn::Attribute], name: &str) -> Result> { + let options = attrs.iter().flat_map(syn::Attribute::parse_meta).find(|meta| meta.path().is_ident(name)); + + match options { + Some(syn::Meta::List(list)) => Ok(list.nested.into_iter().collect()), + _ => Err("Unexpected meta item".into()), + } +} + +fn get_option(options: &[syn::NestedMeta], name: &str) -> Result { + let item = options + .iter() + .flat_map(|nested| match *nested { + syn::NestedMeta::Meta(ref meta) => Some(meta), + _ => None, + }) + .find(|meta| meta.path().is_ident(name)) + .ok_or_else(|| format!("Expected to find option {}", name))?; + + str_value_of_meta_item(item, name) +} + +fn str_value_of_meta_item(item: &syn::Meta, name: &str) -> Result { + if let syn::Meta::NameValue(ref name_value) = *item { + if let syn::Lit::Str(ref value) = name_value.lit { + return Ok(value.value()); + } + } + + Err(format!(r#"`{}` must be in the form `#[{}="something"]`"#, name, name).into()) +} diff --git a/ethabi/src/function.rs b/ethabi/src/function.rs index a29fc7697..b19a646c5 100644 --- a/ethabi/src/function.rs +++ b/ethabi/src/function.rs @@ -79,6 +79,15 @@ impl Function { (_, _) => format!("{}({}):({})", self.name, inputs, outputs), } } + + /// Returns a signature that uniquely identifies this function. + /// + /// Examples: + /// - `d7d15059` + pub fn short_signature(&self) -> [u8; 4] { + let params = self.input_param_types(); + short_signature(&self.name, ¶ms) + } } #[cfg(test)]