Repository: gakonst/ethers-rs
Pull request #1287 (2022-05-20 01:35:54)

return instead of ignore error
## Motivation

I noticed that occasionally my program would get stuck with one thread spinning at near 100% CPU. After a bunch of investigation with rust-gdb and tokio-console and stripping it down to a much [smaller program](https://github.com/SatoshiAndKin/web3-proxy/tree/c3d1d14f1628e415729e55809e3037b422ef16ad/web3-proxy-minimal), I narrowed it down to a websocket task being very active doing nothing.

Here you can see the spawned future stuck running:

![IMAGE 2022-05-19 18:30:24](https://user-images.githubusercontent.com/624221/169430653-945fdac2-9f21-49ff-9218-97b189da0a7d.jpg)

It is being woken up ~253k times per second, so that explains the 100% CPU:

![IMAGE 2022-05-19 18:29:44](https://user-images.githubusercontent.com/624221/169430590-7f279703-03f5-460d-b5d7-50e5b0434271.jpg)

After some digging, I found this related code:

https://github.com/gakonst/ethers-rs/blob/3df1527cef7a7de8cbb26a8c98de74348203176b/ethers-providers/src/transports/ws.rs#L402-L408

Dropping that error is bad because it might be an `Io(Kind(UnexpectedEof))`. If that's the case, the websocket needs to reconnect.

note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


## Solution

Quick fix is to just return an error when the socket gives an error.

I think a more robust fix might be to use something like https://docs.rs/stubborn-io/latest/stubborn_io/.

## PR Checklist

I'm not sure how to write a test for this.

- [ ] Added Tests
- [ ] Added Documentation
- [x] Updated the changelog


gakonst: Thanks for the in-depth investigation!

> Quick fix is to just return an error when the socket gives an error.

How does returning the error fix the CPU issue? Is this error a temporary error, which means that it made sense to ignore it? If we're terminating on a temporary error that doesn't seem right?
WyseNynja: The error I'm seeing (and I *think* all of them), is not a temporary error. Once you get it, checking that future again always gives back an Eof immediately. The CPU issue is because it gets stuck in `tick` just getting an Eof back immediately every time it polls. Changing it to return an error causes the websocket thread to panic higher up in the code. I think there is another issue (or at least some discussion in telegram) to make that give a nicer error instead of panic.

Panic works fine for me though because I detect that my subscription exited and reconnect.
WyseNynja: with this PR, my proxy saw an error and reconnected:

![image](https://user-images.githubusercontent.com/624221/169446007-793714ae-aa9b-40b6-a3bd-800071d84b00.png)

Having the full error would be even better though.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e15b3a61..5d8daf6a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -67,6 +67,7 @@
 - Add `TransactionReceipt::to` and `TransactionReceipt::from`
   [#1184](https://github.com/gakonst/ethers-rs/pull/1184)
 - Add `From<H160>` and From<Vec<H160>> traits to `ValueOrArray<H160>` [#1199](https://github.com/gakonst/ethers-rs/pull/1200)
+- Fix handling of Websocket connection errors [#1287](https://github.com/gakonst/ethers-rs/pull/1287)
 
 ## ethers-contract-abigen
 
@@ -96,6 +97,8 @@
 
 - Bundle svm, svm-builds and sha2 dependencies in new `svm-solc` feature
   [#1071](https://github.com/gakonst/ethers-rs/pull/1071)
+- Emit artifact files for source files without any ContractDefinition
+  [#1296](https://github.com/gakonst/ethers-rs/pull/1296)
 - Wrap `ethabi::Contract` into new type `LosslessAbi` and `abi: Option<Abi>` with `abi: Option<LosslessAbi>` in `ConfigurableContractArtifact`
   [#952](https://github.com/gakonst/ethers-rs/pull/952)
 - Let `Project` take ownership of `ArtifactOutput` and change trait interface
@@ -213,6 +216,8 @@
 
 ### Unreleased
 
+- Added tx builder methods to `ContractFactory`
+  [#1289](https://github.com/gakonst/ethers-rs/pull/1289)
 - Relax Clone requirements when Arc<Middleware> is used
   [#1183](https://github.com/gakonst/ethers-rs/pull/1183)
 - Add `EventStream::select` to combine streams with different event types
diff --git a/Cargo.lock b/Cargo.lock
index 68ce1317..bbe85af4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2328,9 +2328,9 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
 
 [[package]]
 name = "once_cell"
-version = "1.10.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9"
+checksum = "7b10983b38c53aebdf33f542c6275b0f58a238129d00c4ae0e6fb59738d783ca"
 
 [[package]]
 name = "oorandom"
@@ -2928,9 +2928,9 @@ dependencies = [
 
 [[package]]
 name = "regex"
-version = "1.5.5"
+version = "1.5.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286"
+checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -2948,9 +2948,9 @@ dependencies = [
 
 [[package]]
 name = "regex-syntax"
-version = "0.6.25"
+version = "0.6.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
+checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
 
 [[package]]
 name = "remove_dir_all"
diff --git a/ethers-addressbook/Cargo.toml b/ethers-addressbook/Cargo.toml
index 15ec1b64..dde2cb12 100644
--- a/ethers-addressbook/Cargo.toml
+++ b/ethers-addressbook/Cargo.toml
@@ -10,7 +10,7 @@ repository = "https://github.com/gakonst/ethers-rs"
 keywords = ["ethereum", "web3", "celo", "ethers"]
 
 [dependencies]
-once_cell = "1.10.0"
+once_cell = "1.11.0"
 serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0"
 
diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml
index 1f0cd464..cad215cb 100644
--- a/ethers-contract/Cargo.toml
+++ b/ethers-contract/Cargo.toml
@@ -18,8 +18,8 @@ ethers-derive-eip712 = { version = "^0.2.0", path = "../ethers-core/ethers-deriv
 
 serde = { version = "1.0.124", default-features = false }
 serde_json = { version = "1.0.64", default-features = false }
-thiserror = { version = "1.0.31", default-features = false }
-once_cell = { version = "1.10.0" }
+thiserror = { version = "1.0", default-features = false }
+once_cell = { version = "1.11.0" }
 pin-project = {version = "1.0.7", default-features = false }
 futures-util = { version = "^0.3" }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
diff --git a/ethers-contract/ethers-contract-abigen/src/multi.rs b/ethers-contract/ethers-contract-abigen/src/multi.rs
index e57f10f8..3a87fb9e 100644
--- a/ethers-contract/ethers-contract-abigen/src/multi.rs
+++ b/ethers-contract/ethers-contract-abigen/src/multi.rs
@@ -432,7 +432,7 @@ impl MultiBindings {
     ///  let gen = MultiAbigen::from_json_files(&abi_dir).unwrap();
     ///  let bindings = gen.build().unwrap();
     ///  bindings.ensure_consistent_crate(
-    ///     "my-crate", "0.0.1", project_root.join("src/contracts"), false
+    ///     "my-crate", "0.0.1", project_root.join("src/contracts"), false, true
     ///  ).expect("inconsistent bindings");
     /// }
     /// ```
@@ -442,8 +442,15 @@ impl MultiBindings {
         version: impl AsRef<str>,
         crate_path: impl AsRef<Path>,
         single_file: bool,
+        check_cargo_toml: bool,
     ) -> Result<()> {
-        self.into_inner(single_file).ensure_consistent_crate(name, version, crate_path, single_file)
+        self.into_inner(single_file).ensure_consistent_crate(
+            name,
+            version,
+            crate_path,
+            single_file,
+            check_cargo_toml,
+        )
     }
 
     /// This ensures that the already generated bindings module matches the
@@ -668,12 +675,15 @@ serde_json = "1.0.79"
         version: impl AsRef<str>,
         crate_path: impl AsRef<Path>,
         single_file: bool,
+        check_cargo_toml: bool,
     ) -> Result<()> {
         let crate_path = crate_path.as_ref();
 
-        // additionally check the contents of the cargo
-        let cargo_contents = self.generate_cargo_toml(name, version)?;
-        check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
+        if check_cargo_toml {
+            // additionally check the contents of the cargo
+            let cargo_contents = self.generate_cargo_toml(name, version)?;
+            check_file_in_dir(crate_path, "Cargo.toml", &cargo_contents)?;
+        }
 
         self.ensure_consistent_bindings(crate_path.join("src"), true, single_file)?;
         Ok(())
@@ -829,7 +839,7 @@ mod tests {
                 .clone()
                 .build()
                 .unwrap()
-                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .ensure_consistent_crate(name, version, &mod_root, single_file, true)
                 .expect("Inconsistent bindings");
         })
     }
@@ -853,7 +863,7 @@ mod tests {
                 .clone()
                 .build()
                 .unwrap()
-                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .ensure_consistent_crate(name, version, &mod_root, single_file, true)
                 .expect("Inconsistent bindings");
         })
     }
@@ -944,7 +954,7 @@ mod tests {
             let result = cloned
                 .build()
                 .unwrap()
-                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .ensure_consistent_crate(name, version, &mod_root, single_file, true)
                 .is_err();
 
             // ensure inconsistent bindings are detected
@@ -982,7 +992,7 @@ mod tests {
             let result = cloned
                 .build()
                 .unwrap()
-                .ensure_consistent_crate(name, version, &mod_root, single_file)
+                .ensure_consistent_crate(name, version, &mod_root, single_file, true)
                 .is_err();
 
             // ensure inconsistent bindings are detected
diff --git a/ethers-contract/src/factory.rs b/ethers-contract/src/factory.rs
index 23ef6bb9..cb493e0b 100644
--- a/ethers-contract/src/factory.rs
+++ b/ethers-contract/src/factory.rs
@@ -4,8 +4,8 @@ use std::marker::PhantomData;
 use ethers_core::{
     abi::{Abi, Token, Tokenize},
     types::{
-        transaction::eip2718::TypedTransaction, BlockNumber, Bytes, TransactionReceipt,
-        TransactionRequest,
+        transaction::eip2718::TypedTransaction, Address, BlockNumber, Bytes, NameOrAddress,
+        TransactionReceipt, TransactionRequest, U256, U64,
     },
 };
 use ethers_providers::Middleware;
@@ -59,6 +59,54 @@ impl<M: Middleware, C: From<Contract<M>>> ContractDeployer<M, C> {
         self
     }
 
+    /// Sets the `from` field in the deploy transaction to the provided value
+    pub fn from<T: Into<Address>>(mut self, from: T) -> Self {
+        self.deployer.tx.set_from(from.into());
+        self
+    }
+
+    /// Sets the `to` field in the deploy transaction to the provided value
+    pub fn to<T: Into<NameOrAddress>>(mut self, to: T) -> Self {
+        self.deployer.tx.set_to(to.into());
+        self
+    }
+
+    /// Sets the `gas` field in the deploy transaction to the provided value
+    pub fn gas<T: Into<U256>>(mut self, gas: T) -> Self {
+        self.deployer.tx.set_gas(gas.into());
+        self
+    }
+
+    /// Sets the `gas_price` field in the deploy transaction to the provided value
+    pub fn gas_price<T: Into<U256>>(mut self, gas_price: T) -> Self {
+        self.deployer.tx.set_gas_price(gas_price.into());
+        self
+    }
+
+    /// Sets the `value` field in the deploy transaction to the provided value
+    pub fn value<T: Into<U256>>(mut self, value: T) -> Self {
+        self.deployer.tx.set_value(value.into());
+        self
+    }
+
+    /// Sets the `data` field in the deploy transaction to the provided value
+    pub fn data<T: Into<Bytes>>(mut self, data: T) -> Self {
+        self.deployer.tx.set_data(data.into());
+        self
+    }
+
+    /// Sets the `nonce` field in the deploy transaction to the provided value
+    pub fn nonce<T: Into<U256>>(mut self, nonce: T) -> Self {
+        self.deployer.tx.set_nonce(nonce.into());
+        self
+    }
+
+    /// Sets the `chain_id` field in the deploy transaction to the provided value
+    pub fn chain_id<T: Into<U64>>(mut self, chain_id: T) -> Self {
+        self.deployer.tx.set_chain_id(chain_id.into());
+        self
+    }
+
     /// Dry runs the deployment of the contract
     ///
     /// Note: this function _does not_ send a transaction from your account
diff --git a/ethers-contract/src/log.rs b/ethers-contract/src/log.rs
index 6e54e381..d0318424 100644
--- a/ethers-contract/src/log.rs
+++ b/ethers-contract/src/log.rs
@@ -19,7 +19,7 @@ pub fn decode_logs<T: EthLogDecode>(logs: &[RawLog]) -> Result<Vec<T>, Error> {
 }
 
 /// Metadata inside a log
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct LogMeta {
     /// Address from which this log originated
     pub address: Address,
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 8bdafe87..bc1af2fc 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -26,10 +26,10 @@ tiny-keccak = { version = "2.0.2", default-features = false }
 chrono = { version = "0.4", default-features = false }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
-thiserror = { version = "1.0.31", default-features = false }
+thiserror = { version = "1.0", default-features = false }
 bytes = { version = "1.1.0", features = ["serde"] }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
-once_cell = { version = "1.10.0", optional = true }
+once_cell = { version = "1.11.0", optional = true }
 unicode-xid = "0.2.3"
 strum = { version = "0.24", features = ["derive"] }
 
@@ -44,7 +44,7 @@ proc-macro2 = { version = "1.0.39", optional = true }
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
 bincode = { version = "1.3.3", default-features = false }
-once_cell = { version = "1.10.0" }
+once_cell = { version = "1.11.0" }
 hex-literal = "0.3.4"
 rand = "0.8.5"
 
diff --git a/ethers-core/src/abi/human_readable/lexer.rs b/ethers-core/src/abi/human_readable/lexer.rs
index 907f39e3..216f369d 100644
--- a/ethers-core/src/abi/human_readable/lexer.rs
+++ b/ethers-core/src/abi/human_readable/lexer.rs
@@ -10,7 +10,7 @@ macro_rules! unrecognised {
     };
 }
 
-#[derive(Copy, Clone, PartialEq, Debug)]
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
 pub enum Token<'input> {
     Identifier(&'input str),
     Number(&'input str),
@@ -140,7 +140,7 @@ impl<'input> fmt::Display for Token<'input> {
     }
 }
 
-#[derive(Debug, PartialEq, Clone, thiserror::Error)]
+#[derive(Debug, PartialEq, Eq, Clone, thiserror::Error)]
 pub enum LexerError {
     #[error("UnrecognisedToken {0}:{1} `{2}`")]
     UnrecognisedToken(usize, usize, String),
diff --git a/ethers-core/src/abi/struct_def.rs b/ethers-core/src/abi/struct_def.rs
index d9545aca..b4bf661d 100644
--- a/ethers-core/src/abi/struct_def.rs
+++ b/ethers-core/src/abi/struct_def.rs
@@ -74,7 +74,7 @@ pub struct StructFieldDeclaration {
 }
 
 /// How the type of a struct field is referenced
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub struct StructType {
     /// The name of the struct (or rather the name of the rust type)
     name: String,
diff --git a/ethers-core/src/types/block.rs b/ethers-core/src/types/block.rs
index 4a24de83..75beac62 100644
--- a/ethers-core/src/types/block.rs
+++ b/ethers-core/src/types/block.rs
@@ -379,7 +379,7 @@ impl From<Block<Transaction>> for Block<TxHash> {
     }
 }
 
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[cfg(feature = "celo")]
 /// Commit-reveal data for generating randomness in the
 /// [Celo protocol](https://docs.celo.org/celo-codebase/protocol/identity/randomness)
@@ -390,7 +390,7 @@ pub struct Randomness {
     pub revealed: Bytes,
 }
 
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[cfg(feature = "celo")]
 /// SNARK-friendly epoch block signature and bitmap
 pub struct EpochSnarkData {
@@ -400,7 +400,7 @@ pub struct EpochSnarkData {
     pub signature: Bytes,
 }
 
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 /// A Block Hash or Block Number
 pub enum BlockId {
     // TODO: May want to expand this to include the requireCanonical field
diff --git a/ethers-core/src/types/log.rs b/ethers-core/src/types/log.rs
index 89758434..17ce8c74 100644
--- a/ethers-core/src/types/log.rs
+++ b/ethers-core/src/types/log.rs
@@ -9,7 +9,7 @@ use serde::{
 use std::ops::{Range, RangeFrom, RangeTo};
 
 /// A log produced by a transaction.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
 pub struct Log {
     /// H160. the contract that emitted the log
     pub address: Address,
@@ -74,7 +74,7 @@ impl rlp::Encodable for Log {
     }
 }
 
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum FilterBlockOption {
     Range { from_block: Option<BlockNumber>, to_block: Option<BlockNumber> },
     AtBlockHash(H256),
@@ -157,7 +157,7 @@ impl FilterBlockOption {
 }
 
 /// Filter for
-#[derive(Default, Debug, PartialEq, Clone)]
+#[derive(Default, Debug, PartialEq, Eq, Clone)]
 pub struct Filter {
     /// Filter block options, specifying on which blocks the filter should
     /// match.
@@ -471,7 +471,7 @@ mod tests {
 
     #[test]
     fn can_serde_value_or_array() {
-        #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
+        #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
         struct Item {
             value: ValueOrArray<U256>,
         }
diff --git a/ethers-core/src/types/proof.rs b/ethers-core/src/types/proof.rs
index 7ed2277e..011c3576 100644
--- a/ethers-core/src/types/proof.rs
+++ b/ethers-core/src/types/proof.rs
@@ -1,14 +1,14 @@
 use crate::types::{Address, Bytes, H256, U256};
 use serde::{Deserialize, Serialize};
 
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub struct StorageProof {
     pub key: H256,
     pub proof: Vec<Bytes>,
     pub value: U256,
 }
 
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct EIP1186ProofResponse {
     address: Address,
diff --git a/ethers-core/src/types/signature.rs b/ethers-core/src/types/signature.rs
index 956695c1..5cddf4e7 100644
--- a/ethers-core/src/types/signature.rs
+++ b/ethers-core/src/types/signature.rs
@@ -45,7 +45,7 @@ pub enum SignatureError {
 /// The message data can either be a binary message that is first hashed
 /// according to EIP-191 and then recovered based on the signature or a
 /// precomputed hash.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub enum RecoveryMessage {
     /// Message bytes
     Data(Vec<u8>),
diff --git a/ethers-core/src/types/trace/filter.rs b/ethers-core/src/types/trace/filter.rs
index c69cb2cd..f1a2d22c 100644
--- a/ethers-core/src/types/trace/filter.rs
+++ b/ethers-core/src/types/trace/filter.rs
@@ -3,7 +3,7 @@ use crate::types::{Address, BlockNumber, Bytes, H160, H256, U256};
 use serde::{Deserialize, Serialize};
 
 /// Trace filter
-#[derive(Debug, Default, Clone, PartialEq, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
 #[serde(deny_unknown_fields)]
 pub struct TraceFilter {
     /// From block
@@ -76,7 +76,7 @@ impl TraceFilter {
 
 // `LocalizedTrace` in Parity
 /// Trace-Filtering API trace type
-#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
 pub struct Trace {
     /// Action
     pub action: Action,
@@ -107,7 +107,7 @@ pub struct Trace {
 }
 
 /// Response
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[serde(untagged)]
 pub enum Res {
     /// Call
@@ -125,7 +125,7 @@ impl Default for Res {
 }
 
 /// Action
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
 #[serde(untagged, rename_all = "lowercase")]
 pub enum Action {
     /// Call
@@ -139,7 +139,7 @@ pub enum Action {
 }
 
 /// An external action type.
-#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
+#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "lowercase")]
 pub enum ActionType {
     /// Contract call.
@@ -153,7 +153,7 @@ pub enum ActionType {
 }
 
 /// Call Result
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 pub struct CallResult {
     /// Gas used
     #[serde(rename = "gasUsed")]
@@ -163,7 +163,7 @@ pub struct CallResult {
 }
 
 /// Create Result
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 pub struct CreateResult {
     /// Gas used
     #[serde(rename = "gasUsed")]
@@ -175,7 +175,7 @@ pub struct CreateResult {
 }
 
 /// Call response
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 pub struct Call {
     /// Sender
     pub from: Address,
@@ -193,7 +193,7 @@ pub struct Call {
 }
 
 /// Call type.
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub enum CallType {
     /// None
     #[serde(rename = "none")]
@@ -219,7 +219,7 @@ impl Default for CallType {
 }
 
 /// Create response
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 pub struct Create {
     /// Sender
     pub from: Address,
@@ -232,7 +232,7 @@ pub struct Create {
 }
 
 /// Suicide
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 pub struct Suicide {
     /// Address.
     pub address: Address,
@@ -244,7 +244,7 @@ pub struct Suicide {
 }
 
 /// Reward action
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub struct Reward {
     /// Author's address.
     pub author: Address,
@@ -256,7 +256,7 @@ pub struct Reward {
 }
 
 /// Reward type.
-#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub enum RewardType {
     /// Block
     #[serde(rename = "block")]
diff --git a/ethers-core/src/types/trace/mod.rs b/ethers-core/src/types/trace/mod.rs
index 3eceb6a7..26736ce8 100644
--- a/ethers-core/src/types/trace/mod.rs
+++ b/ethers-core/src/types/trace/mod.rs
@@ -86,7 +86,7 @@ pub struct StateDiff(pub BTreeMap<H160, AccountDiff>);
 
 // ------------------ Trace -------------
 /// Trace
-#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
+#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
 pub struct TransactionTrace {
     /// Trace address
     #[serde(rename = "traceAddress")]
@@ -130,7 +130,7 @@ pub struct VMOperation {
     pub sub: Option<VMTrace>,
 }
 
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 #[allow(clippy::upper_case_acronyms)]
 /// A record of an executed VM operation.
 pub struct VMExecutedOperation {
@@ -147,7 +147,7 @@ pub struct VMExecutedOperation {
     pub store: Option<StorageDiff>,
 }
 
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 #[allow(clippy::upper_case_acronyms)]
 /// A diff of some chunk of memory.
 pub struct MemoryDiff {
@@ -157,7 +157,7 @@ pub struct MemoryDiff {
     pub data: Bytes,
 }
 
-#[derive(Debug, Clone, PartialEq, Default, Deserialize, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Deserialize, Serialize)]
 #[allow(clippy::upper_case_acronyms)]
 /// A diff of some storage value.
 pub struct StorageDiff {
diff --git a/ethers-core/src/types/transaction/eip2718.rs b/ethers-core/src/types/transaction/eip2718.rs
index 56fcfafa..b87f0abf 100644
--- a/ethers-core/src/types/transaction/eip2718.rs
+++ b/ethers-core/src/types/transaction/eip2718.rs
@@ -24,7 +24,7 @@ use thiserror::Error;
 /// the `legacy` crate feature. This will disable the `type` flag in the
 /// serialized transaction, and cause contract calls and other common actions
 /// to default to using the legacy transaction type.
-#[derive(Serialize, Deserialize, Clone, PartialEq, Debug)]
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug)]
 #[cfg_attr(not(feature = "legacy"), serde(tag = "type"))]
 #[cfg_attr(feature = "legacy", serde(untagged))]
 pub enum TypedTransaction {
@@ -742,7 +742,7 @@ mod tests {
         let tx = TypedTransaction::decode(&tx_rlp).unwrap();
 
         {
-            let typed_tx: TypedTransaction = tx.clone().into();
+            let typed_tx: TypedTransaction = tx.clone();
 
             let tx0: TransactionRequest = typed_tx.clone().into();
             assert!(typed_tx.as_legacy_ref().is_none());
@@ -752,7 +752,7 @@ mod tests {
             assert_eq!(tx0, tx1);
         }
         {
-            let typed_tx: TypedTransaction = tx.clone().into();
+            let typed_tx: TypedTransaction = tx.clone();
             let tx0: Eip1559TransactionRequest = typed_tx.clone().into();
             assert_eq!(tx.as_eip1559_ref().unwrap(), &tx0);
 
@@ -761,7 +761,7 @@ mod tests {
             assert_eq!(tx0, tx1);
         }
         {
-            let typed_tx: TypedTransaction = tx.clone().into();
+            let typed_tx: TypedTransaction = tx;
             let tx0: Eip2930TransactionRequest = typed_tx.clone().into();
             assert!(typed_tx.as_eip2930_ref().is_none());
 
diff --git a/ethers-core/src/types/transaction/eip2930.rs b/ethers-core/src/types/transaction/eip2930.rs
index 3defdad6..1599072d 100644
--- a/ethers-core/src/types/transaction/eip2930.rs
+++ b/ethers-core/src/types/transaction/eip2930.rs
@@ -73,7 +73,7 @@ pub enum Eip2930RequestError {
 }
 
 /// An EIP-2930 transaction is a legacy transaction including an [`AccessList`].
-#[derive(Clone, Serialize, Deserialize, PartialEq, Debug)]
+#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Debug)]
 pub struct Eip2930TransactionRequest {
     #[serde(flatten)]
     pub tx: TransactionRequest,
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index b30e89b3..056dbdfd 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -11,7 +11,7 @@ use rlp::{Decodable, DecoderError, RlpStream};
 use serde::{Deserialize, Serialize};
 
 /// Details of a signed transaction
-#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
 pub struct Transaction {
     /// The transaction's hash
     pub hash: H256,
@@ -370,7 +370,7 @@ impl Decodable for Transaction {
 }
 
 /// "Receipt" of an executed transaction: details of its execution.
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct TransactionReceipt {
     /// Transaction hash.
     #[serde(rename = "transactionHash")]
diff --git a/ethers-core/src/types/txpool.rs b/ethers-core/src/types/txpool.rs
index 146a5dae..5f6ce423 100644
--- a/ethers-core/src/types/txpool.rs
+++ b/ethers-core/src/types/txpool.rs
@@ -7,7 +7,7 @@ use serde::{
 use std::{collections::BTreeMap, fmt, str::FromStr};
 
 /// Transaction summary as found in the Txpool Inspection property.
-#[derive(Debug, Clone, PartialEq, Serialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
 pub struct TxpoolInspectSummary {
     /// Recipient (None when contract creation)
     pub to: Option<Address>,
@@ -90,7 +90,7 @@ impl<'de> Deserialize<'de> for TxpoolInspectSummary {
 /// as the ones that are being scheduled for future execution only.
 ///
 /// See [here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content) for more details
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub struct TxpoolContent {
     /// pending tx
     pub pending: BTreeMap<Address, BTreeMap<String, TxpoolTransaction>>,
@@ -107,7 +107,7 @@ pub struct TxpoolContent {
 /// transactions in the pool and find any potential issues.
 ///
 /// See [here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_inspect) for more details
-#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
 pub struct TxpoolInspect {
     /// pending tx
     pub pending: BTreeMap<Address, BTreeMap<String, TxpoolInspectSummary>>,
@@ -122,7 +122,7 @@ pub struct TxpoolInspect {
 /// are being scheduled for future execution only.
 ///
 /// See [here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_status) for more details
-#[derive(Debug, Default, Clone, PartialEq, Deserialize, Serialize)]
+#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 pub struct TxpoolStatus {
     /// number of pending tx
     pub pending: U64,
diff --git a/ethers-etherscan/Cargo.toml b/ethers-etherscan/Cargo.toml
index e5ee05d0..32611707 100644
--- a/ethers-etherscan/Cargo.toml
+++ b/ethers-etherscan/Cargo.toml
@@ -20,7 +20,7 @@ reqwest = { version = "0.11.10", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false }
 serde-aux = { version = "3.0.1", default-features = false }
-thiserror = "1.0.31"
+thiserror = "1.0"
 tracing = "0.1.34"
 semver = "1.0.9"
 
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 8da735d7..1c3081a2 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -22,7 +22,7 @@ ethers-signers = { version = "^0.6.0", path = "../ethers-signers", default-featu
 
 async-trait = { version = "0.1.50", default-features = false }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
-thiserror = { version = "1.0.31", default-features = false }
+thiserror = { version = "1.0", default-features = false }
 futures-util = { version = "^0.3" }
 futures-locks = { version = "0.7" }
 tracing = { version = "0.1.34", default-features = false }
@@ -42,7 +42,7 @@ tokio = { version = "1.5" }
 hex = { version = "0.4.3", default-features = false, features = ["std"] }
 rand = { version = "0.8.5", default-features = false }
 ethers-providers = { version = "^0.6.0", path = "../ethers-providers", default-features = false, features = ["ws", "rustls"] }
-once_cell = "1.10.0"
+once_cell = "1.11.0"
 ethers-solc = { version = "^0.3.0", path = "../ethers-solc", default-features = false }
 serial_test = "0.6.0"
 
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index abbb3fa7..22ba4046 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -21,7 +21,7 @@ hex = { version = "0.4.3", default-features = false, features = ["std"] }
 reqwest = { version = "0.11.10", default-features = false, features = ["json"] }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 serde_json = { version = "1.0.64", default-features = false, features = ["raw_value"] }
-thiserror = { version = "1.0.31", default-features = false }
+thiserror = { version = "1.0", default-features = false }
 url = { version = "2.2.2", default-features = false }
 auto_impl = { version = "0.5.0", default-features = false }
 http = { version = "0.2" }
@@ -39,7 +39,7 @@ tracing = { version = "0.1.34", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
 
 bytes = { version  = "1.1.0", default-features = false, optional = true }
-once_cell = "1.10.0"
+once_cell = "1.11.0"
 hashers = "1.0.1"
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
diff --git a/ethers-providers/src/erc.rs b/ethers-providers/src/erc.rs
index 22e2b624..b139f860 100644
--- a/ethers-providers/src/erc.rs
+++ b/ethers-providers/src/erc.rs
@@ -54,7 +54,7 @@ impl FromStr for ERCNFT {
 }
 
 /// Supported ERCs
-#[derive(PartialEq)]
+#[derive(PartialEq, Eq)]
 pub enum ERCNFTType {
     ERC721,
     ERC1155,
diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs
index 99e4a406..b1272bf8 100644
--- a/ethers-providers/src/transports/ws.rs
+++ b/ethers-providers/src/transports/ws.rs
@@ -401,8 +401,9 @@ where
             // Handle ws messages
             resp = self.ws.next() => match resp {
                 Some(Ok(resp)) => self.handle(resp).await?,
-                // TODO: Log the error?
-                Some(Err(_)) => {},
+                Some(Err(_)) => {
+                    return Err(ClientError::UnexpectedClose);
+                }
                 None => {
                     return Err(ClientError::UnexpectedClose);
                 },
diff --git a/ethers-signers/src/wallet/mnemonic.rs b/ethers-signers/src/wallet/mnemonic.rs
index f9329b2a..60b559b8 100644
--- a/ethers-signers/src/wallet/mnemonic.rs
+++ b/ethers-signers/src/wallet/mnemonic.rs
@@ -16,7 +16,7 @@ use thiserror::Error;
 const DEFAULT_DERIVATION_PATH_PREFIX: &str = "m/44'/60'/0'/0/";
 
 /// Represents a structure that can resolve into a `Wallet<SigningKey>`.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
 pub struct MnemonicBuilder<W: Wordlist> {
     /// The mnemonic phrase can be supplied to the builder as a string or a path to the file whose
     /// contents are the phrase. A builder that has a valid phrase should `build` the wallet.
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index 45350789..4182f9b7 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -21,10 +21,10 @@ semver = { version = "1.0.9", features = ["serde"] }
 walkdir = "2.3.2"
 tokio = { version = "1.15.0", default-features = false, features = ["rt"] }
 futures-util = { version = "^0.3", optional = true }
-once_cell = "1.10.0"
-regex = "1.5.5"
+once_cell = "1.11.0"
+regex = "1.5.6"
 md-5 = "0.10.1"
-thiserror = "1.0.31"
+thiserror = "1.0"
 hex = "0.4.3"
 colored = "2.0.0"
 glob = "0.3.0"
diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index ea373cf7..c3dea9ea 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -11,6 +11,7 @@ use crate::{
         Ast, CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
         GeneratedSource, LosslessAbi, Metadata, Offsets, Settings, StorageLayout, UserDoc,
     },
+    sources::VersionedSourceFile,
     ArtifactOutput, SolcConfig, SolcError, SourceFile,
 };
 use serde::{Deserialize, Serialize};
@@ -329,6 +330,18 @@ impl ArtifactOutput for ConfigurableArtifacts {
             generated_sources: generated_sources.unwrap_or_default(),
         }
     }
+
+    fn standalone_source_file_to_artifact(
+        &self,
+        _path: &str,
+        file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact> {
+        file.source_file.ast.clone().map(|ast| ConfigurableContractArtifact {
+            id: Some(file.source_file.id),
+            ast: Some(ast),
+            ..Default::default()
+        })
+    }
 }
 
 /// Determines the additional values to include in the contract's artifact file
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index fa736bc3..d73c8f22 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -9,7 +9,7 @@ use semver::Version;
 use serde::{de::DeserializeOwned, Serialize};
 use std::{
     borrow::Cow,
-    collections::btree_map::BTreeMap,
+    collections::{btree_map::BTreeMap, HashSet},
     fmt, fs, io,
     path::{Path, PathBuf},
 };
@@ -23,6 +23,7 @@ use crate::{
     },
     compile::output::{contracts::VersionedContracts, sources::VersionedSourceFiles},
     sourcemap::{SourceMap, SyntaxError},
+    sources::VersionedSourceFile,
 };
 pub use configurable::*;
 
@@ -632,14 +633,25 @@ pub trait ArtifactOutput {
         sources: &VersionedSourceFiles,
     ) -> Artifacts<Self::Artifact> {
         let mut artifacts = ArtifactsMap::new();
+
+        // this tracks all the `SourceFile`s that we successfully mapped to a contract
+        let mut non_standalone_sources = HashSet::new();
+
+        // loop over all files and their contracts
         for (file, contracts) in contracts.as_ref().iter() {
             let mut entries = BTreeMap::new();
+
+            // loop over all contracts and their versions
             for (name, versioned_contracts) in contracts {
                 let mut contracts = Vec::with_capacity(versioned_contracts.len());
                 // check if the same contract compiled with multiple solc versions
                 for contract in versioned_contracts {
                     let source_file = sources.find_file_and_version(file, &contract.version);
 
+                    if let Some(source) = source_file {
+                        non_standalone_sources.insert((source.id, &contract.version));
+                    }
+
                     let artifact_path = if versioned_contracts.len() > 1 {
                         Self::output_file_versioned(file, name, &contract.version)
                     } else {
@@ -664,8 +676,67 @@ pub trait ArtifactOutput {
             artifacts.insert(file.to_string(), entries);
         }
 
+        // extend with standalone source files and convert them to artifacts
+        for (file, sources) in sources.as_ref().iter() {
+            for source in sources {
+                if !non_standalone_sources.contains(&(source.source_file.id, &source.version)) {
+                    // scan the ast as a safe measure to ensure this file does not include any
+                    // source units
+                    // there's also no need to create a standalone artifact for source files that
+                    // don't contain an ast
+                    if source.source_file.contains_contract_definition() ||
+                        source.source_file.ast.is_none()
+                    {
+                        continue
+                    }
+
+                    // we use file and file stem
+                    if let Some(name) = Path::new(file).file_stem().and_then(|stem| stem.to_str()) {
+                        if let Some(artifact) =
+                            self.standalone_source_file_to_artifact(file, source)
+                        {
+                            let artifact_path = if sources.len() > 1 {
+                                Self::output_file_versioned(file, name, &source.version)
+                            } else {
+                                Self::output_file(file, name)
+                            };
+
+                            let entries = artifacts
+                                .entry(file.to_string())
+                                .or_default()
+                                .entry(name.to_string())
+                                .or_default();
+
+                            if entries.iter().all(|entry| entry.version != source.version) {
+                                entries.push(ArtifactFile {
+                                    artifact,
+                                    file: artifact_path,
+                                    version: source.version.clone(),
+                                });
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
         Artifacts(artifacts)
     }
+
+    /// This converts a `SourceFile` that doesn't contain _any_ contract definitions (interfaces,
+    /// contracts, libraries) to an artifact.
+    ///
+    /// We do this because not all `SourceFile`s emitted by solc have at least 1 corresponding entry
+    /// in the `contracts`
+    /// section of the solc output. For example for an `errors.sol` that only contains custom error
+    /// definitions and no contract, no `Contract` object will be generated by solc. However, we
+    /// still want to emit an `Artifact` for that file that may include the `ast`, docs etc.,
+    /// because other tools depend on this, such as slither.
+    fn standalone_source_file_to_artifact(
+        &self,
+        _path: &str,
+        _file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact>;
 }
 
 /// An `Artifact` implementation that uses a compact representation
@@ -695,6 +766,14 @@ impl ArtifactOutput for MinimalCombinedArtifacts {
     ) -> Self::Artifact {
         Self::Artifact::from(contract)
     }
+
+    fn standalone_source_file_to_artifact(
+        &self,
+        _path: &str,
+        _file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact> {
+        None
+    }
 }
 
 /// An Artifacts handler implementation that works the same as `MinimalCombinedArtifacts` but also
@@ -739,6 +818,14 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
     ) -> Self::Artifact {
         MinimalCombinedArtifacts::default().contract_to_artifact(file, name, contract, source_file)
     }
+
+    fn standalone_source_file_to_artifact(
+        &self,
+        path: &str,
+        file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact> {
+        MinimalCombinedArtifacts::default().standalone_source_file_to_artifact(path, file)
+    }
 }
 
 #[cfg(test)]
diff --git a/ethers-solc/src/artifacts/mod.rs b/ethers-solc/src/artifacts/mod.rs
index 1547313f..ca31c1da 100644
--- a/ethers-solc/src/artifacts/mod.rs
+++ b/ethers-solc/src/artifacts/mod.rs
@@ -1634,6 +1634,22 @@ pub struct SourceFile {
     pub ast: Option<Ast>,
 }
 
+// === impl SourceFile ===
+
+impl SourceFile {
+    /// Returns `true` if the source file contains at least 1 `ContractDefinition` such as
+    /// `contract`, `abstract contract`, `interface` or `library`
+    pub fn contains_contract_definition(&self) -> bool {
+        if let Some(ref ast) = self.ast {
+            // contract definitions are only allowed at the source-unit level <https://docs.soliditylang.org/en/latest/grammar.html>
+            return ast.nodes.iter().any(|node| node.node_type == NodeType::ContractDefinition)
+            // abstract contract, interfaces: ContractDefinition
+        }
+
+        false
+    }
+}
+
 /// A wrapper type for a list of source files
 /// `path -> SourceFile`
 #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
diff --git a/ethers-solc/src/compile/output/sources.rs b/ethers-solc/src/compile/output/sources.rs
index 4a9a556e..b1f52a53 100644
--- a/ethers-solc/src/compile/output/sources.rs
+++ b/ethers-solc/src/compile/output/sources.rs
@@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize};
 use std::{collections::BTreeMap, path::Path};
 
 /// (source_file path  -> `SourceFile` + solc version)
-#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
 #[serde(transparent)]
 pub struct VersionedSourceFiles(pub BTreeMap<String, Vec<VersionedSourceFile>>);
 
@@ -277,7 +277,7 @@ impl IntoIterator for VersionedSourceFiles {
 }
 
 /// A [SourceFile] and the compiler version used to compile it
-#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
 pub struct VersionedSourceFile {
     pub source_file: SourceFile,
     pub version: Version,
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index 22126ca6..59b6774e 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -265,23 +265,25 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
     fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
         let CompiledState { output, cache } = self;
 
-        // write all artifacts via the handler but only if the build succeeded
-        let compiled_artifacts = if cache.project().no_artifacts {
-            cache
-                .project()
-                .artifacts_handler()
-                .output_to_artifacts(&output.contracts, &output.sources)
+        let project = cache.project();
+        // write all artifacts via the handler but only if the build succeeded and project wasn't
+        // configured with `no_artifacts == true`
+        let compiled_artifacts = if project.no_artifacts {
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
         } else if output.has_error() {
             tracing::trace!("skip writing cache file due to solc errors: {:?}", output.errors);
-            cache
-                .project()
-                .artifacts_handler()
-                .output_to_artifacts(&output.contracts, &output.sources)
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
         } else {
-            cache.project().artifacts_handler().on_output(
+            tracing::trace!(
+                "handling artifact output for {} contracts and {} sources",
+                output.contracts.len(),
+                output.sources.len()
+            );
+            // this emits the artifacts via the project's artifacts handler
+            project.artifacts_handler().on_output(
                 &output.contracts,
                 &output.sources,
-                &cache.project().paths,
+                &project.paths,
             )?
         };
 
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index 90307818..0311c286 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -311,41 +311,40 @@ impl ProjectPathsConfig {
         let mut content = content.as_bytes().to_vec();
         let mut offset = 0_isize;
 
-        if strip_license {
-            if let Some(license) = target_node.license() {
-                let license_range = license.loc_by_offset(offset);
-                offset -= license_range.len() as isize;
-                content.splice(license_range, std::iter::empty());
+        let mut statements = [
+            (target_node.license(), strip_license),
+            (target_node.version(), strip_version_pragma),
+            (target_node.experimental(), strip_experimental_pragma),
+        ]
+        .iter()
+        .filter_map(|(data, condition)| if *condition { data.to_owned().as_ref() } else { None })
+        .collect::<Vec<_>>();
+        statements.sort_by_key(|x| x.loc().start);
+
+        let (mut imports, mut statements) =
+            (imports.iter().peekable(), statements.iter().peekable());
+        while imports.peek().is_some() || statements.peek().is_some() {
+            let (next_import_start, next_statement_start) = (
+                imports.peek().map_or(usize::max_value(), |x| x.loc().start),
+                statements.peek().map_or(usize::max_value(), |x| x.loc().start),
+            );
+            if next_statement_start < next_import_start {
+                let repl_range = statements.next().unwrap().loc_by_offset(offset);
+                offset -= repl_range.len() as isize;
+                content.splice(repl_range, std::iter::empty());
+            } else {
+                let import = imports.next().unwrap();
+                let import_path = self.resolve_import(target_dir, import.data().path())?;
+                let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
+
+                let import_content = s.as_bytes();
+                let import_content_len = import_content.len() as isize;
+                let import_range = import.loc_by_offset(offset);
+                offset += import_content_len - (import_range.len() as isize);
+                content.splice(import_range, import_content.iter().copied());
             }
         }
 
-        if strip_version_pragma {
-            if let Some(version) = target_node.version() {
-                let version_range = version.loc_by_offset(offset);
-                offset -= version_range.len() as isize;
-                content.splice(version_range, std::iter::empty());
-            }
-        }
-
-        if strip_experimental_pragma {
-            if let Some(experiment) = target_node.experimental() {
-                let experimental_pragma_range = experiment.loc_by_offset(offset);
-                offset -= experimental_pragma_range.len() as isize;
-                content.splice(experimental_pragma_range, std::iter::empty());
-            }
-        }
-
-        for import in imports.iter() {
-            let import_path = self.resolve_import(target_dir, import.data().path())?;
-            let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
-
-            let import_content = s.as_bytes();
-            let import_content_len = import_content.len() as isize;
-            let import_range = import.loc_by_offset(offset);
-            offset += import_content_len - (import_range.len() as isize);
-            content.splice(import_range, import_content.iter().copied());
-        }
-
         let result = String::from_utf8(content).map_err(|err| {
             SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
         })?;
diff --git a/ethers-solc/src/hh.rs b/ethers-solc/src/hh.rs
index c71fbeb1..a67fb2f2 100644
--- a/ethers-solc/src/hh.rs
+++ b/ethers-solc/src/hh.rs
@@ -6,7 +6,7 @@ use crate::{
         contract::{CompactContract, CompactContractBytecode, Contract, ContractBytecode},
         CompactContractBytecodeCow, LosslessAbi, Offsets,
     },
-    ArtifactOutput, SourceFile,
+    ArtifactOutput, SourceFile, VersionedSourceFile,
 };
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, collections::btree_map::BTreeMap};
@@ -135,6 +135,14 @@ impl ArtifactOutput for HardhatArtifacts {
             deployed_link_references,
         }
     }
+
+    fn standalone_source_file_to_artifact(
+        &self,
+        _path: &str,
+        _file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact> {
+        None
+    }
 }
 
 #[cfg(test)]
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index ac0e8827..bf6c41fc 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -36,7 +36,7 @@ use crate::{
     artifacts::Sources,
     cache::SolFilesCache,
     error::{SolcError, SolcIoError},
-    sources::VersionedSourceFiles,
+    sources::{VersionedSourceFile, VersionedSourceFiles},
 };
 use artifacts::contract::Contract;
 use compile::output::contracts::VersionedContracts;
@@ -830,6 +830,14 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
     ) -> Artifacts<Self::Artifact> {
         self.artifacts_handler().output_to_artifacts(contracts, sources)
     }
+
+    fn standalone_source_file_to_artifact(
+        &self,
+        path: &str,
+        file: &VersionedSourceFile,
+    ) -> Option<Self::Artifact> {
+        self.artifacts_handler().standalone_source_file_to_artifact(path, file)
+    }
 }
 
 #[cfg(test)]
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index 9ca337d5..e8902673 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -57,7 +57,7 @@ impl Remapping {
     }
 }
 
-#[derive(thiserror::Error, Debug, PartialEq, PartialOrd)]
+#[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd)]
 pub enum RemappingError {
     #[error("no prefix found")]
     NoPrefix,
diff --git a/ethers-solc/src/report/mod.rs b/ethers-solc/src/report/mod.rs
index e87a586c..4d33285f 100644
--- a/ethers-solc/src/report/mod.rs
+++ b/ethers-solc/src/report/mod.rs
@@ -330,8 +330,16 @@ pub struct NoReporter(());
 impl Reporter for NoReporter {}
 
 /// A [`Reporter`] that emits some general information to `stdout`
-#[derive(Copy, Clone, Debug, Default)]
-pub struct BasicStdoutReporter(());
+#[derive(Clone, Debug)]
+pub struct BasicStdoutReporter {
+    solc_io_report: SolcCompilerIoReporter,
+}
+
+impl Default for BasicStdoutReporter {
+    fn default() -> Self {
+        Self { solc_io_report: SolcCompilerIoReporter::from_default_env() }
+    }
+}
 
 impl Reporter for BasicStdoutReporter {
     /// Callback invoked right before [`Solc::compile()`] is called
@@ -339,9 +347,10 @@ impl Reporter for BasicStdoutReporter {
         &self,
         _solc: &Solc,
         version: &Version,
-        _input: &CompilerInput,
+        input: &CompilerInput,
         dirty_files: &[PathBuf],
     ) {
+        self.solc_io_report.log_compiler_input(input, version);
         println!(
             "Compiling {} files with {}.{}.{}",
             dirty_files.len(),
@@ -351,6 +360,20 @@ impl Reporter for BasicStdoutReporter {
         );
     }
 
+    fn on_solc_success(
+        &self,
+        _solc: &Solc,
+        version: &Version,
+        output: &CompilerOutput,
+        duration: &Duration,
+    ) {
+        self.solc_io_report.log_compiler_output(output, version);
+        println!(
+            "Solc {}.{}.{} finished in {:.2?}",
+            version.major, version.minor, version.patch, duration
+        );
+    }
+
     /// Invoked before a new [`Solc`] bin is installed
     fn on_solc_installation_start(&self, version: &Version) {
         println!("installing solc version \"{}\"", version);
diff --git a/ethers-solc/src/resolver/parse.rs b/ethers-solc/src/resolver/parse.rs
index 642b5104..94436525 100644
--- a/ethers-solc/src/resolver/parse.rs
+++ b/ethers-solc/src/resolver/parse.rs
@@ -144,7 +144,7 @@ pub struct SolImport {
     aliases: Vec<SolImportAlias>,
 }
 
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Clone, PartialEq, Eq)]
 pub enum SolImportAlias {
     File(String),
     Contract(String, String),
diff --git a/ethers-solc/src/sourcemap.rs b/ethers-solc/src/sourcemap.rs
index e1cf8a62..bc77f62e 100644
--- a/ethers-solc/src/sourcemap.rs
+++ b/ethers-solc/src/sourcemap.rs
@@ -22,7 +22,7 @@ impl SyntaxError {
     }
 }
 
-#[derive(PartialEq)]
+#[derive(PartialEq, Eq)]
 enum Token<'a> {
     Number(&'a str),
     Semicolon,


Test patch:
diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs
index 95fe53c3..e60f563f 100644
--- a/ethers-contract/tests/common/derive.rs
+++ b/ethers-contract/tests/common/derive.rs
@@ -9,7 +9,7 @@ use ethers_core::{
 fn assert_tokenizeable<T: Tokenizable>() {}
 fn assert_ethcall<T: EthCall>() {}
 
-#[derive(Debug, Clone, PartialEq, EthAbiType)]
+#[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
 struct ValueChanged {
     old_author: Address,
     new_author: Address,
@@ -23,7 +23,7 @@ struct ValueChangedWrapper {
     msg: String,
 }
 
-#[derive(Debug, Clone, PartialEq, EthAbiType)]
+#[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
 struct ValueChangedTuple(Address, Address, String, String);
 
 #[derive(Debug, Clone, PartialEq, EthAbiType)]
@@ -49,13 +49,13 @@ fn can_detokenize_struct() {
 
 #[test]
 fn can_derive_abi_type_empty_struct() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     struct Call();
 
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     struct Call2 {};
 
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     struct Call3;
 
     assert_tokenizeable::<Call>();
@@ -130,7 +130,7 @@ fn can_detokenize_single_field() {
 
 #[test]
 fn can_derive_eth_event() {
-    #[derive(Debug, Clone, PartialEq, EthEvent)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthEvent)]
     struct ValueChangedEvent {
         old_author: Address,
         new_author: Address,
@@ -157,7 +157,7 @@ fn can_derive_eth_event() {
 
 #[test]
 fn can_set_eth_event_name_attribute() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     #[ethevent(name = "MyEvent")]
     struct ValueChangedEvent {
         old_author: Address,
@@ -172,7 +172,7 @@ fn can_set_eth_event_name_attribute() {
 
 #[test]
 fn can_detect_various_event_abi_types() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     struct ValueChangedEvent {
         old_author: Address,
         s: String,
@@ -204,7 +204,7 @@ fn can_detect_various_event_abi_types() {
 
 #[test]
 fn can_set_eth_abi_attribute() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     struct SomeType {
         inner: Address,
         msg: String,
@@ -242,7 +242,7 @@ fn can_set_eth_abi_attribute() {
 
 #[test]
 fn can_derive_indexed_and_anonymous_attribute() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     #[ethevent(anonymous)]
     struct ValueChangedEvent {
         old_author: Address,
@@ -280,7 +280,7 @@ fn can_generate_ethevent_from_json() {
 
 #[test]
 fn can_decode_event_with_no_topics() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     pub struct LiquidateBorrow {
         liquidator: Address,
         borrower: Address,
@@ -310,7 +310,7 @@ fn can_decode_event_with_no_topics() {
 
 #[test]
 fn can_decode_event_single_param() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     pub struct OneParam {
         #[ethevent(indexed)]
         param1: U256,
@@ -330,7 +330,7 @@ fn can_decode_event_single_param() {
 
 #[test]
 fn can_decode_event_tuple_single_param() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     struct OneParam(#[ethevent(indexed)] U256);
 
     let log = RawLog {
@@ -347,7 +347,7 @@ fn can_decode_event_tuple_single_param() {
 
 #[test]
 fn can_decode_event_with_no_params() {
-    #[derive(Debug, PartialEq, EthEvent)]
+    #[derive(Debug, PartialEq, Eq, EthEvent)]
     pub struct NoParam {}
 
     let log = RawLog {
@@ -446,7 +446,7 @@ fn can_derive_ethcall() {
 
 #[test]
 fn can_derive_ethcall_with_nested_structs() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     struct SomeType {
         inner: Address,
         msg: String,
@@ -468,7 +468,7 @@ fn can_derive_ethcall_with_nested_structs() {
 
 #[test]
 fn can_derive_for_enum() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType)]
     enum ActionChoices {
         GoLeft,
         GoRight,
@@ -483,7 +483,7 @@ fn can_derive_for_enum() {
 
 #[test]
 fn can_derive_abi_codec() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType, EthAbiCodec)]
     pub struct SomeType {
         inner: Address,
         msg: String,
@@ -498,7 +498,7 @@ fn can_derive_abi_codec() {
 
 #[test]
 fn can_derive_abi_codec_single_field() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType, EthAbiCodec)]
     pub struct SomeType {
         inner: Vec<U256>,
     }
@@ -530,7 +530,7 @@ fn can_derive_abi_codec_single_field() {
 
 #[test]
 fn can_derive_abi_codec_two_field() {
-    #[derive(Debug, Clone, PartialEq, EthAbiType, EthAbiCodec)]
+    #[derive(Debug, Clone, PartialEq, Eq, EthAbiType, EthAbiCodec)]
     pub struct SomeType {
         inner: Vec<U256>,
         addr: Address,
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 55529d51..3a3ee65b 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -873,6 +873,71 @@ contract Contract is ParentContract,
     );
 }
 
+#[test]
+fn can_flatten_with_version_pragma_after_imports() {
+    let project = TempProject::dapptools().unwrap();
+
+    let f = project
+        .add_source(
+            "A",
+            r#"
+pragma solidity ^0.8.10;
+
+import * as B from "./B.sol";
+
+contract A { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "B",
+            r#"
+import D from "./D.sol";
+pragma solidity ^0.8.10;
+import * as C from "./C.sol";
+contract B { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "C",
+            r#"
+pragma solidity ^0.8.10;
+contract C { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "D",
+            r#"
+pragma solidity ^0.8.10;
+contract D { }
+"#,
+        )
+        .unwrap();
+
+    let result = project.flatten(&f).unwrap();
+    assert_eq!(
+        result,
+        r#"pragma solidity ^0.8.10;
+
+contract D { }
+
+contract C { }
+
+contract B { }
+
+contract A { }
+"#
+    );
+}
+
 #[test]
 fn can_detect_type_error() {
     let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
@@ -1239,6 +1304,160 @@ fn can_recompile_unchanged_with_empty_files() {
     assert!(compiled.find("C").is_some());
 }
 
+#[test]
+fn can_emit_empty_artifacts() {
+    let tmp = TempProject::dapptools().unwrap();
+
+    let top_level = tmp
+        .add_source(
+            "top_level",
+            r#"
+    function test() {}
+   "#,
+        )
+        .unwrap();
+
+    tmp.add_source(
+        "Contract",
+        r#"
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity 0.8.10;
+
+import "./top_level.sol";
+
+contract Contract {
+    function a() public{
+        test();
+    }
+}
+   "#,
+    )
+    .unwrap();
+
+    let compiled = tmp.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+    assert!(compiled.find("Contract").is_some());
+    assert!(compiled.find("top_level").is_some());
+    let mut artifacts = tmp.artifacts_snapshot().unwrap();
+
+    assert_eq!(artifacts.artifacts.as_ref().len(), 2);
+
+    let mut top_level =
+        artifacts.artifacts.as_mut().remove(top_level.to_string_lossy().as_ref()).unwrap();
+
+    assert_eq!(top_level.len(), 1);
+
+    let artifact = top_level.remove("top_level").unwrap().remove(0);
+    assert!(artifact.artifact.ast.is_some());
+
+    // recompile
+    let compiled = tmp.compile().unwrap();
+    assert!(compiled.is_unchanged());
+
+    // modify standalone file
+
+    tmp.add_source(
+        "top_level",
+        r#"
+    error MyError();
+    function test() {}
+   "#,
+    )
+    .unwrap();
+    let compiled = tmp.compile().unwrap();
+    assert!(!compiled.is_unchanged());
+}
+
+#[test]
+fn can_detect_contract_def_source_files() {
+    let tmp = TempProject::dapptools().unwrap();
+
+    let mylib = tmp
+        .add_source(
+            "MyLib",
+            r#"
+        pragma solidity 0.8.10;
+        library MyLib {
+        }
+   "#,
+        )
+        .unwrap();
+
+    let myinterface = tmp
+        .add_source(
+            "MyInterface",
+            r#"
+        pragma solidity 0.8.10;
+        interface MyInterface {}
+   "#,
+        )
+        .unwrap();
+
+    let mycontract = tmp
+        .add_source(
+            "MyContract",
+            r#"
+        pragma solidity 0.8.10;
+        contract MyContract {}
+   "#,
+        )
+        .unwrap();
+
+    let myabstract_contract = tmp
+        .add_source(
+            "MyAbstractContract",
+            r#"
+        pragma solidity 0.8.10;
+        contract MyAbstractContract {}
+   "#,
+        )
+        .unwrap();
+
+    let myerr = tmp
+        .add_source(
+            "MyError",
+            r#"
+        pragma solidity 0.8.10;
+       error MyError();
+   "#,
+        )
+        .unwrap();
+
+    let myfunc = tmp
+        .add_source(
+            "MyFunction",
+            r#"
+        pragma solidity 0.8.10;
+        function abc(){}
+   "#,
+        )
+        .unwrap();
+
+    let compiled = tmp.compile().unwrap();
+    println!("{}", compiled);
+    assert!(!compiled.has_compiler_errors());
+
+    let mut sources = compiled.output().sources;
+    let myfunc = sources.remove_by_path(myfunc.to_string_lossy()).unwrap();
+    assert!(!myfunc.contains_contract_definition());
+
+    let myerr = sources.remove_by_path(myerr.to_string_lossy()).unwrap();
+    assert!(!myerr.contains_contract_definition());
+
+    let mylib = sources.remove_by_path(mylib.to_string_lossy()).unwrap();
+    assert!(mylib.contains_contract_definition());
+
+    let myabstract_contract =
+        sources.remove_by_path(myabstract_contract.to_string_lossy()).unwrap();
+    assert!(myabstract_contract.contains_contract_definition());
+
+    let myinterface = sources.remove_by_path(myinterface.to_string_lossy()).unwrap();
+    assert!(myinterface.contains_contract_definition());
+
+    let mycontract = sources.remove_by_path(mycontract.to_string_lossy()).unwrap();
+    assert!(mycontract.contains_contract_definition());
+}
+
 #[test]
 fn can_compile_sparse_with_link_references() {
     let tmp = TempProject::dapptools().unwrap();