Repository: gakonst/ethers-rs
Pull request #2291 (2023-03-21 18:12:42)

fix: ethers-contract circular dep on ethers-signers
As title, ethers-contract depended on signers which messes up the auto-release process, fixes by removing the dep and pulling the EIP712 tests which require both signers + contract + eip712 derive to the top level crate tests


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 708910a9..9e45e770 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -94,7 +94,7 @@ jobs:
             - uses: dtolnay/rust-toolchain@stable
             - uses: Swatinem/rust-cache@v2
             - name: live tests
-              run: cargo test -p ethers --test live --all-features
+              run: cargo test -p ethers --all-features
 
     # TODO: [#2191](https://github.com/gakonst/ethers-rs/issues/2191)
     # feature-checks:
diff --git a/Cargo.lock b/Cargo.lock
index a436fa3d..d4bde293 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1219,6 +1219,7 @@ dependencies = [
  "ethers-providers",
  "ethers-signers",
  "ethers-solc",
+ "serde",
  "tokio",
 ]
 
@@ -1241,7 +1242,6 @@ dependencies = [
  "ethers-core",
  "ethers-derive-eip712",
  "ethers-providers",
- "ethers-signers",
  "ethers-solc",
  "futures-util",
  "hex",
@@ -1368,6 +1368,7 @@ dependencies = [
  "ethers-providers",
  "ethers-signers",
  "ethers-solc",
+ "futures-channel",
  "futures-locks",
  "futures-util",
  "hex",
diff --git a/book/SUMMARY.md b/book/SUMMARY.md
index 1998ee06..0bf0bae0 100644
--- a/book/SUMMARY.md
+++ b/book/SUMMARY.md
@@ -1,11 +1,13 @@
 # Summary
 
 # Getting started
-  - [Intro](./getting-started/intro.md)
-  - [Start a new project](./getting-started/start_a_new_project.md)
-  - [Connect to an Ethereum node](./getting-started/connect_to_an_ethereum_node.md)
+
+- [Intro](./getting-started/intro.md)
+- [Start a new project](./getting-started/start_a_new_project.md)
+- [Connect to an Ethereum node](./getting-started/connect_to_an_ethereum_node.md)
 
 # Reference guide
+
 - [Providers](./providers/providers.md)
   - [Http](./providers/http.md)
   - [WebSocket](./providers/ws.md)
@@ -26,16 +28,16 @@
   - [Signer](./middleware/signer.md)
   - [Time lag]()
   - [Transformer]()
-- [Contracts]()
-  - [Abigen]()
-  - [Compile]()
-  - [Creating Instances]()
-  - [Deploy Anvil]()
-  - [Deploy from ABI and bytecode]()
-  - [Deploy Moonbeam]()
-  - [Events]()
-  - [Events with meta]()
-  - [Methods]()
+- [Contracts](./contracts/contracts.md)
+  - [Abigen](./contracts/abigen.md)
+  - [Compile](./contracts/compile.md)
+  - [Creating Instances](./contracts/creating-instances.md)
+  - [Deploy Anvil](./contracts/deploy-anvil.md)
+  - [Deploy from ABI and bytecode](./contracts/deploy-from-abi-and-bytecode.md)
+  - [Deploy Moonbeam](./contracts/doploy-moonbeam.md)
+  - [Events](./contracts/events.md)
+  - [Events with meta](./contracts/events-with-meta.md)
+  - [Methods](contracts/methods.md)
 - [Events]()
   - [Logs and filtering]()
   - [Solidity topics]()
@@ -56,7 +58,7 @@
   - [Create typed transaction]()
   - [Decode input]()
   - [EIP-1559]()
-  - [ENS]() 
+  - [ENS]()
   - [Estimate gas]()
   - [Get gas price]()
   - [Get gas price USD]()
@@ -78,13 +80,13 @@
   - [Trezor]()
   - [Yubi]()
 - [Big numbers](./big-numbers/intro.md)
-  - [Comparison and equivalence](./big-numbers/comparison-and-equivalence.md) 
+  - [Comparison and equivalence](./big-numbers/comparison-and-equivalence.md)
   - [Conversion](./big-numbers/conversion.md)
   - [Creating Instances](./big-numbers/creating_instances.md)
   - [Math operations](./big-numbers/math-operations.md)
   - [Utilities](./big-numbers/utilities.md)
 - [Anvil]()
-    - [Boot anvil]()
-    - [Deploy contracts]()
-    - [Fork]() 
-    - [Testing]()
+  - [Boot anvil]()
+  - [Deploy contracts]()
+  - [Fork]()
+  - [Testing]()
diff --git a/book/contracts/abigen.md b/book/contracts/abigen.md
new file mode 100644
index 00000000..b93306d6
--- /dev/null
+++ b/book/contracts/abigen.md
@@ -0,0 +1,5 @@
+# Abigen
+
+```rust
+{{#include ../../examples/contracts/examples/abigen.rs}}
+```
diff --git a/book/contracts/compile.md b/book/contracts/compile.md
new file mode 100644
index 00000000..80708c27
--- /dev/null
+++ b/book/contracts/compile.md
@@ -0,0 +1,5 @@
+# Compile
+
+```rust
+{{#include ../../examples/contracts/examples/compile.rs}}
+```
diff --git a/book/contracts/contracts.md b/book/contracts/contracts.md
new file mode 100644
index 00000000..dc490f19
--- /dev/null
+++ b/book/contracts/contracts.md
@@ -0,0 +1,29 @@
+# Contracts
+
+In ethers-rs, contracts are a way to interact with smart contracts on the Ethereum blockchain through rust bindings, which serve as a robust rust API to these objects.
+
+The ethers-contracts module includes the following features:
+
+- [Abigen](): A module for generating Rust code from Solidity contracts.
+- [Compile](): A module for compiling Solidity contracts into bytecode and ABI files.
+- [Creating Instances](): A module for creating instances of smart contracts.
+- [Deploy Anvil](): A module for deploying smart contracts on the Anvil network.
+- [Deploy from ABI and bytecode](): A module for deploying smart contracts from their ABI and bytecode files.
+- [Deploy Moonbeam](): A module for deploying smart contracts on the Moonbeam network.
+- [Events](): A module for listening to smart contract events.
+- [Events with Meta](): A module for listening to smart contract events with metadata.
+- [Methods](): A module for calling smart contract methods.
+
+The ethers-contracts module provides a convenient way to work with Ethereum smart contracts in Rust. With this module, you can easily create instances of smart contracts, deploy them to the network, and interact with their methods and events.
+
+The Abigen module allows you to generate Rust code from Solidity contracts, which can save you a lot of time and effort when writing Rust code for Ethereum smart contracts.
+
+The Compile module makes it easy to compile Solidity contracts into bytecode and ABI files, which are required for deploying smart contracts.
+
+The Deploy Anvil and Deploy Moonbeam modules allow you to deploy smart contracts to specific networks, making it easy to test and deploy your smart contracts on the desired network.
+
+The Events and Events with Meta modules allow you to listen to smart contract events and retrieve event data, which is essential for building applications that interact with Ethereum smart contracts.
+
+Finally, the Methods module provides a simple way to call smart contract methods from Rust code, allowing you to interact with smart contracts in a programmatic way.
+
+Overall, the ethers-contracts module provides a comprehensive set of tools for working with Ethereum smart contracts in Rust, making it an essential tool for Rust developers building decentralized applications on the Ethereum network.
diff --git a/book/contracts/creating-instances.md b/book/contracts/creating-instances.md
new file mode 100644
index 00000000..635d858b
--- /dev/null
+++ b/book/contracts/creating-instances.md
@@ -0,0 +1,5 @@
+# Creating Instances
+
+```rust
+{{#include ../../examples/contracts/examples/instances.rs}}
+```
diff --git a/book/contracts/deploy-anvil.md b/book/contracts/deploy-anvil.md
new file mode 100644
index 00000000..721cfdbf
--- /dev/null
+++ b/book/contracts/deploy-anvil.md
@@ -0,0 +1,5 @@
+# Deploy Anvil
+
+```rust
+{{#include ../../examples/contracts/examples/deploy_anvil.rs}}
+```
diff --git a/book/contracts/deploy-from-abi-and-bytecode.md b/book/contracts/deploy-from-abi-and-bytecode.md
new file mode 100644
index 00000000..650a3d6f
--- /dev/null
+++ b/book/contracts/deploy-from-abi-and-bytecode.md
@@ -0,0 +1,5 @@
+# Deploying a Contract from ABI and Bytecode
+
+```rust
+{{#include ../../examples/contracts/examples/deploy_from_abi_and_bytecode.rs}}
+```
diff --git a/book/contracts/doploy-moonbeam.md b/book/contracts/doploy-moonbeam.md
new file mode 100644
index 00000000..4bea6cf2
--- /dev/null
+++ b/book/contracts/doploy-moonbeam.md
@@ -0,0 +1,5 @@
+# Deploy Moonbeam
+
+```rust
+{{#include ../../examples/contracts/examples/deploy_moonbeam.rs}}
+```
diff --git a/book/contracts/events-with-meta.md b/book/contracts/events-with-meta.md
new file mode 100644
index 00000000..1d4d4f4b
--- /dev/null
+++ b/book/contracts/events-with-meta.md
@@ -0,0 +1,5 @@
+# Events with meta
+
+```rust
+{{#include ../../examples/contracts/examples/events_with_meta.rs}}
+```
diff --git a/book/contracts/events.md b/book/contracts/events.md
new file mode 100644
index 00000000..0c30117c
--- /dev/null
+++ b/book/contracts/events.md
@@ -0,0 +1,5 @@
+# Events
+
+```rust
+{{#include ../../examples/contracts/examples/events.rs}}
+```
diff --git a/book/contracts/methods.md b/book/contracts/methods.md
new file mode 100644
index 00000000..5fe1d579
--- /dev/null
+++ b/book/contracts/methods.md
@@ -0,0 +1,5 @@
+# Methods
+
+```rust
+{{#include ../../examples/contracts/examples/methods.rs}}
+```
diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml
index a0c65795..dc0d5483 100644
--- a/ethers-contract/Cargo.toml
+++ b/ethers-contract/Cargo.toml
@@ -43,8 +43,8 @@ ethers-contract-derive = { workspace = true, optional = true }
 ethers-derive-eip712 = { workspace = true, optional = true }
 
 [dev-dependencies]
-ethers-signers.workspace = true
 ethers-solc.workspace = true
+ethers-providers = { workspace = true, features = ["ws"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
 tokio = { workspace = true, features = ["macros"] }
diff --git a/ethers-contract/src/contract.rs b/ethers-contract/src/contract.rs
index 2fa4f888..eae39a46 100644
--- a/ethers-contract/src/contract.rs
+++ b/ethers-contract/src/contract.rs
@@ -75,7 +75,6 @@ pub type Contract<M> = ContractInstance<std::sync::Arc<M>, M>;
 /// };
 /// use ethers_contract::Contract;
 /// use ethers_providers::{Provider, Http};
-/// use ethers_signers::Wallet;
 /// use std::{convert::TryFrom, sync::Arc};
 ///
 /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
@@ -122,7 +121,6 @@ pub type Contract<M> = ContractInstance<std::sync::Arc<M>, M>;
 /// use ethers_core::{abi::Abi, types::Address};
 /// use ethers_contract::{Contract, EthEvent};
 /// use ethers_providers::{Provider, Http, Middleware};
-/// use ethers_signers::Wallet;
 /// use std::{convert::TryFrom, sync::Arc};
 /// use ethers_core::abi::{Detokenize, Token, InvalidOutputType};
 /// # // this is a fake address used just for this example
diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs
index b396ef0b..711e44a2 100644
--- a/ethers-contract/src/factory.rs
+++ b/ethers-contract/src/factory.rs
@@ -323,7 +323,6 @@ where
 /// use ethers_solc::Solc;
 /// use ethers_contract::ContractFactory;
 /// use ethers_providers::{Provider, Http};
-/// use ethers_signers::Wallet;
 /// use std::convert::TryFrom;
 ///
 /// # async fn foo() -> Result<(), Box<dyn std::error::Error>> {
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index fce13218..a8710206 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -36,6 +36,7 @@ serde.workspace = true
 thiserror.workspace = true
 futures-util.workspace = true
 futures-locks.workspace = true
+futures-channel.workspace = true
 tracing.workspace = true
 tracing-futures.workspace = true
 instant.workspace = true
diff --git a/ethers-middleware/src/gas_escalator/mod.rs b/ethers-middleware/src/gas_escalator/mod.rs
index 384f0291..fb55c7c1 100644
--- a/ethers-middleware/src/gas_escalator/mod.rs
+++ b/ethers-middleware/src/gas_escalator/mod.rs
@@ -1,21 +1,28 @@
 mod geometric;
-use ethers_core::types::transaction::eip2718::TypedTransaction;
 pub use geometric::GeometricGasPrice;
 
 mod linear;
 pub use linear::LinearGasPrice;
 
 use async_trait::async_trait;
-use ethers_core::types::{BlockId, TransactionRequest, TxHash, U256};
-use ethers_providers::{interval, Middleware, MiddlewareError, PendingTransaction, StreamExt};
-use futures_util::lock::Mutex;
+
+use futures_channel::oneshot;
+use futures_util::{lock::Mutex, select_biased};
 use instant::Instant;
 use std::{pin::Pin, sync::Arc};
 use thiserror::Error;
+use tracing_futures::Instrument;
+
+use ethers_core::types::{
+    transaction::eip2718::TypedTransaction, BlockId, TransactionRequest, TxHash, U256,
+};
+use ethers_providers::{interval, Middleware, MiddlewareError, PendingTransaction, StreamExt};
 
 #[cfg(not(target_arch = "wasm32"))]
 use tokio::spawn;
 
+type ToEscalate = Arc<Mutex<Vec<(TxHash, TransactionRequest, Instant, Option<BlockId>)>>>;
+
 #[cfg(target_arch = "wasm32")]
 type WatcherFuture<'a> = Pin<Box<dyn futures_util::stream::Stream<Item = ()> + 'a>>;
 #[cfg(not(target_arch = "wasm32"))]
@@ -29,7 +36,34 @@ pub trait GasEscalator: Send + Sync + std::fmt::Debug {
     fn get_gas_price(&self, initial_price: U256, time_elapsed: u64) -> U256;
 }
 
-#[derive(Debug, Clone)]
+#[derive(Error, Debug)]
+/// Error thrown when the GasEscalator interacts with the blockchain
+pub enum GasEscalatorError<M: Middleware> {
+    #[error("{0}")]
+    /// Thrown when an internal middleware errors
+    MiddlewareError(M::Error),
+
+    #[error("Gas escalation is only supported for EIP2930 or Legacy transactions")]
+    UnsupportedTxType,
+}
+
+// Boilerplate
+impl<M: Middleware> MiddlewareError for GasEscalatorError<M> {
+    type Inner = M::Error;
+
+    fn from_err(src: M::Error) -> GasEscalatorError<M> {
+        GasEscalatorError::MiddlewareError(src)
+    }
+
+    fn as_inner(&self) -> Option<&Self::Inner> {
+        match self {
+            GasEscalatorError::MiddlewareError(e) => Some(e),
+            _ => None,
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy)]
 /// The frequency at which transactions will be bumped
 pub enum Frequency {
     /// On a per block basis using the eth_newBlock filter
@@ -39,9 +73,49 @@ pub enum Frequency {
 }
 
 #[derive(Debug)]
+pub(crate) struct GasEscalatorMiddlewareInternal<M> {
+    pub(crate) inner: Arc<M>,
+    /// The transactions which are currently being monitored for escalation
+    #[allow(clippy::type_complexity)]
+    pub txs: ToEscalate,
+    _background: oneshot::Sender<()>,
+}
+
+#[derive(Debug, Clone)]
 /// A Gas escalator allows bumping transactions' gas price to avoid getting them
 /// stuck in the memory pool.
 ///
+/// GasEscalator runs a background task which monitors the blockchain for tx
+/// confirmation, and bumps fees over time if txns do not occur. This task
+/// periodically loops over a stored history of sent transactions, and checks
+/// if any require fee bumps. If so, it will resend the same transaction with a
+/// higher fee.
+///
+/// Using [`GasEscalatorMiddleware::new`] will create a new instance of the
+/// background task. Using [`GasEscalatorMiddleware::clone`] will crate a new
+/// instance of the middleware, but will not create a new background task. The
+/// background task is shared among all clones.
+///
+/// ## Footgun
+///
+/// If you drop the middleware, the background task will be dropped as well,
+/// and any transactions you have sent will stop escalating. We recommend
+/// holding an instance of the middleware throughout your application's
+/// lifecycle, or leaking an `Arc` of it so that it is never dropped.
+///
+/// ## Outstanding issue
+///
+/// This task is fallible, and will stop if the provider's connection is lost.
+/// If this happens, the middleware will become unable to properly escalate gas
+/// prices. Transactions will still be dispatched, but no fee-bumping will
+/// happen. This will also cause a memory leak, as the middleware will keep
+/// appending to the list of transactions to escalate (and nothing will ever
+/// clear that list).
+///
+/// We intend to fix this issue in a future release.
+///
+/// ## Example
+///
 /// ```no_run
 /// use ethers_providers::{Provider, Http};
 /// use ethers_middleware::{
@@ -63,39 +137,22 @@ pub enum Frequency {
 /// let gas_oracle = GasNow::new().category(GasCategory::SafeLow);
 /// let provider = GasOracleMiddleware::new(provider, gas_oracle);
 /// ```
-pub struct GasEscalatorMiddleware<M, E> {
-    pub(crate) inner: Arc<M>,
-    pub(crate) escalator: E,
-    /// The transactions which are currently being monitored for escalation
-    #[allow(clippy::type_complexity)]
-    pub txs: Arc<Mutex<Vec<(TxHash, TransactionRequest, Instant, Option<BlockId>)>>>,
-    frequency: Frequency,
-}
-
-impl<M, E: Clone> Clone for GasEscalatorMiddleware<M, E> {
-    fn clone(&self) -> Self {
-        GasEscalatorMiddleware {
-            inner: self.inner.clone(),
-            escalator: self.escalator.clone(),
-            txs: self.txs.clone(),
-            frequency: self.frequency.clone(),
-        }
-    }
+pub struct GasEscalatorMiddleware<M> {
+    pub(crate) inner: Arc<GasEscalatorMiddlewareInternal<M>>,
 }
 
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
-impl<M, E> Middleware for GasEscalatorMiddleware<M, E>
+impl<M> Middleware for GasEscalatorMiddleware<M>
 where
     M: Middleware,
-    E: GasEscalator,
 {
     type Error = GasEscalatorError<M>;
     type Provider = M::Provider;
     type Inner = M;
 
-    fn inner(&self) -> &M {
-        &self.inner
+    fn inner(&self) -> &Self::Inner {
+        &self.inner.inner
     }
 
     async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
@@ -103,13 +160,26 @@ where
         tx: T,
         block: Option<BlockId>,
     ) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
+        self.inner.send_transaction(tx, block).await
+    }
+}
+
+impl<M> GasEscalatorMiddlewareInternal<M>
+where
+    M: Middleware,
+{
+    async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
+        &self,
+        tx: T,
+        block: Option<BlockId>,
+    ) -> Result<PendingTransaction<'_, M::Provider>, GasEscalatorError<M>> {
         let tx = tx.into();
 
         let pending_tx = self
-            .inner()
+            .inner
             .send_transaction(tx.clone(), block)
             .await
-            .map_err(GasEscalatorError::MiddlewareError)?;
+            .map_err(MiddlewareError::from_err)?;
 
         let tx = match tx {
             TypedTransaction::Legacy(inner) => inner,
@@ -125,141 +195,164 @@ where
     }
 }
 
-impl<M, E> GasEscalatorMiddleware<M, E>
+impl<M> GasEscalatorMiddleware<M>
 where
     M: Middleware,
-    E: GasEscalator,
 {
     /// Initializes the middleware with the provided gas escalator and the chosen
     /// escalation frequency (per block or per second)
     #[allow(clippy::let_and_return)]
     #[cfg(not(target_arch = "wasm32"))]
-    pub fn new(inner: M, escalator: E, frequency: Frequency) -> Self
+    pub fn new<E>(inner: M, escalator: E, frequency: Frequency) -> Self
     where
-        E: Clone + 'static,
-        M: Clone + 'static,
+        E: GasEscalator + 'static,
+        M: 'static,
     {
-        use tracing_futures::Instrument;
+        let (tx, rx) = oneshot::channel();
+        let inner = Arc::new(inner);
 
-        let this = Self {
-            inner: Arc::new(inner),
-            escalator,
-            frequency,
-            txs: Arc::new(Mutex::new(Vec::new())),
-        };
+        let txs: ToEscalate = Default::default();
+
+        let this = Arc::new(GasEscalatorMiddlewareInternal {
+            inner: inner.clone(),
+            txs: txs.clone(),
+            _background: tx,
+        });
+
+        let esc = EscalationTask { inner, escalator, frequency, txs, shutdown: rx };
 
         {
-            let this2 = this.clone();
-            spawn(async move {
-                this2.escalate().instrument(tracing::trace_span!("gas-escalation")).await.unwrap();
-            });
+            spawn(esc.escalate().instrument(tracing::trace_span!("gas-escalation")));
         }
 
-        this
+        Self { inner: this }
+    }
+}
+
+#[derive(Debug)]
+pub struct EscalationTask<M, E> {
+    inner: M,
+    escalator: E,
+    frequency: Frequency,
+    txs: ToEscalate,
+    shutdown: oneshot::Receiver<()>,
+}
+
+impl<M, E> EscalationTask<M, E> {
+    pub fn new(
+        inner: M,
+        escalator: E,
+        frequency: Frequency,
+        txs: ToEscalate,
+        shutdown: oneshot::Receiver<()>,
+    ) -> Self {
+        Self { inner, escalator, frequency, txs, shutdown }
     }
 
-    /// Re-broadcasts pending transactions with a gas price escalator
-    pub async fn escalate(&self) -> Result<(), GasEscalatorError<M>> {
+    async fn escalate(mut self) -> Result<(), GasEscalatorError<M>>
+    where
+        M: Middleware,
+        E: GasEscalator,
+    {
         // the escalation frequency is either on a per-block basis, or on a duration basis
-        let mut watcher: WatcherFuture = match self.frequency {
+        let watcher: WatcherFuture = match self.frequency {
             Frequency::PerBlock => Box::pin(
-                self.inner
-                    .watch_blocks()
-                    .await
-                    .map_err(GasEscalatorError::MiddlewareError)?
-                    .map(|_| ()),
+                self.inner.watch_blocks().await.map_err(MiddlewareError::from_err)?.map(|_| ()),
             ),
             Frequency::Duration(ms) => Box::pin(interval(std::time::Duration::from_millis(ms))),
         };
 
-        while watcher.next().await.is_some() {
-            let now = Instant::now();
-            let mut txs = self.txs.lock().await;
-            let len = txs.len();
-
-            // Pop all transactions and re-insert those that have not been included yet
-            for _ in 0..len {
-                // this must never panic as we're explicitly within bounds
-                let (tx_hash, mut replacement_tx, time, priority) =
-                    txs.pop().expect("should have element in vector");
-
-                let receipt = self.get_transaction_receipt(tx_hash).await?;
-                tracing::trace!(tx_hash = ?tx_hash, "checking if exists");
-                if receipt.is_none() {
-                    let old_gas_price = replacement_tx.gas_price.expect("gas price must be set");
-                    // Get the new gas price based on how much time passed since the
-                    // tx was last broadcast
-                    let new_gas_price = self
-                        .escalator
-                        .get_gas_price(old_gas_price, now.duration_since(time).as_secs());
-
-                    let new_txhash = if new_gas_price != old_gas_price {
-                        // bump the gas price
-                        replacement_tx.gas_price = Some(new_gas_price);
-
-                        // the tx hash will be different so we need to update it
-                        match self.inner().send_transaction(replacement_tx.clone(), priority).await
-                        {
-                            Ok(new_tx_hash) => {
-                                let new_tx_hash = *new_tx_hash;
-                                tracing::trace!(
-                                    old_tx_hash = ?tx_hash,
-                                    new_tx_hash = ?new_tx_hash,
-                                    old_gas_price = ?old_gas_price,
-                                    new_gas_price = ?new_gas_price,
-                                    "escalated"
-                                );
-                                new_tx_hash
-                            }
-                            Err(err) => {
-                                if err.to_string().contains("nonce too low") {
-                                    // ignore "nonce too low" errors because they
-                                    // may happen if we try to broadcast a higher
-                                    // gas price tx when one of the previous ones
-                                    // was already mined (meaning we also do not
-                                    // push it back to the pending txs vector)
-                                    continue
-                                } else {
-                                    return Err(GasEscalatorError::MiddlewareError(err))
+        let mut watcher = watcher.fuse();
+
+        loop {
+            select_biased! {
+            _ = &mut self.shutdown => {
+                tracing::debug!("Shutting down escalation task, middleware has gone away");
+                return Ok(())
+            }
+            opt = watcher.next() => {
+                if opt.is_none() {
+                    tracing::error!("timing future has gone away");
+                    return Ok(());
+                }
+                let now = Instant::now();
+
+                // We take the contents of the mutex, and then add them back in
+                // later.
+                let mut txs: Vec<_> = {
+                    let mut txs = self.txs.lock().await;
+                    std::mem::take(&mut (*txs))
+                    // Lock scope ends
+                };
+
+                let len = txs.len();
+                // Pop all transactions and re-insert those that have not been included yet
+                for _ in 0..len {
+                    // this must never panic as we're explicitly within bounds
+                    let (tx_hash, mut replacement_tx, time, priority) =
+                        txs.pop().expect("should have element in vector");
+
+                    let receipt = self
+                        .inner
+                        .get_transaction_receipt(tx_hash)
+                        .await
+                        .map_err(MiddlewareError::from_err)?;
+
+                    tracing::trace!(tx_hash = ?tx_hash, "checking if exists");
+
+                    if receipt.is_none() {
+                        let old_gas_price = replacement_tx.gas_price.expect("gas price must be set");
+                        // Get the new gas price based on how much time passed since the
+                        // tx was last broadcast
+                        let new_gas_price = self
+                            .escalator
+                            .get_gas_price(old_gas_price, now.duration_since(time).as_secs());
+
+                        let new_txhash = if new_gas_price == old_gas_price {
+                             tx_hash
+                        } else {
+                            // bump the gas price
+                            replacement_tx.gas_price = Some(new_gas_price);
+
+                            // the tx hash will be different so we need to update it
+                            match self.inner.send_transaction(replacement_tx.clone(), priority).await {
+                                Ok(new_tx_hash) => {
+                                    let new_tx_hash = *new_tx_hash;
+                                    tracing::trace!(
+                                        old_tx_hash = ?tx_hash,
+                                        new_tx_hash = ?new_tx_hash,
+                                        old_gas_price = ?old_gas_price,
+                                        new_gas_price = ?new_gas_price,
+                                        "escalated"
+                                    );
+                                    new_tx_hash
+                                }
+                                Err(err) => {
+                                    if err.to_string().contains("nonce too low") {
+                                        // ignore "nonce too low" errors because they
+                                        // may happen if we try to broadcast a higher
+                                        // gas price tx when one of the previous ones
+                                        // was already mined (meaning we also do not
+                                        // push it back to the pending txs vector)
+                                        continue
+                                    } else {
+                                        tracing::error!(
+                                            err = %err,
+                                            "Killing escalator backend"
+                                        );
+                                        return Err(GasEscalatorError::MiddlewareError(err))
+                                    }
                                 }
                             }
-                        }
-                    } else {
-                        tx_hash
-                    };
-
-                    txs.push((new_txhash, replacement_tx, time, priority));
+                        };
+                        txs.push((new_txhash, replacement_tx, time, priority));
+                    }
                 }
-            }
-        }
-
-        Ok(())
-    }
-}
-
-// Boilerplate
-impl<M: Middleware> MiddlewareError for GasEscalatorError<M> {
-    type Inner = M::Error;
-
-    fn from_err(src: M::Error) -> GasEscalatorError<M> {
-        GasEscalatorError::MiddlewareError(src)
-    }
-
-    fn as_inner(&self) -> Option<&Self::Inner> {
-        match self {
-            GasEscalatorError::MiddlewareError(e) => Some(e),
-            _ => None,
+                // after this big ugly loop, we dump everything back in
+                // we don't replace here, as the vec in the mutex may contain
+                // items!
+                self.txs.lock().await.extend(txs);
+            }}
         }
     }
 }
-
-#[derive(Error, Debug)]
-/// Error thrown when the GasEscalator interacts with the blockchain
-pub enum GasEscalatorError<M: Middleware> {
-    #[error("{0}")]
-    /// Thrown when an internal middleware errors
-    MiddlewareError(M::Error),
-
-    #[error("Gas escalation is only supported for EIP2930 or Legacy transactions")]
-    UnsupportedTxType,
-}
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index dd8a2a5c..0943167f 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -111,14 +111,14 @@ async = [
     "futures-util",
     "criterion/async_tokio",
 ]
-svm-solc = ["svm-builds", "sha2"]
+svm-solc = ["svm", "svm-builds", "sha2"]
 
 # Utilities for creating and testing project workspaces
 project-util = ["tempfile", "fs_extra", "rand"]
 
 tests = []
-openssl = ["svm/openssl"]
-rustls = ["svm/rustls"]
+openssl = ["svm?/openssl"]
+rustls = ["svm?/rustls"]
 
 # Deprecated
 asm = []
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index 17da2b87..5f7b2bc3 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -659,6 +659,7 @@ pub trait ArtifactOutput {
         if let Ok(stripped) = rel_candidate.strip_prefix(artifacts_folder) {
             rel_candidate = stripped.to_path_buf();
         }
+        #[allow(clippy::redundant_clone)] // false positive
         let mut candidate = rel_candidate.clone();
         let contract_file = contract_file.as_ref();
         let mut current_parent = contract_file.parent();
diff --git a/ethers/Cargo.toml b/ethers/Cargo.toml
index 7dff1300..8a5b36ed 100644
--- a/ethers/Cargo.toml
+++ b/ethers/Cargo.toml
@@ -26,14 +26,7 @@ all-features = true
 [features]
 default = ["abigen", "rustls"]
 
-celo = [
-    "ethers-core/celo",
-    "ethers-providers/celo",
-    "ethers-signers/celo",
-    "ethers-contract/celo",
-    "ethers-middleware/celo",
-    "legacy",
-]
+celo = ["ethers-core/celo", "ethers-providers/celo", "ethers-signers/celo", "ethers-contract/celo", "ethers-middleware/celo", "legacy"]
 
 legacy = ["ethers-core/legacy", "ethers-contract/legacy"]
 
@@ -43,20 +36,8 @@ eip712 = ["ethers-contract/eip712", "ethers-core/eip712"]
 ## providers
 ws = ["ethers-providers/ws"]
 ipc = ["ethers-providers/ipc"]
-rustls = [
-    "ethers-middleware/rustls",
-    "ethers-providers/rustls",
-    "ethers-etherscan/rustls",
-    "ethers-contract/rustls",
-    "ethers-solc/rustls",
-]
-openssl = [
-    "ethers-middleware/openssl",
-    "ethers-providers/openssl",
-    "ethers-etherscan/openssl",
-    "ethers-contract/openssl",
-    "ethers-solc/openssl",
-]
+rustls = ["ethers-middleware/rustls", "ethers-providers/rustls", "ethers-etherscan/rustls", "ethers-contract/rustls", "ethers-solc/rustls"]
+openssl = ["ethers-middleware/openssl", "ethers-providers/openssl", "ethers-etherscan/openssl", "ethers-contract/openssl", "ethers-solc/openssl"]
 dev-rpc = ["ethers-providers/dev-rpc"]
 ## signers
 ledger = ["ethers-signers/ledger"]
@@ -67,9 +48,8 @@ abigen = ["ethers-contract/abigen"]
 ### abigen without reqwest
 abigen-offline = ["ethers-contract/abigen-offline"]
 ## solc
-ethers-solc = ["dep:ethers-solc", "ethers-etherscan/ethers-solc"]
-solc-full = ["ethers-solc?/full"]
-solc-tests = ["ethers-solc?/tests"]
+solc-full = ["ethers-solc/full"]
+solc-tests = ["ethers-solc/tests"]
 
 # Deprecated
 solc-sha2-asm = []
@@ -78,12 +58,14 @@ solc-sha2-asm = []
 ethers-addressbook.workspace = true
 ethers-contract.workspace = true
 ethers-core.workspace = true
-ethers-etherscan.workspace = true
+ethers-etherscan = { workspace = true, features = ["ethers-solc"] }
 ethers-middleware.workspace = true
 ethers-providers.workspace = true
 ethers-signers.workspace = true
-
-ethers-solc = { workspace = true, optional = true }
+ethers-solc = { workspace = true }
 
 [dev-dependencies]
+serde.workspace = true
 tokio = { workspace = true, features = ["macros", "rt"] }
+ethers-contract = { workspace = true, features = ["eip712"] }
+ethers-providers = { workspace = true, features = ["rustls"] } # allow https connections
diff --git a/ethers/src/lib.rs b/ethers/src/lib.rs
index 8cdc5983..3d94e43d 100644
--- a/ethers/src/lib.rs
+++ b/ethers/src/lib.rs
@@ -98,7 +98,6 @@ pub use ethers_providers as providers;
 #[doc(inline)]
 pub use ethers_signers as signers;
 #[doc(inline)]
-#[cfg(feature = "ethers-solc")]
 pub use ethers_solc as solc;
 
 #[doc(inline)]
@@ -121,7 +120,6 @@ pub mod prelude {
 
     pub use super::signers::*;
 
-    #[cfg(feature = "ethers-solc")]
     pub use super::solc::*;
 }
 
diff --git a/examples/contracts/Cargo.toml b/examples/contracts/Cargo.toml
index 3fd9bb5f..8cc0618f 100644
--- a/examples/contracts/Cargo.toml
+++ b/examples/contracts/Cargo.toml
@@ -13,7 +13,7 @@ default = ["legacy"]
 legacy = ["ethers/legacy"]
 
 [dev-dependencies]
-ethers = { workspace = true, features = ["abigen", "ethers-solc", "rustls", "ws"] }
+ethers = { workspace = true, features = ["abigen", "rustls", "ws"] }
 
 tokio = { workspace = true, features = ["macros"] }
 


Test patch:
diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs
index 8d4dd44e..88f04ed5 100644
--- a/ethers-contract/tests/it/contract.rs
+++ b/ethers-contract/tests/it/contract.rs
@@ -1,18 +1,14 @@
 use crate::common::*;
 use ethers_contract::{
-    abigen, ContractFactory, ContractInstance, EthAbiType, EthEvent, LogMeta, Multicall,
-    MulticallError, MulticallVersion,
+    abigen, ContractFactory, ContractInstance, EthEvent, LogMeta, Multicall, MulticallError,
+    MulticallVersion,
 };
 use ethers_core::{
     abi::{encode, AbiEncode, Token, Tokenizable},
-    types::{
-        transaction::eip712::Eip712, Address, BlockId, Bytes, Filter, ValueOrArray, H160, H256,
-        I256, U256,
-    },
+    types::{Address, BlockId, Bytes, Filter, ValueOrArray, H160, H256, U256},
     utils::{keccak256, Anvil},
 };
 use ethers_providers::{Http, Middleware, MiddlewareError, Provider, StreamExt};
-use ethers_signers::{LocalWallet, Signer};
 use std::{sync::Arc, time::Duration};
 
 #[derive(Debug)]
@@ -330,7 +326,6 @@ async fn call_past_hash_test() {
 }
 
 #[tokio::test]
-#[cfg(feature = "abigen")]
 async fn watch_events() {
     let (abi, bytecode) = compile_contract("SimpleStorage", "SimpleStorage.sol");
     let anvil = Anvil::new().spawn();
@@ -784,146 +779,3 @@ async fn multicall_aggregate() {
     assert_eq!(bytes[..4], keccak256("CustomErrorWithData(string)")[..4]);
     assert_eq!(bytes[4..], encode(&[Token::String("Data".to_string())]));
 }
-
-#[tokio::test]
-#[cfg(feature = "eip712")]
-async fn test_derive_eip712() {
-    use ethers_derive_eip712::*;
-
-    // Generate Contract ABI Bindings
-    abigen!(
-        DeriveEip712Test,
-        "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json",
-        event_derives(serde::Deserialize, serde::Serialize)
-    );
-
-    // Create derived structs
-
-    #[derive(Debug, Clone, Eip712, EthAbiType)]
-    #[eip712(
-        name = "Eip712Test",
-        version = "1",
-        chain_id = 1,
-        verifying_contract = "0x0000000000000000000000000000000000000001",
-        salt = "eip712-test-75F0CCte"
-    )]
-    struct FooBar {
-        foo: I256,
-        bar: U256,
-        fizz: Bytes,
-        buzz: [u8; 32],
-        far: String,
-        out: Address,
-    }
-
-    // get ABI and bytecode for the DeriveEip712Test contract
-    let (abi, bytecode) = compile_contract("DeriveEip712Test", "DeriveEip712Test.sol");
-
-    // launch the network & connect to it
-    let anvil = Anvil::new().spawn();
-    let from = anvil.addresses()[0];
-    let provider = Provider::try_from(anvil.endpoint())
-        .unwrap()
-        .with_sender(from)
-        .interval(std::time::Duration::from_millis(10));
-    let client = Arc::new(provider);
-
-    let wallet: LocalWallet = anvil.keys()[0].clone().into();
-
-    let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone());
-
-    let contract = factory
-        .deploy(())
-        .expect("failed to deploy DeriveEip712Test contract")
-        .legacy()
-        .send()
-        .await
-        .expect("failed to instantiate factory for DeriveEip712 contract");
-
-    let addr = contract.address();
-
-    let contract = DeriveEip712Test::new(addr, client.clone());
-
-    let foo_bar = FooBar {
-        foo: I256::from(10u64),
-        bar: U256::from(20u64),
-        fizz: b"fizz".into(),
-        buzz: keccak256("buzz"),
-        far: String::from("space"),
-        out: Address::from([0; 20]),
-    };
-
-    let derived_foo_bar = derive_eip_712_test::FooBar {
-        foo: foo_bar.foo,
-        bar: foo_bar.bar,
-        fizz: foo_bar.fizz.clone(),
-        buzz: foo_bar.buzz,
-        far: foo_bar.far.clone(),
-        out: foo_bar.out,
-    };
-
-    let sig = wallet.sign_typed_data(&foo_bar).await.expect("failed to sign typed data");
-
-    let r = <[u8; 32]>::try_from(sig.r)
-        .expect("failed to parse 'r' value from signature into [u8; 32]");
-    let s = <[u8; 32]>::try_from(sig.s)
-        .expect("failed to parse 's' value from signature into [u8; 32]");
-    let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8");
-
-    let domain_separator = contract
-        .domain_separator()
-        .call()
-        .await
-        .expect("failed to retrieve domain_separator from contract");
-    let type_hash =
-        contract.type_hash().call().await.expect("failed to retrieve type_hash from contract");
-    let struct_hash = contract
-        .struct_hash(derived_foo_bar.clone())
-        .call()
-        .await
-        .expect("failed to retrieve struct_hash from contract");
-    let encoded = contract
-        .encode_eip_712(derived_foo_bar.clone())
-        .call()
-        .await
-        .expect("failed to retrieve eip712 encoded hash from contract");
-    let verify = contract
-        .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v)
-        .call()
-        .await
-        .expect("failed to verify signed typed data eip712 payload");
-
-    assert_eq!(
-        domain_separator,
-        foo_bar
-            .domain()
-            .expect("failed to return domain_separator from Eip712 implemented struct")
-            .separator(),
-        "domain separator does not match contract domain separator!"
-    );
-
-    assert_eq!(
-        type_hash,
-        FooBar::type_hash().expect("failed to return type_hash from Eip712 implemented struct"),
-        "type hash does not match contract struct type hash!"
-    );
-
-    assert_eq!(
-        struct_hash,
-        foo_bar
-            .clone()
-            .struct_hash()
-            .expect("failed to return struct_hash from Eip712 implemented struct"),
-        "struct hash does not match contract struct hash!"
-    );
-
-    assert_eq!(
-        encoded,
-        foo_bar
-            .encode_eip712()
-            .expect("failed to return domain_separator from Eip712 implemented struct"),
-        "Encoded value does not match!"
-    );
-
-    assert!(verify, "typed data signature failed!");
-}
diff --git a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol b/ethers/tests/DeriveEip712Test.sol
similarity index 100%
rename from ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol
rename to ethers/tests/DeriveEip712Test.sol
diff --git a/ethers/tests/live/celo.rs b/ethers/tests/celo.rs
similarity index 96%
rename from ethers/tests/live/celo.rs
rename to ethers/tests/celo.rs
index 7457e0dd..0931decf 100644
--- a/ethers/tests/live/celo.rs
+++ b/ethers/tests/celo.rs
@@ -1,7 +1,8 @@
-use crate::simple_storage::SimpleStorage;
 use ethers::prelude::*;
 use std::{sync::Arc, time::Duration};
 
+ethers::contract::abigen!(SimpleStorage, "../testdata/SimpleStorage.json");
+
 static CELO_TESTNET_URL: &str = "https://alfajores-forno.celo-testnet.org";
 
 #[tokio::test]
diff --git a/ethers/tests/eip712.rs b/ethers/tests/eip712.rs
new file mode 100644
index 00000000..f23d8295
--- /dev/null
+++ b/ethers/tests/eip712.rs
@@ -0,0 +1,157 @@
+use ethers::{
+    contract::{abigen, ContractFactory, Eip712, EthAbiType},
+    core::{
+        types::{transaction::eip712::Eip712, Address, Bytes, I256, U256},
+        utils::{keccak256, Anvil},
+    },
+    providers::Provider,
+    signers::LocalWallet,
+    solc::Solc,
+};
+use std::{path::PathBuf, sync::Arc};
+
+#[tokio::test]
+async fn test_derive_eip712() {
+    // Generate Contract ABI Bindings
+    abigen!(
+        DeriveEip712Test,
+        "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json",
+        event_derives(serde::Deserialize, serde::Serialize)
+    );
+
+    // Create derived structs
+
+    #[derive(Debug, Clone, Eip712, EthAbiType)]
+    #[eip712(
+        name = "Eip712Test",
+        version = "1",
+        chain_id = 1,
+        verifying_contract = "0x0000000000000000000000000000000000000001",
+        salt = "eip712-test-75F0CCte"
+    )]
+    struct FooBar {
+        foo: I256,
+        bar: U256,
+        fizz: Bytes,
+        buzz: [u8; 32],
+        far: String,
+        out: Address,
+    }
+
+    // get ABI and bytecode for the DeriveEip712Test contract
+    let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests");
+    let result = Solc::default().compile_source(path).unwrap();
+    let (abi, bytecode, _) = result
+        .find("DeriveEip712Test")
+        .expect("failed to get DeriveEip712Test contract")
+        .into_parts_or_default();
+
+    // launch the network & connect to it
+    let anvil = Anvil::new().spawn();
+    let from = anvil.addresses()[0];
+    let provider = Provider::try_from(anvil.endpoint())
+        .unwrap()
+        .with_sender(from)
+        .interval(std::time::Duration::from_millis(10));
+    let client = Arc::new(provider);
+
+    let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone());
+
+    let contract = factory
+        .deploy(())
+        .expect("failed to deploy DeriveEip712Test contract")
+        .legacy()
+        .send()
+        .await
+        .expect("failed to instantiate factory for DeriveEip712 contract");
+
+    let addr = contract.address();
+
+    let contract = DeriveEip712Test::new(addr, client.clone());
+
+    let foo_bar = FooBar {
+        foo: I256::from(10u64),
+        bar: U256::from(20u64),
+        fizz: b"fizz".into(),
+        buzz: keccak256("buzz"),
+        far: String::from("space"),
+        out: Address::from([0; 20]),
+    };
+
+    let derived_foo_bar = derive_eip_712_test::FooBar {
+        foo: foo_bar.foo,
+        bar: foo_bar.bar,
+        fizz: foo_bar.fizz.clone(),
+        buzz: foo_bar.buzz,
+        far: foo_bar.far.clone(),
+        out: foo_bar.out,
+    };
+
+    use ethers::signers::Signer;
+
+    let wallet: LocalWallet = anvil.keys()[0].clone().into();
+    let sig = wallet.sign_typed_data(&foo_bar).await.expect("failed to sign typed data");
+
+    let r = <[u8; 32]>::try_from(sig.r)
+        .expect("failed to parse 'r' value from signature into [u8; 32]");
+    let s = <[u8; 32]>::try_from(sig.s)
+        .expect("failed to parse 's' value from signature into [u8; 32]");
+    let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8");
+
+    let domain_separator = contract
+        .domain_separator()
+        .call()
+        .await
+        .expect("failed to retrieve domain_separator from contract");
+    let type_hash =
+        contract.type_hash().call().await.expect("failed to retrieve type_hash from contract");
+    let struct_hash = contract
+        .struct_hash(derived_foo_bar.clone())
+        .call()
+        .await
+        .expect("failed to retrieve struct_hash from contract");
+    let encoded = contract
+        .encode_eip_712(derived_foo_bar.clone())
+        .call()
+        .await
+        .expect("failed to retrieve eip712 encoded hash from contract");
+    let verify = contract
+        .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v)
+        .call()
+        .await
+        .expect("failed to verify signed typed data eip712 payload");
+
+    assert_eq!(
+        domain_separator,
+        foo_bar
+            .domain()
+            .expect("failed to return domain_separator from Eip712 implemented struct")
+            .separator(),
+        "domain separator does not match contract domain separator!"
+    );
+
+    assert_eq!(
+        type_hash,
+        FooBar::type_hash().expect("failed to return type_hash from Eip712 implemented struct"),
+        "type hash does not match contract struct type hash!"
+    );
+
+    assert_eq!(
+        struct_hash,
+        foo_bar
+            .clone()
+            .struct_hash()
+            .expect("failed to return struct_hash from Eip712 implemented struct"),
+        "struct hash does not match contract struct hash!"
+    );
+
+    assert_eq!(
+        encoded,
+        foo_bar
+            .encode_eip712()
+            .expect("failed to return domain_separator from Eip712 implemented struct"),
+        "Encoded value does not match!"
+    );
+
+    assert!(verify, "typed data signature failed!");
+}
diff --git a/ethers/tests/live/main.rs b/ethers/tests/live/main.rs
deleted file mode 100644
index a8828646..00000000
--- a/ethers/tests/live/main.rs
+++ /dev/null
@@ -1,13 +0,0 @@
-//! Ethers live tests.
-//!
-//! If a feature or external binary is added, like Solc, please also update
-//! `.github/workflows/ci.yml` at `job.live-test`.
-
-#![cfg(not(target_arch = "wasm32"))]
-
-#[cfg(feature = "celo")]
-mod celo;
-
-pub(crate) mod simple_storage {
-    ethers::contract::abigen!(SimpleStorage, "../testdata/SimpleStorage.json");
-}
diff --git a/ethers/tests/main.rs b/ethers/tests/main.rs
new file mode 100644
index 00000000..7d10133c
--- /dev/null
+++ b/ethers/tests/main.rs
@@ -0,0 +1,7 @@
+//! Ethers integration tests.
+#![cfg(not(target_arch = "wasm32"))]
+
+#[cfg(feature = "celo")]
+mod celo;
+
+mod eip712;