From 3c164bc9bf7d48b0f7c80b38b4883de0c55928d6 Mon Sep 17 00:00:00 2001 From: Matthias Seitz Date: Sun, 19 Dec 2021 15:40:17 +0100 Subject: [PATCH] feat(abigen): add EthAbiCodec proc macro (#704) * feat(abigen): add EthAbiCodec proc macro * rustfmt * fix: tuple codec --- CHANGELOG.md | 2 + .../src/contract/structs.rs | 4 +- .../ethers-contract-derive/src/codec.rs | 33 +++++++ .../ethers-contract-derive/src/lib.rs | 37 +++++++- ethers-contract/src/lib.rs | 2 +- ethers-contract/tests/abigen.rs | 29 +++++-- ethers-contract/tests/common/derive.rs | 86 ++++++++++++++++++- ethers-core/src/abi/codec.rs | 19 ++-- 8 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 ethers-contract/ethers-contract-derive/src/codec.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c552a0f..8271eb7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,8 @@ ### 0.6.0 +- add `EthAbiCodec` proc macro to derive `AbiEncode` `AbiDecode` implementation + [#704](https://github.com/gakonst/ethers-rs/pull/704) - move `AbiEncode` `AbiDecode` trait to ethers-core and implement for core types [#531](https://github.com/gakonst/ethers-rs/pull/531) - Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` diff --git a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs index 64a90bcd..ec5fa69a 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/structs.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/structs.rs @@ -129,7 +129,7 @@ impl Context { let ethers_contract = ethers_contract_crate(); Ok(quote! { #abi_signature_doc - #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #derives)] + #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #ethers_contract::EthAbiCodec, #derives)] pub struct #name { #( #fields ),* } @@ -191,7 +191,7 @@ impl Context { Ok(quote! { #abi_signature_doc - #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #derives)] + #[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #ethers_contract::EthAbiCodec, #derives)] pub struct #name { #( #fields ),* } diff --git a/ethers-contract/ethers-contract-derive/src/codec.rs b/ethers-contract/ethers-contract-derive/src/codec.rs new file mode 100644 index 00000000..54a577f2 --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/codec.rs @@ -0,0 +1,33 @@ +//! Helper functions for deriving `EthAbiType` + +use ethers_core::macros::ethers_core_crate; + +use quote::quote; +use syn::DeriveInput; + +/// Generates the `AbiEncode` + `AbiDecode` implementation +pub fn derive_codec_impl(input: &DeriveInput) -> proc_macro2::TokenStream { + let name = &input.ident; + let core_crate = ethers_core_crate(); + + quote! { + impl #core_crate::abi::AbiDecode for #name { + fn decode(bytes: impl AsRef<[u8]>) -> Result { + if let #core_crate::abi::ParamType::Tuple(params) = ::param_type() { + let tokens = #core_crate::abi::decode(¶ms, bytes.as_ref())?; + Ok(::from_token(#core_crate::abi::Token::Tuple(tokens))?) + } else { + Err( + #core_crate::abi::InvalidOutputType("Expected tuple".to_string()).into() + ) + } + } + } + impl #core_crate::abi::AbiEncode for #name { + fn encode(self) -> ::std::vec::Vec { + let tokens = #core_crate::abi::Tokenize::into_tokens(self); + #core_crate::abi::encode(&tokens) + } + } + } +} diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index 1ab7bec0..5bb89d56 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -10,6 +10,7 @@ use abigen::Contracts; pub(crate) mod abi_ty; mod abigen; mod call; +mod codec; mod display; mod event; mod spanned; @@ -43,7 +44,7 @@ pub(crate) mod utils; /// `ETHERSCAN_API_KEY` environment variable can be set. If it is, it will use /// that API key when retrieving the contract ABI. /// -/// Currently the proc macro accepts additional parameters to configure some +/// Currently, the proc macro accepts additional parameters to configure some /// aspects of the code generation. Specifically it accepts: /// - `methods`: A list of mappings from method signatures to method names allowing methods names to /// be explicitely set for contract methods. This also provides a workaround for generating code @@ -94,7 +95,7 @@ pub fn abigen(input: TokenStream) -> TokenStream { contracts.expand().unwrap_or_else(|err| err.to_compile_error()).into() } -/// Derives the `Tokenizable` trait for the labeled type. +/// Derives the `AbiType` and all `Tokenizable` traits for the labeled type. /// /// This derive macro automatically adds a type bound `field: Tokenizable` for /// each field type. @@ -104,6 +105,36 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream { TokenStream::from(abi_ty::derive_tokenizeable_impl(&input)) } +/// Derives the `AbiEncode`, `AbiDecode` and traits for the labeled type. +/// +/// This is an addition to `EthAbiType` that lacks the `AbiEncode`, `AbiDecode` implementation. +/// +/// The reason why this is a separate macro is the `AbiEncode` / `AbiDecode` are `ethers` +/// generalized codec traits used for types, calls, etc. However, encoding/decoding a call differs +/// from the basic encoding/decoding, (`[selector + encode(self)]`) +/// +/// # Example +/// +/// ```ignore +/// use ethers_contract::{EthAbiCodec, EthAbiType}; +/// use ethers_core::types::*; +/// +/// #[derive(Debug, Clone, EthAbiType, EthAbiCodec)] +/// struct MyStruct { +/// addr: Address, +/// old_value: String, +/// new_value: String, +/// } +/// let val = MyStruct {..}; +/// let bytes = val.encode(); +/// let val = MyStruct::decode(&bytes).unwrap(); +/// ``` +#[proc_macro_derive(EthAbiCodec)] +pub fn derive_abi_codec(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + TokenStream::from(codec::derive_codec_impl(&input)) +} + /// Derives `fmt::Display` trait and generates a convenient format for all the /// underlying primitive types/tokens. /// @@ -113,7 +144,7 @@ pub fn derive_abi_type(input: TokenStream) -> TokenStream { /// # Example /// /// ```ignore -/// use ethers_contract::EthDisplay; +/// use ethers_contract::{EthDisplay, EthAbiType}; /// use ethers_core::types::*; /// /// #[derive(Debug, Clone, EthAbiType, EthDisplay)] diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 2af9a18c..984fef7d 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -35,7 +35,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, EthCall, EthDisplay, EthEvent}; +pub use ethers_contract_derive::{abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] diff --git a/ethers-contract/tests/abigen.rs b/ethers-contract/tests/abigen.rs index f3e7b4e9..a4647be7 100644 --- a/ethers-contract/tests/abigen.rs +++ b/ethers-contract/tests/abigen.rs @@ -9,6 +9,9 @@ use ethers_providers::Provider; use ethers_solc::Solc; use std::{convert::TryFrom, sync::Arc}; +fn assert_codec() {} +fn assert_tokenizeable() {} + #[test] fn can_gen_human_readable() { abigen!( @@ -54,18 +57,24 @@ fn can_gen_structs_readable() { ]"#, event_derives(serde::Deserialize, serde::Serialize) ); - let value = Addresses { + let addr = Addresses { addr: vec!["eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse().unwrap()], s: "hello".to_string(), }; - let token = value.clone().into_token(); - assert_eq!(value, Addresses::from_token(token).unwrap()); + let token = addr.clone().into_token(); + assert_eq!(addr, Addresses::from_token(token).unwrap()); assert_eq!("ValueChanged", ValueChangedFilter::name()); assert_eq!( "ValueChanged((address,string),(address,string),(address[],string))", ValueChangedFilter::abi_signature() ); + + assert_codec::(); + assert_codec::(); + let encoded = addr.clone().encode(); + let other = Addresses::decode(&encoded).unwrap(); + assert_eq!(addr, other); } #[test] @@ -83,9 +92,10 @@ fn can_gen_structs_with_arrays_readable() { "ValueChanged((address,string),(address,string),(address[],string)[])", ValueChangedFilter::abi_signature() ); -} -fn assert_tokenizeable() {} + assert_codec::(); + assert_codec::(); +} #[test] fn can_generate_internal_structs() { @@ -97,6 +107,10 @@ fn can_generate_internal_structs() { assert_tokenizeable::(); assert_tokenizeable::(); assert_tokenizeable::(); + + assert_codec::(); + assert_codec::(); + assert_codec::(); } #[test] @@ -119,6 +133,10 @@ fn can_generate_internal_structs_multiple() { assert_tokenizeable::(); assert_tokenizeable::(); + assert_codec::(); + assert_codec::(); + assert_codec::(); + let (provider, _) = Provider::mocked(); let client = Arc::new(provider); @@ -153,6 +171,7 @@ fn can_gen_human_readable_with_structs() { event_derives(serde::Deserialize, serde::Serialize) ); assert_tokenizeable::(); + assert_codec::(); let (client, _mock) = Provider::mocked(); let contract = SimpleContract::new(Address::default(), Arc::new(client)); diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs index 3889c84e..58c0aa83 100644 --- a/ethers-contract/tests/common/derive.rs +++ b/ethers-contract/tests/common/derive.rs @@ -1,6 +1,8 @@ -use ethers_contract::{abigen, EthAbiType, EthCall, EthDisplay, EthEvent, EthLogDecode}; +use ethers_contract::{ + abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthEvent, EthLogDecode, +}; use ethers_core::{ - abi::{RawLog, Tokenizable}, + abi::{AbiDecode, AbiEncode, RawLog, Tokenizable}, types::{Address, H160, H256, I256, U128, U256}, }; @@ -478,3 +480,83 @@ fn can_derive_for_enum() { let token = ActionChoices::GoLeft.into_token(); assert_eq!(ActionChoices::GoLeft, ActionChoices::from_token(token).unwrap()); } + +#[test] +fn can_derive_abi_codec() { + #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)] + pub struct SomeType { + inner: Address, + msg: String, + } + + let val = SomeType { inner: Default::default(), msg: "hello".to_string() }; + + let encoded = val.clone().encode(); + let other = SomeType::decode(&encoded).unwrap(); + assert_eq!(val, other); +} + +#[test] +fn can_derive_abi_codec_single_field() { + #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)] + pub struct SomeType { + inner: Vec, + } + + let val = SomeType { inner: Default::default() }; + + let encoded = val.clone().encode(); + let decoded = SomeType::decode(&encoded).unwrap(); + assert_eq!(val, decoded); + + let encoded_tuple = (Vec::::default(),).encode(); + + assert_eq!(encoded_tuple, encoded); + let decoded_tuple = SomeType::decode(&encoded_tuple).unwrap(); + assert_eq!(decoded_tuple, decoded); + + let tuple = (val,); + let encoded = tuple.clone().encode(); + let decoded = <(SomeType,)>::decode(&encoded).unwrap(); + assert_eq!(tuple, decoded); + + let wrapped = + ethers_core::abi::encode(ðers_core::abi::Tokenize::into_tokens(tuple.clone())).to_vec(); + assert_eq!(wrapped, encoded); + let decoded_wrapped = <(SomeType,)>::decode(&wrapped).unwrap(); + + assert_eq!(decoded_wrapped, tuple); +} + +#[test] +fn can_derive_abi_codec_two_field() { + #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)] + pub struct SomeType { + inner: Vec, + addr: Address, + } + + let val = SomeType { inner: Default::default(), addr: Default::default() }; + + let encoded = val.clone().encode(); + let decoded = SomeType::decode(&encoded).unwrap(); + assert_eq!(val, decoded); + + let encoded_tuple = (Vec::::default(), Address::default()).encode(); + + assert_eq!(encoded_tuple, encoded); + let decoded_tuple = SomeType::decode(&encoded_tuple).unwrap(); + assert_eq!(decoded_tuple, decoded); + + let tuple = (val,); + let encoded = tuple.clone().encode(); + let decoded = <(SomeType,)>::decode(&encoded).unwrap(); + assert_eq!(tuple, decoded); + + let wrapped = + ethers_core::abi::encode(ðers_core::abi::Tokenize::into_tokens(tuple.clone())).to_vec(); + assert_eq!(wrapped, encoded); + let decoded_wrapped = <(SomeType,)>::decode(&wrapped).unwrap(); + + assert_eq!(decoded_wrapped, tuple); +} diff --git a/ethers-core/src/abi/codec.rs b/ethers-core/src/abi/codec.rs index ba8cdce1..f4de400c 100644 --- a/ethers-core/src/abi/codec.rs +++ b/ethers-core/src/abi/codec.rs @@ -1,5 +1,7 @@ use crate::{ - abi::{AbiArrayType, AbiError, AbiType, Detokenize, Tokenizable, TokenizableItem}, + abi::{ + AbiArrayType, AbiError, AbiType, Detokenize, Token, Tokenizable, TokenizableItem, Tokenize, + }, types::{Address, H256, U128, U256}, }; @@ -109,8 +111,7 @@ macro_rules! impl_abi_codec_tuple { )+ { fn encode(self) -> Vec { - let token = self.into_token(); - crate::abi::encode(&[token]).into() + crate::abi::encode(&self.into_tokens()).into() } } @@ -119,10 +120,14 @@ macro_rules! impl_abi_codec_tuple { $ty: AbiType + Tokenizable, )+ { fn decode(bytes: impl AsRef<[u8]>) -> Result { - let tokens = crate::abi::decode( - &[Self::param_type()], bytes.as_ref() - )?; - Ok(::from_tokens(tokens)?) + if let crate::abi::ParamType::Tuple(params) = ::param_type() { + let tokens = crate::abi::decode(¶ms, bytes.as_ref())?; + Ok(::from_token(Token::Tuple(tokens))?) + } else { + Err( + crate::abi::InvalidOutputType("Expected tuple".to_string()).into() + ) + } } } }