Repository: gakonst/ethers-rs
Pull request #1251 (2022-05-12 06:19:59)

More gas oracles
<!--
Thank you for your Pull Request. Please provide a description above and review
the requirements below.

Bug fixes and new features should include tests.

Contributors guide: https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md

The contributors guide includes instructions for running rustfmt and building the
documentation.
-->

## Motivation

This adds the `ProviderOracle` that allows runtime selection between provider gas methods and gas oracles. Such runtime selection is very useful for building stacks that work on multiple chains, such as production and local test.

~~It also adds a `default_for_chain` constructor that looks at the `chain_id` and tries to build a sensible default. This is a bit proof-of-concept. It would be nice to add a builder pattern where you can provide api-keys.~~

Architecturally this all feels a bit off to me. `ProviderOracle` exists basically because `GasOracle` and `Middleware` overlap in functionality. The `Provider` implementation also does quite a bit of magic under the hood to get EIP1559 estimates. IMHO it would be cleaner if this was all done through something like GasOracle's. But simply factoring it out doesn't work because gas prices are required to sign transactions, and at least currently Provider provides a complete implementation. Makes me think the Middleware really should be broken up in separate parts relating to the transaction lifecycle (gas pricing, gas estimation, gas escalation, nonce, signing, broadcasting, monitoring, etc). (Also seems related to #592)

<!--
Explain the context and why you're making that change. What is the problem
you're trying to solve? In some cases there is not a problem and this can be
thought of as being the motivation for your change.
-->

## Solution

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


mattsse: cc @oliver-giersch 
gakonst: > Makes me think the Middleware really should be broken up in separate parts relating to the transaction lifecycle (gas pricing, gas estimation, gas escalation, nonce, signing, broadcasting, monitoring, etc). (Also seems related to https://github.com/gakonst/ethers-rs/issues/592)

This is not a bad idea, although it does prevent you from join'ing all the futures for gas price + gas estimation + nonce, which is a nice performance boost. Do you wanna drop any thoughts on the architecture in the issue?
gakonst: (Think this needs some fixes around WASM)
recmo: I removed the `default_for_provider` function. It was constructing a reqwest client with timeout, which is not available on WASM, causing the build to fail. I split the `LogQueryError` out to it's own PR #1615 .

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 626ae469..49a7e13f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -117,6 +117,8 @@
 
 ### Unreleased
 
+- Add `OutputContext` to `ArtifactOutput` trait
+  [#1621](https://github.com/gakonst/ethers-rs/pull/1621)
 - On windows all paths in the `ProjectCompilerOutput` are now slashed by default
   [#1540](https://github.com/gakonst/ethers-rs/pull/1540)
 - `ArtifactOutput::write_extras` now takes the `Artifacts` directly
diff --git a/Cargo.lock b/Cargo.lock
index cad1d54e..e7c28986 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -135,6 +135,18 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "auto_impl"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7862e21c893d65a1650125d157eaeec691439379a1cee17ee49031b79236ada4"
+dependencies = [
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
 [[package]]
 name = "auto_impl"
 version = "1.0.1"
@@ -1365,6 +1377,7 @@ name = "ethers-middleware"
 version = "0.17.0"
 dependencies = [
  "async-trait",
+ "auto_impl 0.5.0",
  "ethers-contract",
  "ethers-core",
  "ethers-etherscan",
@@ -1393,7 +1406,7 @@ name = "ethers-providers"
 version = "0.17.0"
 dependencies = [
  "async-trait",
- "auto_impl",
+ "auto_impl 1.0.1",
  "base64 0.13.0",
  "bytes",
  "ethers-core",
@@ -1547,7 +1560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "089263294bb1c38ac73649a6ad563dd9a5142c8dc0482be15b8b9acb22a1611e"
 dependencies = [
  "arrayvec 0.7.2",
- "auto_impl",
+ "auto_impl 1.0.1",
  "bytes",
  "ethereum-types",
  "fastrlp-derive",
diff --git a/ethers-contract/src/call.rs b/ethers-contract/src/call.rs
index 8dcc4fba..1f753414 100644
--- a/ethers-contract/src/call.rs
+++ b/ethers-contract/src/call.rs
@@ -150,7 +150,7 @@ where
 
     /// Returns the estimated gas cost for the underlying transaction to be executed
     pub async fn estimate_gas(&self) -> Result<U256, ContractError<M>> {
-        self.client.estimate_gas(&self.tx).await.map_err(ContractError::MiddlewareError)
+        self.client.estimate_gas(&self.tx, self.block).await.map_err(ContractError::MiddlewareError)
     }
 
     /// Queries the blockchain via an `eth_call` for the provided transaction.
diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml
index 6358c3ae..0a51182c 100644
--- a/ethers-core/Cargo.toml
+++ b/ethers-core/Cargo.toml
@@ -41,7 +41,7 @@ cargo_metadata = { version = "0.15.0", optional = true }
 convert_case = { version = "0.5.0", optional = true }
 syn = { version = "1.0.99", optional = true }
 proc-macro2 = { version = "1.0.43", optional = true }
-rust_decimal = "1.26.1"
+rust_decimal = { version = "1.26.1", features = ["maths"] }
 
 [dev-dependencies]
 serde_json = { version = "1.0.64", default-features = false }
diff --git a/ethers-core/src/types/filter.rs b/ethers-core/src/types/filter.rs
index 077b6045..04f750da 100644
--- a/ethers-core/src/types/filter.rs
+++ b/ethers-core/src/types/filter.rs
@@ -254,13 +254,21 @@ impl Filter {
         self
     }
 
-    /// given the event in string form, it hashes it and adds it to the topics to monitor
+    /// Given the event signature in string form, it hashes it and adds it to the topics to monitor
     #[must_use]
     pub fn event(self, event_name: &str) -> Self {
         let hash = H256::from(keccak256(event_name.as_bytes()));
         self.topic0(hash)
     }
 
+    /// Hashes all event signatures and sets them as array to topic0
+    #[must_use]
+    pub fn events(self, events: impl IntoIterator<Item = impl AsRef<[u8]>>) -> Self {
+        let events =
+            events.into_iter().map(|e| H256::from(keccak256(e.as_ref()))).collect::<Vec<_>>();
+        self.topic0(events)
+    }
+
     /// Sets topic0 (the event name for non-anonymous events)
     #[must_use]
     pub fn topic0<T: Into<Topic>>(mut self, topic: T) -> Self {
@@ -544,6 +552,12 @@ impl From<H256> for Topic {
     }
 }
 
+impl From<Vec<H256>> for ValueOrArray<H256> {
+    fn from(src: Vec<H256>) -> Self {
+        ValueOrArray::Array(src)
+    }
+}
+
 impl From<ValueOrArray<H256>> for Topic {
     fn from(src: ValueOrArray<H256>) -> Self {
         match src {
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index a5703e95..c03e9a7a 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -68,3 +68,6 @@ mod other;
 pub use other::OtherFields;
 
 pub mod serde_helpers;
+
+mod syncing;
+pub use syncing::{SyncProgress, SyncingStatus};
diff --git a/ethers-core/src/types/proof.rs b/ethers-core/src/types/proof.rs
index 011c3576..a1d9cc17 100644
--- a/ethers-core/src/types/proof.rs
+++ b/ethers-core/src/types/proof.rs
@@ -1,4 +1,4 @@
-use crate::types::{Address, Bytes, H256, U256};
+use crate::types::{Address, Bytes, H256, U256, U64};
 use serde::{Deserialize, Serialize};
 
 #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
@@ -11,13 +11,13 @@ pub struct StorageProof {
 #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
 #[serde(rename_all = "camelCase")]
 pub struct EIP1186ProofResponse {
-    address: Address,
-    balance: U256,
-    code_hash: H256,
-    nonce: U256,
-    storage_hash: H256,
-    account_proof: Vec<Bytes>,
-    storage_proof: Vec<StorageProof>,
+    pub address: Address,
+    pub balance: U256,
+    pub code_hash: H256,
+    pub nonce: U64,
+    pub storage_hash: H256,
+    pub account_proof: Vec<Bytes>,
+    pub storage_proof: Vec<StorageProof>,
 }
 
 #[cfg(test)]
diff --git a/ethers-core/src/types/syncing.rs b/ethers-core/src/types/syncing.rs
new file mode 100644
index 00000000..58e685a6
--- /dev/null
+++ b/ethers-core/src/types/syncing.rs
@@ -0,0 +1,164 @@
+//! Types for `eth_syncing` RPC call
+
+use crate::types::U64;
+use serde::{Deserialize, Deserializer, Serialize, Serializer};
+
+/// Structure used in `eth_syncing` RPC
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum SyncingStatus {
+    /// When client is synced to highest block, eth_syncing with return string "false"
+    IsFalse,
+    /// When client is still syncing past blocks we get IsSyncing information.
+    IsSyncing(Box<SyncProgress>),
+}
+
+impl Serialize for SyncingStatus {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        match self {
+            SyncingStatus::IsFalse => serializer.serialize_bool(false),
+            SyncingStatus::IsSyncing(sync) => sync.serialize(serializer),
+        }
+    }
+}
+
+impl<'de> Deserialize<'de> for SyncingStatus {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        #[derive(Debug, Serialize, Deserialize)]
+        #[serde(untagged)]
+        pub enum SyncingStatusIntermediate {
+            /// When client is synced to the highest block, eth_syncing with return string "false"
+            IsFalse(bool),
+            /// When client is still syncing past blocks we get IsSyncing information.
+            IsSyncing(Box<SyncProgress>),
+        }
+
+        match SyncingStatusIntermediate::deserialize(deserializer)? {
+            SyncingStatusIntermediate::IsFalse(false) => Ok(SyncingStatus::IsFalse),
+            SyncingStatusIntermediate::IsFalse(true) => Err(serde::de::Error::custom(
+                "eth_syncing returned `true` that is undefined value.",
+            )),
+            SyncingStatusIntermediate::IsSyncing(sync) => Ok(SyncingStatus::IsSyncing(sync)),
+        }
+    }
+}
+
+/// Represents the sync status of the node
+///
+/// **Note:** while the `eth_syncing` RPC response is defined as:
+///
+/// > Returns:
+/// >
+/// > Object|Boolean, An object with sync status data or FALSE, when not syncing:
+///
+/// > startingBlock: QUANTITY - The block at which the import started (will only be reset, after the
+/// > sync reached his head)
+/// > currentBlock: QUANTITY - The current block, same as eth_blockNumber
+/// > highestBlock: QUANTITY - The estimated highest block
+///
+/// Geth returns additional fields: <https://github.com/ethereum/go-ethereum/blob/0ce494b60cd00d70f1f9f2dd0b9bfbd76204168a/ethclient/ethclient.go#L597-L617>
+#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SyncProgress {
+    pub current_block: U64,
+    pub highest_block: U64,
+    pub starting_block: U64,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub pulled_states: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub known_states: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_bytecode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_bytecodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_trienode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healed_trienodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healing_bytecode: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub healing_trienodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_account_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_accounts: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_bytecode_bytes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_bytecodes: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_storage: Option<U64>,
+    #[serde(default, skip_serializing_if = "Option::is_none")]
+    pub synced_storage_bytes: Option<U64>,
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    // <https://github.com/gakonst/ethers-rs/issues/1623>
+    #[test]
+    fn deserialize_sync_geth() {
+        let s = r#"{
+        "currentBlock": "0xeaa2b4",
+        "healedBytecodeBytes": "0xaad91fe",
+        "healedBytecodes": "0x61d3",
+        "healedTrienodeBytes": "0x156ac02b1",
+        "healedTrienodes": "0x2885aa4",
+        "healingBytecode": "0x0",
+        "healingTrienodes": "0x454",
+        "highestBlock": "0xeaa329",
+        "startingBlock": "0xea97ee",
+        "syncedAccountBytes": "0xa29fec90d",
+        "syncedAccounts": "0xa7ed9ad",
+        "syncedBytecodeBytes": "0xdec39008",
+        "syncedBytecodes": "0x8d407",
+        "syncedStorage": "0x2a517da1",
+        "syncedStorageBytes": "0x23634dbedf"
+    }"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {
+                panic!("unexpected variant")
+            }
+            SyncingStatus::IsSyncing(_) => {}
+        }
+    }
+
+    #[test]
+    fn deserialize_sync_minimal() {
+        let s = r#"{
+        "currentBlock": "0xeaa2b4",
+        "highestBlock": "0xeaa329",
+        "startingBlock": "0xea97ee"
+    }"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {
+                panic!("unexpected variant")
+            }
+            SyncingStatus::IsSyncing(_) => {}
+        }
+    }
+
+    #[test]
+    fn deserialize_sync_false() {
+        let s = r#"false"#;
+
+        let sync: SyncingStatus = serde_json::from_str(s).unwrap();
+        match sync {
+            SyncingStatus::IsFalse => {}
+            SyncingStatus::IsSyncing(_) => {
+                panic!("unexpected variant")
+            }
+        }
+    }
+}
diff --git a/ethers-core/src/utils/mod.rs b/ethers-core/src/utils/mod.rs
index b1cf9ca2..5e3b1c2d 100644
--- a/ethers-core/src/utils/mod.rs
+++ b/ethers-core/src/utils/mod.rs
@@ -155,9 +155,15 @@ where
     S: ToString,
     K: TryInto<Units, Error = ConversionError> + Copy,
 {
-    use rust_decimal::Decimal;
+    use rust_decimal::{Decimal, MathematicalOps};
+
     let num: Decimal = amount.to_string().parse()?;
-    let multiplier: Decimal = 10u64.pow(units.try_into()?.as_num()).into();
+    let exponent = units.try_into()?.as_num();
+
+    let multiplier = Decimal::TEN
+        .checked_powu(exponent.into())
+        .ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
+
     let val =
         num.checked_mul(multiplier).ok_or(rust_decimal::Error::ExceedsMaximumPossibleValue)?;
     let u256_n: U256 = U256::from_dec_str(&val.round().to_string())?;
@@ -441,6 +447,14 @@ mod tests {
         assert_eq!(eth, "1.005633240123456789");
     }
 
+    #[test]
+    fn parse_large_units() {
+        let decimals = 27u32;
+        let val = "10.55";
+        let unit = parse_units(val, decimals).unwrap();
+        assert_eq!(unit.to_string(), "10550000000000000000000000000");
+    }
+
     #[test]
     fn test_parse_units() {
         let gwei = parse_units(1.5, 9).unwrap();
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 6a7bc461..0544e54b 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -21,6 +21,7 @@ ethers-providers = { version = "^0.17.0", path = "../ethers-providers", default-
 ethers-signers = { version = "^0.17.0", path = "../ethers-signers", default-features = false }
 
 async-trait = { version = "0.1.50", default-features = false }
+auto_impl = { version = "0.5.0", default-features = false }
 serde = { version = "1.0.124", default-features = false, features = ["derive"] }
 thiserror = { version = "1.0", default-features = false }
 futures-util = { version = "^0.3" }
diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs
index a1444ff1..65ddc46c 100644
--- a/ethers-middleware/src/gas_oracle/gas_now.rs
+++ b/ethers-middleware/src/gas_oracle/gas_now.rs
@@ -23,7 +23,7 @@ struct GasNowResponseWrapper {
     data: GasNowResponse,
 }
 
-#[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
+#[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
 pub struct GasNowResponse {
     pub rapid: u64,
     pub fast: u64,
@@ -41,7 +41,7 @@ impl GasNow {
     pub fn with_client(client: Client) -> Self {
         let url = Url::parse(GAS_NOW_URL).expect("invalid url");
 
-        Self { url, gas_category: GasCategory::Standard }
+        Self { client, url, gas_category: GasCategory::Standard }
     }
 
     /// Sets the gas price category to be used when fetching the gas price.
@@ -64,7 +64,7 @@ impl GasNow {
 
 impl Default for GasNow {
     fn default() -> Self {
-        Self::new(Client::new())
+        Self::new()
     }
 }
 
diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs
index 844a5169..272b0150 100644
--- a/ethers-middleware/src/gas_oracle/middleware.rs
+++ b/ethers-middleware/src/gas_oracle/middleware.rs
@@ -56,24 +56,11 @@ where
         &self.inner
     }
 
-    async fn get_gas_price(&self) -> Result<U256, Self::Error> {
-        Ok(self.gas_oracle.fetch().await?)
-    }
-
-    async fn estimate_eip1559_fees(
-        &self,
-        _: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
-    ) -> Result<(U256, U256), Self::Error> {
-        Ok(self.gas_oracle.estimate_eip1559_fees().await?)
-    }
-
-    async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
+    async fn fill_transaction(
         &self,
-        tx: T,
+        tx: &mut TypedTransaction,
         block: Option<BlockId>,
-    ) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
-        let mut tx = tx.into();
-
+    ) -> Result<(), Self::Error> {
         match tx {
             TypedTransaction::Legacy(ref mut tx) => {
                 if tx.gas_price.is_none() {
@@ -98,6 +85,28 @@ where
                 }
             }
         };
+
+        self.inner().fill_transaction(tx, block).await.map_err(FromErr::from)
+    }
+
+    async fn get_gas_price(&self) -> Result<U256, Self::Error> {
+        Ok(self.gas_oracle.fetch().await?)
+    }
+
+    async fn estimate_eip1559_fees(
+        &self,
+        _: Option<fn(U256, Vec<Vec<U256>>) -> (U256, U256)>,
+    ) -> Result<(U256, U256), Self::Error> {
+        Ok(self.gas_oracle.estimate_eip1559_fees().await?)
+    }
+
+    async fn send_transaction<T: Into<TypedTransaction> + Send + Sync>(
+        &self,
+        tx: T,
+        block: Option<BlockId>,
+    ) -> Result<PendingTransaction<'_, Self::Provider>, Self::Error> {
+        let mut tx = tx.into();
+        self.fill_transaction(&mut tx, block).await?;
         self.inner.send_transaction(tx, block).await.map_err(MiddlewareError::MiddlewareError)
     }
 }
diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs
index 155c277c..4a7a701c 100644
--- a/ethers-middleware/src/gas_oracle/mod.rs
+++ b/ethers-middleware/src/gas_oracle/mod.rs
@@ -22,10 +22,18 @@ pub use cache::Cache;
 mod polygon;
 pub use polygon::Polygon;
 
+mod gas_now;
+pub use gas_now::GasNow;
+
+mod provider_oracle;
+pub use provider_oracle::ProviderOracle;
+
 use ethers_core::types::U256;
 
 use async_trait::async_trait;
+use auto_impl::auto_impl;
 use reqwest::Error as ReqwestError;
+use std::error::Error;
 use thiserror::Error;
 
 const GWEI_TO_WEI: u64 = 1000000000;
@@ -73,6 +81,10 @@ pub enum GasOracleError {
 
     #[error("Chain is not supported by the oracle")]
     UnsupportedChain,
+
+    /// Error thrown when the provider failed.
+    #[error("Chain is not supported by the oracle")]
+    ProviderError(#[from] Box<dyn Error + Send + Sync>),
 }
 
 /// `GasOracle` is a trait that an underlying gas oracle needs to implement.
@@ -95,6 +107,7 @@ pub enum GasOracleError {
 /// ```
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+#[auto_impl(&, Box, Arc)]
 pub trait GasOracle: Send + Sync + std::fmt::Debug {
     /// Makes an asynchronous HTTP query to the underlying `GasOracle`
     ///
diff --git a/ethers-middleware/src/gas_oracle/provider_oracle.rs b/ethers-middleware/src/gas_oracle/provider_oracle.rs
new file mode 100644
index 00000000..894bb5ab
--- /dev/null
+++ b/ethers-middleware/src/gas_oracle/provider_oracle.rs
@@ -0,0 +1,40 @@
+use crate::gas_oracle::{GasOracle, GasOracleError};
+use async_trait::async_trait;
+use ethers_core::types::U256;
+use ethers_providers::Middleware;
+use std::fmt::Debug;
+
+/// Gas oracle from a [`Middleware`] implementation such as an
+/// Ethereum RPC provider.
+#[derive(Debug)]
+pub struct ProviderOracle<M: Middleware> {
+    provider: M,
+}
+
+impl<M: Middleware> ProviderOracle<M> {
+    pub fn new(provider: M) -> Self {
+        Self { provider }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<M: Middleware> GasOracle for ProviderOracle<M>
+where
+    <M as Middleware>::Error: 'static,
+{
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        self.provider
+            .get_gas_price()
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        // TODO: Allow configuring different estimation functions.
+        self.provider
+            .estimate_eip1559_fees(None)
+            .await
+            .map_err(|err| GasOracleError::ProviderError(Box::new(err)))
+    }
+}
diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs
index e38ed7e1..b911a660 100644
--- a/ethers-middleware/src/signer.rs
+++ b/ethers-middleware/src/signer.rs
@@ -303,9 +303,13 @@ where
         self.signer.sign_message(data.into()).await.map_err(SignerMiddlewareError::SignerError)
     }
 
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, Self::Error> {
         let tx = self.set_tx_from_if_none(tx);
-        self.inner.estimate_gas(&tx).await.map_err(SignerMiddlewareError::MiddlewareError)
+        self.inner.estimate_gas(&tx, block).await.map_err(SignerMiddlewareError::MiddlewareError)
     }
 
     async fn create_access_list(
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index 9e7ee807..8a323db1 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -20,7 +20,7 @@ mod pending_escalator;
 pub use pending_escalator::EscalatingPending;
 
 mod log_query;
-pub use log_query::LogQuery;
+pub use log_query::{LogQuery, LogQueryError};
 
 mod stream;
 pub use futures_util::StreamExt;
@@ -88,15 +88,6 @@ where
     }
 }
 
-/// Structure used in eth_syncing RPC
-#[derive(Clone, Debug)]
-pub enum SyncingStatus {
-    /// When client is synced to highest block, eth_syncing with return string "false"
-    IsFalse,
-    /// When client is still syncing past blocks we get IsSyncing information.
-    IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
-}
-
 /// A middleware allows customizing requests send and received from an ethereum node.
 ///
 /// Writing a middleware is as simple as:
@@ -106,7 +97,7 @@ pub enum SyncingStatus {
 ///
 /// ```rust
 /// use ethers_providers::{Middleware, FromErr};
-/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction};
+/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction, BlockId};
 /// use thiserror::Error;
 /// use async_trait::async_trait;
 ///
@@ -147,9 +138,9 @@ pub enum SyncingStatus {
 ///
 ///     /// Overrides the default `estimate_gas` method to log that it was called,
 ///     /// before forwarding the call to the next layer.
-///     async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
+///     async fn estimate_gas(&self, tx: &TypedTransaction, block: Option<BlockId>) -> Result<U256, Self::Error> {
 ///         println!("Estimating gas...");
-///         self.inner().estimate_gas(tx).await.map_err(FromErr::from)
+///         self.inner().estimate_gas(tx, block).await.map_err(FromErr::from)
 ///     }
 /// }
 /// ```
@@ -182,15 +173,11 @@ pub trait Middleware: Sync + Send + Debug {
     /// This function is defined on providers to behave as follows:
     /// 1. populate the `from` field with the default sender
     /// 2. resolve any ENS names in the tx `to` field
-    /// 3. Estimate gas usage _without_ access lists
-    /// 4. Estimate gas usage _with_ access lists
-    /// 5. Enable access lists IFF they are cheaper
-    /// 6. Poll and set legacy or 1559 gas prices
-    /// 7. Set the chain_id with the provider's, if not already set
+    /// 3. Estimate gas usage
+    /// 4. Poll and set legacy or 1559 gas prices
+    /// 5. Set the chain_id with the provider's, if not already set
     ///
     /// It does NOT set the nonce by default.
-    /// It MAY override the gas amount set by the user, if access lists are
-    /// cheaper.
     ///
     /// Middleware are encouraged to override any values _before_ delegating
     /// to the inner implementation AND/OR modify the values provided by the
@@ -321,8 +308,12 @@ pub trait Middleware: Sync + Send + Debug {
         self.inner().get_transaction_count(from, block).await.map_err(FromErr::from)
     }
 
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, Self::Error> {
-        self.inner().estimate_gas(tx).await.map_err(FromErr::from)
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, Self::Error> {
+        self.inner().estimate_gas(tx, block).await.map_err(FromErr::from)
     }
 
     async fn call(
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index d4d0a0eb..7d36041b 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -29,7 +29,7 @@ use ethers_core::{
     utils,
 };
 use hex::FromHex;
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use serde::{de::DeserializeOwned, Serialize};
 use thiserror::Error;
 use url::{ParseError, Url};
 
@@ -352,34 +352,10 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
             }
         }
 
-        // If the tx has an access list but it is empty, it is an Eip1559 or Eip2930 tx,
-        // and we attempt to populate the acccess list. This may require `eth_estimateGas`,
-        // in which case we save the result in maybe_gas_res for later
-        let mut maybe_gas = None;
-        if let Some(starting_al) = tx.access_list() {
-            if starting_al.0.is_empty() {
-                let (gas_res, al_res) = futures_util::join!(
-                    maybe(tx.gas().cloned(), self.estimate_gas(tx)),
-                    self.create_access_list(tx, block)
-                );
-                let mut gas = gas_res?;
-
-                if let Ok(al_with_gas) = al_res {
-                    // Set access list if it saves gas over the estimated (or previously set) value
-                    if al_with_gas.gas_used < gas {
-                        // Update the gas estimate with the lower amount
-                        gas = al_with_gas.gas_used;
-                        tx.set_access_list(al_with_gas.access_list);
-                    }
-                }
-                maybe_gas = Some(gas);
-            }
-        }
-
         // Set gas to estimated value only if it was not set by the caller,
         // even if the access list has been populated and saves gas
         if tx.gas().is_none() {
-            let gas_estimate = maybe(maybe_gas, self.estimate_gas(tx)).await?;
+            let gas_estimate = self.estimate_gas(tx, block).await?;
             tx.set_gas(gas_estimate);
         }
 
@@ -563,26 +539,7 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
 
     /// Return current client syncing status. If IsFalse sync is over.
     async fn syncing(&self) -> Result<SyncingStatus, Self::Error> {
-        #[derive(Debug, Serialize, Deserialize)]
-        #[serde(untagged)]
-        pub enum SyncingStatusIntermediate {
-            /// When client is synced to highest block, eth_syncing with return string "false"
-            IsFalse(bool),
-            /// When client is still syncing past blocks we get IsSyncing information.
-            IsSyncing { starting_block: U256, current_block: U256, highest_block: U256 },
-        }
-        let intermediate: SyncingStatusIntermediate = self.request("eth_syncing", ()).await?;
-        match intermediate {
-            SyncingStatusIntermediate::IsFalse(false) => Ok(SyncingStatus::IsFalse),
-            SyncingStatusIntermediate::IsFalse(true) => Err(ProviderError::CustomError(
-                "eth_syncing returned `true` that is undefined value.".to_owned(),
-            )),
-            SyncingStatusIntermediate::IsSyncing {
-                starting_block,
-                current_block,
-                highest_block,
-            } => Ok(SyncingStatus::IsSyncing { starting_block, current_block, highest_block }),
-        }
+        self.request("eth_syncing", ()).await
     }
 
     /// Returns the network version.
@@ -611,8 +568,14 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
     /// required (as a U256) to send it This is free, but only an estimate. Providing too little
     /// gas will result in a transaction being rejected (while still consuming all provided
     /// gas).
-    async fn estimate_gas(&self, tx: &TypedTransaction) -> Result<U256, ProviderError> {
-        self.request("eth_estimateGas", [tx]).await
+    async fn estimate_gas(
+        &self,
+        tx: &TypedTransaction,
+        block: Option<BlockId>,
+    ) -> Result<U256, ProviderError> {
+        let tx = utils::serialize(tx);
+        let block = utils::serialize(&block.unwrap_or_else(|| BlockNumber::Latest.into()));
+        self.request("eth_estimateGas", [tx, block]).await
     }
 
     async fn create_access_list(
@@ -2064,42 +2027,14 @@ mod tests {
         assert_eq!(tx.gas_price(), Some(max_fee));
         assert_eq!(tx.access_list(), Some(&access_list));
 
-        // --- fills a 1559 transaction, leaving the existing gas limit unchanged, but including
-        // access list if cheaper
-        let gas_with_al = gas - 1;
+        // --- fills a 1559 transaction, leaving the existing gas limit unchanged,
+        // without generating an access-list
         let mut tx = Eip1559TransactionRequest::new()
             .gas(gas)
             .max_fee_per_gas(max_fee)
             .max_priority_fee_per_gas(prio_fee)
             .into();
 
-        mock.push(AccessListWithGasUsed {
-            access_list: access_list.clone(),
-            gas_used: gas_with_al,
-        })
-        .unwrap();
-
-        provider.fill_transaction(&mut tx, None).await.unwrap();
-
-        assert_eq!(tx.from(), provider.from.as_ref());
-        assert!(tx.to().is_none());
-        assert_eq!(tx.gas(), Some(&gas));
-        assert_eq!(tx.access_list(), Some(&access_list));
-
-        // --- fills a 1559 transaction, ignoring access list if more expensive
-        let gas_with_al = gas + 1;
-        let mut tx = Eip1559TransactionRequest::new()
-            .max_fee_per_gas(max_fee)
-            .max_priority_fee_per_gas(prio_fee)
-            .into();
-
-        mock.push(AccessListWithGasUsed {
-            access_list: access_list.clone(),
-            gas_used: gas_with_al,
-        })
-        .unwrap();
-        mock.push(gas).unwrap();
-
         provider.fill_transaction(&mut tx, None).await.unwrap();
 
         assert_eq!(tx.from(), provider.from.as_ref());
@@ -2107,14 +2042,12 @@ mod tests {
         assert_eq!(tx.gas(), Some(&gas));
         assert_eq!(tx.access_list(), Some(&Default::default()));
 
-        // --- fills a 1559 transaction, using estimated gas if create_access_list() errors
+        // --- fills a 1559 transaction, using estimated gas
         let mut tx = Eip1559TransactionRequest::new()
             .max_fee_per_gas(max_fee)
             .max_priority_fee_per_gas(prio_fee)
             .into();
 
-        // bad mock value causes error response for eth_createAccessList
-        mock.push(b'b').unwrap();
         mock.push(gas).unwrap();
 
         provider.fill_transaction(&mut tx, None).await.unwrap();
diff --git a/ethers-providers/src/pubsub.rs b/ethers-providers/src/pubsub.rs
index 63339437..a31c0dcb 100644
--- a/ethers-providers/src/pubsub.rs
+++ b/ethers-providers/src/pubsub.rs
@@ -12,6 +12,7 @@ use std::{
     pin::Pin,
     task::{Context, Poll},
 };
+use tracing::error;
 
 /// A transport implementation supporting pub sub subscriptions.
 pub trait PubsubClient: JsonRpcClient {
@@ -90,7 +91,10 @@ where
         match futures_util::ready!(this.rx.poll_next(ctx)) {
             Some(item) => match serde_json::from_str(item.get()) {
                 Ok(res) => Poll::Ready(Some(res)),
-                _ => Poll::Pending,
+                Err(err) => {
+                    error!("failed to deserialize item {:?}", err);
+                    Poll::Pending
+                }
             },
             None => Poll::Ready(None),
         }
diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/transports/ws.rs
index 098a2d87..e43d5ed8 100644
--- a/ethers-providers/src/transports/ws.rs
+++ b/ethers-providers/src/transports/ws.rs
@@ -22,6 +22,7 @@ use std::{
     },
 };
 use thiserror::Error;
+use tracing::trace;
 
 use super::common::{Params, Response};
 
@@ -317,6 +318,7 @@ where
     }
 
     async fn handle_text(&mut self, inner: String) -> Result<(), ClientError> {
+        trace!(msg=?inner, "received message");
         let (id, result) = match serde_json::from_str(&inner)? {
             Response::Success { id, result } => (id, Ok(result.to_owned())),
             Response::Error { id, error } => (id, Err(error)),
diff --git a/ethers-solc/src/artifact_output/files.rs b/ethers-solc/src/artifact_output/files.rs
new file mode 100644
index 00000000..12c88ad4
--- /dev/null
+++ b/ethers-solc/src/artifact_output/files.rs
@@ -0,0 +1,60 @@
+use crate::contracts::VersionedContract;
+use std::{
+    collections::HashMap,
+    hash::Hash,
+    ops::{Deref, DerefMut},
+    path::{Path, PathBuf},
+};
+
+/// Container type for all contracts mapped to their output file
+pub struct MappedArtifactFiles<'a> {
+    /// Represents the determined output artifact file and the contract(s) that target it
+    ///
+    /// This is guaranteed to be `len >= 1`
+    ///
+    /// If there is more than 1 contract in the map, this means we have a naming conflict where
+    /// different contracts target the same output file. This happens if the solidity file and
+    /// contract name match, but they are in different folders.
+    pub files: HashMap<MappedArtifactFile, Vec<MappedContract<'a>>>,
+}
+
+impl<'a> MappedArtifactFiles<'a> {
+    pub fn with_capacity(len: usize) -> Self {
+        Self { files: HashMap::with_capacity(len) }
+    }
+}
+
+impl<'a> Deref for MappedArtifactFiles<'a> {
+    type Target = HashMap<MappedArtifactFile, Vec<MappedContract<'a>>>;
+
+    fn deref(&self) -> &Self::Target {
+        &self.files
+    }
+}
+
+impl<'a> DerefMut for MappedArtifactFiles<'a> {
+    fn deref_mut(&mut self) -> &mut Self::Target {
+        &mut self.files
+    }
+}
+
+/// Represents the targeted path of a contract or multiple contracts
+///
+/// To account for case-sensitivity we identify it via lowercase path
+#[derive(Debug, Hash, PartialEq, Eq)]
+pub struct MappedArtifactFile {
+    lower_case_path: String,
+}
+
+impl MappedArtifactFile {
+    pub fn new(path: &Path) -> Self {
+        Self { lower_case_path: path.to_string_lossy().to_lowercase() }
+    }
+}
+
+pub struct MappedContract<'a> {
+    pub file: &'a str,
+    pub name: &'a str,
+    pub contract: &'a VersionedContract,
+    pub artifact_path: PathBuf,
+}
diff --git a/ethers-solc/src/artifact_output/mod.rs b/ethers-solc/src/artifact_output/mod.rs
index 11759fae..37528dc1 100644
--- a/ethers-solc/src/artifact_output/mod.rs
+++ b/ethers-solc/src/artifact_output/mod.rs
@@ -10,23 +10,27 @@ use crate::{
     error::Result,
     sourcemap::{SourceMap, SyntaxError},
     sources::VersionedSourceFile,
-    utils, HardhatArtifact, ProjectPathsConfig, SolcError,
+    utils, HardhatArtifact, ProjectPathsConfig, SolFilesCache, SolcError,
 };
 use ethers_core::{abi::Abi, types::Bytes};
 use semver::Version;
 use serde::{de::DeserializeOwned, Serialize};
 use std::{
     borrow::Cow,
-    collections::{btree_map::BTreeMap, HashMap, HashSet},
+    collections::{btree_map::BTreeMap, HashSet},
     ffi::OsString,
-    fmt, fs, io,
+    fmt, fs,
+    hash::Hash,
+    io,
     ops::Deref,
     path::{Path, PathBuf},
 };
-use tracing::trace;
+use tracing::{error, trace};
 
 mod configurable;
-use crate::contracts::VersionedContract;
+pub(crate) mod files;
+
+use crate::files::MappedContract;
 pub use configurable::*;
 
 /// Represents unique artifact metadata for identifying artifacts on output
@@ -98,6 +102,7 @@ pub struct ArtifactFile<T> {
 impl<T: Serialize> ArtifactFile<T> {
     /// Writes the given contract to the `out` path creating all parent directories
     pub fn write(&self) -> Result<()> {
+        trace!("writing artifact file {:?} {}", self.file, self.version);
         utils::create_parent_dir_all(&self.file)?;
         fs::write(&self.file, serde_json::to_vec_pretty(&self.artifact)?)
             .map_err(|err| SolcError::io(err, &self.file))?;
@@ -119,11 +124,6 @@ impl<T> ArtifactFile<T> {
     }
 }
 
-/// Internal helper type alias that maps all files for the contracts
-/// `output -> [(file, name, contract)]`
-pub(crate) type ArtifactFiles<'a> =
-    HashMap<PathBuf, Vec<(&'a str, &'a str, &'a VersionedContract)>>;
-
 /// local helper type alias `file name -> (contract name  -> Vec<..>)`
 pub(crate) type ArtifactsMap<T> = FileToContractsMap<Vec<ArtifactFile<T>>>;
 
@@ -574,8 +574,9 @@ pub trait ArtifactOutput {
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        let mut artifacts = self.output_to_artifacts(contracts, sources);
+        let mut artifacts = self.output_to_artifacts(contracts, sources, ctx);
         artifacts.join_all(&layout.artifacts);
         artifacts.write_all()?;
 
@@ -792,6 +793,7 @@ pub trait ArtifactOutput {
         &self,
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
+        ctx: OutputContext,
     ) -> Artifacts<Self::Artifact> {
         let mut artifacts = ArtifactsMap::new();
 
@@ -799,14 +801,15 @@ pub trait ArtifactOutput {
         let mut non_standalone_sources = HashSet::new();
 
         // this holds all output files and the contract(s) it belongs to
-        let artifact_files = contracts.artifact_files::<Self>();
+        let artifact_files = contracts.artifact_files::<Self>(&ctx);
 
         // this tracks the final artifacts, which we use as lookup for checking conflicts when
         // converting stand-alone artifacts in the next step
         let mut final_artifact_paths = HashSet::new();
 
-        for (artifact_path, contracts) in artifact_files {
-            for (idx, (file, name, contract)) in contracts.iter().enumerate() {
+        for contracts in artifact_files.files.into_values() {
+            for (idx, mapped_contract) in contracts.iter().enumerate() {
+                let MappedContract { file, name, contract, artifact_path } = mapped_contract;
                 // track `SourceFile`s that can be mapped to contracts
                 let source_file = sources.find_file_and_version(file, &contract.version);
 
@@ -821,11 +824,11 @@ pub trait ArtifactOutput {
                     // contracts need to adjust the paths properly
 
                     // we keep the top most conflicting file unchanged
-                    let is_top_most = contracts.iter().enumerate().filter(|(i, _)| *i != idx).all(
-                        |(_, (f, _, _))| {
-                            Path::new(file).components().count() < Path::new(f).components().count()
-                        },
-                    );
+                    let is_top_most =
+                        contracts.iter().enumerate().filter(|(i, _)| *i != idx).all(|(_, c)| {
+                            Path::new(file).components().count() <
+                                Path::new(c.file).components().count()
+                        });
                     if !is_top_most {
                         // we resolve the conflicting by finding a new unique, alternative path
                         artifact_path = Self::conflict_free_output_file(
@@ -932,6 +935,46 @@ pub trait ArtifactOutput {
     ) -> Option<Self::Artifact>;
 }
 
+/// Additional context to use during [`ArtifactOutput::on_output()`]
+#[derive(Debug, Clone, Default)]
+#[non_exhaustive]
+pub struct OutputContext<'a> {
+    /// Cache file of the project or empty if no caching is enabled
+    ///
+    /// This context is required for partially cached recompile with conflicting files, so that we
+    /// can use the same adjusted output path for conflicting files like:
+    ///
+    /// ```text
+    /// src
+    /// ├── a.sol
+    /// └── inner
+    ///     └── a.sol
+    /// ```
+    pub cache: Cow<'a, SolFilesCache>,
+}
+
+// === impl OutputContext
+
+impl<'a> OutputContext<'a> {
+    /// Create a new context with the given cache file
+    pub fn new(cache: &'a SolFilesCache) -> Self {
+        Self { cache: Cow::Borrowed(cache) }
+    }
+
+    /// Returns the path of the already existing artifact for the `contract` of the `file` compiled
+    /// with the `version`.
+    ///
+    /// Returns `None` if no file exists
+    pub fn existing_artifact(
+        &self,
+        file: impl AsRef<Path>,
+        contract: &str,
+        version: &Version,
+    ) -> Option<&PathBuf> {
+        self.cache.entry(file)?.find_artifact(contract, version)
+    }
+}
+
 /// An `Artifact` implementation that uses a compact representation
 ///
 /// Creates a single json artifact with
@@ -984,8 +1027,9 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
         output: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        MinimalCombinedArtifacts::default().on_output(output, sources, layout)
+        MinimalCombinedArtifacts::default().on_output(output, sources, layout, ctx)
     }
 
     fn read_cached_artifact(path: impl AsRef<Path>) -> Result<Self::Artifact> {
@@ -994,10 +1038,10 @@ impl ArtifactOutput for MinimalCombinedArtifactsHardhatFallback {
         if let Ok(a) = serde_json::from_str(&content) {
             Ok(a)
         } else {
-            tracing::error!("Failed to deserialize compact artifact");
-            tracing::trace!("Fallback to hardhat artifact deserialization");
+            error!("Failed to deserialize compact artifact");
+            trace!("Fallback to hardhat artifact deserialization");
             let artifact = serde_json::from_str::<HardhatArtifact>(&content)?;
-            tracing::trace!("successfully deserialized hardhat artifact");
+            trace!("successfully deserialized hardhat artifact");
             Ok(artifact.into_contract_bytecode())
         }
     }
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index 06657c9c..2329a5a5 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -5,8 +5,8 @@ use crate::{
     error::{Result, SolcError},
     filter::{FilteredSource, FilteredSourceInfo, FilteredSources},
     resolver::GraphEdges,
-    utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Project, ProjectPathsConfig,
-    Source,
+    utils, ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, OutputContext, Project,
+    ProjectPathsConfig, Source,
 };
 use semver::Version;
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
@@ -523,6 +523,11 @@ impl CacheEntry {
         self.artifacts.values().flat_map(|artifacts| artifacts.iter())
     }
 
+    /// Returns the artifact file for the contract and version pair
+    pub fn find_artifact(&self, contract: &str, version: &Version) -> Option<&PathBuf> {
+        self.artifacts.get(contract).and_then(|files| files.get(version))
+    }
+
     /// Iterator that yields all artifact files and their version
     pub fn artifacts_for_version<'a>(
         &'a self,
@@ -864,6 +869,13 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
         }
     }
 
+    pub fn output_ctx(&self) -> OutputContext {
+        match self {
+            ArtifactsCache::Ephemeral(_, _) => Default::default(),
+            ArtifactsCache::Cached(inner) => OutputContext::new(&inner.cache),
+        }
+    }
+
     pub fn project(&self) -> &'a Project<T> {
         match self {
             ArtifactsCache::Ephemeral(_, project) => project,
@@ -948,7 +960,8 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
                                         }).unwrap_or_default();
                                     if !retain {
                                         tracing::trace!(
-                                            "purging obsolete cached artifact for contract {} and version {}",
+                                            "purging obsolete cached artifact {:?} for contract {} and version {}",
+                                            f.file,
                                             name,
                                             f.version
                                         );
diff --git a/ethers-solc/src/compile/output/contracts.rs b/ethers-solc/src/compile/output/contracts.rs
index 09aab711..58787f8a 100644
--- a/ethers-solc/src/compile/output/contracts.rs
+++ b/ethers-solc/src/compile/output/contracts.rs
@@ -3,11 +3,13 @@ use crate::{
         contract::{CompactContractRef, Contract},
         FileToContractsMap,
     },
-    ArtifactFiles, ArtifactOutput,
+    files::{MappedArtifactFile, MappedArtifactFiles, MappedContract},
+    ArtifactOutput, OutputContext,
 };
 use semver::Version;
 use serde::{Deserialize, Serialize};
 use std::{collections::BTreeMap, ops::Deref, path::Path};
+use tracing::trace;
 
 /// file -> [(contract name  -> Contract + solc version)]
 #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
@@ -40,18 +42,46 @@ impl VersionedContracts {
     }
 
     /// Returns all the artifact files mapped with their contracts
-    pub(crate) fn artifact_files<T: ArtifactOutput + ?Sized>(&self) -> ArtifactFiles {
-        let mut output_files = ArtifactFiles::with_capacity(self.len());
+    ///
+    /// This will compute the appropriate output file paths but will _not_ write them.
+    /// The `ctx` is used to avoid possible conflicts
+    pub(crate) fn artifact_files<T: ArtifactOutput + ?Sized>(
+        &self,
+        ctx: &OutputContext,
+    ) -> MappedArtifactFiles {
+        let mut output_files = MappedArtifactFiles::with_capacity(self.len());
         for (file, contracts) in self.iter() {
             for (name, versioned_contracts) in contracts {
                 for contract in versioned_contracts {
-                    let output = if versioned_contracts.len() > 1 {
+                    // if an artifact for the contract already exists (from a previous compile job)
+                    // we reuse the path, this will make sure that even if there are conflicting
+                    // files (files for witch `T::output_file()` would return the same path) we use
+                    // consistent output paths
+                    let artifact_path = if let Some(existing_artifact) =
+                        ctx.existing_artifact(file, name, &contract.version).cloned()
+                    {
+                        trace!("use existing artifact file {:?}", existing_artifact,);
+                        existing_artifact
+                    } else if versioned_contracts.len() > 1 {
                         T::output_file_versioned(file, name, &contract.version)
                     } else {
                         T::output_file(file, name)
                     };
-                    let contract = (file.as_str(), name.as_str(), contract);
-                    output_files.entry(output).or_default().push(contract);
+
+                    trace!(
+                        "use artifact file {:?} for contract file {} {}",
+                        artifact_path,
+                        file,
+                        contract.version
+                    );
+                    let artifact = MappedArtifactFile::new(&artifact_path);
+                    let contract = MappedContract {
+                        file: file.as_str(),
+                        name: name.as_str(),
+                        contract,
+                        artifact_path,
+                    };
+                    output_files.entry(artifact).or_default().push(contract);
                 }
             }
         }
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index f0fdce64..fbb6d0a2 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -116,6 +116,7 @@ use crate::{
 };
 use rayon::prelude::*;
 use std::{collections::btree_map::BTreeMap, path::PathBuf, time::Instant};
+use tracing::trace;
 
 #[derive(Debug)]
 pub struct ProjectCompiler<'a, T: ArtifactOutput> {
@@ -231,6 +232,7 @@ impl<'a, T: ArtifactOutput> ProjectCompiler<'a, T> {
     ///   - sets proper source unit names
     ///   - check cache
     fn preprocess(self) -> Result<PreprocessedState<'a, T>> {
+        trace!("preprocessing");
         let Self { edges, project, mut sources, sparse_output } = self;
 
         // convert paths on windows to ensure consistency with the `CompilerOutput` `solc` emits,
@@ -260,6 +262,7 @@ struct PreprocessedState<'a, T: ArtifactOutput> {
 impl<'a, T: ArtifactOutput> PreprocessedState<'a, T> {
     /// advance to the next state by compiling all sources
     fn compile(self) -> Result<CompiledState<'a, T>> {
+        trace!("compiling");
         let PreprocessedState { sources, cache, sparse_output } = self;
         let project = cache.project();
         let mut output = sources.compile(
@@ -293,19 +296,21 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
     ///
     /// Writes all output contracts to disk if enabled in the `Project` and if the build was
     /// successful
+    #[tracing::instrument(skip_all, name = "write-artifacts")]
     fn write_artifacts(self) -> Result<ArtifactsState<'a, T>> {
         let CompiledState { output, cache } = self;
 
         let project = cache.project();
+        let ctx = cache.output_ctx();
         // 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)
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources, ctx)
         } else if output.has_error() {
-            tracing::trace!("skip writing cache file due to solc errors: {:?}", output.errors);
-            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources)
+            trace!("skip writing cache file due to solc errors: {:?}", output.errors);
+            project.artifacts_handler().output_to_artifacts(&output.contracts, &output.sources, ctx)
         } else {
-            tracing::trace!(
+            trace!(
                 "handling artifact output for {} contracts and {} sources",
                 output.contracts.len(),
                 output.sources.len()
@@ -315,6 +320,7 @@ impl<'a, T: ArtifactOutput> CompiledState<'a, T> {
                 &output.contracts,
                 &output.sources,
                 &project.paths,
+                ctx,
             )?;
 
             // emits all the build infos, if they exist
@@ -340,6 +346,7 @@ impl<'a, T: ArtifactOutput> ArtifactsState<'a, T> {
     ///
     /// this concludes the [`Project::compile()`] statemachine
     fn write_cache(self) -> Result<ProjectCompileOutput<T>> {
+        trace!("write cache");
         let ArtifactsState { output, cache, compiled_artifacts } = self;
         let ignored_error_codes = cache.project().ignored_error_codes.clone();
         let skip_write_to_disk = cache.project().no_artifacts || output.has_error();
@@ -404,9 +411,9 @@ impl CompilerSources {
             sources
                 .into_iter()
                 .map(|(solc, (version, sources))| {
-                    tracing::trace!("Filtering {} sources for {}", sources.len(), version);
+                    trace!("Filtering {} sources for {}", sources.len(), version);
                     let sources = cache.filter(sources, &version);
-                    tracing::trace!(
+                    trace!(
                         "Detected {} dirty sources {:?}",
                         sources.dirty().count(),
                         sources.dirty_files().collect::<Vec<_>>()
@@ -477,18 +484,14 @@ fn compile_sequential(
     create_build_info: bool,
 ) -> Result<AggregatedCompilerOutput> {
     let mut aggregated = AggregatedCompilerOutput::default();
-    tracing::trace!("compiling {} jobs sequentially", input.len());
+    trace!("compiling {} jobs sequentially", input.len());
     for (solc, (version, filtered_sources)) in input {
         if filtered_sources.is_empty() {
             // nothing to compile
-            tracing::trace!(
-                "skip solc {} {} for empty sources set",
-                solc.as_ref().display(),
-                version
-            );
+            trace!("skip solc {} {} for empty sources set", solc.as_ref().display(), version);
             continue
         }
-        tracing::trace!(
+        trace!(
             "compiling {} sources with solc \"{}\" {:?}",
             filtered_sources.len(),
             solc.as_ref().display(),
@@ -512,7 +515,7 @@ fn compile_sequential(
             if actually_dirty.is_empty() {
                 // nothing to compile for this particular language, all dirty files are in the other
                 // language set
-                tracing::trace!(
+                trace!(
                     "skip solc {} {} compilation of {} compiler input due to empty source set",
                     solc.as_ref().display(),
                     version,
@@ -527,7 +530,7 @@ fn compile_sequential(
                 .with_base_path(&paths.root)
                 .sanitized(&version);
 
-            tracing::trace!(
+            trace!(
                 "calling solc `{}` with {} sources {:?}",
                 version,
                 input.sources.len(),
@@ -538,8 +541,8 @@ fn compile_sequential(
             report::solc_spawn(&solc, &version, &input, &actually_dirty);
             let output = solc.compile(&input)?;
             report::solc_success(&solc, &version, &output, &start.elapsed());
-            tracing::trace!("compiled input, output has error: {}", output.has_error());
-            tracing::trace!("received compiler output: {:?}", output.contracts.keys());
+            trace!("compiled input, output has error: {}", output.has_error());
+            trace!("received compiler output: {:?}", output.contracts.keys());
 
             // if configured also create the build info
             if create_build_info {
@@ -564,21 +567,13 @@ fn compile_parallel(
     create_build_info: bool,
 ) -> Result<AggregatedCompilerOutput> {
     debug_assert!(num_jobs > 1);
-    tracing::trace!(
-        "compile {} sources in parallel using up to {} solc jobs",
-        input.len(),
-        num_jobs
-    );
+    trace!("compile {} sources in parallel using up to {} solc jobs", input.len(), num_jobs);
 
     let mut jobs = Vec::with_capacity(input.len());
     for (solc, (version, filtered_sources)) in input {
         if filtered_sources.is_empty() {
             // nothing to compile
-            tracing::trace!(
-                "skip solc {} {} for empty sources set",
-                solc.as_ref().display(),
-                version
-            );
+            trace!("skip solc {} {} for empty sources set", solc.as_ref().display(), version);
             continue
         }
 
@@ -599,7 +594,7 @@ fn compile_parallel(
             if actually_dirty.is_empty() {
                 // nothing to compile for this particular language, all dirty files are in the other
                 // language set
-                tracing::trace!(
+                trace!(
                     "skip solc {} {} compilation of {} compiler input due to empty source set",
                     solc.as_ref().display(),
                     version,
@@ -633,7 +628,7 @@ fn compile_parallel(
                 // set the reporter on this thread
                 let _guard = report::set_scoped(&scoped_report);
 
-                tracing::trace!(
+                trace!(
                     "calling solc `{}` {:?} with {} sources: {:?}",
                     version,
                     solc.args,
diff --git a/ethers-solc/src/error.rs b/ethers-solc/src/error.rs
index fc00601c..72dbc95e 100644
--- a/ethers-solc/src/error.rs
+++ b/ethers-solc/src/error.rs
@@ -32,6 +32,13 @@ pub enum SolcError {
     Resolve(SolcIoError),
     #[error("File could not be resolved due to broken symlink: {0}.")]
     ResolveBadSymlink(SolcIoError),
+    #[error(
+        r#"Failed to resolve file: {0}.
+    --> {1:?}
+        {2:?}
+    Check configured remappings."#
+    )]
+    FailedResolveImport(SolcIoError, PathBuf, PathBuf),
     #[cfg(feature = "svm-solc")]
     #[error(transparent)]
     SvmError(#[from] svm::SolcVmError),
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index ffdf3131..1f2f65c3 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -859,8 +859,9 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
         layout: &ProjectPathsConfig,
+        ctx: OutputContext,
     ) -> Result<Artifacts<Self::Artifact>> {
-        self.artifacts_handler().on_output(contracts, sources, layout)
+        self.artifacts_handler().on_output(contracts, sources, layout, ctx)
     }
 
     fn write_contract_extras(&self, contract: &Contract, file: &Path) -> Result<()> {
@@ -933,8 +934,9 @@ impl<T: ArtifactOutput> ArtifactOutput for Project<T> {
         &self,
         contracts: &VersionedContracts,
         sources: &VersionedSourceFiles,
+        ctx: OutputContext,
     ) -> Artifacts<Self::Artifact> {
-        self.artifacts_handler().output_to_artifacts(contracts, sources)
+        self.artifacts_handler().output_to_artifacts(contracts, sources, ctx)
     }
 
     fn standalone_source_file_to_artifact(
diff --git a/ethers-solc/src/resolver/mod.rs b/ethers-solc/src/resolver/mod.rs
index 945e30e8..252af2ca 100644
--- a/ethers-solc/src/resolver/mod.rs
+++ b/ethers-solc/src/resolver/mod.rs
@@ -368,7 +368,20 @@ impl Graph {
                     &mut resolved_solc_include_paths,
                 ) {
                     Ok(import) => {
-                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import)?;
+                        add_node(&mut unresolved, &mut index, &mut resolved_imports, import)
+                            .map_err(|err| {
+                                match err {
+                                    SolcError::Resolve(err) => {
+                                        // make the error more verbose
+                                        SolcError::FailedResolveImport(
+                                            err,
+                                            node.path.clone(),
+                                            import_path.clone(),
+                                        )
+                                    }
+                                    _ => err,
+                                }
+                            })?
                     }
                     Err(err) => {
                         unresolved_imports.insert((import_path.to_path_buf(), node.path.clone()));


Test patch:
diff --git a/ethers-contract/tests/it/abigen.rs b/ethers-contract/tests/it/abigen.rs
index 5dd25efc..e8d2c441 100644
--- a/ethers-contract/tests/it/abigen.rs
+++ b/ethers-contract/tests/it/abigen.rs
@@ -557,17 +557,18 @@ async fn can_abiencoderv2_output() {
     assert_eq!(res, person);
 }
 
-#[test]
-fn can_gen_multi_etherscan() {
-    abigen!(
-        MyContract, "etherscan:0xdAC17F958D2ee523a2206206994597C13D831ec7";
-        MyContract2, "etherscan:0x8418bb725b3ac45ec8fff3791dd8b4e0480cc2a2";
-    );
-
-    let provider = Arc::new(Provider::new(MockProvider::new()));
-    let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
-    let _contract = MyContract2::new(Address::default(), provider);
-}
+// NOTE: this is commented out because this would result in compiler errors if key not set or
+// etherscan API not working #[test]
+// fn can_gen_multi_etherscan() {
+//     abigen!(
+//         MyContract, "etherscan:0xdAC17F958D2ee523a2206206994597C13D831ec7";
+//         MyContract2, "etherscan:0x8418bb725b3ac45ec8fff3791dd8b4e0480cc2a2";
+//     );
+//
+//     let provider = Arc::new(Provider::new(MockProvider::new()));
+//     let _contract = MyContract::new(Address::default(), Arc::clone(&provider));
+//     let _contract = MyContract2::new(Address::default(), provider);
+// }
 
 #[test]
 fn can_gen_reserved_word_field_names() {
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 4944e0ee..41fcb6f6 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -11,8 +11,8 @@ use ethers_solc::{
     info::ContractInfo,
     project_util::*,
     remappings::Remapping,
-    CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project, ProjectCompileOutput,
-    ProjectPathsConfig, Solc, TestFileFilter,
+    Artifact, CompilerInput, ConfigurableArtifacts, ExtraOutputValues, Graph, Project,
+    ProjectCompileOutput, ProjectPathsConfig, Solc, TestFileFilter,
 };
 use pretty_assertions::assert_eq;
 use semver::Version;
@@ -2129,6 +2129,200 @@ fn can_handle_conflicting_files() {
     );
 }
 
+// <https://github.com/foundry-rs/foundry/issues/2843>
+#[test]
+fn can_handle_conflicting_files_recompile() {
+    let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
+
+    project
+        .add_source(
+            "A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function foo() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function bar() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.is_unchanged());
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    let cache = SolFilesCache::read(project.cache_path()).unwrap();
+
+    let mut source_files = cache.files.keys().cloned().collect::<Vec<_>>();
+    source_files.sort_unstable();
+
+    assert_eq!(source_files, vec![PathBuf::from("src/A.sol"), PathBuf::from("src/inner/A.sol"),]);
+
+    let mut artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(artifacts.len(), 2);
+    let mut artifact_files = artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+
+    let expected_files = vec![PathBuf::from("A.sol/A.json"), PathBuf::from("inner/A.sol/A.json")];
+    assert_eq!(artifact_files, expected_files);
+
+    // overwrite conflicting nested file, effectively changing it
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+    contract A {
+    function bar() public{}
+    function baz() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let mut recompiled_artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    recompiled_artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(recompiled_artifacts.len(), 2);
+    let mut artifact_files =
+        recompiled_artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+    assert_eq!(artifact_files, expected_files);
+
+    // ensure that `a.sol/A.json` is unchanged
+    let outer = artifacts.find("src/A.sol", "A").unwrap();
+    let outer_recompiled = recompiled_artifacts.find("src/A.sol", "A").unwrap();
+    assert_eq!(outer, outer_recompiled);
+
+    let inner_recompiled = recompiled_artifacts.find("src/inner/A.sol", "A").unwrap();
+    assert!(inner_recompiled.get_abi().unwrap().functions.contains_key("baz"));
+}
+
+// <https://github.com/foundry-rs/foundry/issues/2843>
+#[test]
+fn can_handle_conflicting_files_case_sensitive_recompile() {
+    let project = TempProject::<ConfigurableArtifacts>::dapptools().unwrap();
+
+    project
+        .add_source(
+            "a",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function foo() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+
+    contract A {
+            function bar() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    // nothing to compile
+    let compiled = project.compile().unwrap();
+    assert!(compiled.is_unchanged());
+    let artifacts = compiled.artifacts().count();
+    assert_eq!(artifacts, 2);
+
+    let cache = SolFilesCache::read(project.cache_path()).unwrap();
+
+    let mut source_files = cache.files.keys().cloned().collect::<Vec<_>>();
+    source_files.sort_unstable();
+
+    assert_eq!(source_files, vec![PathBuf::from("src/a.sol"), PathBuf::from("src/inner/A.sol"),]);
+
+    let mut artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(artifacts.len(), 2);
+    let mut artifact_files = artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+
+    let expected_files = vec![PathBuf::from("a.sol/A.json"), PathBuf::from("inner/A.sol/A.json")];
+    assert_eq!(artifact_files, expected_files);
+
+    // overwrite conflicting nested file, effectively changing it
+    project
+        .add_source(
+            "inner/A",
+            r#"
+    pragma solidity ^0.8.10;
+    contract A {
+    function bar() public{}
+    function baz() public{}
+    }
+   "#,
+        )
+        .unwrap();
+
+    let compiled = project.compile().unwrap();
+    assert!(!compiled.has_compiler_errors());
+
+    let mut recompiled_artifacts =
+        project.artifacts_snapshot().unwrap().artifacts.into_stripped_file_prefixes(project.root());
+    recompiled_artifacts.strip_prefix_all(&project.paths().artifacts);
+
+    assert_eq!(recompiled_artifacts.len(), 2);
+    let mut artifact_files =
+        recompiled_artifacts.artifact_files().map(|f| f.file.clone()).collect::<Vec<_>>();
+    artifact_files.sort_unstable();
+    assert_eq!(artifact_files, expected_files);
+
+    // ensure that `a.sol/A.json` is unchanged
+    let outer = artifacts.find("src/a.sol", "A").unwrap();
+    let outer_recompiled = recompiled_artifacts.find("src/a.sol", "A").unwrap();
+    assert_eq!(outer, outer_recompiled);
+
+    let inner_recompiled = recompiled_artifacts.find("src/inner/A.sol", "A").unwrap();
+    assert!(inner_recompiled.get_abi().unwrap().functions.contains_key("baz"));
+}
+
 #[test]
 fn can_checkout_repo() {
     let project = TempProject::checkout("transmissions11/solmate").unwrap();