From bef7960a2b1a7cacd1c7b0874e03ad4d5ca20d29 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sat, 16 Oct 2021 15:42:17 +0200 Subject: [PATCH] feat: add display support for events (#513) * chore: move proc macro implementation to separate modules * feat: add display derive macro * chore: reexport hex * feat: add EthDisplay * test: add display test * fix: use ? op * feat: derive EthDisplay in abigen * feat: derive display for event enum * chore: update changelog --- CHANGELOG.md | 1 + .../src/contract/events.rs | 15 ++- .../ethers-contract-derive/src/display.rs | 112 ++++++++++++++++++ .../ethers-contract-derive/src/lib.rs | 34 +++++- .../ethers-contract-derive/src/utils.rs | 3 +- ethers-contract/src/lib.rs | 2 +- ethers-contract/tests/common/derive.rs | 38 +++++- ethers-core/src/utils/mod.rs | 3 + 8 files changed, 201 insertions(+), 7 deletions(-) create mode 100644 ethers-contract/ethers-contract-derive/src/display.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ed9894a..696e6d38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Unreleased +- `abigen!` now generates `Display` for all events using the new `EthDisplay` macro [#513](https://github.com/gakonst/ethers-rs/pull/513) - `abigen!` now supports overloaded functions natively [#501](https://github.com/gakonst/ethers-rs/pull/501) - `abigen!` now supports multiple contracts [#498](https://github.com/gakonst/ethers-rs/pull/498) - Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) diff --git a/ethers-contract/ethers-contract-abigen/src/contract/events.rs b/ethers-contract/ethers-contract-abigen/src/contract/events.rs index f976928a..06492e74 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/events.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/events.rs @@ -105,6 +105,16 @@ impl Context { Err(#ethers_core::abi::Error::InvalidData) } } + + impl ::std::fmt::Display for #enum_name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match self { + #( + #enum_name::#variants(element) => element.fmt(f) + ),* + } + } + } } } @@ -150,7 +160,6 @@ impl Context { /// we can replace it fn expand_input_type(&self, input: &EventParam) -> Result { let ethers_core = util::ethers_core_crate(); - Ok(match (&input.kind, input.indexed) { (ParamType::Array(ty), true) => { if let ParamType::Tuple(..) = **ty { @@ -184,7 +193,7 @@ impl Context { quote! { #ethers_core::types::H256 } } (ParamType::Tuple(..), true) => { - // represents an struct + // represents a struct if let Some(ty) = self .abi_parser .structs @@ -269,7 +278,7 @@ impl Context { let ethers_contract = util::ethers_contract_crate(); Ok(quote! { - #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthEvent, #derives)] + #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthEvent, #ethers_contract::EthDisplay, #derives)] #[ethevent( name = #event_abi_name, abi = #abi_signature )] pub #data_type_definition }) diff --git a/ethers-contract/ethers-contract-derive/src/display.rs b/ethers-contract/ethers-contract-derive/src/display.rs new file mode 100644 index 00000000..23ea5516 --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/display.rs @@ -0,0 +1,112 @@ +//! Helper functions for deriving `Display` + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::spanned::Spanned as _; +use syn::{parse::Error, Data, DeriveInput, Fields}; + +use ethers_contract_abigen::ethers_core_crate; +use ethers_core::abi::ParamType; + +use crate::utils; + +/// Derive `fmt::Display` for the given type +pub(crate) fn derive_eth_display_impl(input: DeriveInput) -> Result { + let fields: Vec<_> = match input.data { + Data::Struct(ref data) => match data.fields { + Fields::Named(ref fields) => fields.named.iter().collect(), + Fields::Unnamed(ref fields) => fields.unnamed.iter().collect(), + Fields::Unit => { + vec![] + } + }, + Data::Enum(_) => { + return Err(Error::new( + input.span(), + "Enum types are not supported by EthDisplay", + )) + } + Data::Union(_) => { + return Err(Error::new( + input.span(), + "Union types are not supported by EthDisplay", + )) + } + }; + let core_crate = ethers_core_crate(); + let hex_encode = quote! {#core_crate::utils::hex::encode}; + let mut fmts = TokenStream::new(); + for (idx, field) in fields.iter().enumerate() { + let ident = field + .ident + .clone() + .unwrap_or_else(|| format_ident!("{}", idx)); + let tokens = if let Ok(param) = utils::find_parameter_type(&field.ty) { + match param { + ParamType::Address | ParamType::Uint(_) | ParamType::Int(_) => { + quote! { + write!(f, "{:?}", self.#ident)?; + } + } + ParamType::Bytes => { + quote! { + write!(f, "0x{}", #hex_encode(self.#ident))?; + } + } + ParamType::Bool | ParamType::String => { + quote! { + self.#ident.fmt(f)?; + } + } + ParamType::Tuple(_) => { + quote! { + write!(f, "{:?}", &self.#ident)?; + } + } + ParamType::Array(ty) | ParamType::FixedArray(ty, _) => { + if *ty == ParamType::Uint(8) { + // `u8` + quote! { + write!(f, "0x{}", #hex_encode(&self.#ident[..]))?; + } + } else { + // format as array with `[arr[0].display, arr[1].display,...]` + quote! { + write!(f, "[")?; + for (idx, val) in self.#ident.iter().enumerate() { + write!(f, "{}", val)?; + if idx < self.#ident.len() - 1 { + write!(f, ", ")?; + } + } + write!(f, "]")?; + } + } + } + ParamType::FixedBytes(_) => { + quote! { + write!(f, "0x{}", #hex_encode(&self.#ident))?; + } + } + } + } else { + // could not detect the parameter type and rely on using debug fmt + quote! { + write!(f, "{:?}", &self.#ident)?; + } + }; + fmts.extend(tokens); + if idx < fields.len() - 1 { + fmts.extend(quote! { write!(f, ", ")?;}); + } + } + let name = &input.ident; + Ok(quote! { + impl ::std::fmt::Display for #name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + #fmts + Ok(()) + } + } + }) +} diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index 37a1010c..bf36f392 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -1,6 +1,6 @@ //! Implementation of procedural macro for generating type-safe bindings to an //! ethereum smart contract. -#![deny(missing_docs, unsafe_code, unused)] +#![deny(missing_docs, unsafe_code)] use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; @@ -9,6 +9,7 @@ use abigen::Contracts; pub(crate) mod abi_ty; mod abigen; +mod display; mod event; mod spanned; pub(crate) mod utils; @@ -101,6 +102,37 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream { TokenStream::from(abi_ty::derive_tokenizeable_impl(&input)) } +/// Derives `fmt::Display` trait and generates a convenient format for all the +/// underlying primitive types/tokens. +/// +/// The fields of the structure are formatted comma separated, like `self.0, +/// self.1, self.2,...` +/// +/// # Example +/// +/// ```ignore +/// #[derive(Debug, Clone, EthAbiType, EthDisplay)] +/// struct MyStruct { +/// addr: Address, +/// old_value: String, +/// new_value: String, +/// h: H256, +/// arr_u8: [u8; 32], +/// arr_u16: [u16; 32], +/// v: Vec, +/// } +/// let val = MyStruct {..}; +/// format!("{}", val); +/// ``` +#[proc_macro_derive(EthDisplay, attributes(ethdisplay))] +pub fn derive_eth_display(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + match display::derive_eth_display_impl(input) { + Ok(tokens) => TokenStream::from(tokens), + Err(err) => err.to_compile_error().into(), + } +} + /// Derives the `EthEvent` and `Tokenizeable` trait for the labeled type. /// /// Additional arguments can be specified using the `#[ethevent(...)]` diff --git a/ethers-contract/ethers-contract-derive/src/utils.rs b/ethers-contract/ethers-contract-derive/src/utils.rs index 12bbf770..29e4d849 100644 --- a/ethers-contract/ethers-contract-derive/src/utils.rs +++ b/ethers-contract/ethers-contract-derive/src/utils.rs @@ -94,7 +94,8 @@ pub fn param_type_quote(kind: &ParamType) -> proc_macro2::TokenStream { } } -/// Tries to find the corresponding `ParamType` for the given type +/// Tries to find the corresponding `ParamType` used for tokenization for the +/// given type pub fn find_parameter_type(ty: &Type) -> Result { match ty { Type::Array(ty) => { diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 2a285816..db3d2a22 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -50,7 +50,7 @@ pub use ethers_contract_abigen::Abigen; #[cfg(any(test, feature = "abigen"))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] -pub use ethers_contract_derive::{abigen, EthAbiType, EthEvent}; +pub use ethers_contract_derive::{abigen, EthAbiType, EthDisplay, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index be1d87df..7c05eacb 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -1,5 +1,5 @@ use ethers_contract::EthLogDecode; -use ethers_contract::{abigen, EthAbiType, EthEvent}; +use ethers_contract::{abigen, EthAbiType, EthDisplay, EthEvent}; use ethers_core::abi::{RawLog, Tokenizable}; use ethers_core::types::Address; use ethers_core::types::{H160, H256, I256, U128, U256}; @@ -335,3 +335,39 @@ fn can_decode_event_with_no_params() { let _ = ::decode_log(&log).unwrap(); } + +#[test] +fn eth_display_works() { + #[derive(Debug, Clone, EthAbiType, EthDisplay)] + struct MyStruct { + addr: Address, + old_value: String, + new_value: String, + h: H256, + i: I256, + arr_u8: [u8; 32], + arr_u16: [u16; 32], + v: Vec, + } + let item = MyStruct { + addr: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse().unwrap(), + old_value: "50".to_string(), + new_value: "100".to_string(), + h: H256::random(), + i: I256::zero(), + arr_u8: [0; 32], + arr_u16: [1; 32], + v: vec![0; 32], + }; + + let val = format!( + "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa, 50, 100, 0x{}, {}, 0x{}, {:?}, 0x{}", + hex::encode(&item.h), + item.i, + hex::encode(&item.arr_u8), + item.arr_u16, + hex::encode(&item.v), + ); + + assert_eq!(val, format!("{}", item)); +} diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index 0d69d8d2..9197228f 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -33,6 +33,9 @@ pub use units::Units; /// Re-export RLP pub use rlp; +/// Re-export hex +pub use hex; + use crate::types::{Address, Bytes, U256}; use k256::{ecdsa::SigningKey, EncodedPoint as K256PublicKey}; use std::ops::Neg;