Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

feat(abigen): add EthAbiCodec proc macro #704

Merged
merged 3 commits into from
Dec 19, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ),*
}
Expand Down Expand Up @@ -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 ),*
}
Expand Down
33 changes: 33 additions & 0 deletions ethers-contract/ethers-contract-derive/src/codec.rs
Original file line number Diff line number Diff line change
@@ -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<Self, #core_crate::abi::AbiError> {
if let #core_crate::abi::ParamType::Tuple(params) = <Self as #core_crate::abi::AbiType>::param_type() {
let tokens = #core_crate::abi::decode(&params, bytes.as_ref())?;
Ok(<Self as #core_crate::abi::Tokenizable>::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<u8> {
let tokens = #core_crate::abi::Tokenize::into_tokens(self);
#core_crate::abi::encode(&tokens)
}
}
}
}
37 changes: 34 additions & 3 deletions ethers-contract/ethers-contract-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use abigen::Contracts;
pub(crate) mod abi_ty;
mod abigen;
mod call;
mod codec;
mod display;
mod event;
mod spanned;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.
///
Expand All @@ -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)]
Expand Down
2 changes: 1 addition & 1 deletion ethers-contract/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
29 changes: 24 additions & 5 deletions ethers-contract/tests/abigen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ use ethers_providers::Provider;
use ethers_solc::Solc;
use std::{convert::TryFrom, sync::Arc};

fn assert_codec<T: AbiDecode + AbiEncode>() {}
fn assert_tokenizeable<T: Tokenizable>() {}

#[test]
fn can_gen_human_readable() {
abigen!(
Expand Down Expand Up @@ -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::<Value>();
assert_codec::<Addresses>();
let encoded = addr.clone().encode();
let other = Addresses::decode(&encoded).unwrap();
assert_eq!(addr, other);
}

#[test]
Expand All @@ -83,9 +92,10 @@ fn can_gen_structs_with_arrays_readable() {
"ValueChanged((address,string),(address,string),(address[],string)[])",
ValueChangedFilter::abi_signature()
);
}

fn assert_tokenizeable<T: Tokenizable>() {}
assert_codec::<Value>();
assert_codec::<Addresses>();
}

#[test]
fn can_generate_internal_structs() {
Expand All @@ -97,6 +107,10 @@ fn can_generate_internal_structs() {
assert_tokenizeable::<VerifyingKey>();
assert_tokenizeable::<G1Point>();
assert_tokenizeable::<G2Point>();

assert_codec::<VerifyingKey>();
assert_codec::<G1Point>();
assert_codec::<G2Point>();
}

#[test]
Expand All @@ -119,6 +133,10 @@ fn can_generate_internal_structs_multiple() {
assert_tokenizeable::<G1Point>();
assert_tokenizeable::<G2Point>();

assert_codec::<VerifyingKey>();
assert_codec::<G1Point>();
assert_codec::<G2Point>();

let (provider, _) = Provider::mocked();
let client = Arc::new(provider);

Expand Down Expand Up @@ -153,6 +171,7 @@ fn can_gen_human_readable_with_structs() {
event_derives(serde::Deserialize, serde::Serialize)
);
assert_tokenizeable::<Foo>();
assert_codec::<Foo>();

let (client, _mock) = Provider::mocked();
let contract = SimpleContract::new(Address::default(), Arc::new(client));
Expand Down
86 changes: 84 additions & 2 deletions ethers-contract/tests/common/derive.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand Down Expand Up @@ -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<U256>,
}

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::<U256>::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(&ethers_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<U256>,
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::<U256>::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();
Comment on lines +551 to +553
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this tuple syntax seems like a footgun? can we get rid of the x,) requirement?

Copy link
Collaborator Author

@mattsse mattsse Dec 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bit weird but this ist the required rust syntax for single-element tuple, otherwise it would just be a parenthesized expression

image
https://doc.rust-lang.org/1.30.0/book/second-edition/appendix-02-operators.html

assert_eq!(tuple, decoded);

let wrapped =
ethers_core::abi::encode(&ethers_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);
}
19 changes: 12 additions & 7 deletions ethers-core/src/abi/codec.rs
Original file line number Diff line number Diff line change
@@ -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},
};

Expand Down Expand Up @@ -109,8 +111,7 @@ macro_rules! impl_abi_codec_tuple {
)+
{
fn encode(self) -> Vec<u8> {
let token = self.into_token();
crate::abi::encode(&[token]).into()
crate::abi::encode(&self.into_tokens()).into()
}
}

Expand All @@ -119,10 +120,14 @@ macro_rules! impl_abi_codec_tuple {
$ty: AbiType + Tokenizable,
)+ {
fn decode(bytes: impl AsRef<[u8]>) -> Result<Self, AbiError> {
let tokens = crate::abi::decode(
&[Self::param_type()], bytes.as_ref()
)?;
Ok(<Self as Detokenize>::from_tokens(tokens)?)
if let crate::abi::ParamType::Tuple(params) = <Self as AbiType>::param_type() {
let tokens = crate::abi::decode(&params, bytes.as_ref())?;
Ok(<Self as Tokenizable>::from_token(Token::Tuple(tokens))?)
} else {
Err(
crate::abi::InvalidOutputType("Expected tuple".to_string()).into()
)
}
}
}
}
Expand Down