diff --git a/Cargo.lock b/Cargo.lock index 73deacce..54a54dcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -651,15 +651,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "core-foundation" version = "0.9.3" @@ -1239,7 +1230,6 @@ dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", "ethers-core", - "ethers-derive-eip712", "ethers-providers", "ethers-signers", "ethers-solc", @@ -1284,11 +1274,13 @@ dependencies = [ name = "ethers-contract-derive" version = "2.0.0" dependencies = [ + "Inflector", "ethers-contract-abigen", "ethers-core", "hex", "proc-macro2", "quote", + "serde_json", "syn 2.0.0", ] @@ -1301,7 +1293,6 @@ dependencies = [ "bytes", "cargo_metadata", "chrono", - "convert_case", "elliptic-curve", "ethabi", "generic-array", @@ -1324,19 +1315,6 @@ dependencies = [ "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]] name = "ethers-etherscan" version = "2.0.0" @@ -1435,7 +1413,6 @@ dependencies = [ "eth-keystore", "ethers-contract-derive", "ethers-core", - "ethers-derive-eip712", "futures-executor", "futures-util", "hex", @@ -4484,12 +4461,6 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-segmentation" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" - [[package]] name = "unicode-width" version = "0.1.10" diff --git a/Cargo.toml b/Cargo.toml index 91f3a247..ba6dfb92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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-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 tokio = "1.26" @@ -112,6 +111,7 @@ async-trait = "0.1.66" auto_impl = "1.0" # misc +Inflector = "0.11" thiserror = "1.0" once_cell = "1.17" hex = "0.4" diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index a0c65795..8294b6c1 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -39,10 +39,8 @@ hex.workspace = true ethers-contract-abigen = { workspace = true, optional = true } ethers-contract-derive = { workspace = true, optional = true } -# eip712 -ethers-derive-eip712 = { workspace = true, optional = true } - [dev-dependencies] +ethers-providers = { workspace = true, features = ["ws"] } ethers-signers.workspace = true ethers-solc.workspace = true @@ -52,8 +50,6 @@ tokio = { workspace = true, features = ["macros"] } [features] default = ["abigen"] -eip712 = ["ethers-derive-eip712", "ethers-core/eip712"] - abigen-offline = ["ethers-contract-abigen", "ethers-contract-derive"] abigen = ["abigen-offline", "ethers-contract-abigen/online"] diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 3c15c8a0..9eed14a6 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -33,7 +33,7 @@ quote.workspace = true syn = { workspace = true, features = ["full"] } prettyplease = "0.1.25" -Inflector = "0.11" +Inflector.workspace = true serde.workspace = true serde_json.workspace = true hex.workspace = true diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index 65348d4b..86226b07 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -28,4 +28,6 @@ proc-macro2.workspace = true quote.workspace = true syn.workspace = true +Inflector.workspace = true hex.workspace = true +serde_json.workspace = true diff --git a/ethers-contract/ethers-contract-derive/src/eip712.rs b/ethers-contract/ethers-contract-derive/src/eip712.rs new file mode 100644 index 00000000..2f91af92 --- /dev/null +++ b/ethers-contract/ethers-contract-derive/src/eip712.rs @@ -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 { + // 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 { + let mut domain = EIP712Domain::default(); + utils::parse_attributes!(input.attrs.iter(), "eip712", meta, + "name", domain.name => { + meta.input.parse::()?; + let litstr: LitStr = meta.input.parse()?; + domain.name = Some(litstr.value()); + } + "version", domain.version => { + meta.input.parse::()?; + let litstr: LitStr = meta.input.parse()?; + domain.version = Some(litstr.value()); + } + "chain_id", domain.chain_id => { + meta.input.parse::()?; + 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::()?; + 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::()?; + 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> { + 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::>().join(","); + + let sig = format!("{primary_type}({parameters})"); + + keccak256(sig) +} diff --git a/ethers-contract/ethers-contract-derive/src/lib.rs b/ethers-contract/ethers-contract-derive/src/lib.rs index c41de57e..79ac6faa 100644 --- a/ethers-contract/ethers-contract-derive/src/lib.rs +++ b/ethers-contract/ethers-contract-derive/src/lib.rs @@ -13,6 +13,7 @@ mod call; pub(crate) mod calllike; mod codec; mod display; +mod eip712; mod error; mod event; mod spanned; @@ -357,3 +358,75 @@ pub fn derive_abi_error(input: TokenStream) -> TokenStream { } .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 +/// +/// 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::() +/// .expect("failed to parse address"), +/// contributor: "0000000000000000000000000000000000000000" +/// .parse::() +/// .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() +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index 8c8474bc..f5d27a1f 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -59,16 +59,13 @@ pub use ethers_contract_abigen::{ #[cfg(any(test, feature = "abigen"))] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] 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 #[doc(hidden)] pub use once_cell::sync::Lazy; -#[cfg(feature = "eip712")] -pub use ethers_derive_eip712::*; - // For macro expansions only, not public API. // See: [#2235](https://github.com/gakonst/ethers-rs/pull/2235) diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index 8d4dd44e..9b46d7e8 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -1,6 +1,6 @@ use crate::common::*; use ethers_contract::{ - abigen, ContractFactory, ContractInstance, EthAbiType, EthEvent, LogMeta, Multicall, + abigen, ContractFactory, ContractInstance, Eip712, EthAbiType, EthEvent, LogMeta, Multicall, MulticallError, MulticallVersion, }; use ethers_core::{ @@ -11,7 +11,7 @@ use ethers_core::{ }, 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 std::{sync::Arc, time::Duration}; @@ -342,7 +342,7 @@ async fn watch_events() { let mut stream = event.stream().await.unwrap(); // Also set up a subscription for the same thing - let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); + let ws = Provider::::connect(anvil.ws_endpoint()).await.unwrap(); let contract2 = ethers_contract::Contract::new(contract.address(), abi, ws.into()); let event2 = contract2.event::(); 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_2 = deploy(client.clone(), abi.clone(), bytecode).await; - let ws = Provider::connect(anvil.ws_endpoint()).await.unwrap(); + let ws = Provider::::connect(anvil.ws_endpoint()).await.unwrap(); let filter = Filter::new() .address(ValueOrArray::Array(vec![contract_1.address(), contract_2.address()])); let mut stream = ws.subscribe_logs(&filter).await.unwrap(); @@ -786,16 +786,15 @@ async fn multicall_aggregate() { } #[tokio::test] -#[cfg(feature = "eip712")] async fn test_derive_eip712() { - use ethers_derive_eip712::*; - // Generate Contract ABI Bindings - abigen!( - DeriveEip712Test, - "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", - event_derives(serde::Deserialize, serde::Serialize) - ); + mod contract { + ethers_contract::abigen!( + DeriveEip712Test, + "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", + derives(serde::Deserialize, serde::Serialize) + ); + } // Create derived structs @@ -842,7 +841,7 @@ async fn test_derive_eip712() { let addr = contract.address(); - let contract = DeriveEip712Test::new(addr, client.clone()); + let contract = contract::DeriveEip712Test::new(addr, client.clone()); let foo_bar = FooBar { foo: I256::from(10u64), @@ -853,7 +852,7 @@ async fn test_derive_eip712() { out: Address::from([0; 20]), }; - let derived_foo_bar = derive_eip_712_test::FooBar { + let derived_foo_bar = contract::FooBar { foo: foo_bar.foo, bar: foo_bar.bar, fizz: foo_bar.fizz.clone(), diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-contract/tests/it/eip712.rs similarity index 97% rename from ethers-core/ethers-derive-eip712/tests/derive_eip712.rs rename to ethers-contract/tests/it/eip712.rs index 7018f1dc..82ded36a 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-contract/tests/it/eip712.rs @@ -1,6 +1,4 @@ -#![allow(clippy::extra_unused_type_parameters)] - -use ethers_contract_derive::EthAbiType; +use ethers_contract_derive::{Eip712, EthAbiType}; use ethers_core::{ types::{ transaction::eip712::{ @@ -11,7 +9,6 @@ use ethers_core::{ }, utils::{keccak256, parse_ether}, }; -use ethers_derive_eip712::*; #[test] fn test_derive_eip712() { diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs index 135d053a..9e1f2888 100644 --- a/ethers-contract/tests/it/main.rs +++ b/ethers-contract/tests/it/main.rs @@ -7,6 +7,8 @@ mod derive; mod contract_call; +mod eip712; + #[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] mod common; diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 3966a99f..f1f72975 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -51,9 +51,6 @@ num_enum = "0.5" # macros feature enabled dependencies 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 } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] @@ -72,5 +69,4 @@ rand.workspace = true [features] celo = ["legacy"] # celo support extends the transaction format with extra fields legacy = [] -eip712 = ["convert_case", "syn"] macros = ["syn", "cargo_metadata", "once_cell"] diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml deleted file mode 100644 index 3eea2719..00000000 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "ethers-derive-eip712" -authors = ["Ryan Tate "] -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 diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs deleted file mode 100644 index 8b62e2d1..00000000 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ /dev/null @@ -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::() -//! .expect("failed to parse address"), -//! contributor: "0000000000000000000000000000000000000000" -//! .parse::() -//! .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 { - // 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) -} diff --git a/ethers-core/src/macros/ethers_crate.rs b/ethers-core/src/macros/ethers_crate.rs index 83b20b5c..f30d2fe5 100644 --- a/ethers-core/src/macros/ethers_crate.rs +++ b/ethers-core/src/macros/ethers_crate.rs @@ -220,7 +220,6 @@ pub enum EthersCrate { EthersContractAbigen, EthersContractDerive, EthersCore, - EthersDeriveEip712, EthersEtherscan, EthersMiddleware, EthersProviders, @@ -250,7 +249,6 @@ impl EthersCrate { Self::EthersContractAbigen => "ethers-contract-abigen", Self::EthersContractDerive => "ethers-contract-derive", Self::EthersCore => "ethers-core", - Self::EthersDeriveEip712 => "ethers-derive-eip712", Self::EthersEtherscan => "ethers-etherscan", Self::EthersMiddleware => "ethers-middleware", Self::EthersProviders => "ethers-providers", @@ -268,7 +266,6 @@ impl EthersCrate { Self::EthersContractAbigen => "::ethers_contract_abigen", Self::EthersContractDerive => "::ethers_contract_derive", Self::EthersCore => "::ethers_core", - Self::EthersDeriveEip712 => "::ethers_derive_eip712", Self::EthersEtherscan => "::ethers_etherscan", Self::EthersMiddleware => "::ethers_middleware", Self::EthersProviders => "::ethers_providers", @@ -284,7 +281,6 @@ impl EthersCrate { // re-exported in ethers::contract Self::EthersContractAbigen => "::ethers::contract", // partially Self::EthersContractDerive => "::ethers::contract", - Self::EthersDeriveEip712 => "::ethers::contract", Self::EthersAddressbook => "::ethers::addressbook", Self::EthersContract => "::ethers::contract", @@ -303,7 +299,6 @@ impl EthersCrate { match self { Self::EthersContractAbigen => "ethers-contract/ethers-contract-abigen", Self::EthersContractDerive => "ethers-contract/ethers-contract-derive", - Self::EthersDeriveEip712 => "ethers-core/ethers-derive-eip712", _ => self.crate_name(), } } diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 14891b69..d3df08a8 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -4,19 +4,12 @@ use crate::{ types::{serde_helpers::StringifiedNumeric, Address, Bytes, U256}, utils::keccak256, }; -use convert_case::{Case, Casing}; -use core::convert::TryFrom; use ethabi::encode; use serde::{Deserialize, Deserializer, Serialize}; use std::{ collections::{BTreeMap, HashSet}, - convert::TryInto, iter::FromIterator, }; -use syn::{ - parse::Error, spanned::Spanned, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, LitInt, - LitStr, PathArguments, Type, -}; /// Custom types for `TypedData` pub type Types = BTreeMap>; @@ -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, ]; -/// Error typed used by Eip712 derive macro +/// An EIP-712 error. #[derive(Debug, thiserror::Error)] pub enum Eip712Error { #[error("Failed to serialize serde JSON object")] @@ -56,8 +49,7 @@ pub enum Eip712Error { Message(String), } -/// The Eip712 trait provides helper methods for computing -/// the typed data hash used in `eth_signTypedData`. +/// Helper methods for computing the typed data hash used in `eth_signTypedData`. /// /// The ethers-rs `derive_eip712` crate provides a derive macro to /// implement the trait for a given struct. See documentation @@ -244,69 +236,6 @@ impl Eip712 for EIP712WithDomain { } } -// 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 { - 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. /// /// Typed data is a JSON object containing type information, domain separator parameters and the @@ -641,125 +570,6 @@ pub fn encode_field( Ok(token) } -/// Parse the eth abi parameter type based on the syntax type; -/// this method is copied from -/// with additional modifications for finding byte arrays -pub fn find_parameter_type(ty: &Type) -> Result { - 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::() { - 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::, _>>()?; - Ok(ParamType::Tuple(params)) - } - _ => Err(Error::new(ty.span(), "Failed to derive proper ABI from fields")), - } -} - -fn parse_int_param_type(s: &str) -> Option { - let size = s.chars().skip(1).collect::().parse::().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, 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; pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [u8; 32] { let parameters = diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs index 816f4539..41c277cb 100644 --- a/ethers-core/src/types/transaction/mod.rs +++ b/ethers-core/src/types/transaction/mod.rs @@ -5,7 +5,6 @@ pub mod eip1559; pub mod eip2718; pub mod eip2930; -#[cfg(feature = "eip712")] pub mod eip712; pub(crate) const BASE_NUM_TX_FIELDS: usize = 9; diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs index bda3e97e..7ec7e9ba 100644 --- a/ethers-core/src/utils/mod.rs +++ b/ethers-core/src/utils/mod.rs @@ -52,11 +52,9 @@ const OVERFLOW_I256_UNITS: usize = 77; /// U256 overflows for numbers wider than 78 units. const OVERFLOW_U256_UNITS: usize = 78; -/// Re-export of serde-json +// Re-export serde-json for macro usage #[doc(hidden)] -pub mod __serde_json { - pub use serde_json::*; -} +pub use serde_json as __serde_json; #[derive(Error, Debug)] pub enum ConversionError { diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index cd23c776..78edc24f 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -24,7 +24,7 @@ rustdoc-args = ["--cfg", "docsrs"] all-features = true [dependencies] -ethers-core = { workspace = true, features = ["eip712"] } +ethers-core.workspace = true # crypto coins-bip32 = "0.8.3" @@ -66,7 +66,6 @@ yubihsm = { version = "0.42.0-pre.0", features = ["secp256k1", "http", "usb"], o [dev-dependencies] ethers-contract-derive.workspace = true -ethers-derive-eip712.workspace = true serde_json.workspace = true tempfile.workspace = true diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index 05adbc2f..bed0741c 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -293,11 +293,10 @@ impl LedgerEthereum { mod tests { use super::*; use crate::Signer; - use ethers_contract_derive::EthAbiType; + use ethers_contract_derive::{Eip712, EthAbiType}; use ethers_core::types::{ transaction::eip712::Eip712, Address, TransactionRequest, I256, U256, }; - use ethers_derive_eip712::*; use std::str::FromStr; #[derive(Debug, Clone, Eip712, EthAbiType)] diff --git a/ethers-signers/src/trezor/app.rs b/ethers-signers/src/trezor/app.rs index a9e7b555..471e4e58 100644 --- a/ethers-signers/src/trezor/app.rs +++ b/ethers-signers/src/trezor/app.rs @@ -233,7 +233,7 @@ impl TrezorEthereum { mod tests { use super::*; use crate::Signer; - use ethers_contract_derive::EthAbiType; + use ethers_contract_derive::{Eip712, EthAbiType}; use ethers_core::types::{ transaction::{ eip2930::{AccessList, AccessListItem}, @@ -241,7 +241,6 @@ mod tests { }, Address, Eip1559TransactionRequest, TransactionRequest, I256, U256, }; - use ethers_derive_eip712::*; use std::str::FromStr; #[derive(Debug, Clone, Eip712, EthAbiType)] diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 7dff1300..c204b434 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -38,8 +38,6 @@ celo = [ legacy = ["ethers-core/legacy", "ethers-contract/legacy"] # individual features per sub-crate -## core -eip712 = ["ethers-contract/eip712", "ethers-core/eip712"] ## providers ws = ["ethers-providers/ws"] ipc = ["ethers-providers/ipc"] diff --git a/examples/wallets/Cargo.toml b/examples/wallets/Cargo.toml index 0d37ab5b..02b4c8c3 100644 --- a/examples/wallets/Cargo.toml +++ b/examples/wallets/Cargo.toml @@ -15,7 +15,7 @@ trezor = ["ethers/trezor"] yubi = ["ethers/yubi"] [dev-dependencies] -ethers = { workspace = true, features = ["abigen", "eip712", "ws", "rustls"] } +ethers = { workspace = true, features = ["abigen", "ws", "rustls"] } tokio = { workspace = true, features = ["macros"] }