From 5c9fae93aa2817189b169a74e31774e2d8dba60d Mon Sep 17 00:00:00 2001 From: Yiannis Marangos Date: Sat, 5 Aug 2023 12:39:22 +0300 Subject: [PATCH 1/5] feat(ethers-contract): Make ethers-providers optional --- Cargo.toml | 1 + ethers-contract/Cargo.toml | 9 +- .../ethers-contract-abigen/Cargo.toml | 1 + .../ethers-contract-abigen/src/contract.rs | 111 +++++++++++------- .../src/contract/methods.rs | 1 + .../ethers-contract-derive/Cargo.toml | 3 + ethers-contract/src/base.rs | 20 +++- ethers-contract/src/call.rs | 22 +--- ethers-contract/src/call_core.rs | 20 ++++ ethers-contract/src/contract.rs | 3 +- ethers-contract/src/error.rs | 2 + ethers-contract/src/event.rs | 63 +--------- ethers-contract/src/event_core.rs | 71 +++++++++++ ethers-contract/src/lib.rs | 65 +++++----- ethers-contract/src/multicall/mod.rs | 53 ++++++--- ethers/Cargo.toml | 2 +- 16 files changed, 271 insertions(+), 176 deletions(-) create mode 100644 ethers-contract/src/call_core.rs create mode 100644 ethers-contract/src/event_core.rs diff --git a/Cargo.toml b/Cargo.toml index 96ae99f71..e5f5059bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,6 +115,7 @@ auto_impl = "1.1" # misc bytes = "1.4" +cfg-if = "1.0.0" criterion = "0.5" dunce = "1.0" eyre = "0.6" diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 85edac30c..e359a31ad 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -23,10 +23,10 @@ rustdoc-args = ["--cfg", "docsrs"] all-features = true [dependencies] -ethers-providers.workspace = true +ethers-providers = { workspace = true, optional = true } ethers-core.workspace = true -ethers-signers.workspace = true +cfg-if.workspace = true serde.workspace = true serde_json.workspace = true futures-util.workspace = true @@ -41,12 +41,15 @@ ethers-contract-derive = { workspace = true, optional = true } [dev-dependencies] ethers-providers = { workspace = true, features = ["ws"] } +ethers-signers.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } [features] -default = ["abigen"] +default = ["abigen", "providers"] + +providers = ["ethers-providers", "ethers-contract-abigen/providers", "ethers-contract-derive/providers"] abigen = ["ethers-contract-abigen", "ethers-contract-derive"] abigen-online = ["abigen", "ethers-contract-abigen/online"] diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index e1552b8c4..0d29fef3d 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -50,6 +50,7 @@ tokio = { workspace = true, optional = true } url = { workspace = true, optional = true } [features] +providers = [] online = ["reqwest", "ethers-etherscan", "url", "tokio"] rustls = ["reqwest?/rustls-tls", "ethers-etherscan?/rustls"] openssl = ["reqwest?/native-tls", "ethers-etherscan?/openssl"] diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs index 729b11c76..ab91fee88 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract.rs @@ -8,9 +8,11 @@ mod types; use super::{util, Abigen}; use crate::contract::{methods::MethodAlias, structs::InternalStructs}; +#[cfg(feature = "providers")] +use ethers_core::macros::ethers_providers_crate; use ethers_core::{ abi::{Abi, AbiParser, ErrorExt, EventExt, JsonAbi}, - macros::{ethers_contract_crate, ethers_core_crate, ethers_providers_crate}, + macros::{ethers_contract_crate, ethers_core_crate}, types::Bytes, }; use eyre::{eyre, Context as _, Result}; @@ -131,9 +133,7 @@ pub struct Context { impl Context { /// Generates the tokens. pub fn expand(&self) -> Result { - let name = &self.contract_ident; let name_mod = util::ident(&util::safe_module_name(&self.contract_name)); - let abi_name = self.inline_abi_ident(); // 1. Declare Contract struct let struct_decl = self.struct_declaration(); @@ -141,27 +141,36 @@ impl Context { // 2. Declare events structs & impl FromTokens for each event let events_decl = self.events_declaration()?; - // 3. impl block for the event functions - let contract_events = self.event_methods()?; - - // 4. impl block for the contract methods and their corresponding types + // 3. impl block for the contract methods and their corresponding types let (contract_methods, call_structs) = self.methods_and_call_structs()?; - // 5. The deploy method, only if the contract has a bytecode object - let deployment_methods = self.deployment_methods(); - - // 6. Declare the structs parsed from the human readable abi + // 4. Declare the structs parsed from the human readable abi let abi_structs_decl = self.abi_structs()?; - // 7. declare all error types + // 5. declare all error types let errors_decl = self.errors()?; - let ethers_core = ethers_core_crate(); - let ethers_contract = ethers_contract_crate(); - let ethers_providers = ethers_providers_crate(); - let contract = quote! { - #struct_decl + #struct_decl + }; + + #[cfg(feature = "providers")] + let contract = { + let name = &self.contract_ident; + let abi_name = self.inline_abi_ident(); + + // 6. impl block for the event functions + let contract_events = self.event_methods()?; + + // 7. The deploy method, only if the contract has a bytecode object + let deployment_methods = self.deployment_methods(); + + let ethers_core = ethers_core_crate(); + let ethers_contract = ethers_contract_crate(); + let ethers_providers = ethers_providers_crate(); + + quote! { + #contract impl #name { /// Creates a new contract instance with the specified `ethers` client at @@ -182,8 +191,13 @@ impl Context { Self::new(contract.address(), contract.client()) } } + } }; + // Avoid having a warning + #[cfg(not(feature = "providers"))] + let _ = contract_methods; + Ok(ExpandedContract { module: name_mod, imports: quote!(), @@ -339,8 +353,6 @@ impl Context { /// Generates the token stream for the contract's ABI, bytecode and struct declarations. pub(crate) fn struct_declaration(&self) -> TokenStream { - let name = &self.contract_ident; - let ethers_core = ethers_core_crate(); let ethers_contract = ethers_contract_crate(); @@ -390,7 +402,7 @@ impl Context { } }); - quote! { + let code = quote! { // The `Lazy` ABI #abi @@ -399,41 +411,52 @@ impl Context { // The static deployed Bytecode, if present #deployed_bytecode + }; + + #[cfg(feature = "providers")] + let code = { + let name = &self.contract_ident; - // Struct declaration - pub struct #name(#ethers_contract::Contract); + quote! { + #code + + // Struct declaration + pub struct #name(#ethers_contract::Contract); - // Manual implementation since `M` is stored in `Arc` and does not need to be `Clone` - impl ::core::clone::Clone for #name { - fn clone(&self) -> Self { - Self(::core::clone::Clone::clone(&self.0)) + // Manual implementation since `M` is stored in `Arc` and does not need to be `Clone` + impl ::core::clone::Clone for #name { + fn clone(&self) -> Self { + Self(::core::clone::Clone::clone(&self.0)) + } } - } - // Deref to the inner contract to have access to all its methods - impl ::core::ops::Deref for #name { - type Target = #ethers_contract::Contract; + // Deref to the inner contract to have access to all its methods + impl ::core::ops::Deref for #name { + type Target = #ethers_contract::Contract; - fn deref(&self) -> &Self::Target { - &self.0 + fn deref(&self) -> &Self::Target { + &self.0 + } } - } - impl ::core::ops::DerefMut for #name { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 + impl ::core::ops::DerefMut for #name { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } } - } - // `(
)` - impl ::core::fmt::Debug for #name { - fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { - f.debug_tuple(::core::stringify!(#name)) - .field(&self.address()) - .finish() + // `(
)` + impl ::core::fmt::Debug for #name { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + f.debug_tuple(::core::stringify!(#name)) + .field(&self.address()) + .finish() + } } } - } + }; + + code } } diff --git a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs index de11b029e..9c0791e2d 100644 --- a/ethers-contract/ethers-contract-abigen/src/contract/methods.rs +++ b/ethers-contract/ethers-contract-abigen/src/contract/methods.rs @@ -49,6 +49,7 @@ impl Context { } /// Returns all deploy (constructor) implementations + #[cfg(feature = "providers")] pub(crate) fn deployment_methods(&self) -> Option { // don't generate deploy if no bytecode self.contract_bytecode.as_ref()?; diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index 86226b071..49d0fd2db 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -31,3 +31,6 @@ syn.workspace = true Inflector.workspace = true hex.workspace = true serde_json.workspace = true + +[features] +providers = ["ethers-contract-abigen/providers"] diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 077ccbc5c..9cc5bae64 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -1,17 +1,23 @@ -use crate::ContractInstance; pub use ethers_core::abi::AbiError; use ethers_core::{ abi::{Abi, Detokenize, Error, Event, Function, FunctionExt, RawLog, Token, Tokenize}, - types::{Address, Bytes, Selector, H256}, + types::{Bytes, Selector, H256}, }; -use ethers_providers::Middleware; use std::{ - borrow::Borrow, collections::{BTreeMap, HashMap}, fmt::Debug, hash::Hash, }; +cfg_if::cfg_if! { + if #[cfg(feature = "providers")] { + use crate::ContractInstance; + use ethers_providers::Middleware; + use ethers_core::types::Address; + use std::borrow::Borrow; + } +} + /// A reduced form of `Contract` which just takes the `abi` and produces /// ABI encoded data for its functions. #[derive(Debug, Clone)] @@ -219,6 +225,7 @@ impl BaseContract { } /// Upgrades a `BaseContract` into a full fledged contract with an address and middleware. + #[cfg(feature = "providers")] pub fn into_contract(self, address: Address, client: B) -> ContractInstance where B: Borrow, @@ -314,7 +321,10 @@ where #[cfg(test)] mod tests { use super::*; - use ethers_core::{abi::parse_abi, types::U256}; + use ethers_core::{ + abi::parse_abi, + types::{Address, U256}, + }; #[test] fn can_parse_function_inputs() { diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs index ad1cfa0c4..701207561 100644 --- a/ethers-contract/src/call.rs +++ b/ethers-contract/src/call.rs @@ -4,12 +4,10 @@ use crate::{error::ContractRevert, EthError}; use super::base::{decode_function_data, AbiError}; use ethers_core::{ - abi::{AbiDecode, AbiEncode, Detokenize, Function, InvalidOutputType, Tokenizable}, + abi::{Detokenize, Function, InvalidOutputType}, types::{ - transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, Selector, - TransactionRequest, U256, + transaction::eip2718::TypedTransaction, Address, BlockId, Bytes, TransactionRequest, U256, }, - utils::id, }; use ethers_providers::{ call_raw::{CallBuilder, RawCall}, @@ -17,7 +15,7 @@ use ethers_providers::{ }; use std::{ - borrow::{Borrow, Cow}, + borrow::Borrow, fmt::Debug, future::{Future, IntoFuture}, marker::PhantomData, @@ -26,20 +24,6 @@ use std::{ use thiserror::Error as ThisError; -/// A helper trait for types that represent all call input parameters of a specific function -pub trait EthCall: Tokenizable + AbiDecode + AbiEncode + Send + Sync { - /// The name of the function - fn function_name() -> Cow<'static, str>; - - /// Retrieves the ABI signature for the call - fn abi_signature() -> Cow<'static, str>; - - /// The selector of the function - fn selector() -> Selector { - id(Self::abi_signature()) - } -} - #[derive(ThisError, Debug)] /// An Error which is thrown when interacting with a smart contract pub enum ContractError { diff --git a/ethers-contract/src/call_core.rs b/ethers-contract/src/call_core.rs new file mode 100644 index 000000000..558676d0f --- /dev/null +++ b/ethers-contract/src/call_core.rs @@ -0,0 +1,20 @@ +use ethers_core::{ + abi::{AbiDecode, AbiEncode, Tokenizable}, + types::Selector, + utils::id, +}; +use std::borrow::Cow; + +/// A helper trait for types that represent all call input parameters of a specific function +pub trait EthCall: Tokenizable + AbiDecode + AbiEncode + Send + Sync { + /// The name of the function + fn function_name() -> Cow<'static, str>; + + /// Retrieves the ABI signature for the call + fn abi_signature() -> Cow<'static, str>; + + /// The selector of the function + fn selector() -> Selector { + id(Self::abi_signature()) + } +} diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs index d05726237..cb66b89c6 100644 --- a/ethers-contract/src/contract.rs +++ b/ethers-contract/src/contract.rs @@ -1,7 +1,8 @@ use crate::{ base::{encode_function_data, AbiError, BaseContract}, call::FunctionCall, - event::{EthEvent, Event}, + event::Event, + event_core::EthEvent, }; use ethers_core::{ abi::{Abi, Detokenize, Error, EventExt, Function, Tokenize}, diff --git a/ethers-contract/src/error.rs b/ethers-contract/src/error.rs index e7373c8ad..c15eb3b57 100644 --- a/ethers-contract/src/error.rs +++ b/ethers-contract/src/error.rs @@ -3,6 +3,7 @@ use ethers_core::{ types::Selector, utils::id, }; +#[cfg(feature = "providers")] use ethers_providers::JsonRpcError; use std::borrow::Cow; @@ -46,6 +47,7 @@ pub trait EthError: Tokenizable + AbiDecode + AbiEncode + Send + Sync { /// Attempt to decode from a [`JsonRpcError`] by extracting revert data /// /// Fails if the error is not a revert, or decoding fails + #[cfg(feature = "providers")] fn from_rpc_response(response: &JsonRpcError) -> Option { Self::decode_with_selector(&response.as_revert_data()?) } diff --git a/ethers-contract/src/event.rs b/ethers-contract/src/event.rs index 4a24987b1..7b62340bb 100644 --- a/ethers-contract/src/event.rs +++ b/ethers-contract/src/event.rs @@ -1,67 +1,14 @@ #![allow(clippy::return_self_not_must_use)] -use crate::{log::LogMeta, stream::EventStream, ContractError, EthLogDecode}; +use crate::{ + event_core::parse_log, log::LogMeta, stream::EventStream, ContractError, EthLogDecode, +}; use ethers_core::{ - abi::{Address, Detokenize, Error as AbiError, RawLog}, + abi::Address, types::{BlockNumber, Filter, Log, Topic, ValueOrArray, H256}, }; use ethers_providers::{FilterWatcher, Middleware, PubsubClient, SubscriptionStream}; -use std::{ - borrow::{Borrow, Cow}, - marker::PhantomData, -}; - -/// Attempt to parse a log into a specific output type. -pub fn parse_log(log: Log) -> std::result::Result -where - D: EthLogDecode, -{ - D::decode_log(&RawLog { topics: log.topics, data: log.data.to_vec() }) -} - -/// A trait for implementing event bindings -pub trait EthEvent: Detokenize + Send + Sync { - /// The name of the event this type represents - fn name() -> Cow<'static, str>; - - /// Retrieves the signature for the event this data corresponds to. - /// This signature is the Keccak-256 hash of the ABI signature of - /// this event. - fn signature() -> H256; - - /// Retrieves the ABI signature for the event this data corresponds - /// to. - fn abi_signature() -> Cow<'static, str>; - - /// Decodes an Ethereum `RawLog` into an instance of the type. - fn decode_log(log: &RawLog) -> Result - where - Self: Sized; - - /// Returns true if this is an anonymous event - fn is_anonymous() -> bool; - - /// Returns an Event builder for the ethereum event represented by this types ABI signature. - fn new(filter: Filter, provider: B) -> Event - where - Self: Sized, - B: Borrow, - M: Middleware, - { - let filter = filter.event(&Self::abi_signature()); - Event { filter, provider, datatype: PhantomData, _m: PhantomData } - } -} - -// Convenience implementation -impl EthLogDecode for T { - fn decode_log(log: &RawLog) -> Result - where - Self: Sized, - { - T::decode_log(log) - } -} +use std::{borrow::Borrow, marker::PhantomData}; /// Helper for managing the event filter before querying or streaming its logs #[derive(Debug)] diff --git a/ethers-contract/src/event_core.rs b/ethers-contract/src/event_core.rs new file mode 100644 index 000000000..627ff0e46 --- /dev/null +++ b/ethers-contract/src/event_core.rs @@ -0,0 +1,71 @@ +#![allow(clippy::return_self_not_must_use)] + +use crate::EthLogDecode; +use ethers_core::{ + abi::{Detokenize, Error as AbiError, RawLog}, + types::{Log, H256}, +}; +use std::borrow::Cow; + +cfg_if::cfg_if! { + if #[cfg(feature = "providers")] { + use std::borrow::Borrow; + use std::marker::PhantomData; + use ethers_core::types::Filter; + use ethers_providers::Middleware; + use crate::event::Event; + } +} + +/// Attempt to parse a log into a specific output type. +pub fn parse_log(log: Log) -> std::result::Result +where + D: EthLogDecode, +{ + D::decode_log(&RawLog { topics: log.topics, data: log.data.to_vec() }) +} + +/// A trait for implementing event bindings +pub trait EthEvent: Detokenize + Send + Sync { + /// The name of the event this type represents + fn name() -> Cow<'static, str>; + + /// Retrieves the signature for the event this data corresponds to. + /// This signature is the Keccak-256 hash of the ABI signature of + /// this event. + fn signature() -> H256; + + /// Retrieves the ABI signature for the event this data corresponds + /// to. + fn abi_signature() -> Cow<'static, str>; + + /// Decodes an Ethereum `RawLog` into an instance of the type. + fn decode_log(log: &RawLog) -> Result + where + Self: Sized; + + /// Returns true if this is an anonymous event + fn is_anonymous() -> bool; + + /// Returns an Event builder for the ethereum event represented by this types ABI signature. + #[cfg(feature = "providers")] + fn new(filter: Filter, provider: B) -> Event + where + Self: Sized, + B: Borrow, + M: Middleware, + { + let filter = filter.event(&Self::abi_signature()); + Event { filter, provider, datatype: PhantomData, _m: PhantomData } + } +} + +// Convenience implementation +impl EthLogDecode for T { + fn decode_log(log: &RawLog) -> Result + where + Self: Sized, + { + T::decode_log(log) + } +} diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index b23524785..d439fc160 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -3,24 +3,17 @@ #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] -#[path = "contract.rs"] -mod _contract; -pub use _contract::{Contract, ContractInstance}; - mod base; pub use base::{decode_function_data, encode_function_data, AbiError, BaseContract}; -mod call; -pub use call::{ContractCall, ContractError, EthCall, FunctionCall}; +mod call_core; +pub use call_core::EthCall; mod error; pub use error::{ContractRevert, EthError}; -mod factory; -pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; - -mod event; -pub use event::{parse_log, EthEvent, Event}; +mod event_core; +pub use event_core::{parse_log, EthEvent}; mod log; pub use log::{decode_logs, EthLogDecode, LogMeta}; @@ -34,22 +27,9 @@ mod multicall; #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] pub use multicall::{ constants::{MULTICALL_ADDRESS, MULTICALL_SUPPORTED_CHAIN_IDS}, - contract as multicall_contract, - error::MulticallError, - Call, Multicall, MulticallContract, MulticallVersion, + contract as multicall_contract, MulticallVersion, }; -/// This module exposes low lever builder structures which are only consumed by the -/// type-safe ABI bindings generators. -#[doc(hidden)] -pub mod builders { - pub use super::{ - call::ContractCall, - event::Event, - factory::{ContractDeployer, Deployer}, - }; -} - #[cfg(feature = "abigen")] #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] pub use ethers_contract_abigen::{ @@ -85,5 +65,36 @@ pub mod contract { #[doc(hidden)] pub use ethers_core as core; -#[doc(hidden)] -pub use ethers_providers as providers; +cfg_if::cfg_if! { + if #[cfg(feature = "providers")] { + mod event; + pub use event::Event; + + #[path = "contract.rs"] + mod _contract; + pub use _contract::{Contract, ContractInstance}; + + mod call; + pub use call::{ContractCall, ContractError, FunctionCall}; + + mod factory; + pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; + + #[cfg(all(feature = "abigen"))] + pub use multicall::{error::MulticallError, Call, Multicall, MulticallContract}; + + /// This module exposes low lever builder structures which are only consumed by the + /// type-safe ABI bindings generators. + #[doc(hidden)] + pub mod builders { + pub use super::{ + call::ContractCall, + event::Event, + factory::{ContractDeployer, Deployer}, + }; + } + + #[doc(hidden)] + pub use ethers_providers as providers; + } +} diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 97749eb67..2a6cf524d 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -1,32 +1,45 @@ -use crate::call::{ContractCall, ContractError}; -use ethers_core::{ - abi::{Detokenize, Function, Token, Tokenizable}, - types::{ - transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress, U256, - }, -}; -use ethers_providers::{Middleware, PendingTransaction}; -use std::{convert::TryFrom, fmt, result::Result as StdResult, sync::Arc}; +use std::{convert::TryFrom, result::Result as StdResult}; /// The Multicall contract bindings. Auto-generated with `abigen`. pub mod contract; -pub use contract::Multicall3 as MulticallContract; -use contract::{ - Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue, - Result as MulticallResult, -}; pub mod constants; -/// Type alias for `Result>` -pub type Result = StdResult>; +cfg_if::cfg_if! { + if #[cfg(feature = "providers")] { + use crate::call::{ContractCall, ContractError}; -/// MultiCall error type -pub mod error; + use ethers_core::{ + abi::{Detokenize, Function, Token, Tokenizable}, + types::{ + transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, + NameOrAddress, U256, + }, + }; + use std::{fmt, sync::Arc}; + + use ethers_providers::{Middleware, PendingTransaction}; + + pub use contract::Multicall3 as MulticallContract; + use contract::{ + Call as Multicall1Call, + Call3 as Multicall3Call, + Call3Value as Multicall3CallValue, + Result as MulticallResult, + }; + + /// MultiCall error type + pub mod error; + + /// Type alias for `Result>` + pub type Result = StdResult>; + } +} /// Helper struct for managing calls to be made to the `function` in smart contract `target` /// with `data`. #[derive(Clone, Debug)] +#[cfg(feature = "providers")] pub struct Call { target: Address, data: Bytes, @@ -194,6 +207,7 @@ impl MulticallVersion { /// [`call`]: #method.call /// [`send`]: #method.send #[must_use = "Multicall does nothing unless you use `call` or `send`"] +#[cfg(feature = "providers")] pub struct Multicall { /// The Multicall contract interface. pub contract: MulticallContract, @@ -212,6 +226,7 @@ pub struct Multicall { } // Manually implement Clone and Debug to avoid trait bounds. +#[cfg(feature = "providers")] impl Clone for Multicall { fn clone(&self) -> Self { Self { @@ -224,6 +239,7 @@ impl Clone for Multicall { } } +#[cfg(feature = "providers")] impl fmt::Debug for Multicall { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Multicall") @@ -236,6 +252,7 @@ impl fmt::Debug for Multicall { } } +#[cfg(feature = "providers")] impl Multicall { /// Creates a new Multicall instance from the provided client. If provided with an `address`, /// it instantiates the Multicall contract with that address, otherwise it defaults to diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml index 3bd14612a..27a985317 100644 --- a/ethers/Cargo.toml +++ b/ethers/Cargo.toml @@ -86,7 +86,7 @@ solc-sha2-asm = [] [dependencies] ethers-addressbook.workspace = true -ethers-contract.workspace = true +ethers-contract = { workspace = true, features = ["providers"] } ethers-core.workspace = true ethers-etherscan.workspace = true ethers-middleware.workspace = true From 7693a01fe5e73e073ed9c2960479ecfc6435dc09 Mon Sep 17 00:00:00 2001 From: Yiannis Marangos Date: Sat, 5 Aug 2023 14:46:20 +0300 Subject: [PATCH 2/5] multicall: Move providers feature implementation in another file --- Cargo.toml | 1 - ethers-contract/Cargo.toml | 1 - ethers-contract/src/base.rs | 12 +- ethers-contract/src/event_core.rs | 14 +- ethers-contract/src/lib.rs | 69 +- ethers-contract/src/multicall/middleware.rs | 815 +++++++++++++++++++ ethers-contract/src/multicall/mod.rs | 833 +------------------- 7 files changed, 868 insertions(+), 877 deletions(-) create mode 100644 ethers-contract/src/multicall/middleware.rs diff --git a/Cargo.toml b/Cargo.toml index e5f5059bb..96ae99f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,7 +115,6 @@ auto_impl = "1.1" # misc bytes = "1.4" -cfg-if = "1.0.0" criterion = "0.5" dunce = "1.0" eyre = "0.6" diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index e359a31ad..33d29dd04 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -26,7 +26,6 @@ all-features = true ethers-providers = { workspace = true, optional = true } ethers-core.workspace = true -cfg-if.workspace = true serde.workspace = true serde_json.workspace = true futures-util.workspace = true diff --git a/ethers-contract/src/base.rs b/ethers-contract/src/base.rs index 9cc5bae64..1374b8b56 100644 --- a/ethers-contract/src/base.rs +++ b/ethers-contract/src/base.rs @@ -9,13 +9,11 @@ use std::{ hash::Hash, }; -cfg_if::cfg_if! { - if #[cfg(feature = "providers")] { - use crate::ContractInstance; - use ethers_providers::Middleware; - use ethers_core::types::Address; - use std::borrow::Borrow; - } +if_providers! { + use crate::ContractInstance; + use ethers_providers::Middleware; + use ethers_core::types::Address; + use std::borrow::Borrow; } /// A reduced form of `Contract` which just takes the `abi` and produces diff --git a/ethers-contract/src/event_core.rs b/ethers-contract/src/event_core.rs index 627ff0e46..bea17959a 100644 --- a/ethers-contract/src/event_core.rs +++ b/ethers-contract/src/event_core.rs @@ -7,14 +7,12 @@ use ethers_core::{ }; use std::borrow::Cow; -cfg_if::cfg_if! { - if #[cfg(feature = "providers")] { - use std::borrow::Borrow; - use std::marker::PhantomData; - use ethers_core::types::Filter; - use ethers_providers::Middleware; - use crate::event::Event; - } +if_providers! { + use std::borrow::Borrow; + use std::marker::PhantomData; + use ethers_core::types::Filter; + use ethers_providers::Middleware; + use crate::event::Event; } /// Attempt to parse a log into a specific output type. diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index d439fc160..859b2102b 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -3,6 +3,14 @@ #![warn(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] +macro_rules! if_providers { + ($($item:item)*) => {$( + #[cfg(feature = "providers")] + #[cfg_attr(docsrs, doc(cfg(feature = "providers")))] + $item + )*} +} + mod base; pub use base::{decode_function_data, encode_function_data, AbiError, BaseContract}; @@ -65,36 +73,35 @@ pub mod contract { #[doc(hidden)] pub use ethers_core as core; -cfg_if::cfg_if! { - if #[cfg(feature = "providers")] { - mod event; - pub use event::Event; - - #[path = "contract.rs"] - mod _contract; - pub use _contract::{Contract, ContractInstance}; - - mod call; - pub use call::{ContractCall, ContractError, FunctionCall}; - - mod factory; - pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; - - #[cfg(all(feature = "abigen"))] - pub use multicall::{error::MulticallError, Call, Multicall, MulticallContract}; - - /// This module exposes low lever builder structures which are only consumed by the - /// type-safe ABI bindings generators. - #[doc(hidden)] - pub mod builders { - pub use super::{ - call::ContractCall, - event::Event, - factory::{ContractDeployer, Deployer}, - }; - } - - #[doc(hidden)] - pub use ethers_providers as providers; +if_providers! { + mod event; + pub use event::Event; + + #[path = "contract.rs"] + mod _contract; + pub use _contract::{Contract, ContractInstance}; + + mod call; + pub use call::{ContractCall, ContractError, FunctionCall}; + + mod factory; + pub use factory::{ContractDeployer, ContractDeploymentTx, ContractFactory, DeploymentTxFactory}; + + #[cfg(all(feature = "abigen"))] + #[cfg_attr(docsrs, doc(cfg(feature = "abigen")))] + pub use multicall::{error::MulticallError, Call, Multicall, MulticallContract}; + + /// This module exposes low lever builder structures which are only consumed by the + /// type-safe ABI bindings generators. + #[doc(hidden)] + pub mod builders { + pub use super::{ + call::ContractCall, + event::Event, + factory::{ContractDeployer, Deployer}, + }; } + + #[doc(hidden)] + pub use ethers_providers as providers; } diff --git a/ethers-contract/src/multicall/middleware.rs b/ethers-contract/src/multicall/middleware.rs new file mode 100644 index 000000000..f94515e53 --- /dev/null +++ b/ethers-contract/src/multicall/middleware.rs @@ -0,0 +1,815 @@ +use crate::call::{ContractCall, ContractError}; +use ethers_core::{ + abi::{Detokenize, Function, Token, Tokenizable}, + types::{ + transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress, U256, + }, +}; +use ethers_providers::{Middleware, PendingTransaction}; +use std::{fmt, result::Result as StdResult, sync::Arc}; + +pub use super::contract::Multicall3 as MulticallContract; +use super::contract::{ + Call as Multicall1Call, Call3 as Multicall3Call, Call3Value as Multicall3CallValue, + Result as MulticallResult, +}; +use super::{constants, error, MulticallVersion}; + +/// Type alias for `Result>` +pub type Result = StdResult>; + +/// Helper struct for managing calls to be made to the `function` in smart contract `target` +/// with `data`. +#[derive(Clone, Debug)] +pub struct Call { + target: Address, + data: Bytes, + value: U256, + allow_failure: bool, + function: Function, +} + +/// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain. +/// It stores an instance of the [`Multicall` smart contract](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code) +/// and the user provided list of transactions to be called or executed on chain. +/// +/// `Multicall` can be instantiated asynchronously from the chain ID of the provided client using +/// [`new`] or synchronously by providing a chain ID in [`new_with_chain`]. This, by default, uses +/// [`constants::MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`. +/// A list of all the supported chains is available [`here`](https://github.com/mds1/multicall#multicall3-contract-addresses). +/// +/// Set the contract's version by using [`version`]. +/// +/// The `block` number can be provided for the call by using [`block`]. +/// +/// Transactions default to `EIP1559`. This can be changed by using [`legacy`]. +/// +/// Build on the `Multicall` instance by adding calls using [`add_call`] and call or broadcast them +/// all at once by using [`call`] and [`send`] respectively. +/// +/// # Example +/// +/// Using Multicall (version 1): +/// +/// ```no_run +/// use ethers_core::{ +/// abi::Abi, +/// types::{Address, H256, U256}, +/// }; +/// use ethers_contract::{Contract, Multicall, MulticallVersion}; +/// use ethers_providers::{Middleware, Http, Provider, PendingTransaction}; +/// use std::{convert::TryFrom, sync::Arc}; +/// +/// # async fn bar() -> Result<(), Box> { +/// // this is a dummy address used for illustration purposes +/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; +/// +/// // (ugly way to write the ABI inline, you can otherwise read it from a file) +/// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?; +/// +/// // connect to the network +/// let client = Provider::::try_from("http://localhost:8545")?; +/// +/// // create the contract object. This will be used to construct the calls for multicall +/// let client = Arc::new(client); +/// let contract = Contract::>::new(address, abi, client.clone()); +/// +/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve. +/// // But we will let `Multicall` to take care of that for us +/// let first_call = contract.method::<_, String>("getValue", ())?; +/// let second_call = contract.method::<_, Address>("lastSender", ())?; +/// +/// // Since this example connects to a known chain, we need not provide an address for +/// // the Multicall contract and we set that to `None`. If you wish to provide the address +/// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument. +/// // Construction of the `Multicall` instance follows the builder pattern: +/// let mut multicall = Multicall::new(client.clone(), None).await?; +/// multicall +/// .add_call(first_call, false) +/// .add_call(second_call, false); +/// +/// // `await`ing on the `call` method lets us fetch the return values of both the above calls +/// // in one single RPC call +/// let return_data: (String, Address) = multicall.call().await?; +/// +/// // the same `Multicall` instance can be re-used to do a different batch of transactions. +/// // Say we wish to broadcast (send) a couple of transactions via the Multicall contract. +/// let first_broadcast = contract.method::<_, H256>("setValue", "some value".to_owned())?; +/// let second_broadcast = contract.method::<_, H256>("setValue", "new value".to_owned())?; +/// multicall +/// .clear_calls() +/// .add_call(first_broadcast, false) +/// .add_call(second_broadcast, false); +/// +/// // `await`ing the `send` method waits for the transaction to be broadcast, which also +/// // returns the transaction hash +/// let tx_receipt = multicall.send().await?.await.expect("tx dropped"); +/// +/// // you can also query ETH balances of multiple addresses +/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::
()?; +/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::
()?; +/// +/// multicall +/// .clear_calls() +/// .add_get_eth_balance(address_1, false) +/// .add_get_eth_balance(address_2, false); +/// let balances: (U256, U256) = multicall.call().await?; +/// +/// # Ok(()) +/// # } +/// ``` +/// +/// [`new`]: #method.new +/// [`new_with_chain`]: #method.new_with_chain +/// [`version`]: #method.version +/// [`block`]: #method.block +/// [`legacy`]: #method.legacy +/// [`add_call`]: #method.add_call +/// [`call`]: #method.call +/// [`send`]: #method.send +#[must_use = "Multicall does nothing unless you use `call` or `send`"] +pub struct Multicall { + /// The Multicall contract interface. + pub contract: MulticallContract, + + /// The version of which methods to use when making the contract call. + pub version: MulticallVersion, + + /// Whether to use a legacy or a EIP-1559 transaction. + pub legacy: bool, + + /// The `block` field of the Multicall aggregate call. + pub block: Option, + + /// The internal call vector. + calls: Vec, +} + +// Manually implement Clone and Debug to avoid trait bounds. +impl Clone for Multicall { + fn clone(&self) -> Self { + Self { + contract: self.contract.clone(), + version: self.version, + legacy: self.legacy, + block: self.block, + calls: self.calls.clone(), + } + } +} + +impl fmt::Debug for Multicall { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Multicall") + .field("address", &self.contract.address()) + .field("version", &self.version) + .field("legacy", &self.legacy) + .field("block", &self.block) + .field("calls", &self.calls) + .finish() + } +} + +impl Multicall { + /// Creates a new Multicall instance from the provided client. If provided with an `address`, + /// it instantiates the Multicall contract with that address, otherwise it defaults to + /// [`constants::MULTICALL_ADDRESS`]. + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if the provider returns an error while getting + /// `network_version`. + /// + /// # Panics + /// + /// If a `None` address is provided and the client's network is + /// [not supported](constants::MULTICALL_SUPPORTED_CHAIN_IDS). + pub async fn new(client: impl Into>, address: Option
) -> Result { + let client = client.into(); + + // Fetch chain id and the corresponding address of Multicall contract + // preference is given to Multicall contract's address if provided + // otherwise check the supported chain IDs for the client's chain ID + let address: Address = match address { + Some(addr) => addr, + None => { + let chain_id = client + .get_chainid() + .await + .map_err(ContractError::from_middleware_error)? + .as_u64(); + if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) { + return Err(error::MulticallError::InvalidChainId(chain_id)) + } + constants::MULTICALL_ADDRESS + } + }; + + // Instantiate the multicall contract + let contract = MulticallContract::new(address, client); + + Ok(Self { + version: MulticallVersion::Multicall3, + legacy: false, + block: None, + calls: vec![], + contract, + }) + } + + /// Creates a new Multicall instance synchronously from the provided client and address or chain + /// ID. Uses the [default multicall address](constants::MULTICALL_ADDRESS) if no address is + /// provided. + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if the provided chain_id is not in the + /// [supported networks](constants::MULTICALL_SUPPORTED_CHAIN_IDS). + /// + /// # Panics + /// + /// If neither an address or chain_id are provided. Since this is not an async function, it will + /// not be able to query `net_version` to check if it is supported by the default multicall + /// address. Use new(client, None).await instead. + pub fn new_with_chain_id( + client: impl Into>, + address: Option
, + chain_id: Option>, + ) -> Result { + // If no address is provided, check if chain_id is supported and use the default multicall + // address. + let address: Address = match (address, chain_id) { + (Some(addr), _) => addr, + (_, Some(chain_id)) => { + let chain_id = chain_id.into(); + if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) { + return Err(error::MulticallError::InvalidChainId(chain_id)) + } + constants::MULTICALL_ADDRESS + } + _ => { + // Can't fetch chain_id from provider since we're not in an async function so we + // panic instead. + panic!("Must provide at least one of: address or chain ID.") + } + }; + + // Instantiate the multicall contract + let contract = MulticallContract::new(address, client.into()); + + Ok(Self { + version: MulticallVersion::Multicall3, + legacy: false, + block: None, + calls: vec![], + contract, + }) + } + + /// Changes which functions to use when making the contract call. The default is 3. Version + /// differences (adapted from [here](https://github.com/mds1/multicall#multicall---)): + /// + /// - Multicall (v1): This is the recommended version for simple calls. The original contract + /// containing an aggregate method to batch calls. Each call returns only the return data and + /// none are allowed to fail. + /// + /// - Multicall2 (v2): The same as Multicall, but provides additional methods that allow either + /// all or no calls within the batch to fail. Included for backward compatibility. Use v3 to + /// allow failure on a per-call basis. + /// + /// - Multicall3 (v3): This is the recommended version for allowing failing calls. It's cheaper + /// to use (so you can fit more calls into a single request), and it adds an aggregate3 method + /// so you can specify whether calls are allowed to fail on a per-call basis. + /// + /// Note: all these versions are available in the same contract address + /// ([`constants::MULTICALL_ADDRESS`]) so changing version just changes the methods used, + /// not the contract address. + pub fn version(mut self, version: MulticallVersion) -> Self { + self.version = version; + self + } + + /// Makes a legacy transaction instead of an EIP-1559 one. + pub fn legacy(mut self) -> Self { + self.legacy = true; + self + } + + /// Sets the `block` field of the Multicall aggregate call. + pub fn block(mut self, block: impl Into) -> Self { + self.block = Some(block.into()); + self + } + + /// Appends a `call` to the list of calls of the Multicall instance. + /// + /// Version specific details: + /// - `1`: `allow_failure` is ignored. + /// - `>=2`: `allow_failure` specifies whether or not this call is allowed to revert in the + /// multicall. + /// - `3`: Transaction values are used when broadcasting transactions with [`send`], otherwise + /// they are always ignored. + /// + /// [`send`]: #method.send + pub fn add_call( + &mut self, + call: ContractCall, + allow_failure: bool, + ) -> &mut Self { + let (to, data, value) = match call.tx { + TypedTransaction::Legacy(tx) => (tx.to, tx.data, tx.value), + TypedTransaction::Eip2930(tx) => (tx.tx.to, tx.tx.data, tx.tx.value), + TypedTransaction::Eip1559(tx) => (tx.to, tx.data, tx.value), + #[cfg(feature = "optimism")] + TypedTransaction::OptimismDeposited(tx) => (tx.tx.to, tx.tx.data, tx.tx.value), + }; + if data.is_none() && !call.function.outputs.is_empty() { + return self + } + if let Some(NameOrAddress::Address(target)) = to { + let call = Call { + target, + data: data.unwrap_or_default(), + value: value.unwrap_or_default(), + allow_failure, + function: call.function, + }; + self.calls.push(call); + } + self + } + + /// Appends multiple `call`s to the list of calls of the Multicall instance. + /// + /// See [`add_call`] for more details. + /// + /// [`add_call`]: #method.add_call + pub fn add_calls( + &mut self, + allow_failure: bool, + calls: impl IntoIterator>, + ) -> &mut Self { + for call in calls { + self.add_call(call, allow_failure); + } + self + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the block hash + /// of a given block number. + /// + /// Note: this call will return 0 if `block_number` is not one of the most recent 256 blocks. + /// ([Reference](https://docs.soliditylang.org/en/latest/units-and-global-variables.html?highlight=blockhash#block-and-transaction-properties)) + pub fn add_get_block_hash(&mut self, block_number: impl Into) -> &mut Self { + let call = self.contract.get_block_hash(block_number.into()); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block number. + pub fn add_get_block_number(&mut self) -> &mut Self { + let call = self.contract.get_block_number(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block coinbase address. + pub fn add_get_current_block_coinbase(&mut self) -> &mut Self { + let call = self.contract.get_current_block_coinbase(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block difficulty. + /// + /// Note: in a post-merge environment, the return value of this call will be the output of the + /// randomness beacon provided by the beacon chain. + /// ([Reference](https://eips.ethereum.org/EIPS/eip-4399#abstract)) + pub fn add_get_current_block_difficulty(&mut self) -> &mut Self { + let call = self.contract.get_current_block_difficulty(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block gas limit. + pub fn add_get_current_block_gas_limit(&mut self) -> &mut Self { + let call = self.contract.get_current_block_gas_limit(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block timestamp. + pub fn add_get_current_block_timestamp(&mut self) -> &mut Self { + let call = self.contract.get_current_block_timestamp(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the ETH + /// balance of an address. + pub fn add_get_eth_balance( + &mut self, + address: impl Into
, + allow_failure: bool, + ) -> &mut Self { + let call = self.contract.get_eth_balance(address.into()); + self.add_call(call, allow_failure) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the last + /// block hash. + pub fn add_get_last_block_hash(&mut self) -> &mut Self { + let call = self.contract.get_last_block_hash(); + self.add_call(call, false) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the current + /// block base fee. + /// + /// Note: this call will fail if the chain that it is called on does not implement the + /// [BASEFEE opcode](https://eips.ethereum.org/EIPS/eip-3198). + pub fn add_get_basefee(&mut self, allow_failure: bool) -> &mut Self { + let call = self.contract.get_basefee(); + self.add_call(call, allow_failure) + } + + /// Appends a `call` to the list of calls of the Multicall instance for querying the chain id. + pub fn add_get_chain_id(&mut self) -> &mut Self { + let call = self.contract.get_chain_id(); + self.add_call(call, false) + } + + /// Clears the batch of calls from the Multicall instance. + /// Re-use the already instantiated Multicall to send a different batch of transactions or do + /// another aggregate query. + /// + /// # Examples + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_core::{abi::Abi, types::{Address, H256}}; + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::{Multicall, Contract}; + /// # use std::{sync::Arc, convert::TryFrom}; + /// # + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # let client = Arc::new(client); + /// # + /// # let abi: Abi = serde_json::from_str("")?; + /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; + /// # let contract = Contract::>::new(address, abi, client.clone()); + /// # + /// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?; + /// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?; + /// # + /// let mut multicall = Multicall::new(client, None).await?; + /// multicall + /// .add_call(broadcast_1, false) + /// .add_call(broadcast_2, false); + /// + /// let _tx_receipt = multicall.send().await?.await.expect("tx dropped"); + /// + /// # let call_1 = contract.method::<_, String>("getValue", ())?; + /// # let call_2 = contract.method::<_, Address>("lastSender", ())?; + /// multicall + /// .clear_calls() + /// .add_call(call_1, false) + /// .add_call(call_2, false); + /// // Version 1: + /// let return_data: (String, Address) = multicall.call().await?; + /// // Version 2 and above (each call returns also the success status as the first element): + /// let return_data: ((bool, String), (bool, Address)) = multicall.call().await?; + /// # Ok(()) + /// # } + /// ``` + pub fn clear_calls(&mut self) -> &mut Self { + self.calls.clear(); + self + } + + /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. + /// + /// For handling calls that have the same result type, see [`call_array`]. + /// + /// For handling each call's result individually, see [`call_raw`]. + /// + /// [`call_raw`]: #method.call_raw + /// [`call_array`]: #method.call_array + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while + /// detokenizing the tokens back to the expected return type. + /// + /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data + /// was empty. + /// + /// # Examples + /// + /// The return type must be annotated as a tuple when calling this method: + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_core::types::{U256, Address}; + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::Multicall; + /// # use std::convert::TryFrom; + /// # + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # + /// # let multicall = Multicall::new(client, None).await?; + /// // If the Solidity function calls has the following return types: + /// // 1. `returns (uint256)` + /// // 2. `returns (string, address)` + /// // 3. `returns (bool)` + /// let result: (U256, (String, Address), bool) = multicall.call().await?; + /// // or using the turbofish syntax: + /// let result = multicall.call::<(U256, (String, Address), bool)>().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn call(&self) -> Result { + let results = self.call_raw().await?; + let tokens = results + .into_iter() + .map(|res| { + res.map_err(|data| { + error::MulticallError::ContractError(ContractError::Revert(data)) + }) + }) + .collect::>()?; + T::from_token(Token::Tuple(tokens)).map_err(Into::into) + } + + /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming + /// that every call returns same type. + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while + /// detokenizing the tokens back to the expected return type. + /// + /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data + /// was empty. + /// + /// # Examples + /// + /// The return type must be annotated while calling this method: + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_core::types::{U256, Address}; + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::Multicall; + /// # use std::convert::TryFrom; + /// # + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # + /// # let multicall = Multicall::new(client, None).await?; + /// // If the all Solidity function calls `returns (uint256)`: + /// let result: Vec = multicall.call_array().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn call_array(&self) -> Result, M> { + self.call_raw() + .await? + .into_iter() + .map(|res| { + res.map_err(|data| { + error::MulticallError::ContractError(ContractError::Revert(data)) + }) + .and_then(|token| T::from_token(token).map_err(Into::into)) + }) + .collect() + } + + /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. + /// + /// Returns a vector of `Result` for each call added to the Multicall: + /// `Err(Bytes)` if the individual call failed while allowed or the return data was empty, + /// `Ok(Token)` otherwise. + /// + /// If the Multicall version is 1, this will always be a vector of `Ok`. + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if there are any errors in the RPC call. + /// + /// # Examples + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_core::types::{U256, Address}; + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::Multicall; + /// # use std::convert::TryFrom; + /// # + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # + /// # let multicall = Multicall::new(client, None).await?; + /// // The consumer of the API is responsible for detokenizing the results + /// let tokens = multicall.call_raw().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn call_raw(&self) -> Result>, M> { + // Different call result types based on version + match self.version { + // Wrap the return data with `success: true` since version 1 reverts if any call failed + MulticallVersion::Multicall => { + let call = self.as_aggregate(); + let (_, bytes) = ContractCall::call(&call).await?; + self.parse_call_result( + bytes + .into_iter() + .map(|return_data| MulticallResult { success: true, return_data }), + ) + } + // Same result type (`MulticallResult`) + MulticallVersion::Multicall2 | MulticallVersion::Multicall3 => { + let call = if self.version.is_v2() { + self.as_try_aggregate() + } else { + self.as_aggregate_3() + }; + let results = ContractCall::call(&call).await?; + self.parse_call_result(results.into_iter()) + } + } + } + + /// For each call and its `return_data`: if `success` is true, parses `return_data` with the + /// call's function outputs, otherwise returns the bytes in `Err`. + fn parse_call_result( + &self, + return_data: impl Iterator, + ) -> Result>, M> { + let mut results = Vec::with_capacity(self.calls.len()); + for (call, MulticallResult { success, return_data }) in self.calls.iter().zip(return_data) { + let result = if !success || return_data.is_empty() { + // v2: In the function call to `tryAggregate`, the `allow_failure` check + // is done on a per-transaction basis, and we set this transaction-wide + // check to true when *any* call is allowed to fail. If this is true + // then a call that is not allowed to revert (`call.allow_failure`) may + // still do so because of other calls that are in the same multicall + // aggregate. + if !success && !call.allow_failure { + return Err(error::MulticallError::IllegalRevert) + } + + Err(return_data) + } else { + let mut res_tokens = call.function.decode_output(return_data.as_ref())?; + Ok(if res_tokens.len() == 1 { + res_tokens.pop().unwrap() + } else { + Token::Tuple(res_tokens) + }) + }; + results.push(result); + } + Ok(results) + } + + /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy, + /// returning the pending transaction. + /// + /// Note: this method will broadcast a transaction from an account, meaning it must have + /// sufficient funds for gas and transaction value. + /// + /// # Errors + /// + /// Returns a [`error::MulticallError`] if there are any errors in the RPC call. + /// + /// # Examples + /// + /// ```no_run + /// # async fn foo() -> Result<(), Box> { + /// # use ethers_providers::{Provider, Http}; + /// # use ethers_contract::Multicall; + /// # use std::convert::TryFrom; + /// # let client = Provider::::try_from("http://localhost:8545")?; + /// # let multicall = Multicall::new(client, None).await?; + /// let tx_hash = multicall.send().await?; + /// # Ok(()) + /// # } + /// ``` + pub async fn send(&self) -> Result, M> { + let tx = match self.version { + MulticallVersion::Multicall => self.as_aggregate().tx, + MulticallVersion::Multicall2 => self.as_try_aggregate().tx, + MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx, + }; + let client: &M = self.contract.client_ref(); + client.send_transaction(tx, self.block.map(Into::into)).await.map_err(|e| { + error::MulticallError::ContractError(ContractError::from_middleware_error(e)) + }) + } + + /// v1 + #[inline] + fn as_aggregate(&self) -> ContractCall)> { + // Map the calls vector into appropriate types for `aggregate` function + let calls: Vec = self + .calls + .clone() + .into_iter() + .map(|call| Multicall1Call { target: call.target, call_data: call.data }) + .collect(); + + // Construct the ContractCall for `aggregate` function to broadcast the transaction + let contract_call = self.contract.aggregate(calls); + + self.set_call_flags(contract_call) + } + + /// v2 + #[inline] + fn as_try_aggregate(&self) -> ContractCall> { + let mut allow_failure = false; + // Map the calls vector into appropriate types for `try_aggregate` function + let calls: Vec = self + .calls + .clone() + .into_iter() + .map(|call| { + // Allow entire call failure if at least one call is allowed to fail. + // To avoid iterating multiple times, equivalent of: + // self.calls.iter().any(|call| call.allow_failure) + allow_failure |= call.allow_failure; + Multicall1Call { target: call.target, call_data: call.data } + }) + .collect(); + + // Construct the ContractCall for `try_aggregate` function to broadcast the transaction + let contract_call = self.contract.try_aggregate(!allow_failure, calls); + + self.set_call_flags(contract_call) + } + + /// v3 + #[inline] + fn as_aggregate_3(&self) -> ContractCall> { + // Map the calls vector into appropriate types for `aggregate_3` function + let calls: Vec = self + .calls + .clone() + .into_iter() + .map(|call| Multicall3Call { + target: call.target, + call_data: call.data, + allow_failure: call.allow_failure, + }) + .collect(); + + // Construct the ContractCall for `aggregate_3` function to broadcast the transaction + let contract_call = self.contract.aggregate_3(calls); + + self.set_call_flags(contract_call) + } + + /// v3 + values (only .send()) + #[inline] + fn as_aggregate_3_value(&self) -> ContractCall> { + // Map the calls vector into appropriate types for `aggregate_3_value` function + let mut total_value = U256::zero(); + let calls: Vec = self + .calls + .clone() + .into_iter() + .map(|call| { + total_value += call.value; + Multicall3CallValue { + target: call.target, + call_data: call.data, + allow_failure: call.allow_failure, + value: call.value, + } + }) + .collect(); + + if total_value.is_zero() { + // No value is being sent + self.as_aggregate_3() + } else { + // Construct the ContractCall for `aggregate_3_value` function to broadcast the + // transaction + let contract_call = self.contract.aggregate_3_value(calls); + + self.set_call_flags(contract_call).value(total_value) + } + } + + /// Sets the block and legacy flags on a [ContractCall] if they were set on Multicall. + fn set_call_flags(&self, mut call: ContractCall) -> ContractCall { + if let Some(block) = self.block { + call.block = Some(block.into()); + } + + if self.legacy { + call.legacy() + } else { + call + } + } +} diff --git a/ethers-contract/src/multicall/mod.rs b/ethers-contract/src/multicall/mod.rs index 2a6cf524d..a332d5f88 100644 --- a/ethers-contract/src/multicall/mod.rs +++ b/ethers-contract/src/multicall/mod.rs @@ -5,47 +5,11 @@ pub mod contract; pub mod constants; -cfg_if::cfg_if! { - if #[cfg(feature = "providers")] { - use crate::call::{ContractCall, ContractError}; +if_providers! { + mod middleware; + pub use middleware::{Call, Multicall, MulticallContract, Result}; - use ethers_core::{ - abi::{Detokenize, Function, Token, Tokenizable}, - types::{ - transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, - NameOrAddress, U256, - }, - }; - use std::{fmt, sync::Arc}; - - use ethers_providers::{Middleware, PendingTransaction}; - - pub use contract::Multicall3 as MulticallContract; - use contract::{ - Call as Multicall1Call, - Call3 as Multicall3Call, - Call3Value as Multicall3CallValue, - Result as MulticallResult, - }; - - /// MultiCall error type - pub mod error; - - /// Type alias for `Result>` - pub type Result = StdResult>; - } -} - -/// Helper struct for managing calls to be made to the `function` in smart contract `target` -/// with `data`. -#[derive(Clone, Debug)] -#[cfg(feature = "providers")] -pub struct Call { - target: Address, - data: Bytes, - value: U256, - allow_failure: bool, - function: Function, + pub mod error; } /// The version of the [`Multicall`](super::Multicall). @@ -107,792 +71,3 @@ impl MulticallVersion { matches!(self, Self::Multicall3) } } - -/// A Multicall is an abstraction for sending batched calls/transactions to the Ethereum blockchain. -/// It stores an instance of the [`Multicall` smart contract](https://etherscan.io/address/0xcA11bde05977b3631167028862bE2a173976CA11#code) -/// and the user provided list of transactions to be called or executed on chain. -/// -/// `Multicall` can be instantiated asynchronously from the chain ID of the provided client using -/// [`new`] or synchronously by providing a chain ID in [`new_with_chain`]. This, by default, uses -/// [`constants::MULTICALL_ADDRESS`], but can be overridden by providing `Some(address)`. -/// A list of all the supported chains is available [`here`](https://github.com/mds1/multicall#multicall3-contract-addresses). -/// -/// Set the contract's version by using [`version`]. -/// -/// The `block` number can be provided for the call by using [`block`]. -/// -/// Transactions default to `EIP1559`. This can be changed by using [`legacy`]. -/// -/// Build on the `Multicall` instance by adding calls using [`add_call`] and call or broadcast them -/// all at once by using [`call`] and [`send`] respectively. -/// -/// # Example -/// -/// Using Multicall (version 1): -/// -/// ```no_run -/// use ethers_core::{ -/// abi::Abi, -/// types::{Address, H256, U256}, -/// }; -/// use ethers_contract::{Contract, Multicall, MulticallVersion}; -/// use ethers_providers::{Middleware, Http, Provider, PendingTransaction}; -/// use std::{convert::TryFrom, sync::Arc}; -/// -/// # async fn bar() -> Result<(), Box> { -/// // this is a dummy address used for illustration purposes -/// let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; -/// -/// // (ugly way to write the ABI inline, you can otherwise read it from a file) -/// let abi: Abi = serde_json::from_str(r#"[{"inputs":[{"internalType":"string","name":"value","type":"string"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"author","type":"address"},{"indexed":true,"internalType":"address","name":"oldAuthor","type":"address"},{"indexed":false,"internalType":"string","name":"oldValue","type":"string"},{"indexed":false,"internalType":"string","name":"newValue","type":"string"}],"name":"ValueChanged","type":"event"},{"inputs":[],"name":"getValue","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"lastSender","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"value","type":"string"}],"name":"setValue","outputs":[],"stateMutability":"nonpayable","type":"function"}]"#)?; -/// -/// // connect to the network -/// let client = Provider::::try_from("http://localhost:8545")?; -/// -/// // create the contract object. This will be used to construct the calls for multicall -/// let client = Arc::new(client); -/// let contract = Contract::>::new(address, abi, client.clone()); -/// -/// // note that these [`ContractCall`]s are futures, and need to be `.await`ed to resolve. -/// // But we will let `Multicall` to take care of that for us -/// let first_call = contract.method::<_, String>("getValue", ())?; -/// let second_call = contract.method::<_, Address>("lastSender", ())?; -/// -/// // Since this example connects to a known chain, we need not provide an address for -/// // the Multicall contract and we set that to `None`. If you wish to provide the address -/// // for the Multicall contract, you can pass the `Some(multicall_addr)` argument. -/// // Construction of the `Multicall` instance follows the builder pattern: -/// let mut multicall = Multicall::new(client.clone(), None).await?; -/// multicall -/// .add_call(first_call, false) -/// .add_call(second_call, false); -/// -/// // `await`ing on the `call` method lets us fetch the return values of both the above calls -/// // in one single RPC call -/// let return_data: (String, Address) = multicall.call().await?; -/// -/// // the same `Multicall` instance can be re-used to do a different batch of transactions. -/// // Say we wish to broadcast (send) a couple of transactions via the Multicall contract. -/// let first_broadcast = contract.method::<_, H256>("setValue", "some value".to_owned())?; -/// let second_broadcast = contract.method::<_, H256>("setValue", "new value".to_owned())?; -/// multicall -/// .clear_calls() -/// .add_call(first_broadcast, false) -/// .add_call(second_broadcast, false); -/// -/// // `await`ing the `send` method waits for the transaction to be broadcast, which also -/// // returns the transaction hash -/// let tx_receipt = multicall.send().await?.await.expect("tx dropped"); -/// -/// // you can also query ETH balances of multiple addresses -/// let address_1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".parse::
()?; -/// let address_2 = "ffffffffffffffffffffffffffffffffffffffff".parse::
()?; -/// -/// multicall -/// .clear_calls() -/// .add_get_eth_balance(address_1, false) -/// .add_get_eth_balance(address_2, false); -/// let balances: (U256, U256) = multicall.call().await?; -/// -/// # Ok(()) -/// # } -/// ``` -/// -/// [`new`]: #method.new -/// [`new_with_chain`]: #method.new_with_chain -/// [`version`]: #method.version -/// [`block`]: #method.block -/// [`legacy`]: #method.legacy -/// [`add_call`]: #method.add_call -/// [`call`]: #method.call -/// [`send`]: #method.send -#[must_use = "Multicall does nothing unless you use `call` or `send`"] -#[cfg(feature = "providers")] -pub struct Multicall { - /// The Multicall contract interface. - pub contract: MulticallContract, - - /// The version of which methods to use when making the contract call. - pub version: MulticallVersion, - - /// Whether to use a legacy or a EIP-1559 transaction. - pub legacy: bool, - - /// The `block` field of the Multicall aggregate call. - pub block: Option, - - /// The internal call vector. - calls: Vec, -} - -// Manually implement Clone and Debug to avoid trait bounds. -#[cfg(feature = "providers")] -impl Clone for Multicall { - fn clone(&self) -> Self { - Self { - contract: self.contract.clone(), - version: self.version, - legacy: self.legacy, - block: self.block, - calls: self.calls.clone(), - } - } -} - -#[cfg(feature = "providers")] -impl fmt::Debug for Multicall { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Multicall") - .field("address", &self.contract.address()) - .field("version", &self.version) - .field("legacy", &self.legacy) - .field("block", &self.block) - .field("calls", &self.calls) - .finish() - } -} - -#[cfg(feature = "providers")] -impl Multicall { - /// Creates a new Multicall instance from the provided client. If provided with an `address`, - /// it instantiates the Multicall contract with that address, otherwise it defaults to - /// [`constants::MULTICALL_ADDRESS`]. - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if the provider returns an error while getting - /// `network_version`. - /// - /// # Panics - /// - /// If a `None` address is provided and the client's network is - /// [not supported](constants::MULTICALL_SUPPORTED_CHAIN_IDS). - pub async fn new(client: impl Into>, address: Option
) -> Result { - let client = client.into(); - - // Fetch chain id and the corresponding address of Multicall contract - // preference is given to Multicall contract's address if provided - // otherwise check the supported chain IDs for the client's chain ID - let address: Address = match address { - Some(addr) => addr, - None => { - let chain_id = client - .get_chainid() - .await - .map_err(ContractError::from_middleware_error)? - .as_u64(); - if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) { - return Err(error::MulticallError::InvalidChainId(chain_id)) - } - constants::MULTICALL_ADDRESS - } - }; - - // Instantiate the multicall contract - let contract = MulticallContract::new(address, client); - - Ok(Self { - version: MulticallVersion::Multicall3, - legacy: false, - block: None, - calls: vec![], - contract, - }) - } - - /// Creates a new Multicall instance synchronously from the provided client and address or chain - /// ID. Uses the [default multicall address](constants::MULTICALL_ADDRESS) if no address is - /// provided. - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if the provided chain_id is not in the - /// [supported networks](constants::MULTICALL_SUPPORTED_CHAIN_IDS). - /// - /// # Panics - /// - /// If neither an address or chain_id are provided. Since this is not an async function, it will - /// not be able to query `net_version` to check if it is supported by the default multicall - /// address. Use new(client, None).await instead. - pub fn new_with_chain_id( - client: impl Into>, - address: Option
, - chain_id: Option>, - ) -> Result { - // If no address is provided, check if chain_id is supported and use the default multicall - // address. - let address: Address = match (address, chain_id) { - (Some(addr), _) => addr, - (_, Some(chain_id)) => { - let chain_id = chain_id.into(); - if !constants::MULTICALL_SUPPORTED_CHAIN_IDS.contains(&chain_id) { - return Err(error::MulticallError::InvalidChainId(chain_id)) - } - constants::MULTICALL_ADDRESS - } - _ => { - // Can't fetch chain_id from provider since we're not in an async function so we - // panic instead. - panic!("Must provide at least one of: address or chain ID.") - } - }; - - // Instantiate the multicall contract - let contract = MulticallContract::new(address, client.into()); - - Ok(Self { - version: MulticallVersion::Multicall3, - legacy: false, - block: None, - calls: vec![], - contract, - }) - } - - /// Changes which functions to use when making the contract call. The default is 3. Version - /// differences (adapted from [here](https://github.com/mds1/multicall#multicall---)): - /// - /// - Multicall (v1): This is the recommended version for simple calls. The original contract - /// containing an aggregate method to batch calls. Each call returns only the return data and - /// none are allowed to fail. - /// - /// - Multicall2 (v2): The same as Multicall, but provides additional methods that allow either - /// all or no calls within the batch to fail. Included for backward compatibility. Use v3 to - /// allow failure on a per-call basis. - /// - /// - Multicall3 (v3): This is the recommended version for allowing failing calls. It's cheaper - /// to use (so you can fit more calls into a single request), and it adds an aggregate3 method - /// so you can specify whether calls are allowed to fail on a per-call basis. - /// - /// Note: all these versions are available in the same contract address - /// ([`constants::MULTICALL_ADDRESS`]) so changing version just changes the methods used, - /// not the contract address. - pub fn version(mut self, version: MulticallVersion) -> Self { - self.version = version; - self - } - - /// Makes a legacy transaction instead of an EIP-1559 one. - pub fn legacy(mut self) -> Self { - self.legacy = true; - self - } - - /// Sets the `block` field of the Multicall aggregate call. - pub fn block(mut self, block: impl Into) -> Self { - self.block = Some(block.into()); - self - } - - /// Appends a `call` to the list of calls of the Multicall instance. - /// - /// Version specific details: - /// - `1`: `allow_failure` is ignored. - /// - `>=2`: `allow_failure` specifies whether or not this call is allowed to revert in the - /// multicall. - /// - `3`: Transaction values are used when broadcasting transactions with [`send`], otherwise - /// they are always ignored. - /// - /// [`send`]: #method.send - pub fn add_call( - &mut self, - call: ContractCall, - allow_failure: bool, - ) -> &mut Self { - let (to, data, value) = match call.tx { - TypedTransaction::Legacy(tx) => (tx.to, tx.data, tx.value), - TypedTransaction::Eip2930(tx) => (tx.tx.to, tx.tx.data, tx.tx.value), - TypedTransaction::Eip1559(tx) => (tx.to, tx.data, tx.value), - #[cfg(feature = "optimism")] - TypedTransaction::OptimismDeposited(tx) => (tx.tx.to, tx.tx.data, tx.tx.value), - }; - if data.is_none() && !call.function.outputs.is_empty() { - return self - } - if let Some(NameOrAddress::Address(target)) = to { - let call = Call { - target, - data: data.unwrap_or_default(), - value: value.unwrap_or_default(), - allow_failure, - function: call.function, - }; - self.calls.push(call); - } - self - } - - /// Appends multiple `call`s to the list of calls of the Multicall instance. - /// - /// See [`add_call`] for more details. - /// - /// [`add_call`]: #method.add_call - pub fn add_calls( - &mut self, - allow_failure: bool, - calls: impl IntoIterator>, - ) -> &mut Self { - for call in calls { - self.add_call(call, allow_failure); - } - self - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the block hash - /// of a given block number. - /// - /// Note: this call will return 0 if `block_number` is not one of the most recent 256 blocks. - /// ([Reference](https://docs.soliditylang.org/en/latest/units-and-global-variables.html?highlight=blockhash#block-and-transaction-properties)) - pub fn add_get_block_hash(&mut self, block_number: impl Into) -> &mut Self { - let call = self.contract.get_block_hash(block_number.into()); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block number. - pub fn add_get_block_number(&mut self) -> &mut Self { - let call = self.contract.get_block_number(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block coinbase address. - pub fn add_get_current_block_coinbase(&mut self) -> &mut Self { - let call = self.contract.get_current_block_coinbase(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block difficulty. - /// - /// Note: in a post-merge environment, the return value of this call will be the output of the - /// randomness beacon provided by the beacon chain. - /// ([Reference](https://eips.ethereum.org/EIPS/eip-4399#abstract)) - pub fn add_get_current_block_difficulty(&mut self) -> &mut Self { - let call = self.contract.get_current_block_difficulty(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block gas limit. - pub fn add_get_current_block_gas_limit(&mut self) -> &mut Self { - let call = self.contract.get_current_block_gas_limit(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block timestamp. - pub fn add_get_current_block_timestamp(&mut self) -> &mut Self { - let call = self.contract.get_current_block_timestamp(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the ETH - /// balance of an address. - pub fn add_get_eth_balance( - &mut self, - address: impl Into
, - allow_failure: bool, - ) -> &mut Self { - let call = self.contract.get_eth_balance(address.into()); - self.add_call(call, allow_failure) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the last - /// block hash. - pub fn add_get_last_block_hash(&mut self) -> &mut Self { - let call = self.contract.get_last_block_hash(); - self.add_call(call, false) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the current - /// block base fee. - /// - /// Note: this call will fail if the chain that it is called on does not implement the - /// [BASEFEE opcode](https://eips.ethereum.org/EIPS/eip-3198). - pub fn add_get_basefee(&mut self, allow_failure: bool) -> &mut Self { - let call = self.contract.get_basefee(); - self.add_call(call, allow_failure) - } - - /// Appends a `call` to the list of calls of the Multicall instance for querying the chain id. - pub fn add_get_chain_id(&mut self) -> &mut Self { - let call = self.contract.get_chain_id(); - self.add_call(call, false) - } - - /// Clears the batch of calls from the Multicall instance. - /// Re-use the already instantiated Multicall to send a different batch of transactions or do - /// another aggregate query. - /// - /// # Examples - /// - /// ```no_run - /// # async fn foo() -> Result<(), Box> { - /// # use ethers_core::{abi::Abi, types::{Address, H256}}; - /// # use ethers_providers::{Provider, Http}; - /// # use ethers_contract::{Multicall, Contract}; - /// # use std::{sync::Arc, convert::TryFrom}; - /// # - /// # let client = Provider::::try_from("http://localhost:8545")?; - /// # let client = Arc::new(client); - /// # - /// # let abi: Abi = serde_json::from_str("")?; - /// # let address = "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee".parse::
()?; - /// # let contract = Contract::>::new(address, abi, client.clone()); - /// # - /// # let broadcast_1 = contract.method::<_, H256>("setValue", "some value".to_owned())?; - /// # let broadcast_2 = contract.method::<_, H256>("setValue", "new value".to_owned())?; - /// # - /// let mut multicall = Multicall::new(client, None).await?; - /// multicall - /// .add_call(broadcast_1, false) - /// .add_call(broadcast_2, false); - /// - /// let _tx_receipt = multicall.send().await?.await.expect("tx dropped"); - /// - /// # let call_1 = contract.method::<_, String>("getValue", ())?; - /// # let call_2 = contract.method::<_, Address>("lastSender", ())?; - /// multicall - /// .clear_calls() - /// .add_call(call_1, false) - /// .add_call(call_2, false); - /// // Version 1: - /// let return_data: (String, Address) = multicall.call().await?; - /// // Version 2 and above (each call returns also the success status as the first element): - /// let return_data: ((bool, String), (bool, Address)) = multicall.call().await?; - /// # Ok(()) - /// # } - /// ``` - pub fn clear_calls(&mut self) -> &mut Self { - self.calls.clear(); - self - } - - /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. - /// - /// For handling calls that have the same result type, see [`call_array`]. - /// - /// For handling each call's result individually, see [`call_raw`]. - /// - /// [`call_raw`]: #method.call_raw - /// [`call_array`]: #method.call_array - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while - /// detokenizing the tokens back to the expected return type. - /// - /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data - /// was empty. - /// - /// # Examples - /// - /// The return type must be annotated as a tuple when calling this method: - /// - /// ```no_run - /// # async fn foo() -> Result<(), Box> { - /// # use ethers_core::types::{U256, Address}; - /// # use ethers_providers::{Provider, Http}; - /// # use ethers_contract::Multicall; - /// # use std::convert::TryFrom; - /// # - /// # let client = Provider::::try_from("http://localhost:8545")?; - /// # - /// # let multicall = Multicall::new(client, None).await?; - /// // If the Solidity function calls has the following return types: - /// // 1. `returns (uint256)` - /// // 2. `returns (string, address)` - /// // 3. `returns (bool)` - /// let result: (U256, (String, Address), bool) = multicall.call().await?; - /// // or using the turbofish syntax: - /// let result = multicall.call::<(U256, (String, Address), bool)>().await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn call(&self) -> Result { - let results = self.call_raw().await?; - let tokens = results - .into_iter() - .map(|res| { - res.map_err(|data| { - error::MulticallError::ContractError(ContractError::Revert(data)) - }) - }) - .collect::>()?; - T::from_token(Token::Tuple(tokens)).map_err(Into::into) - } - - /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract, assuming - /// that every call returns same type. - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if there are any errors in the RPC call or while - /// detokenizing the tokens back to the expected return type. - /// - /// Returns an error if any call failed, even if `allow_failure` was set, or if the return data - /// was empty. - /// - /// # Examples - /// - /// The return type must be annotated while calling this method: - /// - /// ```no_run - /// # async fn foo() -> Result<(), Box> { - /// # use ethers_core::types::{U256, Address}; - /// # use ethers_providers::{Provider, Http}; - /// # use ethers_contract::Multicall; - /// # use std::convert::TryFrom; - /// # - /// # let client = Provider::::try_from("http://localhost:8545")?; - /// # - /// # let multicall = Multicall::new(client, None).await?; - /// // If the all Solidity function calls `returns (uint256)`: - /// let result: Vec = multicall.call_array().await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn call_array(&self) -> Result, M> { - self.call_raw() - .await? - .into_iter() - .map(|res| { - res.map_err(|data| { - error::MulticallError::ContractError(ContractError::Revert(data)) - }) - .and_then(|token| T::from_token(token).map_err(Into::into)) - }) - .collect() - } - - /// Queries the Ethereum blockchain using `eth_call`, but via the Multicall contract. - /// - /// Returns a vector of `Result` for each call added to the Multicall: - /// `Err(Bytes)` if the individual call failed while allowed or the return data was empty, - /// `Ok(Token)` otherwise. - /// - /// If the Multicall version is 1, this will always be a vector of `Ok`. - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if there are any errors in the RPC call. - /// - /// # Examples - /// - /// ```no_run - /// # async fn foo() -> Result<(), Box> { - /// # use ethers_core::types::{U256, Address}; - /// # use ethers_providers::{Provider, Http}; - /// # use ethers_contract::Multicall; - /// # use std::convert::TryFrom; - /// # - /// # let client = Provider::::try_from("http://localhost:8545")?; - /// # - /// # let multicall = Multicall::new(client, None).await?; - /// // The consumer of the API is responsible for detokenizing the results - /// let tokens = multicall.call_raw().await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn call_raw(&self) -> Result>, M> { - // Different call result types based on version - match self.version { - // Wrap the return data with `success: true` since version 1 reverts if any call failed - MulticallVersion::Multicall => { - let call = self.as_aggregate(); - let (_, bytes) = ContractCall::call(&call).await?; - self.parse_call_result( - bytes - .into_iter() - .map(|return_data| MulticallResult { success: true, return_data }), - ) - } - // Same result type (`MulticallResult`) - MulticallVersion::Multicall2 | MulticallVersion::Multicall3 => { - let call = if self.version.is_v2() { - self.as_try_aggregate() - } else { - self.as_aggregate_3() - }; - let results = ContractCall::call(&call).await?; - self.parse_call_result(results.into_iter()) - } - } - } - - /// For each call and its `return_data`: if `success` is true, parses `return_data` with the - /// call's function outputs, otherwise returns the bytes in `Err`. - fn parse_call_result( - &self, - return_data: impl Iterator, - ) -> Result>, M> { - let mut results = Vec::with_capacity(self.calls.len()); - for (call, MulticallResult { success, return_data }) in self.calls.iter().zip(return_data) { - let result = if !success || return_data.is_empty() { - // v2: In the function call to `tryAggregate`, the `allow_failure` check - // is done on a per-transaction basis, and we set this transaction-wide - // check to true when *any* call is allowed to fail. If this is true - // then a call that is not allowed to revert (`call.allow_failure`) may - // still do so because of other calls that are in the same multicall - // aggregate. - if !success && !call.allow_failure { - return Err(error::MulticallError::IllegalRevert) - } - - Err(return_data) - } else { - let mut res_tokens = call.function.decode_output(return_data.as_ref())?; - Ok(if res_tokens.len() == 1 { - res_tokens.pop().unwrap() - } else { - Token::Tuple(res_tokens) - }) - }; - results.push(result); - } - Ok(results) - } - - /// Signs and broadcasts a batch of transactions by using the Multicall contract as proxy, - /// returning the pending transaction. - /// - /// Note: this method will broadcast a transaction from an account, meaning it must have - /// sufficient funds for gas and transaction value. - /// - /// # Errors - /// - /// Returns a [`error::MulticallError`] if there are any errors in the RPC call. - /// - /// # Examples - /// - /// ```no_run - /// # async fn foo() -> Result<(), Box> { - /// # use ethers_providers::{Provider, Http}; - /// # use ethers_contract::Multicall; - /// # use std::convert::TryFrom; - /// # let client = Provider::::try_from("http://localhost:8545")?; - /// # let multicall = Multicall::new(client, None).await?; - /// let tx_hash = multicall.send().await?; - /// # Ok(()) - /// # } - /// ``` - pub async fn send(&self) -> Result, M> { - let tx = match self.version { - MulticallVersion::Multicall => self.as_aggregate().tx, - MulticallVersion::Multicall2 => self.as_try_aggregate().tx, - MulticallVersion::Multicall3 => self.as_aggregate_3_value().tx, - }; - let client: &M = self.contract.client_ref(); - client.send_transaction(tx, self.block.map(Into::into)).await.map_err(|e| { - error::MulticallError::ContractError(ContractError::from_middleware_error(e)) - }) - } - - /// v1 - #[inline] - fn as_aggregate(&self) -> ContractCall)> { - // Map the calls vector into appropriate types for `aggregate` function - let calls: Vec = self - .calls - .clone() - .into_iter() - .map(|call| Multicall1Call { target: call.target, call_data: call.data }) - .collect(); - - // Construct the ContractCall for `aggregate` function to broadcast the transaction - let contract_call = self.contract.aggregate(calls); - - self.set_call_flags(contract_call) - } - - /// v2 - #[inline] - fn as_try_aggregate(&self) -> ContractCall> { - let mut allow_failure = false; - // Map the calls vector into appropriate types for `try_aggregate` function - let calls: Vec = self - .calls - .clone() - .into_iter() - .map(|call| { - // Allow entire call failure if at least one call is allowed to fail. - // To avoid iterating multiple times, equivalent of: - // self.calls.iter().any(|call| call.allow_failure) - allow_failure |= call.allow_failure; - Multicall1Call { target: call.target, call_data: call.data } - }) - .collect(); - - // Construct the ContractCall for `try_aggregate` function to broadcast the transaction - let contract_call = self.contract.try_aggregate(!allow_failure, calls); - - self.set_call_flags(contract_call) - } - - /// v3 - #[inline] - fn as_aggregate_3(&self) -> ContractCall> { - // Map the calls vector into appropriate types for `aggregate_3` function - let calls: Vec = self - .calls - .clone() - .into_iter() - .map(|call| Multicall3Call { - target: call.target, - call_data: call.data, - allow_failure: call.allow_failure, - }) - .collect(); - - // Construct the ContractCall for `aggregate_3` function to broadcast the transaction - let contract_call = self.contract.aggregate_3(calls); - - self.set_call_flags(contract_call) - } - - /// v3 + values (only .send()) - #[inline] - fn as_aggregate_3_value(&self) -> ContractCall> { - // Map the calls vector into appropriate types for `aggregate_3_value` function - let mut total_value = U256::zero(); - let calls: Vec = self - .calls - .clone() - .into_iter() - .map(|call| { - total_value += call.value; - Multicall3CallValue { - target: call.target, - call_data: call.data, - allow_failure: call.allow_failure, - value: call.value, - } - }) - .collect(); - - if total_value.is_zero() { - // No value is being sent - self.as_aggregate_3() - } else { - // Construct the ContractCall for `aggregate_3_value` function to broadcast the - // transaction - let contract_call = self.contract.aggregate_3_value(calls); - - self.set_call_flags(contract_call).value(total_value) - } - } - - /// Sets the block and legacy flags on a [ContractCall] if they were set on Multicall. - fn set_call_flags(&self, mut call: ContractCall) -> ContractCall { - if let Some(block) = self.block { - call.block = Some(block.into()); - } - - if self.legacy { - call.legacy() - } else { - call - } - } -} From 2a88d2ce19828a24080684614f4207c3dafad9a7 Mon Sep 17 00:00:00 2001 From: Yiannis Marangos Date: Sat, 5 Aug 2023 19:32:22 +0300 Subject: [PATCH 3/5] add providers feature guard in tests --- ethers-contract/tests/it/abigen.rs | 30 +++++++++++++++++++++++++----- ethers-contract/tests/it/common.rs | 14 ++++++++++++-- ethers-contract/tests/it/main.rs | 3 ++- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs index a49384294..a58b2661c 100644 --- a/ethers-contract/tests/it/abigen.rs +++ b/ethers-contract/tests/it/abigen.rs @@ -1,17 +1,25 @@ //! Test cases to validate the `abigen!` macro -use ethers_contract::{abigen, ContractError, EthCall, EthError, EthEvent}; +use ethers_contract::{abigen, EthEvent}; use ethers_core::{ - abi::{AbiDecode, AbiEncode, Address, Tokenizable}, + abi::{AbiDecode, AbiEncode, Tokenizable}, rand::thread_rng, types::{Bytes, U256}, - utils::Anvil, }; -use ethers_providers::{MockProvider, Provider}; use ethers_signers::{LocalWallet, Signer}; -use std::{fmt::Debug, hash::Hash, str::FromStr, sync::Arc}; +use std::{fmt::Debug, hash::Hash, str::FromStr}; + +#[cfg(feature = "providers")] +use ethers_contract::{ContractError, EthCall, EthError}; +#[cfg(feature = "providers")] +use ethers_core::{abi::Address, utils::Anvil}; +#[cfg(feature = "providers")] +use ethers_providers::{MockProvider, Provider}; +#[cfg(feature = "providers")] +use std::sync::Arc; const fn assert_codec() {} +#[cfg(feature = "providers")] const fn assert_tokenizeable() {} const fn assert_call() {} const fn assert_event() {} @@ -148,6 +156,7 @@ fn can_generate_internal_structs_2() { } #[test] +#[cfg(feature = "providers")] fn can_generate_internal_structs_multiple() { // NOTE: nesting here is necessary due to how tests are structured... use contract::*; @@ -221,6 +230,7 @@ fn can_generate_return_struct() { } #[test] +#[cfg(feature = "providers")] fn can_generate_human_readable_with_structs() { abigen!( SimpleContract, @@ -268,6 +278,7 @@ fn can_generate_human_readable_with_structs() { } #[test] +#[cfg(feature = "providers")] fn can_handle_overloaded_functions() { abigen!( SimpleContract, @@ -383,6 +394,7 @@ fn can_handle_unique_underscore_functions() { } #[test] +#[cfg(feature = "providers")] fn can_handle_underscore_numeric() { abigen!( Test, @@ -426,6 +438,7 @@ fn can_abican_generate_console_sol() { } #[test] +#[cfg(feature = "providers")] fn can_generate_nested_types() { abigen!( Test, @@ -453,6 +466,7 @@ fn can_generate_nested_types() { } #[test] +#[cfg(feature = "providers")] fn can_handle_different_calls() { abigen!( Test, @@ -470,6 +484,7 @@ fn can_handle_different_calls() { } #[test] +#[cfg(feature = "providers")] fn can_handle_case_sensitive_calls() { abigen!( StakedOHM, @@ -487,6 +502,7 @@ fn can_handle_case_sensitive_calls() { } #[tokio::test] +#[cfg(feature = "providers")] async fn can_deploy_greeter() { abigen!(Greeter, "ethers-contract/tests/solidity-contracts/greeter.json",); let anvil = Anvil::new().spawn(); @@ -505,6 +521,7 @@ async fn can_deploy_greeter() { } #[tokio::test] +#[cfg(feature = "providers")] async fn can_abiencoderv2_output() { abigen!(AbiEncoderv2Test, "ethers-contract/tests/solidity-contracts/Abiencoderv2Test.json"); @@ -568,6 +585,7 @@ fn can_handle_overloaded_events() { #[tokio::test] #[cfg(not(feature = "celo"))] +#[cfg(feature = "providers")] async fn can_send_struct_param() { abigen!(StructContract, "./tests/solidity-contracts/StructContract.json"); @@ -592,6 +610,7 @@ async fn can_send_struct_param() { } #[test] +#[cfg(feature = "providers")] fn can_generate_seaport_1_0() { abigen!(Seaport, "./tests/solidity-contracts/seaport_1_0.json"); @@ -768,6 +787,7 @@ fn can_handle_overloaded_function_with_array() { } #[test] +#[cfg(feature = "providers")] #[allow(clippy::disallowed_names)] fn convert_uses_correct_abi() { abigen!( diff --git a/ethers-contract/tests/it/common.rs b/ethers-contract/tests/it/common.rs index 8e91aec19..5eb78192e 100644 --- a/ethers-contract/tests/it/common.rs +++ b/ethers-contract/tests/it/common.rs @@ -1,10 +1,17 @@ -use ethers_contract::{Contract, ContractFactory, EthEvent}; +use ethers_contract::EthEvent; +use ethers_core::types::Address; + +#[cfg(feature = "providers")] +use ethers_contract::{Contract, ContractFactory}; +#[cfg(feature = "providers")] use ethers_core::{ abi::{Abi, JsonAbi}, - types::{Address, Bytes}, + types::Bytes, utils::AnvilInstance, }; +#[cfg(feature = "providers")] use ethers_providers::{Http, Middleware, Provider}; +#[cfg(feature = "providers")] use std::{convert::TryFrom, fs, sync::Arc, time::Duration}; // Note: The `EthEvent` derive macro implements the necessary conversion between `Tokens` and @@ -21,6 +28,7 @@ pub struct ValueChanged { } /// Gets the contract ABI and bytecode from a JSON file +#[cfg(feature = "providers")] #[track_caller] pub fn get_contract(filename: &str) -> (Abi, Bytes) { let path = format!("./tests/solidity-contracts/{filename}"); @@ -34,6 +42,7 @@ pub fn get_contract(filename: &str) -> (Abi, Bytes) { } /// connects the private key to http://localhost:8545 +#[cfg(feature = "providers")] pub fn connect(anvil: &AnvilInstance, idx: usize) -> Arc> { let sender = anvil.addresses()[idx]; let provider = Provider::::try_from(anvil.endpoint()) @@ -44,6 +53,7 @@ pub fn connect(anvil: &AnvilInstance, idx: usize) -> Arc> { } /// Launches a Anvil instance and deploys the SimpleStorage contract +#[cfg(feature = "providers")] pub async fn deploy(client: Arc, abi: Abi, bytecode: Bytes) -> Contract { let factory = ContractFactory::new(abi, bytecode, client); let deployer = factory.deploy("initial value".to_string()).unwrap(); diff --git a/ethers-contract/tests/it/main.rs b/ethers-contract/tests/it/main.rs index 9e1f2888c..e5cf1c631 100644 --- a/ethers-contract/tests/it/main.rs +++ b/ethers-contract/tests/it/main.rs @@ -5,6 +5,7 @@ mod abigen; mod derive; +#[cfg(feature = "providers")] mod contract_call; mod eip712; @@ -12,5 +13,5 @@ mod eip712; #[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] mod common; -#[cfg(all(not(target_arch = "wasm32"), not(feature = "celo")))] +#[cfg(all(feature = "providers", not(target_arch = "wasm32"), not(feature = "celo")))] mod contract; From 3740d349ff78107547941c7e122156cd0eaf1c25 Mon Sep 17 00:00:00 2001 From: Yiannis Marangos Date: Sat, 5 Aug 2023 19:59:03 +0300 Subject: [PATCH 4/5] Enable `providers` in ethers-middleware --- ethers-middleware/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml index 5af188b1d..baae44d85 100644 --- a/ethers-middleware/Cargo.toml +++ b/ethers-middleware/Cargo.toml @@ -23,7 +23,7 @@ rustdoc-args = ["--cfg", "docsrs"] all-features = true [dependencies] -ethers-contract = { workspace = true, features = ["abigen"] } +ethers-contract = { workspace = true, features = ["abigen", "providers"] } ethers-core.workspace = true ethers-etherscan.workspace = true ethers-providers.workspace = true From d942a386ff186583dc6c893b64c6e6175b9a4a7f Mon Sep 17 00:00:00 2001 From: Yiannis Marangos Date: Fri, 18 Aug 2023 02:27:17 +0300 Subject: [PATCH 5/5] Enable providers by default --- ethers-contract/ethers-contract-abigen/Cargo.toml | 3 +++ ethers-contract/ethers-contract-derive/Cargo.toml | 2 ++ 2 files changed, 5 insertions(+) diff --git a/ethers-contract/ethers-contract-abigen/Cargo.toml b/ethers-contract/ethers-contract-abigen/Cargo.toml index 0d29fef3d..efa4569ab 100644 --- a/ethers-contract/ethers-contract-abigen/Cargo.toml +++ b/ethers-contract/ethers-contract-abigen/Cargo.toml @@ -50,7 +50,10 @@ tokio = { workspace = true, optional = true } url = { workspace = true, optional = true } [features] +default = ["providers"] + providers = [] + online = ["reqwest", "ethers-etherscan", "url", "tokio"] rustls = ["reqwest?/rustls-tls", "ethers-etherscan?/rustls"] openssl = ["reqwest?/native-tls", "ethers-etherscan?/openssl"] diff --git a/ethers-contract/ethers-contract-derive/Cargo.toml b/ethers-contract/ethers-contract-derive/Cargo.toml index 49d0fd2db..679624ab9 100644 --- a/ethers-contract/ethers-contract-derive/Cargo.toml +++ b/ethers-contract/ethers-contract-derive/Cargo.toml @@ -33,4 +33,6 @@ hex.workspace = true serde_json.workspace = true [features] +default = ["providers"] + providers = ["ethers-contract-abigen/providers"]