From f0dde168a42b9e91b81ade3b985f378a8f6025e9 Mon Sep 17 00:00:00 2001 From: Yaroslav Bolyukin Date: Tue, 12 Jul 2022 13:23:24 +0000 Subject: [PATCH] feat(evm-coder): parse #[doc] for interface types Previously, only documentation on items was parsed and preserved in generated code, now you can have documentation on interface itself --- --- a/Cargo.lock +++ b/Cargo.lock @@ -2172,7 +2172,7 @@ version = "0.1.1" dependencies = [ "ethereum", - "evm-coder-macros", + "evm-coder-procedural", "evm-core", "hex", "hex-literal", @@ -2181,7 +2181,7 @@ ] [[package]] -name = "evm-coder-macros" +name = "evm-coder-procedural" version = "0.2.0" dependencies = [ "Inflector", --- a/crates/evm-coder-macros/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "evm-coder-macros" -version = "0.2.0" -license = "GPLv3" -edition = "2021" - -[lib] -proc-macro = true - -[dependencies] -sha3 = "0.10.1" -quote = "1.0" -proc-macro2 = "1.0" -syn = { version = "1.0", features = ["full"] } -hex = "0.4.3" -Inflector = "0.11.4" --- a/crates/evm-coder-macros/src/lib.rs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -#![allow(dead_code)] - -use inflector::cases; -use proc_macro::TokenStream; -use quote::quote; -use sha3::{Digest, Keccak256}; -use syn::{ - DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, - PathSegment, Type, parse_macro_input, spanned::Spanned, -}; - -mod solidity_interface; -mod to_log; - -fn fn_selector_str(input: &str) -> u32 { - let mut hasher = Keccak256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - - let mut selector_bytes = [0; 4]; - selector_bytes.copy_from_slice(&result[0..4]); - - u32::from_be_bytes(selector_bytes) -} - -/// Returns solidity function selector (first 4 bytes of hash) by its -/// textual representation -/// -/// ```ignore -/// use evm_coder_macros::fn_selector; -/// -/// assert_eq!(fn_selector!(transfer(address, uint256)), 0xa9059cbb); -/// ``` -#[proc_macro] -pub fn fn_selector(input: TokenStream) -> TokenStream { - let input = input.to_string().replace(' ', ""); - let selector = fn_selector_str(&input); - - (quote! { - #selector - }) - .into() -} - -fn event_selector_str(input: &str) -> [u8; 32] { - let mut hasher = Keccak256::new(); - hasher.update(input.as_bytes()); - let result = hasher.finalize(); - - let mut selector_bytes = [0; 32]; - selector_bytes.copy_from_slice(&result[0..32]); - selector_bytes -} - -/// Returns solidity topic (hash) by its textual representation -/// -/// ```ignore -/// use evm_coder_macros::event_topic; -/// -/// assert_eq!( -/// format!("{:x}", event_topic!(Transfer(address, address, uint256))), -/// "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", -/// ); -/// ``` -#[proc_macro] -pub fn event_topic(stream: TokenStream) -> TokenStream { - let input = stream.to_string().replace(' ', ""); - let selector_bytes = event_selector_str(&input); - - (quote! { - ::primitive_types::H256([#( - #selector_bytes, - )*]) - }) - .into() -} - -fn parse_path(ty: &Type) -> syn::Result<&Path> { - match &ty { - syn::Type::Path(pat) => { - if let Some(qself) = &pat.qself { - return Err(syn::Error::new(qself.ty.span(), "no receiver expected")); - } - Ok(&pat.path) - } - _ => Err(syn::Error::new(ty.span(), "expected ty to be path")), - } -} - -fn parse_path_segment(path: &Path) -> syn::Result<&PathSegment> { - if path.segments.len() != 1 { - return Err(syn::Error::new( - path.span(), - "expected path to have only segment", - )); - } - let last_segment = &path.segments.last().unwrap(); - Ok(last_segment) -} - -fn parse_ident_from_pat(pat: &Pat) -> syn::Result<&Ident> { - match pat { - Pat::Ident(i) => Ok(&i.ident), - _ => Err(syn::Error::new(pat.span(), "expected pat ident")), - } -} - -fn parse_ident_from_segment(segment: &PathSegment, allow_generics: bool) -> syn::Result<&Ident> { - if segment.arguments != PathArguments::None && !allow_generics { - return Err(syn::Error::new( - segment.arguments.span(), - "unexpected generic type", - )); - } - Ok(&segment.ident) -} - -fn parse_ident_from_path(path: &Path, allow_generics: bool) -> syn::Result<&Ident> { - let segment = parse_path_segment(path)?; - parse_ident_from_segment(segment, allow_generics) -} - -fn parse_ident_from_type(ty: &Type, allow_generics: bool) -> syn::Result<&Ident> { - let path = parse_path(ty)?; - parse_ident_from_path(path, allow_generics) -} - -// Gets T out of Result -fn parse_result_ok(ty: &Type) -> syn::Result<&Type> { - let path = parse_path(ty)?; - let segment = parse_path_segment(path)?; - - if segment.ident != "Result" { - return Err(syn::Error::new( - ty.span(), - "expected Result as return type (no renamed aliases allowed)", - )); - } - let args = match &segment.arguments { - PathArguments::AngleBracketed(e) => e, - _ => { - return Err(syn::Error::new( - segment.arguments.span(), - "missing Result generics", - )) - } - }; - - let args = &args.args; - let arg = args.first().unwrap(); - - let ty = match arg { - GenericArgument::Type(ty) => ty, - _ => { - return Err(syn::Error::new( - arg.span(), - "expected first generic to be type", - )) - } - }; - - Ok(ty) -} - -fn pascal_ident_to_call(ident: &Ident) -> Ident { - let name = format!("{}Call", ident); - Ident::new(&name, ident.span()) -} -fn snake_ident_to_pascal(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::pascalcase::to_pascal_case(&name); - Ident::new(&name, ident.span()) -} -fn snake_ident_to_screaming(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::screamingsnakecase::to_screaming_snake_case(&name); - Ident::new(&name, ident.span()) -} -fn pascal_ident_to_snake_call(ident: &Ident) -> Ident { - let name = ident.to_string(); - let name = cases::snakecase::to_snake_case(&name); - let name = format!("call_{}", name); - Ident::new(&name, ident.span()) -} - -/// Derives call enum implementing [`evm_coder::Callable`], [`evm_coder::Weighted`] -/// and [`evm_coder::Call`] from impl block -/// -/// ## Macro syntax -/// -/// `#[solidity_interface(name, is, inline_is, events)]` -/// - *name*: used in generated code, and for Call enum name -/// - *is*: used to provide call inheritance, not found methods will be delegated to all contracts -/// specified in is/inline_is -/// - *inline_is*: same as is, but selectors for passed contracts will be used by derived ERC165 -/// implementation -/// -/// `#[weight(value)]` -/// Can be added to every method of impl block, used for deriving [`evm_coder::Weighted`], which -/// is used by substrate bridge -/// - *value*: expression, which evaluates to weight required to call this method. -/// This expression can use call arguments to calculate non-constant execution time. -/// This expression should evaluate faster than actual execution does, and may provide worser case -/// than one is called -/// -/// `#[solidity_interface(rename_selector)]` -/// - *rename_selector*: by default, selector name will be generated by transforming method name -/// from snake_case to camelCase. Use this option, if other naming convention is required. -/// I.e: method `token_uri` will be automatically renamed to `tokenUri` in selector, but name -/// required by ERC721 standard is `tokenURI`, thus we need to specify `rename_selector = "tokenURI"` -/// explicitly -/// -/// Also, any contract method may have doc comments, which will be automatically added to generated -/// solidity interface definitions -/// -/// ## Example -/// -/// ```ignore -/// struct SuperContract; -/// struct InlineContract; -/// struct Contract; -/// -/// #[derive(ToLog)] -/// enum ContractEvents { -/// Event(#[indexed] uint32), -/// } -/// -/// #[solidity_interface(name = MyContract, is(SuperContract), inline_is(InlineContract))] -/// impl Contract { -/// /// Multiply two numbers -/// #[weight(200 + a + b)] -/// #[solidity_interface(rename_selector = "mul")] -/// fn mul(&mut self, a: uint32, b: uint32) -> Result { -/// Ok(a.checked_mul(b).ok_or("overflow")?) -/// } -/// } -/// ``` -#[proc_macro_attribute] -pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { - let args = parse_macro_input!(args as solidity_interface::InterfaceInfo); - - let input: ItemImpl = match syn::parse(stream) { - Ok(t) => t, - Err(e) => return e.to_compile_error().into(), - }; - - let expanded = match solidity_interface::SolidityInterface::try_from(args, &input) { - Ok(v) => v.expand(), - Err(e) => e.to_compile_error(), - }; - - (quote! { - #input - - #expanded - }) - .into() -} - -#[proc_macro_attribute] -pub fn solidity(_args: TokenStream, stream: TokenStream) -> TokenStream { - stream -} -#[proc_macro_attribute] -pub fn weight(_args: TokenStream, stream: TokenStream) -> TokenStream { - stream -} - -/// ## Syntax -/// -/// `#[indexed]` -/// Marks this field as indexed, so it will appear in [`ethereum::Log`] topics instead of data -#[proc_macro_derive(ToLog, attributes(indexed))] -pub fn to_log(value: TokenStream) -> TokenStream { - let input = parse_macro_input!(value as DeriveInput); - - match to_log::Events::try_from(&input) { - Ok(e) => e.expand(), - Err(e) => e.to_compile_error(), - } - .into() -} --- a/crates/evm-coder-macros/src/solidity_interface.rs +++ /dev/null @@ -1,1076 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -#![allow(dead_code)] - -use quote::{quote, ToTokens}; -use inflector::cases; -use std::fmt::Write; -use syn::{ - Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta, - MetaNameValue, PatType, PathArguments, ReturnType, Type, - spanned::Spanned, - parse::{Parse, ParseStream}, - parenthesized, Token, LitInt, LitStr, -}; - -use crate::{ - fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_path, parse_path_segment, - parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal, - snake_ident_to_screaming, -}; - -struct Is { - name: Ident, - pascal_call_name: Ident, - snake_call_name: Ident, - via: Option<(Type, Ident)>, -} -impl Is { - fn expand_call_def(&self, gen_ref: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - quote! { - #name(#pascal_call_name #gen_ref) - } - } - - fn expand_interface_id(&self) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - interface_id ^= u32::from_be_bytes(#pascal_call_name::interface_id()); - } - } - - fn expand_supports_interface( - &self, - generics: &proc_macro2::TokenStream, - ) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - <#pascal_call_name #generics>::supports_interface(interface_id) - } - } - - fn expand_variant_weight(&self) -> proc_macro2::TokenStream { - let name = &self.name; - quote! { - Self::#name(call) => call.weight() - } - } - - fn expand_variant_call( - &self, - call_name: &proc_macro2::Ident, - generics: &proc_macro2::TokenStream, - ) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - let via_typ = self - .via - .as_ref() - .map(|(t, _)| quote! {#t}) - .unwrap_or_else(|| quote! {Self}); - let via_map = self - .via - .as_ref() - .map(|(_, i)| quote! {.#i()}) - .unwrap_or_default(); - quote! { - #call_name::#name(call) => return <#via_typ as ::evm_coder::Callable<#pascal_call_name #generics>>::call(self #via_map, Msg { - call, - caller: c.caller, - value: c.value, - }) - } - } - - fn expand_parse(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let name = &self.name; - let pascal_call_name = &self.pascal_call_name; - quote! { - if let Some(parsed_call) = <#pascal_call_name #generics>::parse(method_id, reader)? { - return Ok(Some(Self::#name(parsed_call))) - } - } - } - - fn expand_generator(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { - let pascal_call_name = &self.pascal_call_name; - quote! { - <#pascal_call_name #generics>::generate_solidity_interface(tc, is_impl); - } - } - - fn expand_event_generator(&self) -> proc_macro2::TokenStream { - let name = &self.name; - quote! { - #name::generate_solidity_interface(tc, is_impl); - } - } -} - -#[derive(Default)] -struct IsList(Vec); -impl Parse for IsList { - fn parse(input: ParseStream) -> syn::Result { - let mut out = vec![]; - loop { - if input.is_empty() { - break; - } - let name = input.parse::()?; - let lookahead = input.lookahead1(); - let via = if lookahead.peek(syn::token::Paren) { - let contents; - parenthesized!(contents in input); - let method = contents.parse::()?; - contents.parse::()?; - let ty = contents.parse::()?; - Some((ty, method)) - } else if lookahead.peek(Token![,]) { - None - } else if input.is_empty() { - None - } else { - return Err(lookahead.error()); - }; - out.push(Is { - pascal_call_name: pascal_ident_to_call(&name), - snake_call_name: pascal_ident_to_snake_call(&name), - name, - via, - }); - if input.peek(Token![,]) { - input.parse::()?; - continue; - } else { - break; - } - } - Ok(Self(out)) - } -} - -pub struct InterfaceInfo { - name: Ident, - is: IsList, - inline_is: IsList, - events: IsList, - expect_selector: Option, -} -impl Parse for InterfaceInfo { - fn parse(input: ParseStream) -> syn::Result { - let mut name = None; - let mut is = None; - let mut inline_is = None; - let mut events = None; - let mut expect_selector = None; - // TODO: create proc-macro to optimize proc-macro boilerplate? :D - loop { - let lookahead = input.lookahead1(); - if lookahead.peek(kw::name) { - let k = input.parse::()?; - input.parse::()?; - if name.replace(input.parse::()?).is_some() { - return Err(syn::Error::new(k.span(), "name is already set")); - } - } else if lookahead.peek(kw::is) { - let k = input.parse::()?; - let contents; - parenthesized!(contents in input); - if is.replace(contents.parse::()?).is_some() { - return Err(syn::Error::new(k.span(), "is is already set")); - } - } else if lookahead.peek(kw::inline_is) { - let k = input.parse::()?; - let contents; - parenthesized!(contents in input); - if inline_is.replace(contents.parse::()?).is_some() { - return Err(syn::Error::new(k.span(), "inline_is is already set")); - } - } else if lookahead.peek(kw::events) { - let k = input.parse::()?; - let contents; - parenthesized!(contents in input); - if events.replace(contents.parse::()?).is_some() { - return Err(syn::Error::new(k.span(), "events is already set")); - } - } else if lookahead.peek(kw::expect_selector) { - let k = input.parse::()?; - input.parse::()?; - let value = input.parse::()?; - if expect_selector - .replace(value.base10_parse::()?) - .is_some() - { - return Err(syn::Error::new(k.span(), "expect_selector is already set")); - } - } else if input.is_empty() { - break; - } else { - return Err(lookahead.error()); - } - if input.peek(Token![,]) { - input.parse::()?; - } else { - break; - } - } - Ok(Self { - name: name.ok_or_else(|| syn::Error::new(input.span(), "missing name"))?, - is: is.unwrap_or_default(), - inline_is: inline_is.unwrap_or_default(), - events: events.unwrap_or_default(), - expect_selector, - }) - } -} - -struct MethodInfo { - rename_selector: Option, -} -impl Parse for MethodInfo { - fn parse(input: ParseStream) -> syn::Result { - let mut rename_selector = None; - let lookahead = input.lookahead1(); - if lookahead.peek(kw::rename_selector) { - let k = input.parse::()?; - input.parse::()?; - if rename_selector - .replace(input.parse::()?.value()) - .is_some() - { - return Err(syn::Error::new(k.span(), "rename_selector is already set")); - } - } - Ok(Self { rename_selector }) - } -} - -enum AbiType { - // type - Plain(Ident), - // (type1,type2) - Tuple(Vec), - // type[] - Vec(Box), - // type[20] - Array(Box, usize), -} -impl AbiType { - fn try_from(value: &Type) -> syn::Result { - let value = Self::try_maybe_special_from(value)?; - if value.is_special() { - return Err(syn::Error::new(value.span(), "unexpected special type")); - } - Ok(value) - } - fn try_maybe_special_from(value: &Type) -> syn::Result { - match value { - Type::Array(arr) => { - let wrapped = AbiType::try_from(&arr.elem)?; - match &arr.len { - Expr::Lit(l) => match &l.lit { - Lit::Int(i) => { - let num = i.base10_parse::()?; - Ok(AbiType::Array(Box::new(wrapped), num as usize)) - } - _ => Err(syn::Error::new(arr.len.span(), "should be int literal")), - }, - _ => Err(syn::Error::new(arr.len.span(), "should be literal")), - } - } - Type::Path(_) => { - let path = parse_path(value)?; - let segment = parse_path_segment(path)?; - if segment.ident == "Vec" { - let args = match &segment.arguments { - PathArguments::AngleBracketed(e) => e, - _ => { - return Err(syn::Error::new( - segment.arguments.span(), - "missing Vec generic", - )) - } - }; - let args = &args.args; - if args.len() != 1 { - return Err(syn::Error::new( - args.span(), - "expected only one generic for vec", - )); - } - let arg = args.first().expect("first arg"); - - let ty = match arg { - GenericArgument::Type(ty) => ty, - _ => { - return Err(syn::Error::new( - arg.span(), - "expected first generic to be type", - )) - } - }; - - let wrapped = AbiType::try_from(ty)?; - Ok(Self::Vec(Box::new(wrapped))) - } else { - if !segment.arguments.is_empty() { - return Err(syn::Error::new( - segment.arguments.span(), - "unexpected generic arguments for non-vec type", - )); - } - Ok(Self::Plain(segment.ident.clone())) - } - } - Type::Tuple(t) => { - let mut out = Vec::with_capacity(t.elems.len()); - for el in t.elems.iter() { - out.push(AbiType::try_from(el)?) - } - Ok(Self::Tuple(out)) - } - _ => Err(syn::Error::new( - value.span(), - "unexpected type, only arrays, plain types and tuples are supported", - )), - } - } - fn is_value(&self) -> bool { - matches!(self, Self::Plain(v) if v == "value") - } - fn is_caller(&self) -> bool { - matches!(self, Self::Plain(v) if v == "caller") - } - fn is_special(&self) -> bool { - self.is_caller() || self.is_value() - } - fn selector_ty_buf(&self, buf: &mut String) -> std::fmt::Result { - match self { - AbiType::Plain(t) => { - write!(buf, "{}", t) - } - AbiType::Tuple(t) => { - write!(buf, "(")?; - for (i, t) in t.iter().enumerate() { - if i != 0 { - write!(buf, ",")?; - } - t.selector_ty_buf(buf)?; - } - write!(buf, ")") - } - AbiType::Vec(v) => { - v.selector_ty_buf(buf)?; - write!(buf, "[]") - } - AbiType::Array(v, len) => { - v.selector_ty_buf(buf)?; - write!(buf, "[{}]", len) - } - } - } - fn selector_ty(&self) -> String { - let mut out = String::new(); - self.selector_ty_buf(&mut out).expect("no fmt error"); - out - } -} -impl ToTokens for AbiType { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - match self { - AbiType::Plain(t) => tokens.extend(quote! {#t}), - AbiType::Tuple(t) => { - tokens.extend(quote! {( - #(#t),* - )}); - } - AbiType::Vec(v) => tokens.extend(quote! {Vec<#v>}), - AbiType::Array(v, l) => tokens.extend(quote! {[#v; #l]}), - } - } -} - -struct MethodArg { - name: Ident, - camel_name: String, - ty: AbiType, -} -impl MethodArg { - fn try_from(value: &PatType) -> syn::Result { - let name = parse_ident_from_pat(&value.pat)?.clone(); - Ok(Self { - camel_name: cases::camelcase::to_camel_case(&name.to_string()), - name, - ty: AbiType::try_maybe_special_from(&value.ty)?, - }) - } - fn is_value(&self) -> bool { - self.ty.is_value() - } - fn is_caller(&self) -> bool { - self.ty.is_caller() - } - fn is_special(&self) -> bool { - self.ty.is_special() - } - fn selector_ty(&self) -> String { - assert!(!self.is_special()); - self.ty.selector_ty() - } - - fn expand_call_def(&self) -> proc_macro2::TokenStream { - assert!(!self.is_special()); - let name = &self.name; - let ty = &self.ty; - - quote! { - #name: #ty - } - } - - fn expand_parse(&self) -> proc_macro2::TokenStream { - assert!(!self.is_special()); - let name = &self.name; - quote! { - #name: reader.abi_read()? - } - } - - fn expand_call_arg(&self) -> proc_macro2::TokenStream { - if self.is_value() { - quote! { - c.value.clone() - } - } else if self.is_caller() { - quote! { - c.caller.clone() - } - } else { - let name = &self.name; - quote! { - #name - } - } - } - - fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name.to_string(); - let ty = &self.ty; - quote! { - >::new(#camel_name) - } - } -} - -#[derive(PartialEq)] -enum Mutability { - Mutable, - View, - Pure, -} - -mod kw { - syn::custom_keyword!(weight); - - syn::custom_keyword!(via); - syn::custom_keyword!(name); - syn::custom_keyword!(is); - syn::custom_keyword!(inline_is); - syn::custom_keyword!(events); - syn::custom_keyword!(expect_selector); - - syn::custom_keyword!(rename_selector); -} - -struct Method { - name: Ident, - camel_name: String, - pascal_name: Ident, - screaming_name: Ident, - selector_str: String, - selector: u32, - args: Vec, - has_normal_args: bool, - mutability: Mutability, - result: Type, - weight: Option, - docs: Vec, -} -impl Method { - fn try_from(value: &ImplItemMethod) -> syn::Result { - let mut info = MethodInfo { - rename_selector: None, - }; - let mut docs = Vec::new(); - let mut weight = None; - for attr in &value.attrs { - let ident = parse_ident_from_path(&attr.path, false)?; - if ident == "solidity" { - info = attr.parse_args::()?; - } else if ident == "doc" { - let args = attr.parse_meta().unwrap(); - let value = match args { - Meta::NameValue(MetaNameValue { - lit: Lit::Str(str), .. - }) => str.value(), - _ => unreachable!(), - }; - docs.push(value); - } else if ident == "weight" { - weight = Some(attr.parse_args::()?); - } - } - let ident = &value.sig.ident; - let ident_str = ident.to_string(); - if !cases::snakecase::is_snake_case(&ident_str) { - return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute")); - } - - let mut mutability = Mutability::Pure; - - if let Some(FnArg::Receiver(receiver)) = value - .sig - .inputs - .iter() - .find(|arg| matches!(arg, FnArg::Receiver(_))) - { - if receiver.reference.is_none() { - return Err(syn::Error::new( - receiver.span(), - "receiver should be by ref", - )); - } - if receiver.mutability.is_some() { - mutability = Mutability::Mutable; - } else { - mutability = Mutability::View; - } - } - let mut args = Vec::new(); - for typ in value - .sig - .inputs - .iter() - .filter(|arg| matches!(arg, FnArg::Typed(_))) - { - let typ = match typ { - FnArg::Typed(typ) => typ, - _ => unreachable!(), - }; - args.push(MethodArg::try_from(typ)?); - } - - if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) { - return Err(syn::Error::new( - args.iter().find(|arg| arg.is_value()).unwrap().ty.span(), - "payable function should be mutable", - )); - } - - let result = match &value.sig.output { - ReturnType::Type(_, ty) => ty, - _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result\nif there is no value to return - specify void (which is alias to unit)")), - }; - let result = parse_result_ok(result)?; - - let camel_name = info - .rename_selector - .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); - let mut selector_str = camel_name.clone(); - selector_str.push('('); - let mut has_normal_args = false; - for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { - if i != 0 { - selector_str.push(','); - } - write!(selector_str, "{}", arg.selector_ty()).unwrap(); - has_normal_args = true; - } - selector_str.push(')'); - let selector = fn_selector_str(&selector_str); - - Ok(Self { - name: ident.clone(), - camel_name, - pascal_name: snake_ident_to_pascal(ident), - screaming_name: snake_ident_to_screaming(ident), - selector_str, - selector, - args, - has_normal_args, - mutability, - result: result.clone(), - weight, - docs, - }) - } - fn expand_call_def(&self) -> proc_macro2::TokenStream { - let defs = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| a.expand_call_def()); - let pascal_name = &self.pascal_name; - - if self.has_normal_args { - quote! { - #pascal_name { - #( - #defs, - )* - } - } - } else { - quote! {#pascal_name} - } - } - - fn expand_const(&self) -> proc_macro2::TokenStream { - let screaming_name = &self.screaming_name; - let selector = u32::to_be_bytes(self.selector); - let selector_str = &self.selector_str; - quote! { - #[doc = #selector_str] - const #screaming_name: ::evm_coder::types::bytes4 = [#(#selector,)*]; - } - } - - fn expand_interface_id(&self) -> proc_macro2::TokenStream { - let screaming_name = &self.screaming_name; - quote! { - interface_id ^= u32::from_be_bytes(Self::#screaming_name); - } - } - - fn expand_parse(&self) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - let screaming_name = &self.screaming_name; - if self.has_normal_args { - let parsers = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| a.expand_parse()); - quote! { - Self::#screaming_name => return Ok(Some(Self::#pascal_name { - #( - #parsers, - )* - })) - } - } else { - quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) } - } - } - - fn expand_variant_call(&self, call_name: &proc_macro2::Ident) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - let name = &self.name; - - let matcher = if self.has_normal_args { - let names = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| &a.name); - - quote! {{ - #( - #names, - )* - }} - } else { - quote! {} - }; - - let receiver = match self.mutability { - Mutability::Mutable | Mutability::View => quote! {self.}, - Mutability::Pure => quote! {Self::}, - }; - let args = self.args.iter().map(|a| a.expand_call_arg()); - - quote! { - #call_name::#pascal_name #matcher => { - let result = #receiver #name( - #( - #args, - )* - )?; - (&result).to_result() - } - } - } - - fn expand_variant_weight(&self) -> proc_macro2::TokenStream { - let pascal_name = &self.pascal_name; - if let Some(weight) = &self.weight { - let matcher = if self.has_normal_args { - let names = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(|a| &a.name); - - quote! {{ - #( - #names, - )* - }} - } else { - quote! {} - }; - quote! { - Self::#pascal_name #matcher => (#weight).into() - } - } else { - let matcher = if self.has_normal_args { - quote! {{..}} - } else { - quote! {} - }; - quote! { - Self::#pascal_name #matcher => ().into() - } - } - } - - fn expand_solidity_function(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name; - let mutability = match self.mutability { - Mutability::Mutable => quote! {SolidityMutability::Mutable}, - Mutability::View => quote! { SolidityMutability::View }, - Mutability::Pure => quote! {SolidityMutability::Pure}, - }; - let result = &self.result; - - let args = self - .args - .iter() - .filter(|a| !a.is_special()) - .map(MethodArg::expand_solidity_argument); - let docs = self.docs.iter(); - let selector_str = &self.selector_str; - let selector = self.selector; - - quote! { - SolidityFunction { - docs: &[#(#docs),*], - selector_str: #selector_str, - selector: #selector, - name: #camel_name, - mutability: #mutability, - args: ( - #( - #args, - )* - ), - result: >::default(), - } - } - } -} - -fn generics_list(gen: &Generics) -> proc_macro2::TokenStream { - if gen.params.is_empty() { - return quote! {}; - } - let params = gen.params.iter().map(|p| match p { - syn::GenericParam::Type(id) => { - let v = &id.ident; - quote! {#v} - } - syn::GenericParam::Lifetime(lt) => { - let v = <.lifetime; - quote! {#v} - } - syn::GenericParam::Const(c) => { - let i = &c.ident; - quote! {#i} - } - }); - quote! { #(#params),* } -} -fn generics_reference(gen: &Generics) -> proc_macro2::TokenStream { - if gen.params.is_empty() { - return quote! {}; - } - let list = generics_list(gen); - quote! { <#list> } -} -fn generics_data(gen: &Generics) -> proc_macro2::TokenStream { - let list = generics_list(gen); - if gen.params.len() == 1 { - quote! {#list} - } else { - quote! { (#list) } - } -} - -pub struct SolidityInterface { - generics: Generics, - name: Box, - info: InterfaceInfo, - methods: Vec, -} -impl SolidityInterface { - pub fn try_from(info: InterfaceInfo, value: &ItemImpl) -> syn::Result { - let mut methods = Vec::new(); - - for item in &value.items { - if let ImplItem::Method(method) = item { - methods.push(Method::try_from(method)?) - } - } - Ok(Self { - generics: value.generics.clone(), - name: value.self_ty.clone(), - info, - methods, - }) - } - pub fn expand(self) -> proc_macro2::TokenStream { - let name = self.name; - - let solidity_name = self.info.name.to_string(); - let call_name = pascal_ident_to_call(&self.info.name); - let generics = self.generics; - let gen_ref = generics_reference(&generics); - let gen_data = generics_data(&generics); - let gen_where = &generics.where_clause; - - let call_sub = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|c| Is::expand_call_def(c, &gen_ref)); - let call_parse = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|is| Is::expand_parse(is, &gen_ref)); - let call_variants = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(|c| Is::expand_variant_call(c, &call_name, &gen_ref)); - let weight_variants = self - .info - .inline_is - .0 - .iter() - .chain(self.info.is.0.iter()) - .map(Is::expand_variant_weight); - - let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id); - let supports_interface = self - .info - .is - .0 - .iter() - .map(|is| Is::expand_supports_interface(is, &gen_ref)); - - let calls = self.methods.iter().map(Method::expand_call_def); - let consts = self.methods.iter().map(Method::expand_const); - let interface_id = self.methods.iter().map(Method::expand_interface_id); - let parsers = self.methods.iter().map(Method::expand_parse); - let call_variants_this = self - .methods - .iter() - .map(|m| Method::expand_variant_call(m, &call_name)); - let weight_variants_this = self.methods.iter().map(Method::expand_variant_weight); - let solidity_functions = self.methods.iter().map(Method::expand_solidity_function); - - // TODO: Inline inline_is - let solidity_is = self - .info - .is - .0 - .iter() - .chain(self.info.inline_is.0.iter()) - .map(|is| is.name.to_string()); - let solidity_events_is = self.info.events.0.iter().map(|is| is.name.to_string()); - let solidity_generators = self - .info - .is - .0 - .iter() - .chain(self.info.inline_is.0.iter()) - .map(|is| Is::expand_generator(is, &gen_ref)); - let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator); - - if let Some(expect_selector) = &self.info.expect_selector { - if !self.info.inline_is.0.is_empty() { - return syn::Error::new( - name.span(), - "expect_selector is not compatible with inline_is", - ) - .to_compile_error(); - } - let selector = self - .methods - .iter() - .map(|m| m.selector) - .fold(0, |a, b| a ^ b); - - if *expect_selector != selector { - let mut methods = String::new(); - for meth in self.methods.iter() { - write!(methods, "\n- {}", meth.selector_str).expect("write to string"); - } - return syn::Error::new(name.span(), format!("expected selector mismatch, expected {expect_selector:0>8x}, but implementation has {selector:0>8x}{methods}")).to_compile_error(); - } - } - // let methods = self.methods.iter().map(Method::solidity_def); - - quote! { - #[derive(Debug)] - pub enum #call_name #gen_ref { - ERC165Call(::evm_coder::ERC165Call, ::core::marker::PhantomData<#gen_data>), - #( - #calls, - )* - #( - #call_sub, - )* - } - impl #gen_ref #call_name #gen_ref { - #( - #consts - )* - pub fn interface_id() -> ::evm_coder::types::bytes4 { - let mut interface_id = 0; - #(#interface_id)* - #(#inline_interface_id)* - u32::to_be_bytes(interface_id) - } - pub fn supports_interface(interface_id: ::evm_coder::types::bytes4) -> bool { - interface_id != u32::to_be_bytes(0xffffff) && ( - interface_id == ::evm_coder::ERC165Call::INTERFACE_ID || - interface_id == Self::interface_id() - #( - || #supports_interface - )* - ) - } - pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { - use evm_coder::solidity::*; - use core::fmt::Write; - let interface = SolidityInterface { - name: #solidity_name, - selector: Self::interface_id(), - is: &["Dummy", "ERC165", #( - #solidity_is, - )* #( - #solidity_events_is, - )* ], - functions: (#( - #solidity_functions, - )*), - }; - - let mut out = string::new(); - if #solidity_name.starts_with("Inline") { - out.push_str("/// @dev inlined interface\n"); - } - let _ = interface.format(is_impl, &mut out, tc); - tc.collect(out); - #( - #solidity_event_generators - )* - #( - #solidity_generators - )* - if is_impl { - tc.collect("/// @dev common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); - } else { - tc.collect("/// @dev common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); - } - } - } - impl #gen_ref ::evm_coder::Call for #call_name #gen_ref { - fn parse(method_id: ::evm_coder::types::bytes4, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::execution::Result> { - use ::evm_coder::abi::AbiRead; - match method_id { - ::evm_coder::ERC165Call::INTERFACE_ID => return Ok( - ::evm_coder::ERC165Call::parse(method_id, reader)? - .map(|c| Self::ERC165Call(c, ::core::marker::PhantomData)) - ), - #( - #parsers, - )* - _ => {}, - } - #( - #call_parse - )else* - return Ok(None); - } - } - impl #generics ::evm_coder::Weighted for #call_name #gen_ref - #gen_where - { - #[allow(unused_variables)] - fn weight(&self) -> ::evm_coder::execution::DispatchInfo { - match self { - #( - #weight_variants, - )* - // TODO: It should be very cheap, but not free - Self::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {..}, _) => 100u64.into(), - #( - #weight_variants_this, - )* - } - } - } - impl #generics ::evm_coder::Callable<#call_name #gen_ref> for #name - #gen_where - { - #[allow(unreachable_code)] // In case of no inner calls - fn call(&mut self, c: Msg<#call_name #gen_ref>) -> ::evm_coder::execution::ResultWithPostInfo<::evm_coder::abi::AbiWriter> { - use ::evm_coder::abi::AbiWrite; - match c.call { - #( - #call_variants, - )* - #call_name::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {interface_id}, _) => { - let mut writer = ::evm_coder::abi::AbiWriter::default(); - writer.bool(&<#call_name #gen_ref>::supports_interface(interface_id)); - return Ok(writer.into()); - } - _ => {}, - } - let mut writer = ::evm_coder::abi::AbiWriter::default(); - match c.call { - #( - #call_variants_this, - )* - _ => unreachable!() - } - } - } - } - } -} --- a/crates/evm-coder-macros/src/to_log.rs +++ /dev/null @@ -1,237 +0,0 @@ -// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. -// This file is part of Unique Network. - -// Unique Network is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Unique Network is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Unique Network. If not, see . - -use inflector::cases; -use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; -use std::fmt::Write; -use quote::quote; - -use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; - -struct EventField { - name: Ident, - camel_name: String, - ty: Ident, - indexed: bool, -} - -impl EventField { - fn try_from(field: &Field) -> syn::Result { - let name = field.ident.as_ref().unwrap(); - let ty = parse_ident_from_type(&field.ty, false)?; - let mut indexed = false; - for attr in &field.attrs { - if let Ok(ident) = parse_ident_from_path(&attr.path, false) { - if ident == "indexed" { - indexed = true; - } - } - } - Ok(Self { - name: name.to_owned(), - camel_name: cases::camelcase::to_camel_case(&name.to_string()), - ty: ty.to_owned(), - indexed, - }) - } - fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { - let camel_name = &self.camel_name; - let ty = &self.ty; - let indexed = self.indexed; - quote! { - >::new(#indexed, #camel_name) - } - } -} - -struct Event { - name: Ident, - name_screaming: Ident, - fields: Vec, - selector: [u8; 32], - selector_str: String, -} - -impl Event { - fn try_from(variant: &Variant) -> syn::Result { - let name = &variant.ident; - let name_screaming = snake_ident_to_screaming(name); - - let named = match &variant.fields { - Fields::Named(named) => named, - _ => { - return Err(syn::Error::new( - variant.fields.span(), - "expected named fields", - )) - } - }; - let mut fields = Vec::new(); - for field in &named.named { - fields.push(EventField::try_from(field)?); - } - if fields.iter().filter(|f| f.indexed).count() > 3 { - return Err(syn::Error::new( - variant.fields.span(), - "events can have at most 4 indexed fields (1 indexed field is reserved for event signature)" - )); - } - let mut selector_str = format!("{}(", name); - for (i, arg) in fields.iter().enumerate() { - if i != 0 { - write!(selector_str, ",").unwrap(); - } - write!(selector_str, "{}", arg.ty).unwrap(); - } - selector_str.push(')'); - let selector = crate::event_selector_str(&selector_str); - - Ok(Self { - name: name.to_owned(), - name_screaming, - fields, - selector, - selector_str, - }) - } - - fn expand_serializers(&self) -> proc_macro2::TokenStream { - let name = &self.name; - let name_screaming = &self.name_screaming; - let fields = self.fields.iter().map(|f| &f.name); - - let indexed = self.fields.iter().filter(|f| f.indexed).map(|f| &f.name); - let plain = self.fields.iter().filter(|f| !f.indexed).map(|f| &f.name); - - quote! { - Self::#name {#( - #fields, - )*} => { - topics.push(topic::from(Self::#name_screaming)); - #( - topics.push(#indexed.to_topic()); - )* - #( - #plain.abi_write(&mut writer); - )* - } - } - } - - fn expand_consts(&self) -> proc_macro2::TokenStream { - let name_screaming = &self.name_screaming; - let selector_str = &self.selector_str; - let selector = &self.selector; - - quote! { - #[doc = #selector_str] - const #name_screaming: [u8; 32] = [#( - #selector, - )*]; - } - } - - fn expand_solidity_function(&self) -> proc_macro2::TokenStream { - let name = self.name.to_string(); - let args = self.fields.iter().map(EventField::expand_solidity_argument); - quote! { - SolidityEvent { - name: #name, - args: ( - #( - #args, - )* - ), - } - } - } -} - -pub struct Events { - name: Ident, - events: Vec, -} - -impl Events { - pub fn try_from(data: &DeriveInput) -> syn::Result { - let name = &data.ident; - let en = match &data.data { - Data::Enum(en) => en, - _ => return Err(syn::Error::new(data.span(), "expected enum")), - }; - let mut events = Vec::new(); - for variant in &en.variants { - events.push(Event::try_from(variant)?); - } - Ok(Self { - name: name.to_owned(), - events, - }) - } - pub fn expand(&self) -> proc_macro2::TokenStream { - let name = &self.name; - - let consts = self.events.iter().map(Event::expand_consts); - let serializers = self.events.iter().map(Event::expand_serializers); - let solidity_name = self.name.to_string(); - let solidity_functions = self.events.iter().map(Event::expand_solidity_function); - - quote! { - impl #name { - #( - #consts - )* - - pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { - use evm_coder::solidity::*; - use core::fmt::Write; - let interface = SolidityInterface { - selector: [0; 4], - name: #solidity_name, - is: &[], - functions: (#( - #solidity_functions, - )*), - }; - let mut out = string::new(); - out.push_str("/// @dev inlined interface\n"); - let _ = interface.format(is_impl, &mut out, tc); - tc.collect(out); - } - } - - #[automatically_derived] - impl ::evm_coder::events::ToLog for #name { - fn to_log(&self, contract: address) -> ::ethereum::Log { - use ::evm_coder::events::ToTopic; - use ::evm_coder::abi::AbiWrite; - let mut writer = ::evm_coder::abi::AbiWriter::new(); - let mut topics = Vec::new(); - match self { - #( - #serializers, - )* - } - ::ethereum::Log { - address: contract, - topics, - data: writer.finish(), - } - } - } - } - } -} --- /dev/null +++ b/crates/evm-coder/procedural/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "evm-coder-procedural" +version = "0.2.0" +license = "GPLv3" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +sha3 = "0.10.1" +quote = "1.0" +proc-macro2 = "1.0" +syn = { version = "1.0", features = ["full"] } +hex = "0.4.3" +Inflector = "0.11.4" --- /dev/null +++ b/crates/evm-coder/procedural/src/lib.rs @@ -0,0 +1,298 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +#![allow(dead_code)] + +use inflector::cases; +use proc_macro::TokenStream; +use quote::quote; +use sha3::{Digest, Keccak256}; +use syn::{ + DeriveInput, GenericArgument, Ident, ItemImpl, Pat, Path, PathArguments, + PathSegment, Type, parse_macro_input, spanned::Spanned, +}; + +mod solidity_interface; +mod to_log; + +fn fn_selector_str(input: &str) -> u32 { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 4]; + selector_bytes.copy_from_slice(&result[0..4]); + + u32::from_be_bytes(selector_bytes) +} + +/// Returns solidity function selector (first 4 bytes of hash) by its +/// textual representation +/// +/// ```ignore +/// use evm_coder_macros::fn_selector; +/// +/// assert_eq!(fn_selector!(transfer(address, uint256)), 0xa9059cbb); +/// ``` +#[proc_macro] +pub fn fn_selector(input: TokenStream) -> TokenStream { + let input = input.to_string().replace(' ', ""); + let selector = fn_selector_str(&input); + + (quote! { + #selector + }) + .into() +} + +fn event_selector_str(input: &str) -> [u8; 32] { + let mut hasher = Keccak256::new(); + hasher.update(input.as_bytes()); + let result = hasher.finalize(); + + let mut selector_bytes = [0; 32]; + selector_bytes.copy_from_slice(&result[0..32]); + selector_bytes +} + +/// Returns solidity topic (hash) by its textual representation +/// +/// ```ignore +/// use evm_coder_macros::event_topic; +/// +/// assert_eq!( +/// format!("{:x}", event_topic!(Transfer(address, address, uint256))), +/// "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", +/// ); +/// ``` +#[proc_macro] +pub fn event_topic(stream: TokenStream) -> TokenStream { + let input = stream.to_string().replace(' ', ""); + let selector_bytes = event_selector_str(&input); + + (quote! { + ::primitive_types::H256([#( + #selector_bytes, + )*]) + }) + .into() +} + +fn parse_path(ty: &Type) -> syn::Result<&Path> { + match &ty { + syn::Type::Path(pat) => { + if let Some(qself) = &pat.qself { + return Err(syn::Error::new(qself.ty.span(), "no receiver expected")); + } + Ok(&pat.path) + } + _ => Err(syn::Error::new(ty.span(), "expected ty to be path")), + } +} + +fn parse_path_segment(path: &Path) -> syn::Result<&PathSegment> { + if path.segments.len() != 1 { + return Err(syn::Error::new( + path.span(), + "expected path to have only segment", + )); + } + let last_segment = &path.segments.last().unwrap(); + Ok(last_segment) +} + +fn parse_ident_from_pat(pat: &Pat) -> syn::Result<&Ident> { + match pat { + Pat::Ident(i) => Ok(&i.ident), + _ => Err(syn::Error::new(pat.span(), "expected pat ident")), + } +} + +fn parse_ident_from_segment(segment: &PathSegment, allow_generics: bool) -> syn::Result<&Ident> { + if segment.arguments != PathArguments::None && !allow_generics { + return Err(syn::Error::new( + segment.arguments.span(), + "unexpected generic type", + )); + } + Ok(&segment.ident) +} + +fn parse_ident_from_path(path: &Path, allow_generics: bool) -> syn::Result<&Ident> { + let segment = parse_path_segment(path)?; + parse_ident_from_segment(segment, allow_generics) +} + +fn parse_ident_from_type(ty: &Type, allow_generics: bool) -> syn::Result<&Ident> { + let path = parse_path(ty)?; + parse_ident_from_path(path, allow_generics) +} + +// Gets T out of Result +fn parse_result_ok(ty: &Type) -> syn::Result<&Type> { + let path = parse_path(ty)?; + let segment = parse_path_segment(path)?; + + if segment.ident != "Result" { + return Err(syn::Error::new( + ty.span(), + "expected Result as return type (no renamed aliases allowed)", + )); + } + let args = match &segment.arguments { + PathArguments::AngleBracketed(e) => e, + _ => { + return Err(syn::Error::new( + segment.arguments.span(), + "missing Result generics", + )) + } + }; + + let args = &args.args; + let arg = args.first().unwrap(); + + let ty = match arg { + GenericArgument::Type(ty) => ty, + _ => { + return Err(syn::Error::new( + arg.span(), + "expected first generic to be type", + )) + } + }; + + Ok(ty) +} + +fn pascal_ident_to_call(ident: &Ident) -> Ident { + let name = format!("{}Call", ident); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_pascal(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::pascalcase::to_pascal_case(&name); + Ident::new(&name, ident.span()) +} +fn snake_ident_to_screaming(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::screamingsnakecase::to_screaming_snake_case(&name); + Ident::new(&name, ident.span()) +} +fn pascal_ident_to_snake_call(ident: &Ident) -> Ident { + let name = ident.to_string(); + let name = cases::snakecase::to_snake_case(&name); + let name = format!("call_{}", name); + Ident::new(&name, ident.span()) +} + +/// Derives call enum implementing [`evm_coder::Callable`], [`evm_coder::Weighted`] +/// and [`evm_coder::Call`] from impl block +/// +/// ## Macro syntax +/// +/// `#[solidity_interface(name, is, inline_is, events)]` +/// - *name*: used in generated code, and for Call enum name +/// - *is*: used to provide call inheritance, not found methods will be delegated to all contracts +/// specified in is/inline_is +/// - *inline_is*: same as is, but selectors for passed contracts will be used by derived ERC165 +/// implementation +/// +/// `#[weight(value)]` +/// Can be added to every method of impl block, used for deriving [`evm_coder::Weighted`], which +/// is used by substrate bridge +/// - *value*: expression, which evaluates to weight required to call this method. +/// This expression can use call arguments to calculate non-constant execution time. +/// This expression should evaluate faster than actual execution does, and may provide worser case +/// than one is called +/// +/// `#[solidity_interface(rename_selector)]` +/// - *rename_selector*: by default, selector name will be generated by transforming method name +/// from snake_case to camelCase. Use this option, if other naming convention is required. +/// I.e: method `token_uri` will be automatically renamed to `tokenUri` in selector, but name +/// required by ERC721 standard is `tokenURI`, thus we need to specify `rename_selector = "tokenURI"` +/// explicitly +/// +/// Also, any contract method may have doc comments, which will be automatically added to generated +/// solidity interface definitions +/// +/// ## Example +/// +/// ```ignore +/// struct SuperContract; +/// struct InlineContract; +/// struct Contract; +/// +/// #[derive(ToLog)] +/// enum ContractEvents { +/// Event(#[indexed] uint32), +/// } +/// +/// #[solidity_interface(name = MyContract, is(SuperContract), inline_is(InlineContract))] +/// impl Contract { +/// /// Multiply two numbers +/// #[weight(200 + a + b)] +/// #[solidity_interface(rename_selector = "mul")] +/// fn mul(&mut self, a: uint32, b: uint32) -> Result { +/// Ok(a.checked_mul(b).ok_or("overflow")?) +/// } +/// } +/// ``` +#[proc_macro_attribute] +pub fn solidity_interface(args: TokenStream, stream: TokenStream) -> TokenStream { + let args = parse_macro_input!(args as solidity_interface::InterfaceInfo); + + let input: ItemImpl = match syn::parse(stream) { + Ok(t) => t, + Err(e) => return e.to_compile_error().into(), + }; + + let expanded = match solidity_interface::SolidityInterface::try_from(args, &input) { + Ok(v) => v.expand(), + Err(e) => e.to_compile_error(), + }; + + (quote! { + #input + + #expanded + }) + .into() +} + +#[proc_macro_attribute] +pub fn solidity(_args: TokenStream, stream: TokenStream) -> TokenStream { + stream +} +#[proc_macro_attribute] +pub fn weight(_args: TokenStream, stream: TokenStream) -> TokenStream { + stream +} + +/// ## Syntax +/// +/// `#[indexed]` +/// Marks this field as indexed, so it will appear in [`ethereum::Log`] topics instead of data +#[proc_macro_derive(ToLog, attributes(indexed))] +pub fn to_log(value: TokenStream) -> TokenStream { + let input = parse_macro_input!(value as DeriveInput); + + match to_log::Events::try_from(&input) { + Ok(e) => e.expand(), + Err(e) => e.to_compile_error(), + } + .into() +} --- /dev/null +++ b/crates/evm-coder/procedural/src/solidity_interface.rs @@ -0,0 +1,1095 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +#![allow(dead_code)] + +use quote::{quote, ToTokens}; +use inflector::cases; +use std::fmt::Write; +use syn::{ + Expr, FnArg, GenericArgument, Generics, Ident, ImplItem, ImplItemMethod, ItemImpl, Lit, Meta, + MetaNameValue, PatType, PathArguments, ReturnType, Type, + spanned::Spanned, + parse::{Parse, ParseStream}, + parenthesized, Token, LitInt, LitStr, +}; + +use crate::{ + fn_selector_str, parse_ident_from_pat, parse_ident_from_path, parse_path, parse_path_segment, + parse_result_ok, pascal_ident_to_call, pascal_ident_to_snake_call, snake_ident_to_pascal, + snake_ident_to_screaming, +}; + +struct Is { + name: Ident, + pascal_call_name: Ident, + snake_call_name: Ident, + via: Option<(Type, Ident)>, +} +impl Is { + fn expand_call_def(&self, gen_ref: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + #name(#pascal_call_name #gen_ref) + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + interface_id ^= u32::from_be_bytes(#pascal_call_name::interface_id()); + } + } + + fn expand_supports_interface( + &self, + generics: &proc_macro2::TokenStream, + ) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + <#pascal_call_name #generics>::supports_interface(interface_id) + } + } + + fn expand_variant_weight(&self) -> proc_macro2::TokenStream { + let name = &self.name; + quote! { + Self::#name(call) => call.weight() + } + } + + fn expand_variant_call( + &self, + call_name: &proc_macro2::Ident, + generics: &proc_macro2::TokenStream, + ) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + let via_typ = self + .via + .as_ref() + .map(|(t, _)| quote! {#t}) + .unwrap_or_else(|| quote! {Self}); + let via_map = self + .via + .as_ref() + .map(|(_, i)| quote! {.#i()}) + .unwrap_or_default(); + quote! { + #call_name::#name(call) => return <#via_typ as ::evm_coder::Callable<#pascal_call_name #generics>>::call(self #via_map, Msg { + call, + caller: c.caller, + value: c.value, + }) + } + } + + fn expand_parse(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let name = &self.name; + let pascal_call_name = &self.pascal_call_name; + quote! { + if let Some(parsed_call) = <#pascal_call_name #generics>::parse(method_id, reader)? { + return Ok(Some(Self::#name(parsed_call))) + } + } + } + + fn expand_generator(&self, generics: &proc_macro2::TokenStream) -> proc_macro2::TokenStream { + let pascal_call_name = &self.pascal_call_name; + quote! { + <#pascal_call_name #generics>::generate_solidity_interface(tc, is_impl); + } + } + + fn expand_event_generator(&self) -> proc_macro2::TokenStream { + let name = &self.name; + quote! { + #name::generate_solidity_interface(tc, is_impl); + } + } +} + +#[derive(Default)] +struct IsList(Vec); +impl Parse for IsList { + fn parse(input: ParseStream) -> syn::Result { + let mut out = vec![]; + loop { + if input.is_empty() { + break; + } + let name = input.parse::()?; + let lookahead = input.lookahead1(); + let via = if lookahead.peek(syn::token::Paren) { + let contents; + parenthesized!(contents in input); + let method = contents.parse::()?; + contents.parse::()?; + let ty = contents.parse::()?; + Some((ty, method)) + } else if lookahead.peek(Token![,]) { + None + } else if input.is_empty() { + None + } else { + return Err(lookahead.error()); + }; + out.push(Is { + pascal_call_name: pascal_ident_to_call(&name), + snake_call_name: pascal_ident_to_snake_call(&name), + name, + via, + }); + if input.peek(Token![,]) { + input.parse::()?; + continue; + } else { + break; + } + } + Ok(Self(out)) + } +} + +pub struct InterfaceInfo { + name: Ident, + is: IsList, + inline_is: IsList, + events: IsList, + expect_selector: Option, +} +impl Parse for InterfaceInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut name = None; + let mut is = None; + let mut inline_is = None; + let mut events = None; + let mut expect_selector = None; + // TODO: create proc-macro to optimize proc-macro boilerplate? :D + loop { + let lookahead = input.lookahead1(); + if lookahead.peek(kw::name) { + let k = input.parse::()?; + input.parse::()?; + if name.replace(input.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "name is already set")); + } + } else if lookahead.peek(kw::is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "is is already set")); + } + } else if lookahead.peek(kw::inline_is) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if inline_is.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "inline_is is already set")); + } + } else if lookahead.peek(kw::events) { + let k = input.parse::()?; + let contents; + parenthesized!(contents in input); + if events.replace(contents.parse::()?).is_some() { + return Err(syn::Error::new(k.span(), "events is already set")); + } + } else if lookahead.peek(kw::expect_selector) { + let k = input.parse::()?; + input.parse::()?; + let value = input.parse::()?; + if expect_selector + .replace(value.base10_parse::()?) + .is_some() + { + return Err(syn::Error::new(k.span(), "expect_selector is already set")); + } + } else if input.is_empty() { + break; + } else { + return Err(lookahead.error()); + } + if input.peek(Token![,]) { + input.parse::()?; + } else { + break; + } + } + Ok(Self { + name: name.ok_or_else(|| syn::Error::new(input.span(), "missing name"))?, + is: is.unwrap_or_default(), + inline_is: inline_is.unwrap_or_default(), + events: events.unwrap_or_default(), + expect_selector, + }) + } +} + +struct MethodInfo { + rename_selector: Option, +} +impl Parse for MethodInfo { + fn parse(input: ParseStream) -> syn::Result { + let mut rename_selector = None; + let lookahead = input.lookahead1(); + if lookahead.peek(kw::rename_selector) { + let k = input.parse::()?; + input.parse::()?; + if rename_selector + .replace(input.parse::()?.value()) + .is_some() + { + return Err(syn::Error::new(k.span(), "rename_selector is already set")); + } + } + Ok(Self { rename_selector }) + } +} + +enum AbiType { + // type + Plain(Ident), + // (type1,type2) + Tuple(Vec), + // type[] + Vec(Box), + // type[20] + Array(Box, usize), +} +impl AbiType { + fn try_from(value: &Type) -> syn::Result { + let value = Self::try_maybe_special_from(value)?; + if value.is_special() { + return Err(syn::Error::new(value.span(), "unexpected special type")); + } + Ok(value) + } + fn try_maybe_special_from(value: &Type) -> syn::Result { + match value { + Type::Array(arr) => { + let wrapped = AbiType::try_from(&arr.elem)?; + match &arr.len { + Expr::Lit(l) => match &l.lit { + Lit::Int(i) => { + let num = i.base10_parse::()?; + Ok(AbiType::Array(Box::new(wrapped), num as usize)) + } + _ => Err(syn::Error::new(arr.len.span(), "should be int literal")), + }, + _ => Err(syn::Error::new(arr.len.span(), "should be literal")), + } + } + Type::Path(_) => { + let path = parse_path(value)?; + let segment = parse_path_segment(path)?; + if segment.ident == "Vec" { + let args = match &segment.arguments { + PathArguments::AngleBracketed(e) => e, + _ => { + return Err(syn::Error::new( + segment.arguments.span(), + "missing Vec generic", + )) + } + }; + let args = &args.args; + if args.len() != 1 { + return Err(syn::Error::new( + args.span(), + "expected only one generic for vec", + )); + } + let arg = args.first().expect("first arg"); + + let ty = match arg { + GenericArgument::Type(ty) => ty, + _ => { + return Err(syn::Error::new( + arg.span(), + "expected first generic to be type", + )) + } + }; + + let wrapped = AbiType::try_from(ty)?; + Ok(Self::Vec(Box::new(wrapped))) + } else { + if !segment.arguments.is_empty() { + return Err(syn::Error::new( + segment.arguments.span(), + "unexpected generic arguments for non-vec type", + )); + } + Ok(Self::Plain(segment.ident.clone())) + } + } + Type::Tuple(t) => { + let mut out = Vec::with_capacity(t.elems.len()); + for el in t.elems.iter() { + out.push(AbiType::try_from(el)?) + } + Ok(Self::Tuple(out)) + } + _ => Err(syn::Error::new( + value.span(), + "unexpected type, only arrays, plain types and tuples are supported", + )), + } + } + fn is_value(&self) -> bool { + matches!(self, Self::Plain(v) if v == "value") + } + fn is_caller(&self) -> bool { + matches!(self, Self::Plain(v) if v == "caller") + } + fn is_special(&self) -> bool { + self.is_caller() || self.is_value() + } + fn selector_ty_buf(&self, buf: &mut String) -> std::fmt::Result { + match self { + AbiType::Plain(t) => { + write!(buf, "{}", t) + } + AbiType::Tuple(t) => { + write!(buf, "(")?; + for (i, t) in t.iter().enumerate() { + if i != 0 { + write!(buf, ",")?; + } + t.selector_ty_buf(buf)?; + } + write!(buf, ")") + } + AbiType::Vec(v) => { + v.selector_ty_buf(buf)?; + write!(buf, "[]") + } + AbiType::Array(v, len) => { + v.selector_ty_buf(buf)?; + write!(buf, "[{}]", len) + } + } + } + fn selector_ty(&self) -> String { + let mut out = String::new(); + self.selector_ty_buf(&mut out).expect("no fmt error"); + out + } +} +impl ToTokens for AbiType { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + match self { + AbiType::Plain(t) => tokens.extend(quote! {#t}), + AbiType::Tuple(t) => { + tokens.extend(quote! {( + #(#t),* + )}); + } + AbiType::Vec(v) => tokens.extend(quote! {Vec<#v>}), + AbiType::Array(v, l) => tokens.extend(quote! {[#v; #l]}), + } + } +} + +struct MethodArg { + name: Ident, + camel_name: String, + ty: AbiType, +} +impl MethodArg { + fn try_from(value: &PatType) -> syn::Result { + let name = parse_ident_from_pat(&value.pat)?.clone(); + Ok(Self { + camel_name: cases::camelcase::to_camel_case(&name.to_string()), + name, + ty: AbiType::try_maybe_special_from(&value.ty)?, + }) + } + fn is_value(&self) -> bool { + self.ty.is_value() + } + fn is_caller(&self) -> bool { + self.ty.is_caller() + } + fn is_special(&self) -> bool { + self.ty.is_special() + } + fn selector_ty(&self) -> String { + assert!(!self.is_special()); + self.ty.selector_ty() + } + + fn expand_call_def(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + let ty = &self.ty; + + quote! { + #name: #ty + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + assert!(!self.is_special()); + let name = &self.name; + quote! { + #name: reader.abi_read()? + } + } + + fn expand_call_arg(&self) -> proc_macro2::TokenStream { + if self.is_value() { + quote! { + c.value.clone() + } + } else if self.is_caller() { + quote! { + c.caller.clone() + } + } else { + let name = &self.name; + quote! { + #name + } + } + } + + fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name.to_string(); + let ty = &self.ty; + quote! { + >::new(#camel_name) + } + } +} + +#[derive(PartialEq)] +enum Mutability { + Mutable, + View, + Pure, +} + +mod kw { + syn::custom_keyword!(weight); + + syn::custom_keyword!(via); + syn::custom_keyword!(name); + syn::custom_keyword!(is); + syn::custom_keyword!(inline_is); + syn::custom_keyword!(events); + syn::custom_keyword!(expect_selector); + + syn::custom_keyword!(rename_selector); +} + +struct Method { + name: Ident, + camel_name: String, + pascal_name: Ident, + screaming_name: Ident, + selector_str: String, + selector: u32, + args: Vec, + has_normal_args: bool, + mutability: Mutability, + result: Type, + weight: Option, + docs: Vec, +} +impl Method { + fn try_from(value: &ImplItemMethod) -> syn::Result { + let mut info = MethodInfo { + rename_selector: None, + }; + let mut docs = Vec::new(); + let mut weight = None; + for attr in &value.attrs { + let ident = parse_ident_from_path(&attr.path, false)?; + if ident == "solidity" { + info = attr.parse_args::()?; + } else if ident == "doc" { + let args = attr.parse_meta().unwrap(); + let value = match args { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(str), .. + }) => str.value(), + _ => unreachable!(), + }; + docs.push(value); + } else if ident == "weight" { + weight = Some(attr.parse_args::()?); + } + } + let ident = &value.sig.ident; + let ident_str = ident.to_string(); + if !cases::snakecase::is_snake_case(&ident_str) { + return Err(syn::Error::new(ident.span(), "method name should be snake_cased\nif alternative solidity name needs to be set - use #[solidity] attribute")); + } + + let mut mutability = Mutability::Pure; + + if let Some(FnArg::Receiver(receiver)) = value + .sig + .inputs + .iter() + .find(|arg| matches!(arg, FnArg::Receiver(_))) + { + if receiver.reference.is_none() { + return Err(syn::Error::new( + receiver.span(), + "receiver should be by ref", + )); + } + if receiver.mutability.is_some() { + mutability = Mutability::Mutable; + } else { + mutability = Mutability::View; + } + } + let mut args = Vec::new(); + for typ in value + .sig + .inputs + .iter() + .filter(|arg| matches!(arg, FnArg::Typed(_))) + { + let typ = match typ { + FnArg::Typed(typ) => typ, + _ => unreachable!(), + }; + args.push(MethodArg::try_from(typ)?); + } + + if mutability != Mutability::Mutable && args.iter().any(|arg| arg.is_value()) { + return Err(syn::Error::new( + args.iter().find(|arg| arg.is_value()).unwrap().ty.span(), + "payable function should be mutable", + )); + } + + let result = match &value.sig.output { + ReturnType::Type(_, ty) => ty, + _ => return Err(syn::Error::new(value.sig.output.span(), "interface method should return Result\nif there is no value to return - specify void (which is alias to unit)")), + }; + let result = parse_result_ok(result)?; + + let camel_name = info + .rename_selector + .unwrap_or_else(|| cases::camelcase::to_camel_case(&ident.to_string())); + let mut selector_str = camel_name.clone(); + selector_str.push('('); + let mut has_normal_args = false; + for (i, arg) in args.iter().filter(|arg| !arg.is_special()).enumerate() { + if i != 0 { + selector_str.push(','); + } + write!(selector_str, "{}", arg.selector_ty()).unwrap(); + has_normal_args = true; + } + selector_str.push(')'); + let selector = fn_selector_str(&selector_str); + + Ok(Self { + name: ident.clone(), + camel_name, + pascal_name: snake_ident_to_pascal(ident), + screaming_name: snake_ident_to_screaming(ident), + selector_str, + selector, + args, + has_normal_args, + mutability, + result: result.clone(), + weight, + docs, + }) + } + fn expand_call_def(&self) -> proc_macro2::TokenStream { + let defs = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_call_def()); + let pascal_name = &self.pascal_name; + + if self.has_normal_args { + quote! { + #pascal_name { + #( + #defs, + )* + } + } + } else { + quote! {#pascal_name} + } + } + + fn expand_const(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + let selector = u32::to_be_bytes(self.selector); + let selector_str = &self.selector_str; + quote! { + #[doc = #selector_str] + const #screaming_name: ::evm_coder::types::bytes4 = [#(#selector,)*]; + } + } + + fn expand_interface_id(&self) -> proc_macro2::TokenStream { + let screaming_name = &self.screaming_name; + quote! { + interface_id ^= u32::from_be_bytes(Self::#screaming_name); + } + } + + fn expand_parse(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let screaming_name = &self.screaming_name; + if self.has_normal_args { + let parsers = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| a.expand_parse()); + quote! { + Self::#screaming_name => return Ok(Some(Self::#pascal_name { + #( + #parsers, + )* + })) + } + } else { + quote! { Self::#screaming_name => return Ok(Some(Self::#pascal_name)) } + } + } + + fn expand_variant_call(&self, call_name: &proc_macro2::Ident) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + let name = &self.name; + + let matcher = if self.has_normal_args { + let names = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| &a.name); + + quote! {{ + #( + #names, + )* + }} + } else { + quote! {} + }; + + let receiver = match self.mutability { + Mutability::Mutable | Mutability::View => quote! {self.}, + Mutability::Pure => quote! {Self::}, + }; + let args = self.args.iter().map(|a| a.expand_call_arg()); + + quote! { + #call_name::#pascal_name #matcher => { + let result = #receiver #name( + #( + #args, + )* + )?; + (&result).to_result() + } + } + } + + fn expand_variant_weight(&self) -> proc_macro2::TokenStream { + let pascal_name = &self.pascal_name; + if let Some(weight) = &self.weight { + let matcher = if self.has_normal_args { + let names = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(|a| &a.name); + + quote! {{ + #( + #names, + )* + }} + } else { + quote! {} + }; + quote! { + Self::#pascal_name #matcher => (#weight).into() + } + } else { + let matcher = if self.has_normal_args { + quote! {{..}} + } else { + quote! {} + }; + quote! { + Self::#pascal_name #matcher => ().into() + } + } + } + + fn expand_solidity_function(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name; + let mutability = match self.mutability { + Mutability::Mutable => quote! {SolidityMutability::Mutable}, + Mutability::View => quote! { SolidityMutability::View }, + Mutability::Pure => quote! {SolidityMutability::Pure}, + }; + let result = &self.result; + + let args = self + .args + .iter() + .filter(|a| !a.is_special()) + .map(MethodArg::expand_solidity_argument); + let docs = self.docs.iter(); + let selector_str = &self.selector_str; + let selector = self.selector; + + quote! { + SolidityFunction { + docs: &[#(#docs),*], + selector_str: #selector_str, + selector: #selector, + name: #camel_name, + mutability: #mutability, + args: ( + #( + #args, + )* + ), + result: >::default(), + } + } + } +} + +fn generics_list(gen: &Generics) -> proc_macro2::TokenStream { + if gen.params.is_empty() { + return quote! {}; + } + let params = gen.params.iter().map(|p| match p { + syn::GenericParam::Type(id) => { + let v = &id.ident; + quote! {#v} + } + syn::GenericParam::Lifetime(lt) => { + let v = <.lifetime; + quote! {#v} + } + syn::GenericParam::Const(c) => { + let i = &c.ident; + quote! {#i} + } + }); + quote! { #(#params),* } +} +fn generics_reference(gen: &Generics) -> proc_macro2::TokenStream { + if gen.params.is_empty() { + return quote! {}; + } + let list = generics_list(gen); + quote! { <#list> } +} +fn generics_data(gen: &Generics) -> proc_macro2::TokenStream { + let list = generics_list(gen); + if gen.params.len() == 1 { + quote! {#list} + } else { + quote! { (#list) } + } +} + +pub struct SolidityInterface { + generics: Generics, + name: Box, + info: InterfaceInfo, + methods: Vec, + docs: Vec, +} +impl SolidityInterface { + pub fn try_from(info: InterfaceInfo, value: &ItemImpl) -> syn::Result { + let mut methods = Vec::new(); + + for item in &value.items { + if let ImplItem::Method(method) = item { + methods.push(Method::try_from(method)?) + } + } + let mut docs = vec![]; + for attr in &value.attrs { + let ident = parse_ident_from_path(&attr.path, false)?; + if ident == "doc" { + let args = attr.parse_meta().unwrap(); + let value = match args { + Meta::NameValue(MetaNameValue { + lit: Lit::Str(str), .. + }) => str.value(), + _ => unreachable!(), + }; + docs.push(value); + } + } + Ok(Self { + generics: value.generics.clone(), + name: value.self_ty.clone(), + info, + methods, + docs, + }) + } + pub fn expand(self) -> proc_macro2::TokenStream { + let name = self.name; + + let solidity_name = self.info.name.to_string(); + let call_name = pascal_ident_to_call(&self.info.name); + let generics = self.generics; + let gen_ref = generics_reference(&generics); + let gen_data = generics_data(&generics); + let gen_where = &generics.where_clause; + + let call_sub = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|c| Is::expand_call_def(c, &gen_ref)); + let call_parse = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|is| Is::expand_parse(is, &gen_ref)); + let call_variants = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(|c| Is::expand_variant_call(c, &call_name, &gen_ref)); + let weight_variants = self + .info + .inline_is + .0 + .iter() + .chain(self.info.is.0.iter()) + .map(Is::expand_variant_weight); + + let inline_interface_id = self.info.inline_is.0.iter().map(Is::expand_interface_id); + let supports_interface = self + .info + .is + .0 + .iter() + .map(|is| Is::expand_supports_interface(is, &gen_ref)); + + let calls = self.methods.iter().map(Method::expand_call_def); + let consts = self.methods.iter().map(Method::expand_const); + let interface_id = self.methods.iter().map(Method::expand_interface_id); + let parsers = self.methods.iter().map(Method::expand_parse); + let call_variants_this = self + .methods + .iter() + .map(|m| Method::expand_variant_call(m, &call_name)); + let weight_variants_this = self.methods.iter().map(Method::expand_variant_weight); + let solidity_functions = self.methods.iter().map(Method::expand_solidity_function); + + // TODO: Inline inline_is + let solidity_is = self + .info + .is + .0 + .iter() + .chain(self.info.inline_is.0.iter()) + .map(|is| is.name.to_string()); + let solidity_events_is = self.info.events.0.iter().map(|is| is.name.to_string()); + let solidity_generators = self + .info + .is + .0 + .iter() + .chain(self.info.inline_is.0.iter()) + .map(|is| Is::expand_generator(is, &gen_ref)); + let solidity_event_generators = self.info.events.0.iter().map(Is::expand_event_generator); + + let docs = self.docs.iter(); + + if let Some(expect_selector) = &self.info.expect_selector { + if !self.info.inline_is.0.is_empty() { + return syn::Error::new( + name.span(), + "expect_selector is not compatible with inline_is", + ) + .to_compile_error(); + } + let selector = self + .methods + .iter() + .map(|m| m.selector) + .fold(0, |a, b| a ^ b); + + if *expect_selector != selector { + let mut methods = String::new(); + for meth in self.methods.iter() { + write!(methods, "\n- {}", meth.selector_str).expect("write to string"); + } + return syn::Error::new(name.span(), format!("expected selector mismatch, expected {expect_selector:0>8x}, but implementation has {selector:0>8x}{methods}")).to_compile_error(); + } + } + // let methods = self.methods.iter().map(Method::solidity_def); + + quote! { + #[derive(Debug)] + pub enum #call_name #gen_ref { + ERC165Call(::evm_coder::ERC165Call, ::core::marker::PhantomData<#gen_data>), + #( + #calls, + )* + #( + #call_sub, + )* + } + impl #gen_ref #call_name #gen_ref { + #( + #consts + )* + pub fn interface_id() -> ::evm_coder::types::bytes4 { + let mut interface_id = 0; + #(#interface_id)* + #(#inline_interface_id)* + u32::to_be_bytes(interface_id) + } + pub fn supports_interface(interface_id: ::evm_coder::types::bytes4) -> bool { + interface_id != u32::to_be_bytes(0xffffff) && ( + interface_id == ::evm_coder::ERC165Call::INTERFACE_ID || + interface_id == Self::interface_id() + #( + || #supports_interface + )* + ) + } + pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { + use evm_coder::solidity::*; + use core::fmt::Write; + let interface = SolidityInterface { + docs: &[#(#docs),*], + name: #solidity_name, + selector: Self::interface_id(), + is: &["Dummy", "ERC165", #( + #solidity_is, + )* #( + #solidity_events_is, + )* ], + functions: (#( + #solidity_functions, + )*), + }; + + let mut out = string::new(); + if #solidity_name.starts_with("Inline") { + out.push_str("/// @dev inlined interface\n"); + } + let _ = interface.format(is_impl, &mut out, tc); + tc.collect(out); + #( + #solidity_event_generators + )* + #( + #solidity_generators + )* + if is_impl { + tc.collect("/// @dev common stubs holder\ncontract Dummy {\n\tuint8 dummy;\n\tstring stub_error = \"this contract is implemented in native\";\n}\ncontract ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool) {\n\t\trequire(false, stub_error);\n\t\tinterfaceID;\n\t\treturn true;\n\t}\n}\n".into()); + } else { + tc.collect("/// @dev common stubs holder\ninterface Dummy {\n}\ninterface ERC165 is Dummy {\n\tfunction supportsInterface(bytes4 interfaceID) external view returns (bool);\n}\n".into()); + } + } + } + impl #gen_ref ::evm_coder::Call for #call_name #gen_ref { + fn parse(method_id: ::evm_coder::types::bytes4, reader: &mut ::evm_coder::abi::AbiReader) -> ::evm_coder::execution::Result> { + use ::evm_coder::abi::AbiRead; + match method_id { + ::evm_coder::ERC165Call::INTERFACE_ID => return Ok( + ::evm_coder::ERC165Call::parse(method_id, reader)? + .map(|c| Self::ERC165Call(c, ::core::marker::PhantomData)) + ), + #( + #parsers, + )* + _ => {}, + } + #( + #call_parse + )else* + return Ok(None); + } + } + impl #generics ::evm_coder::Weighted for #call_name #gen_ref + #gen_where + { + #[allow(unused_variables)] + fn weight(&self) -> ::evm_coder::execution::DispatchInfo { + match self { + #( + #weight_variants, + )* + // TODO: It should be very cheap, but not free + Self::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {..}, _) => 100u64.into(), + #( + #weight_variants_this, + )* + } + } + } + impl #generics ::evm_coder::Callable<#call_name #gen_ref> for #name + #gen_where + { + #[allow(unreachable_code)] // In case of no inner calls + fn call(&mut self, c: Msg<#call_name #gen_ref>) -> ::evm_coder::execution::ResultWithPostInfo<::evm_coder::abi::AbiWriter> { + use ::evm_coder::abi::AbiWrite; + match c.call { + #( + #call_variants, + )* + #call_name::ERC165Call(::evm_coder::ERC165Call::SupportsInterface {interface_id}, _) => { + let mut writer = ::evm_coder::abi::AbiWriter::default(); + writer.bool(&<#call_name #gen_ref>::supports_interface(interface_id)); + return Ok(writer.into()); + } + _ => {}, + } + let mut writer = ::evm_coder::abi::AbiWriter::default(); + match c.call { + #( + #call_variants_this, + )* + _ => unreachable!() + } + } + } + } + } +} --- /dev/null +++ b/crates/evm-coder/procedural/src/to_log.rs @@ -0,0 +1,238 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// This file is part of Unique Network. + +// Unique Network is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Unique Network is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Unique Network. If not, see . + +use inflector::cases; +use syn::{Data, DeriveInput, Field, Fields, Ident, Variant, spanned::Spanned}; +use std::fmt::Write; +use quote::quote; + +use crate::{parse_ident_from_path, parse_ident_from_type, snake_ident_to_screaming}; + +struct EventField { + name: Ident, + camel_name: String, + ty: Ident, + indexed: bool, +} + +impl EventField { + fn try_from(field: &Field) -> syn::Result { + let name = field.ident.as_ref().unwrap(); + let ty = parse_ident_from_type(&field.ty, false)?; + let mut indexed = false; + for attr in &field.attrs { + if let Ok(ident) = parse_ident_from_path(&attr.path, false) { + if ident == "indexed" { + indexed = true; + } + } + } + Ok(Self { + name: name.to_owned(), + camel_name: cases::camelcase::to_camel_case(&name.to_string()), + ty: ty.to_owned(), + indexed, + }) + } + fn expand_solidity_argument(&self) -> proc_macro2::TokenStream { + let camel_name = &self.camel_name; + let ty = &self.ty; + let indexed = self.indexed; + quote! { + >::new(#indexed, #camel_name) + } + } +} + +struct Event { + name: Ident, + name_screaming: Ident, + fields: Vec, + selector: [u8; 32], + selector_str: String, +} + +impl Event { + fn try_from(variant: &Variant) -> syn::Result { + let name = &variant.ident; + let name_screaming = snake_ident_to_screaming(name); + + let named = match &variant.fields { + Fields::Named(named) => named, + _ => { + return Err(syn::Error::new( + variant.fields.span(), + "expected named fields", + )) + } + }; + let mut fields = Vec::new(); + for field in &named.named { + fields.push(EventField::try_from(field)?); + } + if fields.iter().filter(|f| f.indexed).count() > 3 { + return Err(syn::Error::new( + variant.fields.span(), + "events can have at most 4 indexed fields (1 indexed field is reserved for event signature)" + )); + } + let mut selector_str = format!("{}(", name); + for (i, arg) in fields.iter().enumerate() { + if i != 0 { + write!(selector_str, ",").unwrap(); + } + write!(selector_str, "{}", arg.ty).unwrap(); + } + selector_str.push(')'); + let selector = crate::event_selector_str(&selector_str); + + Ok(Self { + name: name.to_owned(), + name_screaming, + fields, + selector, + selector_str, + }) + } + + fn expand_serializers(&self) -> proc_macro2::TokenStream { + let name = &self.name; + let name_screaming = &self.name_screaming; + let fields = self.fields.iter().map(|f| &f.name); + + let indexed = self.fields.iter().filter(|f| f.indexed).map(|f| &f.name); + let plain = self.fields.iter().filter(|f| !f.indexed).map(|f| &f.name); + + quote! { + Self::#name {#( + #fields, + )*} => { + topics.push(topic::from(Self::#name_screaming)); + #( + topics.push(#indexed.to_topic()); + )* + #( + #plain.abi_write(&mut writer); + )* + } + } + } + + fn expand_consts(&self) -> proc_macro2::TokenStream { + let name_screaming = &self.name_screaming; + let selector_str = &self.selector_str; + let selector = &self.selector; + + quote! { + #[doc = #selector_str] + const #name_screaming: [u8; 32] = [#( + #selector, + )*]; + } + } + + fn expand_solidity_function(&self) -> proc_macro2::TokenStream { + let name = self.name.to_string(); + let args = self.fields.iter().map(EventField::expand_solidity_argument); + quote! { + SolidityEvent { + name: #name, + args: ( + #( + #args, + )* + ), + } + } + } +} + +pub struct Events { + name: Ident, + events: Vec, +} + +impl Events { + pub fn try_from(data: &DeriveInput) -> syn::Result { + let name = &data.ident; + let en = match &data.data { + Data::Enum(en) => en, + _ => return Err(syn::Error::new(data.span(), "expected enum")), + }; + let mut events = Vec::new(); + for variant in &en.variants { + events.push(Event::try_from(variant)?); + } + Ok(Self { + name: name.to_owned(), + events, + }) + } + pub fn expand(&self) -> proc_macro2::TokenStream { + let name = &self.name; + + let consts = self.events.iter().map(Event::expand_consts); + let serializers = self.events.iter().map(Event::expand_serializers); + let solidity_name = self.name.to_string(); + let solidity_functions = self.events.iter().map(Event::expand_solidity_function); + + quote! { + impl #name { + #( + #consts + )* + + pub fn generate_solidity_interface(tc: &evm_coder::solidity::TypeCollector, is_impl: bool) { + use evm_coder::solidity::*; + use core::fmt::Write; + let interface = SolidityInterface { + docs: &[], + selector: [0; 4], + name: #solidity_name, + is: &[], + functions: (#( + #solidity_functions, + )*), + }; + let mut out = string::new(); + out.push_str("/// @dev inlined interface\n"); + let _ = interface.format(is_impl, &mut out, tc); + tc.collect(out); + } + } + + #[automatically_derived] + impl ::evm_coder::events::ToLog for #name { + fn to_log(&self, contract: address) -> ::ethereum::Log { + use ::evm_coder::events::ToTopic; + use ::evm_coder::abi::AbiWrite; + let mut writer = ::evm_coder::abi::AbiWriter::new(); + let mut topics = Vec::new(); + match self { + #( + #serializers, + )* + } + ::ethereum::Log { + address: contract, + topics, + data: writer.finish(), + } + } + } + } + } +} --- a/crates/evm-coder/src/solidity.rs +++ b/crates/evm-coder/src/solidity.rs @@ -427,9 +427,6 @@ for doc in self.docs { writeln!(writer, "\t///{}", doc)?; } - if !self.docs.is_empty() { - writeln!(writer, "\t///")?; - } writeln!(writer, "\t/// @dev EVM selector for this function is: 0x{:0>8x},", self.selector)?; writeln!(writer, "\t/// or in textual repr: {}", self.selector_str)?; write!(writer, "\tfunction {}(", self.name)?; @@ -491,6 +488,7 @@ } pub struct SolidityInterface { + pub docs: &'static [&'static str], pub selector: bytes4, pub name: &'static str, pub is: &'static [&'static str], @@ -505,6 +503,9 @@ tc: &TypeCollector, ) -> fmt::Result { const ZERO_BYTES: [u8; 4] = [0; 4]; + for doc in self.docs { + writeln!(out, "///{}", doc)?; + } if self.selector != ZERO_BYTES { writeln!( out, -- gitstuff