chore: move eip712 into ethers-contract-derive

This commit is contained in:
DaniPopes 2023-03-18 19:46:17 +01:00
parent 85f6710471
commit 66a9995df7
No known key found for this signature in database
GPG Key ID: 0F09640DDB7AC692
23 changed files with 277 additions and 468 deletions

33
Cargo.lock generated
View File

@ -651,15 +651,6 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "convert_case"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
dependencies = [
"unicode-segmentation",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.3" version = "0.9.3"
@ -1239,7 +1230,6 @@ dependencies = [
"ethers-contract-abigen", "ethers-contract-abigen",
"ethers-contract-derive", "ethers-contract-derive",
"ethers-core", "ethers-core",
"ethers-derive-eip712",
"ethers-providers", "ethers-providers",
"ethers-signers", "ethers-signers",
"ethers-solc", "ethers-solc",
@ -1284,11 +1274,13 @@ dependencies = [
name = "ethers-contract-derive" name = "ethers-contract-derive"
version = "2.0.0" version = "2.0.0"
dependencies = [ dependencies = [
"Inflector",
"ethers-contract-abigen", "ethers-contract-abigen",
"ethers-core", "ethers-core",
"hex", "hex",
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde_json",
"syn 2.0.0", "syn 2.0.0",
] ]
@ -1301,7 +1293,6 @@ dependencies = [
"bytes", "bytes",
"cargo_metadata", "cargo_metadata",
"chrono", "chrono",
"convert_case",
"elliptic-curve", "elliptic-curve",
"ethabi", "ethabi",
"generic-array", "generic-array",
@ -1324,19 +1315,6 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "ethers-derive-eip712"
version = "2.0.0"
dependencies = [
"ethers-contract-derive",
"ethers-core",
"hex",
"proc-macro2",
"quote",
"serde_json",
"syn 2.0.0",
]
[[package]] [[package]]
name = "ethers-etherscan" name = "ethers-etherscan"
version = "2.0.0" version = "2.0.0"
@ -1435,7 +1413,6 @@ dependencies = [
"eth-keystore", "eth-keystore",
"ethers-contract-derive", "ethers-contract-derive",
"ethers-core", "ethers-core",
"ethers-derive-eip712",
"futures-executor", "futures-executor",
"futures-util", "futures-util",
"hex", "hex",
@ -4484,12 +4461,6 @@ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.10" version = "0.1.10"

View File

@ -76,7 +76,6 @@ ethers-solc = { version = "2.0.0", path = "ethers-solc", default-features = fals
ethers-contract-abigen = { version = "2.0.0", path = "ethers-contract/ethers-contract-abigen", default-features = false } ethers-contract-abigen = { version = "2.0.0", path = "ethers-contract/ethers-contract-abigen", default-features = false }
ethers-contract-derive = { version = "2.0.0", path = "ethers-contract/ethers-contract-derive", default-features = false } ethers-contract-derive = { version = "2.0.0", path = "ethers-contract/ethers-contract-derive", default-features = false }
ethers-derive-eip712 = { version = "2.0.0", path = "ethers-core/ethers-derive-eip712", default-features = false }
# async / async utils # async / async utils
tokio = "1.26" tokio = "1.26"
@ -112,6 +111,7 @@ async-trait = "0.1.66"
auto_impl = "1.0" auto_impl = "1.0"
# misc # misc
Inflector = "0.11"
thiserror = "1.0" thiserror = "1.0"
once_cell = "1.17" once_cell = "1.17"
hex = "0.4" hex = "0.4"

View File

@ -39,10 +39,8 @@ hex.workspace = true
ethers-contract-abigen = { workspace = true, optional = true } ethers-contract-abigen = { workspace = true, optional = true }
ethers-contract-derive = { workspace = true, optional = true } ethers-contract-derive = { workspace = true, optional = true }
# eip712
ethers-derive-eip712 = { workspace = true, optional = true }
[dev-dependencies] [dev-dependencies]
ethers-providers = { workspace = true, features = ["ws"] }
ethers-signers.workspace = true ethers-signers.workspace = true
ethers-solc.workspace = true ethers-solc.workspace = true
@ -52,8 +50,6 @@ tokio = { workspace = true, features = ["macros"] }
[features] [features]
default = ["abigen"] default = ["abigen"]
eip712 = ["ethers-derive-eip712", "ethers-core/eip712"]
abigen-offline = ["ethers-contract-abigen", "ethers-contract-derive"] abigen-offline = ["ethers-contract-abigen", "ethers-contract-derive"]
abigen = ["abigen-offline", "ethers-contract-abigen/online"] abigen = ["abigen-offline", "ethers-contract-abigen/online"]

View File

@ -33,7 +33,7 @@ quote.workspace = true
syn = { workspace = true, features = ["full"] } syn = { workspace = true, features = ["full"] }
prettyplease = "0.1.25" prettyplease = "0.1.25"
Inflector = "0.11" Inflector.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
hex.workspace = true hex.workspace = true

View File

@ -28,4 +28,6 @@ proc-macro2.workspace = true
quote.workspace = true quote.workspace = true
syn.workspace = true syn.workspace = true
Inflector.workspace = true
hex.workspace = true hex.workspace = true
serde_json.workspace = true

View File

@ -0,0 +1,172 @@
use crate::utils;
use ethers_core::{
abi::{Address, ParamType},
macros::ethers_core_crate,
types::transaction::eip712::EIP712Domain,
utils::keccak256,
};
use inflector::Inflector;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{spanned::Spanned, Data, DeriveInput, Error, Fields, LitInt, LitStr, Result, Token};
pub(crate) fn impl_derive_eip712(input: &DeriveInput) -> Result<TokenStream> {
// Primary type should match the type in the ethereum verifying contract;
let primary_type = &input.ident;
// Instantiate domain from parsed attributes
let domain = parse_attributes(input)?;
let domain_separator = hex::encode(domain.separator());
let domain_str = serde_json::to_string(&domain).unwrap();
// Must parse the AST at compile time.
let parsed_fields = parse_fields(input)?;
// Compute the type hash for the derived struct using the parsed fields from above.
let type_hash = hex::encode(make_type_hash(primary_type.to_string(), &parsed_fields));
// Use reference to ethers_core instead of directly using the crate itself.
let ethers_core = ethers_core_crate();
let tokens = quote! {
impl Eip712 for #primary_type {
type Error = #ethers_core::types::transaction::eip712::Eip712Error;
fn type_hash() -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
let decoded = #ethers_core::utils::hex::decode(#type_hash)?;
let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
Ok(byte_array)
}
// Return the pre-computed domain separator from compile time;
fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
let decoded = #ethers_core::utils::hex::decode(#domain_separator)?;
let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
Ok(byte_array)
}
fn domain(&self) -> Result<#ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
let domain: #ethers_core::types::transaction::eip712::EIP712Domain = # ethers_core::utils::__serde_json::from_str(#domain_str)?;
Ok(domain)
}
fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
use #ethers_core::abi::Tokenizable;
let mut items = vec![#ethers_core::abi::Token::Uint(
#ethers_core::types::U256::from(&Self::type_hash()?[..]),
)];
if let #ethers_core::abi::Token::Tuple(tokens) = self.clone().into_token() {
for token in tokens {
match &token {
#ethers_core::abi::Token::Tuple(t) => {
// TODO: check for nested Eip712 Type;
// Challenge is determining the type hash
return Err(Self::Error::NestedEip712StructNotImplemented);
},
_ => {
items.push(#ethers_core::types::transaction::eip712::encode_eip712_type(token));
}
}
}
}
let struct_hash = #ethers_core::utils::keccak256(#ethers_core::abi::encode(
&items,
));
Ok(struct_hash)
}
}
};
Ok(tokens)
}
fn parse_attributes(input: &DeriveInput) -> Result<EIP712Domain> {
let mut domain = EIP712Domain::default();
utils::parse_attributes!(input.attrs.iter(), "eip712", meta,
"name", domain.name => {
meta.input.parse::<Token![=]>()?;
let litstr: LitStr = meta.input.parse()?;
domain.name = Some(litstr.value());
}
"version", domain.version => {
meta.input.parse::<Token![=]>()?;
let litstr: LitStr = meta.input.parse()?;
domain.version = Some(litstr.value());
}
"chain_id", domain.chain_id => {
meta.input.parse::<Token![=]>()?;
let litint: LitInt = meta.input.parse()?;
let n: u64 = litint.base10_parse()?;
domain.chain_id = Some(n.into());
}
"verifying_contract", domain.verifying_contract => {
meta.input.parse::<Token![=]>()?;
let litstr: LitStr = meta.input.parse()?;
let addr: Address =
litstr.value().parse().map_err(|e| Error::new(litstr.span(), e))?;
domain.verifying_contract = Some(addr);
}
"salt", domain.salt => {
meta.input.parse::<Token![=]>()?;
let litstr: LitStr = meta.input.parse()?;
let hash = keccak256(litstr.value());
domain.salt = Some(hash);
}
);
Ok(domain)
}
/// Returns a Vec of `(name, param_type)`
fn parse_fields(input: &DeriveInput) -> Result<Vec<(String, ParamType)>> {
let data = match &input.data {
Data::Struct(s) => s,
Data::Enum(e) => {
return Err(Error::new(e.enum_token.span, "Eip712 is not derivable for enums"))
}
Data::Union(u) => {
return Err(Error::new(u.union_token.span, "Eip712 is not derivable for unions"))
}
};
let named_fields = match &data.fields {
Fields::Named(fields) => fields,
_ => return Err(Error::new(input.span(), "unnamed fields are not supported")),
};
let mut fields = Vec::with_capacity(named_fields.named.len());
for f in named_fields.named.iter() {
let field_name = f.ident.as_ref().unwrap().to_string().to_camel_case();
let field_type =
match f.attrs.iter().find(|a| a.path().segments.iter().any(|s| s.ident == "eip712")) {
// Found nested Eip712 Struct
// TODO: Implement custom
Some(a) => {
return Err(Error::new(a.span(), "nested Eip712 struct are not yet supported"))
}
// Not a nested eip712 struct, return the field param type;
None => crate::utils::find_parameter_type(&f.ty)?,
};
fields.push((field_name, field_type));
}
Ok(fields)
}
/// Convert hash map of field names and types into a type hash corresponding to enc types;
fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [u8; 32] {
let parameters =
fields.iter().map(|(k, v)| format!("{v} {k}")).collect::<Vec<String>>().join(",");
let sig = format!("{primary_type}({parameters})");
keccak256(sig)
}

View File

@ -13,6 +13,7 @@ mod call;
pub(crate) mod calllike; pub(crate) mod calllike;
mod codec; mod codec;
mod display; mod display;
mod eip712;
mod error; mod error;
mod event; mod event;
mod spanned; mod spanned;
@ -357,3 +358,75 @@ pub fn derive_abi_error(input: TokenStream) -> TokenStream {
} }
.into() .into()
} }
/// EIP-712 derive macro.
///
/// This crate provides a derive macro `Eip712` that is used to encode a rust struct
/// into a payload hash, according to <https://eips.ethereum.org/EIPS/eip-712>
///
/// The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712`
/// Both the derive macro and the trait must be in context when using
///
/// This derive macro requires the `#[eip712]` attributes to be included
/// for specifying the domain separator used in encoding the hash.
///
/// NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived.
/// This allows the struct to be parsed into `ethers_core::abi::Token` for encoding.
///
/// # Optional Eip712 Parameters
///
/// The only optional parameter is `salt`, which accepts a string
/// that is hashed using keccak256 and stored as bytes.
///
/// # Example Usage
///
/// ```ignore
/// use ethers_contract::EthAbiType;
/// use ethers_derive_eip712::*;
/// use ethers_core::types::{transaction::eip712::Eip712, H160};
///
/// #[derive(Debug, Eip712, EthAbiType)]
/// #[eip712(
/// name = "Radicle",
/// version = "1",
/// chain_id = 1,
/// verifying_contract = "0x0000000000000000000000000000000000000000"
/// // salt is an optional parameter
/// salt = "my-unique-spice"
/// )]
/// pub struct Puzzle {
/// pub organization: H160,
/// pub contributor: H160,
/// pub commit: String,
/// pub project: String,
/// }
///
/// let puzzle = Puzzle {
/// organization: "0000000000000000000000000000000000000000"
/// .parse::<H160>()
/// .expect("failed to parse address"),
/// contributor: "0000000000000000000000000000000000000000"
/// .parse::<H160>()
/// .expect("failed to parse address"),
/// commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
/// project: "radicle-reward".to_string(),
/// };
///
/// let hash = puzzle.encode_eip712().unwrap();
/// ```
///
/// # Limitations
///
/// At the moment, the derive macro does not recursively encode nested Eip712 structs.
///
/// There is an Inner helper attribute `#[eip712]` for fields that will eventually be used to
/// determine if there is a nested eip712 struct. However, this work is not yet complete.
#[proc_macro_derive(Eip712, attributes(eip712))]
pub fn derive_eip712(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
match eip712::impl_derive_eip712(&input) {
Ok(tokens) => tokens,
Err(e) => e.to_compile_error(),
}
.into()
}

View File

@ -59,16 +59,13 @@ pub use ethers_contract_abigen::{
#[cfg(any(test, feature = "abigen"))] #[cfg(any(test, feature = "abigen"))]
#[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))]
pub use ethers_contract_derive::{ pub use ethers_contract_derive::{
abigen, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthError, EthEvent, abigen, Eip712, EthAbiCodec, EthAbiType, EthCall, EthDisplay, EthError, EthEvent,
}; };
// Hide the Lazy re-export, it's just for convenience // Hide the Lazy re-export, it's just for convenience
#[doc(hidden)] #[doc(hidden)]
pub use once_cell::sync::Lazy; pub use once_cell::sync::Lazy;
#[cfg(feature = "eip712")]
pub use ethers_derive_eip712::*;
// For macro expansions only, not public API. // For macro expansions only, not public API.
// See: [#2235](https://github.com/gakonst/ethers-rs/pull/2235) // See: [#2235](https://github.com/gakonst/ethers-rs/pull/2235)

View File

@ -1,6 +1,6 @@
use crate::common::*; use crate::common::*;
use ethers_contract::{ use ethers_contract::{
abigen, ContractFactory, ContractInstance, EthAbiType, EthEvent, LogMeta, Multicall, abigen, ContractFactory, ContractInstance, Eip712, EthAbiType, EthEvent, LogMeta, Multicall,
MulticallError, MulticallVersion, MulticallError, MulticallVersion,
}; };
use ethers_core::{ use ethers_core::{
@ -11,7 +11,7 @@ use ethers_core::{
}, },
utils::{keccak256, Anvil}, utils::{keccak256, Anvil},
}; };
use ethers_providers::{Http, Middleware, MiddlewareError, Provider, StreamExt}; use ethers_providers::{Http, Middleware, MiddlewareError, Provider, StreamExt, Ws};
use ethers_signers::{LocalWallet, Signer}; use ethers_signers::{LocalWallet, Signer};
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
@ -342,7 +342,7 @@ async fn watch_events() {
let mut stream = event.stream().await.unwrap(); let mut stream = event.stream().await.unwrap();
// Also set up a subscription for the same thing // Also set up a subscription for the same thing
let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); let ws = Provider::<Ws>::connect(anvil.ws_endpoint()).await.unwrap();
let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into()); let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into());
let event2 = contract2.event::<ValueChanged>(); let event2 = contract2.event::<ValueChanged>();
let mut subscription = event2.subscribe().await.unwrap(); let mut subscription = event2.subscribe().await.unwrap();
@ -381,7 +381,7 @@ async fn watch_subscription_events_multiple_addresses() {
let contract_1 = deploy(client.clone(), abi.clone(), bytecode.clone()).await; let contract_1 = deploy(client.clone(), abi.clone(), bytecode.clone()).await;
let contract_2 = deploy(client.clone(), abi.clone(), bytecode).await; let contract_2 = deploy(client.clone(), abi.clone(), bytecode).await;
let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); let ws = Provider::<Ws>::connect(anvil.ws_endpoint()).await.unwrap();
let filter = Filter::new() let filter = Filter::new()
.address(ValueOrArray::Array(vec![contract_1.address(), contract_2.address()])); .address(ValueOrArray::Array(vec![contract_1.address(), contract_2.address()]));
let mut stream = ws.subscribe_logs(&filter).await.unwrap(); let mut stream = ws.subscribe_logs(&filter).await.unwrap();
@ -786,16 +786,15 @@ async fn multicall_aggregate() {
} }
#[tokio::test] #[tokio::test]
#[cfg(feature = "eip712")]
async fn test_derive_eip712() { async fn test_derive_eip712() {
use ethers_derive_eip712::*;
// Generate Contract ABI Bindings // Generate Contract ABI Bindings
abigen!( mod contract {
DeriveEip712Test, ethers_contract::abigen!(
"./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", DeriveEip712Test,
event_derives(serde::Deserialize, serde::Serialize) "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json",
); derives(serde::Deserialize, serde::Serialize)
);
}
// Create derived structs // Create derived structs
@ -842,7 +841,7 @@ async fn test_derive_eip712() {
let addr = contract.address(); let addr = contract.address();
let contract = DeriveEip712Test::new(addr, client.clone()); let contract = contract::DeriveEip712Test::new(addr, client.clone());
let foo_bar = FooBar { let foo_bar = FooBar {
foo: I256::from(10u64), foo: I256::from(10u64),
@ -853,7 +852,7 @@ async fn test_derive_eip712() {
out: Address::from([0; 20]), out: Address::from([0; 20]),
}; };
let derived_foo_bar = derive_eip_712_test::FooBar { let derived_foo_bar = contract::FooBar {
foo: foo_bar.foo, foo: foo_bar.foo,
bar: foo_bar.bar, bar: foo_bar.bar,
fizz: foo_bar.fizz.clone(), fizz: foo_bar.fizz.clone(),

View File

@ -1,6 +1,4 @@
#![allow(clippy::extra_unused_type_parameters)] use ethers_contract_derive::{Eip712, EthAbiType};
use ethers_contract_derive::EthAbiType;
use ethers_core::{ use ethers_core::{
types::{ types::{
transaction::eip712::{ transaction::eip712::{
@ -11,7 +9,6 @@ use ethers_core::{
}, },
utils::{keccak256, parse_ether}, utils::{keccak256, parse_ether},
}; };
use ethers_derive_eip712::*;
#[test] #[test]
fn test_derive_eip712() { fn test_derive_eip712() {

View File

@ -7,6 +7,8 @@ mod derive;
mod contract_call; mod contract_call;
mod eip712;
#[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] #[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))]
mod common; mod common;

View File

@ -51,9 +51,6 @@ num_enum = "0.5"
# macros feature enabled dependencies # macros feature enabled dependencies
cargo_metadata = { version = "0.15.3", optional = true } cargo_metadata = { version = "0.15.3", optional = true }
# eip712 feature enabled dependencies
convert_case = { version = "0.6.0", optional = true }
syn = { workspace = true, optional = true } syn = { workspace = true, optional = true }
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
@ -72,5 +69,4 @@ rand.workspace = true
[features] [features]
celo = ["legacy"] # celo support extends the transaction format with extra fields celo = ["legacy"] # celo support extends the transaction format with extra fields
legacy = [] legacy = []
eip712 = ["convert_case", "syn"]
macros = ["syn", "cargo_metadata", "once_cell"] macros = ["syn", "cargo_metadata", "once_cell"]

View File

@ -1,30 +0,0 @@
[package]
name = "ethers-derive-eip712"
authors = ["Ryan Tate <ryan.michael.tate@gmail.com>"]
description = "Derive procedural macro for EIP-712 typed data"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
documentation.workspace = true
repository.workspace = true
homepage.workspace = true
categories.workspace = true
keywords.workspace = true
[lib]
proc-macro = true
[dependencies]
ethers-core = { workspace = true, features = ["eip712", "macros"] }
proc-macro2.workspace = true
quote.workspace = true
syn.workspace = true
hex.workspace = true
serde_json.workspace = true
[dev-dependencies]
ethers-contract-derive.workspace = true

View File

@ -1,163 +0,0 @@
//! # EIP-712 Derive Macro
//!
//! This crate provides a derive macro `Eip712` that is used to encode a rust struct
//! into a payload hash, according to [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712)
//!
//! The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712`
//! Both the derive macro and the trait must be in context when using
//!
//! This derive macro requires the `#[eip712]` attributes to be included
//! for specifying the domain separator used in encoding the hash.
//!
//! NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived.
//! This allows the struct to be parsed into `ethers_core::abi::Token` for encoding.
//!
//! # Optional Eip712 Parameters
//!
//! The only optional parameter is `salt`, which accepts a string
//! that is hashed using keccak256 and stored as bytes.
//!
//! # Example Usage
//!
//! ```ignore
//! use ethers_contract::EthAbiType;
//! use ethers_derive_eip712::*;
//! use ethers_core::types::{transaction::eip712::Eip712, H160};
//!
//! #[derive(Debug, Eip712, EthAbiType)]
//! #[eip712(
//! name = "Radicle",
//! version = "1",
//! chain_id = 1,
//! verifying_contract = "0x0000000000000000000000000000000000000000"
//! // salt is an optional parameter
//! salt = "my-unique-spice"
//! )]
//! pub struct Puzzle {
//! pub organization: H160,
//! pub contributor: H160,
//! pub commit: String,
//! pub project: String,
//! }
//!
//! let puzzle = Puzzle {
//! organization: "0000000000000000000000000000000000000000"
//! .parse::<H160>()
//! .expect("failed to parse address"),
//! contributor: "0000000000000000000000000000000000000000"
//! .parse::<H160>()
//! .expect("failed to parse address"),
//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
//! project: "radicle-reward".to_string(),
//! };
//!
//! let hash = puzzle.encode_eip712().unwrap();
//! ```
//!
//! # Limitations
//!
//! At the moment, the derive macro does not recursively encode nested Eip712 structs.
//!
//! There is an Inner helper attribute `#[eip712]` for fields that will eventually be used to
//! determine if there is a nested eip712 struct. However, this work is not yet complete.
#![deny(missing_docs, unsafe_code, rustdoc::broken_intra_doc_links)]
use ethers_core::{macros::ethers_core_crate, types::transaction::eip712};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, Result};
/// Derive macro for `Eip712`
#[proc_macro_derive(Eip712, attributes(eip712))]
pub fn eip_712_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input);
match impl_eip_712_macro(&input) {
Ok(tokens) => tokens,
Err(e) => e.to_compile_error(),
}
.into()
}
// Main implementation macro, used to compute static values and define
// method for encoding the final eip712 payload;
fn impl_eip_712_macro(input: &syn::DeriveInput) -> Result<TokenStream2> {
// Primary type should match the type in the ethereum verifying contract;
let primary_type = &input.ident;
// Instantiate domain from parsed attributes
let domain = eip712::EIP712Domain::try_from(input)?;
let domain_separator = hex::encode(domain.separator());
//
let domain_str = serde_json::to_string(&domain).unwrap();
// Must parse the AST at compile time.
let parsed_fields = eip712::parse_fields(input)?;
// Compute the type hash for the derived struct using the parsed fields from above.
let type_hash =
hex::encode(eip712::make_type_hash(primary_type.clone().to_string(), &parsed_fields));
// Use reference to ethers_core instead of directly using the crate itself.
let ethers_core = ethers_core_crate();
let tokens = quote! {
impl Eip712 for #primary_type {
type Error = #ethers_core::types::transaction::eip712::Eip712Error;
fn type_hash() -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
let decoded = #ethers_core::utils::hex::decode(#type_hash)?;
let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
Ok(byte_array)
}
// Return the pre-computed domain separator from compile time;
fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
let decoded = #ethers_core::utils::hex::decode(#domain_separator)?;
let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
Ok(byte_array)
}
fn domain(&self) -> Result<#ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
let domain: #ethers_core::types::transaction::eip712::EIP712Domain = # ethers_core::utils::__serde_json::from_str(#domain_str)?;
Ok(domain)
}
fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
use #ethers_core::abi::Tokenizable;
let mut items = vec![#ethers_core::abi::Token::Uint(
#ethers_core::types::U256::from(&Self::type_hash()?[..]),
)];
if let #ethers_core::abi::Token::Tuple(tokens) = self.clone().into_token() {
for token in tokens {
match &token {
#ethers_core::abi::Token::Tuple(t) => {
// TODO: check for nested Eip712 Type;
// Challenge is determining the type hash
return Err(Self::Error::NestedEip712StructNotImplemented);
},
_ => {
items.push(#ethers_core::types::transaction::eip712::encode_eip712_type(token));
}
}
}
}
let struct_hash = #ethers_core::utils::keccak256(#ethers_core::abi::encode(
&items,
));
Ok(struct_hash)
}
}
};
Ok(tokens)
}

View File

@ -220,7 +220,6 @@ pub enum EthersCrate {
EthersContractAbigen, EthersContractAbigen,
EthersContractDerive, EthersContractDerive,
EthersCore, EthersCore,
EthersDeriveEip712,
EthersEtherscan, EthersEtherscan,
EthersMiddleware, EthersMiddleware,
EthersProviders, EthersProviders,
@ -250,7 +249,6 @@ impl EthersCrate {
Self::EthersContractAbigen => "ethers-contract-abigen", Self::EthersContractAbigen => "ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract-derive", Self::EthersContractDerive => "ethers-contract-derive",
Self::EthersCore => "ethers-core", Self::EthersCore => "ethers-core",
Self::EthersDeriveEip712 => "ethers-derive-eip712",
Self::EthersEtherscan => "ethers-etherscan", Self::EthersEtherscan => "ethers-etherscan",
Self::EthersMiddleware => "ethers-middleware", Self::EthersMiddleware => "ethers-middleware",
Self::EthersProviders => "ethers-providers", Self::EthersProviders => "ethers-providers",
@ -268,7 +266,6 @@ impl EthersCrate {
Self::EthersContractAbigen => "::ethers_contract_abigen", Self::EthersContractAbigen => "::ethers_contract_abigen",
Self::EthersContractDerive => "::ethers_contract_derive", Self::EthersContractDerive => "::ethers_contract_derive",
Self::EthersCore => "::ethers_core", Self::EthersCore => "::ethers_core",
Self::EthersDeriveEip712 => "::ethers_derive_eip712",
Self::EthersEtherscan => "::ethers_etherscan", Self::EthersEtherscan => "::ethers_etherscan",
Self::EthersMiddleware => "::ethers_middleware", Self::EthersMiddleware => "::ethers_middleware",
Self::EthersProviders => "::ethers_providers", Self::EthersProviders => "::ethers_providers",
@ -284,7 +281,6 @@ impl EthersCrate {
// re-exported in ethers::contract // re-exported in ethers::contract
Self::EthersContractAbigen => "::ethers::contract", // partially Self::EthersContractAbigen => "::ethers::contract", // partially
Self::EthersContractDerive => "::ethers::contract", Self::EthersContractDerive => "::ethers::contract",
Self::EthersDeriveEip712 => "::ethers::contract",
Self::EthersAddressbook => "::ethers::addressbook", Self::EthersAddressbook => "::ethers::addressbook",
Self::EthersContract => "::ethers::contract", Self::EthersContract => "::ethers::contract",
@ -303,7 +299,6 @@ impl EthersCrate {
match self { match self {
Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen", Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen",
Self::EthersContractDerive => "ethers-contract/ethers-contract-derive", Self::EthersContractDerive => "ethers-contract/ethers-contract-derive",
Self::EthersDeriveEip712 => "ethers-core/ethers-derive-eip712",
_ => self.crate_name(), _ => self.crate_name(),
} }
} }

View File

@ -4,19 +4,12 @@ use crate::{
types::{serde_helpers::StringifiedNumeric, Address, Bytes, U256}, types::{serde_helpers::StringifiedNumeric, Address, Bytes, U256},
utils::keccak256, utils::keccak256,
}; };
use convert_case::{Case, Casing};
use core::convert::TryFrom;
use ethabi::encode; use ethabi::encode;
use serde::{Deserialize, Deserializer, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use std::{ use std::{
collections::{BTreeMap, HashSet}, collections::{BTreeMap, HashSet},
convert::TryInto,
iter::FromIterator, iter::FromIterator,
}; };
use syn::{
parse::Error, spanned::Spanned, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, LitInt,
LitStr, PathArguments, Type,
};
/// Custom types for `TypedData` /// Custom types for `TypedData`
pub type Types = BTreeMap<String, Vec<Eip712DomainType>>; pub type Types = BTreeMap<String, Vec<Eip712DomainType>>;
@ -39,7 +32,7 @@ pub const EIP712_DOMAIN_TYPE_HASH_WITH_SALT: [u8; 32] = [
202, 46, 220, 207, 34, 164, 108, 114, 154, 197, 100, 114, 202, 46, 220, 207, 34, 164, 108, 114, 154, 197, 100, 114,
]; ];
/// Error typed used by Eip712 derive macro /// An EIP-712 error.
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Eip712Error { pub enum Eip712Error {
#[error("Failed to serialize serde JSON object")] #[error("Failed to serialize serde JSON object")]
@ -56,8 +49,7 @@ pub enum Eip712Error {
Message(String), Message(String),
} }
/// The Eip712 trait provides helper methods for computing /// Helper methods for computing the typed data hash used in `eth_signTypedData`.
/// the typed data hash used in `eth_signTypedData`.
/// ///
/// The ethers-rs `derive_eip712` crate provides a derive macro to /// The ethers-rs `derive_eip712` crate provides a derive macro to
/// implement the trait for a given struct. See documentation /// implement the trait for a given struct. See documentation
@ -244,69 +236,6 @@ impl<T: Eip712 + Clone> Eip712 for EIP712WithDomain<T> {
} }
} }
// Parse the AST of the struct to determine the domain attributes
impl TryFrom<&syn::DeriveInput> for EIP712Domain {
type Error = Error;
fn try_from(input: &syn::DeriveInput) -> Result<EIP712Domain, Self::Error> {
const ERROR: &str = "unrecognized eip712 attribute";
const ALREADY_SPECIFIED: &str = "eip712 attribute already specified";
let mut domain = EIP712Domain::default();
for attr in input.attrs.iter() {
if !attr.path().is_ident("eip712") {
continue
}
attr.parse_nested_meta(|meta| {
let ident = meta.path.get_ident().ok_or_else(|| meta.error(ERROR))?.to_string();
match ident.as_str() {
"name" if domain.name.is_none() => {
let litstr: LitStr = meta.input.parse()?;
domain.name = Some(litstr.value());
}
"name" => return Err(meta.error(ALREADY_SPECIFIED)),
"version" if domain.version.is_none() => {
let litstr: LitStr = meta.input.parse()?;
domain.version = Some(litstr.value());
}
"version" => return Err(meta.error(ALREADY_SPECIFIED)),
"chain_id" if domain.chain_id.is_none() => {
let litint: LitInt = meta.input.parse()?;
let n: u64 = litint.base10_parse()?;
domain.chain_id = Some(n.into());
}
"chain_id" => return Err(meta.error(ALREADY_SPECIFIED)),
"verifying_contract" if domain.verifying_contract.is_none() => {
let litstr: LitStr = meta.input.parse()?;
let addr: Address =
litstr.value().parse().map_err(|e| Error::new(litstr.span(), e))?;
domain.verifying_contract = Some(addr);
}
"verifying_contract" => return Err(meta.error(ALREADY_SPECIFIED)),
"salt" if domain.salt.is_none() => {
let litstr: LitStr = meta.input.parse()?;
let hash = keccak256(litstr.value());
domain.salt = Some(hash);
}
"salt" => return Err(meta.error(ALREADY_SPECIFIED)),
_ => return Err(meta.error(ERROR)),
}
Ok(())
})?;
}
Ok(domain)
}
}
/// Represents the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data object. /// Represents the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data object.
/// ///
/// Typed data is a JSON object containing type information, domain separator parameters and the /// Typed data is a JSON object containing type information, domain separator parameters and the
@ -641,125 +570,6 @@ pub fn encode_field(
Ok(token) Ok(token)
} }
/// Parse the eth abi parameter type based on the syntax type;
/// this method is copied from <https://github.com/gakonst/ethers-rs/blob/master/ethers-contract/ethers-contract-derive/src/lib.rs#L600>
/// with additional modifications for finding byte arrays
pub fn find_parameter_type(ty: &Type) -> Result<ParamType, Error> {
match ty {
Type::Array(ty) => {
let param = find_parameter_type(ty.elem.as_ref())?;
if let Expr::Lit(ref expr) = ty.len {
if let Lit::Int(ref len) = expr.lit {
if let Ok(size) = len.base10_parse::<usize>() {
if let ParamType::Uint(_) = param {
return Ok(ParamType::FixedBytes(size))
}
return Ok(ParamType::FixedArray(Box::new(param), size))
}
}
}
Err(Error::new(ty.span(), "Failed to derive proper ABI from array field"))
}
Type::Path(ty) => {
if let Some(ident) = ty.path.get_ident() {
let ident = ident.to_string().to_lowercase();
return match ident.as_str() {
"address" => Ok(ParamType::Address),
"string" => Ok(ParamType::String),
"bool" => Ok(ParamType::Bool),
"int256" | "int" | "uint" | "uint256" => Ok(ParamType::Uint(256)),
"h160" => Ok(ParamType::FixedBytes(20)),
"h256" | "secret" | "hash" => Ok(ParamType::FixedBytes(32)),
"h512" | "public" => Ok(ParamType::FixedBytes(64)),
"bytes" => Ok(ParamType::Bytes),
s => parse_int_param_type(s).ok_or_else(|| {
Error::new(
ty.span(),
format!("Failed to derive proper ABI from field: {s})"),
)
}),
}
}
// check for `Vec`
if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" {
if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments {
if args.args.len() == 1 {
if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() {
let kind = find_parameter_type(ty)?;
// Check if byte array is found
if let ParamType::Uint(size) = kind {
if size == 8 {
return Ok(ParamType::Bytes)
}
}
return Ok(ParamType::Array(Box::new(kind)))
}
}
}
}
Err(Error::new(ty.span(), "Failed to derive proper ABI from fields"))
}
Type::Tuple(ty) => {
let params = ty.elems.iter().map(find_parameter_type).collect::<Result<Vec<_>, _>>()?;
Ok(ParamType::Tuple(params))
}
_ => Err(Error::new(ty.span(), "Failed to derive proper ABI from fields")),
}
}
fn parse_int_param_type(s: &str) -> Option<ParamType> {
let size = s.chars().skip(1).collect::<String>().parse::<usize>().ok()?;
if s.starts_with('u') {
Some(ParamType::Uint(size))
} else if s.starts_with('i') {
Some(ParamType::Int(size))
} else {
None
}
}
/// Return HashMap of the field name and the field type
pub fn parse_fields(input: &DeriveInput) -> Result<Vec<(String, ParamType)>, Error> {
let mut fields = Vec::new();
let data = match &input.data {
Data::Struct(s) => s,
Data::Enum(e) => {
return Err(Error::new(e.enum_token.span, "Eip712 is not derivable for enums"))
}
Data::Union(u) => {
return Err(Error::new(u.union_token.span, "Eip712 is not derivable for unions"))
}
};
let named_fields = match &data.fields {
Fields::Named(name) => name,
_ => return Err(Error::new(input.span(), "unnamed fields are not supported")),
};
for f in named_fields.named.iter() {
let field_name = f.ident.as_ref().unwrap().to_string().to_case(Case::Camel);
let field_type =
match f.attrs.iter().find(|a| a.path().segments.iter().any(|s| s.ident == "eip712")) {
// Found nested Eip712 Struct
// TODO: Implement custom
Some(a) => {
return Err(Error::new(a.span(), "nested Eip712 struct are not yet supported"))
}
// Not a nested eip712 struct, return the field param type;
None => find_parameter_type(&f.ty)?,
};
fields.push((field_name, field_type));
}
Ok(fields)
}
/// Convert hash map of field names and types into a type hash corresponding to enc types; /// Convert hash map of field names and types into a type hash corresponding to enc types;
pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [u8; 32] { pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [u8; 32] {
let parameters = let parameters =

View File

@ -5,7 +5,6 @@ pub mod eip1559;
pub mod eip2718; pub mod eip2718;
pub mod eip2930; pub mod eip2930;
#[cfg(feature = "eip712")]
pub mod eip712; pub mod eip712;
pub(crate) const BASE_NUM_TX_FIELDS: usize = 9; pub(crate) const BASE_NUM_TX_FIELDS: usize = 9;

View File

@ -52,11 +52,9 @@ const OVERFLOW_I256_UNITS: usize = 77;
/// U256 overflows for numbers wider than 78 units. /// U256 overflows for numbers wider than 78 units.
const OVERFLOW_U256_UNITS: usize = 78; const OVERFLOW_U256_UNITS: usize = 78;
/// Re-export of serde-json // Re-export serde-json for macro usage
#[doc(hidden)] #[doc(hidden)]
pub mod __serde_json { pub use serde_json as __serde_json;
pub use serde_json::*;
}
#[derive(Error, Debug)] #[derive(Error, Debug)]
pub enum ConversionError { pub enum ConversionError {

View File

@ -24,7 +24,7 @@ rustdoc-args = ["--cfg", "docsrs"]
all-features = true all-features = true
[dependencies] [dependencies]
ethers-core = { workspace = true, features = ["eip712"] } ethers-core.workspace = true
# crypto # crypto
coins-bip32 = "0.8.3" coins-bip32 = "0.8.3"
@ -66,7 +66,6 @@ yubihsm = { version = "0.42.0-pre.0", features = ["secp256k1", "http", "usb"], o
[dev-dependencies] [dev-dependencies]
ethers-contract-derive.workspace = true ethers-contract-derive.workspace = true
ethers-derive-eip712.workspace = true
serde_json.workspace = true serde_json.workspace = true
tempfile.workspace = true tempfile.workspace = true

View File

@ -293,11 +293,10 @@ impl LedgerEthereum {
mod tests { mod tests {
use super::*; use super::*;
use crate::Signer; use crate::Signer;
use ethers_contract_derive::EthAbiType; use ethers_contract_derive::{Eip712, EthAbiType};
use ethers_core::types::{ use ethers_core::types::{
transaction::eip712::Eip712, Address, TransactionRequest, I256, U256, transaction::eip712::Eip712, Address, TransactionRequest, I256, U256,
}; };
use ethers_derive_eip712::*;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, Clone, Eip712, EthAbiType)] #[derive(Debug, Clone, Eip712, EthAbiType)]

View File

@ -233,7 +233,7 @@ impl TrezorEthereum {
mod tests { mod tests {
use super::*; use super::*;
use crate::Signer; use crate::Signer;
use ethers_contract_derive::EthAbiType; use ethers_contract_derive::{Eip712, EthAbiType};
use ethers_core::types::{ use ethers_core::types::{
transaction::{ transaction::{
eip2930::{AccessList, AccessListItem}, eip2930::{AccessList, AccessListItem},
@ -241,7 +241,6 @@ mod tests {
}, },
Address, Eip1559TransactionRequest, TransactionRequest, I256, U256, Address, Eip1559TransactionRequest, TransactionRequest, I256, U256,
}; };
use ethers_derive_eip712::*;
use std::str::FromStr; use std::str::FromStr;
#[derive(Debug, Clone, Eip712, EthAbiType)] #[derive(Debug, Clone, Eip712, EthAbiType)]

View File

@ -38,8 +38,6 @@ celo = [
legacy = ["ethers-core/legacy", "ethers-contract/legacy"] legacy = ["ethers-core/legacy", "ethers-contract/legacy"]
# individual features per sub-crate # individual features per sub-crate
## core
eip712 = ["ethers-contract/eip712", "ethers-core/eip712"]
## providers ## providers
ws = ["ethers-providers/ws"] ws = ["ethers-providers/ws"]
ipc = ["ethers-providers/ipc"] ipc = ["ethers-providers/ipc"]

View File

@ -15,7 +15,7 @@ trezor = ["ethers/trezor"]
yubi = ["ethers/yubi"] yubi = ["ethers/yubi"]
[dev-dependencies] [dev-dependencies]
ethers = { workspace = true, features = ["abigen", "eip712", "ws", "rustls"] } ethers = { workspace = true, features = ["abigen", "ws", "rustls"] }
tokio = { workspace = true, features = ["macros"] } tokio = { workspace = true, features = ["macros"] }