Refactor crate determination in new ethers-macro crate (#555)

* fix: compute ethers-core path in derive eip712

Co-authored-by: Ryan <ryan.tate@fieldresponder.io>

* refactor: move crate determination to ethers-macro

* docs: update fmt command

* fix: change cargo_metadata dep to optional

Co-authored-by: Ryan <ryan.tate@fieldresponder.io>
This commit is contained in:
Sebastian Martinez 2021-11-05 14:00:01 +01:00 committed by GitHub
parent 6cd5625787
commit e72636210c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 153 additions and 129 deletions

View File

@ -118,7 +118,7 @@ This section lists some commonly needed commands.
```
cargo check --all-features
cargo fmt --all
cargo +nightly fmt --all
cargo build --all-features
cargo test --all-features
```

2
Cargo.lock generated
View File

@ -950,7 +950,6 @@ version = "0.5.3"
dependencies = [
"Inflector",
"anyhow",
"cargo_metadata",
"cfg-if 1.0.0",
"ethers-core",
"getrandom 0.2.3",
@ -985,6 +984,7 @@ dependencies = [
"arrayvec 0.7.2",
"bincode",
"bytes",
"cargo_metadata",
"convert_case",
"ecdsa",
"elliptic-curve",

View File

@ -10,7 +10,7 @@ repository = "https://github.com/gakonst/ethers-rs"
keywords = ["ethereum", "web3", "celo", "ethers"]
[dependencies]
ethers-core = { version = "^0.5.0", path = "../../ethers-core" }
ethers-core = { version = "^0.5.0", path = "../../ethers-core", features = ["macros"] }
anyhow = "1.0.37"
Inflector = "0.11"
@ -23,7 +23,6 @@ serde = { version = "1.0.124", features = ["derive"] }
hex = { version = "0.4.2", default-features = false, features = ["std"] }
reqwest = { version = "0.11.3", features = ["blocking"] }
once_cell = "1.8.0"
cargo_metadata = "0.14.0"
cfg-if = "1.0.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@ -8,7 +8,10 @@ mod types;
use super::{util, Abigen};
use crate::{contract::structs::InternalStructs, rawabi::RawAbi};
use anyhow::{anyhow, Context as _, Result};
use ethers_core::abi::{Abi, AbiParser};
use ethers_core::{
abi::{Abi, AbiParser},
macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate},
};
use proc_macro2::{Ident, Literal, TokenStream};
use quote::quote;
@ -111,9 +114,9 @@ impl Context {
// 5. Declare the structs parsed from the human readable abi
let abi_structs_decl = self.abi_structs()?;
let ethers_core = util::ethers_core_crate();
let ethers_contract = util::ethers_contract_crate();
let ethers_providers = util::ethers_providers_crate();
let ethers_core = ethers_core_crate();
let ethers_contract = ethers_contract_crate();
let ethers_providers = ethers_providers_crate();
let contract = quote! {
#struct_decl

View File

@ -3,7 +3,7 @@ use super::{util, Context};
use proc_macro2::TokenStream;
use quote::quote;
use super::util::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate};
use ethers_core::macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate};
pub(crate) fn imports(name: &str) -> TokenStream {
let doc = util::expand_doc(&format!("{} was auto-generated with ethers-rs Abigen. More information at: https://github.com/gakonst/ethers-rs", name));

View File

@ -1,6 +1,9 @@
use super::{types, util, Context};
use anyhow::Result;
use ethers_core::abi::{Event, EventExt, EventParam, ParamType, SolStruct};
use ethers_core::{
abi::{Event, EventExt, EventParam, ParamType, SolStruct},
macros::{ethers_contract_crate, ethers_core_crate},
};
use inflector::Inflector;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::quote;
@ -61,8 +64,8 @@ impl Context {
let enum_name = self.expand_event_enum_name();
let ethers_core = util::ethers_core_crate();
let ethers_contract = util::ethers_contract_crate();
let ethers_core = ethers_core_crate();
let ethers_contract = ethers_contract_crate();
quote! {
#[derive(Debug, Clone, PartialEq, Eq, #ethers_contract::EthAbiType)]
@ -106,7 +109,7 @@ impl Context {
let sorted_events: BTreeMap<_, _> = self.abi.events.clone().into_iter().collect();
let mut iter = sorted_events.values().flatten();
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
if let Some(event) = iter.next() {
let ty = if iter.next().is_some() {
@ -134,7 +137,7 @@ impl Context {
/// If a complex types matches with a struct previously parsed by the AbiParser,
/// we can replace it
fn expand_input_type(&self, input: &EventParam) -> Result<TokenStream> {
let ethers_core = util::ethers_core_crate();
let ethers_core = ethers_core_crate();
Ok(match (&input.kind, input.indexed) {
(ParamType::Array(ty), true) => {
if let ParamType::Tuple(..) = **ty {
@ -202,7 +205,7 @@ impl Context {
/// Expands into a single method for contracting an event stream.
fn expand_filter(&self, event: &Event) -> TokenStream {
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
let alias = self.event_aliases.get(&event.abi_signature()).cloned();
let name = if let Some(id) = alias.clone() {
@ -246,7 +249,7 @@ impl Context {
let derives = util::expand_derives(&self.event_derives);
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
Ok(quote! {
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthEvent, #ethers_contract::EthDisplay, #derives)]
@ -318,7 +321,7 @@ mod tests {
/// quasi-quoting for code generation. We do this to avoid allocating at runtime
fn expand_hash(hash: Hash) -> TokenStream {
let bytes = hash.as_bytes().iter().copied().map(Literal::u8_unsuffixed);
let ethers_core = util::ethers_core_crate();
let ethers_core = ethers_core_crate();
quote! {
#ethers_core::types::H256([#( #bytes ),*])

View File

@ -8,6 +8,7 @@ use syn::Ident;
use ethers_core::{
abi::{Function, FunctionExt, Param, ParamType},
macros::{ethers_contract_crate, ethers_core_crate},
types::Selector,
};
@ -63,7 +64,7 @@ impl Context {
function.selector()
);
let abi_signature_doc = util::expand_doc(&doc);
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
// use the same derives as for events
let derives = util::expand_derives(&self.event_derives);
@ -98,8 +99,8 @@ impl Context {
return Ok(struct_def_tokens)
}
let ethers_core = util::ethers_core_crate();
let ethers_contract = util::ethers_contract_crate();
let ethers_core = ethers_core_crate();
let ethers_contract = ethers_contract_crate();
let enum_name = self.expand_calls_enum_name();
Ok(quote! {
@ -238,7 +239,7 @@ impl Context {
// TODO use structs
let outputs = expand_fn_outputs(&function.outputs)?;
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
let result = quote! { #ethers_contract::builders::ContractCall<M, #outputs> };

View File

@ -6,10 +6,13 @@ use inflector::Inflector;
use proc_macro2::TokenStream;
use quote::quote;
use ethers_core::abi::{
use ethers_core::{
abi::{
param_type::Reader,
struct_def::{FieldDeclaration, FieldType, StructFieldType, StructType},
ParamType, SolStruct,
},
macros::ethers_contract_crate,
};
use crate::{
@ -123,7 +126,7 @@ impl Context {
// use the same derives as for events
let derives = util::expand_derives(&self.event_derives);
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
Ok(quote! {
#abi_signature_doc
#[derive(Clone, Debug, Default, Eq, PartialEq, #ethers_contract::EthAbiType, #derives)]
@ -184,7 +187,7 @@ impl Context {
let derives = &self.event_derives;
let derives = quote! {#(#derives),*};
let ethers_contract = util::ethers_contract_crate();
let ethers_contract = ethers_contract_crate();
Ok(quote! {
#abi_signature_doc

View File

@ -1,12 +1,10 @@
use anyhow::{anyhow, Result};
use ethers_core::abi::ParamType;
use ethers_core::{abi::ParamType, macros::ethers_core_crate};
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use super::util;
pub(crate) fn expand(kind: &ParamType) -> Result<TokenStream> {
let ethers_core = util::ethers_core_crate();
let ethers_core = ethers_core_crate();
match kind {
ParamType::Address => Ok(quote! { #ethers_core::types::Address }),

View File

@ -21,7 +21,7 @@ mod util;
pub use ethers_core::types::Address;
pub use source::Source;
pub use util::{ethers_contract_crate, ethers_core_crate, parse_address};
pub use util::parse_address;
use anyhow::Result;
use proc_macro2::TokenStream;

View File

@ -1,86 +1,12 @@
use ethers_core::types::Address;
use anyhow::{anyhow, Result};
use cargo_metadata::{DependencyKind, MetadataCommand};
use inflector::Inflector;
use once_cell::sync::Lazy;
use proc_macro2::{Ident, Literal, Span, TokenStream};
use quote::quote;
use syn::{Ident as SynIdent, Path};
/// See `determine_ethers_crates`
///
/// This ensures that the `MetadataCommand` is only run once
static ETHERS_CRATES: Lazy<(&'static str, &'static str, &'static str)> =
Lazy::new(determine_ethers_crates);
/// Convenience function to turn the `ethers_core` name in `ETHERS_CRATE` into a `Path`
pub fn ethers_core_crate() -> Path {
syn::parse_str(ETHERS_CRATES.0).expect("valid path; qed")
}
/// Convenience function to turn the `ethers_contract` name in `ETHERS_CRATE` into an `Path`
pub fn ethers_contract_crate() -> Path {
syn::parse_str(ETHERS_CRATES.1).expect("valid path; qed")
}
pub fn ethers_providers_crate() -> Path {
syn::parse_str(ETHERS_CRATES.2).expect("valid path; qed")
}
/// The crates name to use when deriving macros: (`core`, `contract`)
///
/// We try to determine which crate ident to use based on the dependencies of
/// the project in which the macro is used. This is useful because the macros,
/// like `EthEvent` are provided by the `ethers-contract` crate which depends on
/// `ethers_core`. Most commonly `ethers` will be used as dependency which
/// reexports all the different crates, essentially `ethers::core` is
/// `ethers_core` So depending on the dependency used `ethers` ors `ethers_core
/// | ethers_contract`, we need to use the fitting crate ident when expand the
/// macros This will attempt to parse the current `Cargo.toml` and check the
/// ethers related dependencies.
///
/// This process is a bit hacky, we run `cargo metadata` internally which
/// resolves the current package but creates a new `Cargo.lock` file in the
/// process. This is not a problem for regular workspaces but becomes an issue
/// during publishing with `cargo publish` if the project does not ignore
/// `Cargo.lock` in `.gitignore`, because then cargo can't proceed with
/// publishing the crate because the created `Cargo.lock` leads to a modified
/// workspace, not the `CARGO_MANIFEST_DIR` but the workspace `cargo publish`
/// created in `./target/package/..`. Therefore we check prior to executing
/// `cargo metadata` if a `Cargo.lock` file exists and delete it afterwards if
/// it was created by `cargo metadata`.
pub fn determine_ethers_crates() -> (&'static str, &'static str, &'static str) {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("No Manifest found");
// check if the lock file exists, if it's missing we need to clean up afterward
let lock_file = format!("{}/Cargo.lock", manifest_dir);
let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists();
let res = MetadataCommand::new()
.manifest_path(&format!("{}/Cargo.toml", manifest_dir))
.exec()
.ok()
.and_then(|metadata| {
metadata.root_package().and_then(|pkg| {
pkg.dependencies.iter().filter(|dep| dep.kind == DependencyKind::Normal).find_map(
|dep| {
(dep.name == "ethers")
.then(|| ("ethers::core", "ethers::contract", "ethers::providers"))
},
)
})
})
.unwrap_or(("ethers_core", "ethers_contract", "ethers_providers"));
if needs_lock_file_cleanup {
// delete the `Cargo.lock` file that was created by `cargo metadata`
// if the package is not part of a workspace
let _ = std::fs::remove_file(lock_file);
}
res
}
/// Expands a identifier string into an token.
pub fn ident(name: &str) -> Ident {
Ident::new(name, Span::call_site())

View File

@ -1,6 +1,6 @@
//! Helper functions for deriving `EthAbiType`
use ethers_contract_abigen::ethers_core_crate;
use ethers_core::macros::ethers_core_crate;
use proc_macro2::{Ident, Literal, TokenStream};
use quote::{quote, quote_spanned};
use syn::{parse::Error, spanned::Spanned as _, Data, DeriveInput, Fields, Variant};

View File

@ -1,11 +1,13 @@
//! Helper functions for deriving `EthCall`
use ethers_contract_abigen::{ethers_contract_crate, ethers_core_crate};
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{parse::Error, spanned::Spanned as _, AttrStyle, DeriveInput, Lit, Meta, NestedMeta};
use ethers_core::abi::{param_type::Reader, AbiParser, Function, FunctionExt, Param, ParamType};
use ethers_core::{
abi::{param_type::Reader, AbiParser, Function, FunctionExt, Param, ParamType},
macros::{ethers_contract_crate, ethers_core_crate},
};
use crate::{abi_ty, utils};

View File

@ -4,8 +4,7 @@ use proc_macro2::TokenStream;
use quote::quote;
use syn::{parse::Error, spanned::Spanned as _, Data, DeriveInput, Fields, Index};
use ethers_contract_abigen::ethers_core_crate;
use ethers_core::abi::ParamType;
use ethers_core::{abi::ParamType, macros::ethers_core_crate};
use crate::utils;

View File

@ -1,6 +1,6 @@
//! Helper functions for deriving `EthEvent`
use ethers_contract_abigen::{ethers_contract_crate, ethers_core_crate, Source};
use ethers_contract_abigen::Source;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use syn::{
@ -8,7 +8,10 @@ use syn::{
NestedMeta,
};
use ethers_core::abi::{param_type::Reader, AbiParser, Event, EventExt, EventParam, ParamType};
use ethers_core::{
abi::{param_type::Reader, AbiParser, Event, EventExt, EventParam, ParamType},
macros::{ethers_contract_crate, ethers_core_crate},
};
use hex::FromHex;
use crate::{abi_ty, utils};

View File

@ -1,5 +1,4 @@
use ethers_contract_abigen::ethers_core_crate;
use ethers_core::{abi::ParamType, types::Selector};
use ethers_core::{abi::ParamType, macros::ethers_core_crate, types::Selector};
use proc_macro2::Literal;
use quote::quote;
use syn::{

View File

@ -31,6 +31,9 @@ bytes = { version = "1.1.0", features = ["serde"] }
hex = { version = "0.4.3", default-features = false, features = ["std"] }
once_cell = "1.8.0"
# macros feature enabled dependencies
cargo_metadata = { version = "0.14.0", optional = true }
# eip712 feature enabled dependencies
convert_case = { version = "0.4.0", optional = true }
syn = { version = "1.0.81", optional = true }
@ -55,6 +58,7 @@ celo = ["legacy"] # celo support extends the transaction format with extra field
setup = ["tokio", "futures-util"] # async support for concurrent setup
legacy = []
eip712 = ["convert_case", "syn", "quote", "proc-macro2"]
macros = ["syn", "cargo_metadata"]
[package.metadata.docs.rs]
all-features = true

View File

@ -10,7 +10,7 @@ proc-macro = true
[dependencies]
quote = "1.0.9"
syn = "1.0.77"
ethers-core = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712"] }
ethers-core = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712", "macros"] }
hex = "0.4.3"
serde = "1.0.130"
serde_json = "1.0.68"

View File

@ -61,7 +61,7 @@
//! determine if there is a nested eip712 struct. However, this work is not yet complete.
use std::convert::TryFrom;
use ethers_core::types::transaction::eip712;
use ethers_core::{macros::ethers_core_crate, types::transaction::eip712};
use proc_macro::TokenStream;
use quote::quote;
@ -104,13 +104,16 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream {
Err(e) => return TokenStream::from(e),
};
// Compute the type hash for the derived struct using the parsed fields from above;
// 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 implementation = quote! {
impl Eip712 for #primary_type {
type Error = ethers_core::types::transaction::eip712::Eip712Error;
type Error = #ethers_core::types::transaction::eip712::Eip712Error;
fn type_hash() -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
@ -127,34 +130,34 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream {
Ok(byte_array)
}
fn domain(&self) -> Result<ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
let domain: ethers_core::types::transaction::eip712::EIP712Domain = serde_json::from_str(#domain_str)?;
fn domain(&self) -> Result<#ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
let domain: #ethers_core::types::transaction::eip712::EIP712Domain = 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()?[..]),
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() {
if let #ethers_core::abi::Token::Tuple(tokens) = self.clone().into_token() {
for token in tokens {
match &token {
ethers_core::abi::Token::Tuple(t) => {
#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));
items.push(#ethers_core::types::transaction::eip712::encode_eip712_type(token));
}
}
}
}
let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode(
let struct_hash = #ethers_core::utils::keccak256(#ethers_core::abi::encode(
&items,
));

View File

@ -51,6 +51,8 @@ pub mod abi;
/// Various utilities
pub mod utils;
pub mod macros;
// re-export rand to avoid potential confusion when there's rand version mismatches
pub use rand;

View File

@ -0,0 +1,75 @@
use cargo_metadata::{DependencyKind, MetadataCommand};
use once_cell::sync::Lazy;
use syn::Path;
/// See `determine_ethers_crates`
///
/// This ensures that the `MetadataCommand` is only run once
static ETHERS_CRATES: Lazy<(&'static str, &'static str, &'static str)> =
Lazy::new(determine_ethers_crates);
/// Convenience function to turn the `ethers_core` name in `ETHERS_CRATE` into a `Path`
pub fn ethers_core_crate() -> Path {
syn::parse_str(ETHERS_CRATES.0).expect("valid path; qed")
}
/// Convenience function to turn the `ethers_contract` name in `ETHERS_CRATE` into an `Path`
pub fn ethers_contract_crate() -> Path {
syn::parse_str(ETHERS_CRATES.1).expect("valid path; qed")
}
pub fn ethers_providers_crate() -> Path {
syn::parse_str(ETHERS_CRATES.2).expect("valid path; qed")
}
/// The crates name to use when deriving macros: (`core`, `contract`)
///
/// We try to determine which crate ident to use based on the dependencies of
/// the project in which the macro is used. This is useful because the macros,
/// like `EthEvent` are provided by the `ethers-contract` crate which depends on
/// `ethers_core`. Most commonly `ethers` will be used as dependency which
/// reexports all the different crates, essentially `ethers::core` is
/// `ethers_core` So depending on the dependency used `ethers` ors `ethers_core
/// | ethers_contract`, we need to use the fitting crate ident when expand the
/// macros This will attempt to parse the current `Cargo.toml` and check the
/// ethers related dependencies.
///
/// This process is a bit hacky, we run `cargo metadata` internally which
/// resolves the current package but creates a new `Cargo.lock` file in the
/// process. This is not a problem for regular workspaces but becomes an issue
/// during publishing with `cargo publish` if the project does not ignore
/// `Cargo.lock` in `.gitignore`, because then cargo can't proceed with
/// publishing the crate because the created `Cargo.lock` leads to a modified
/// workspace, not the `CARGO_MANIFEST_DIR` but the workspace `cargo publish`
/// created in `./target/package/..`. Therefore we check prior to executing
/// `cargo metadata` if a `Cargo.lock` file exists and delete it afterwards if
/// it was created by `cargo metadata`.
pub fn determine_ethers_crates() -> (&'static str, &'static str, &'static str) {
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("No Manifest found");
// check if the lock file exists, if it's missing we need to clean up afterward
let lock_file = format!("{}/Cargo.lock", manifest_dir);
let needs_lock_file_cleanup = !std::path::Path::new(&lock_file).exists();
let res = MetadataCommand::new()
.manifest_path(&format!("{}/Cargo.toml", manifest_dir))
.exec()
.ok()
.and_then(|metadata| {
metadata.root_package().and_then(|pkg| {
pkg.dependencies.iter().filter(|dep| dep.kind == DependencyKind::Normal).find_map(
|dep| {
(dep.name == "ethers")
.then(|| ("ethers::core", "ethers::contract", "ethers::providers"))
},
)
})
})
.unwrap_or(("ethers_core", "ethers_contract", "ethers_providers"));
if needs_lock_file_cleanup {
// delete the `Cargo.lock` file that was created by `cargo metadata`
// if the package is not part of a workspace
let _ = std::fs::remove_file(lock_file);
}
res
}

View File

@ -0,0 +1,4 @@
mod ethers_crate;
#[cfg(feature = "macros")]
pub use ethers_crate::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate};