Repository: gakonst/ethers-rs
Pull request #1222 (2022-05-04 20:19:34)

Robust 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

Currently most gas oracles construct a default`reqwest::Client`. By default, reqwest does not set a timout, meaning the gas oracles can hang indefinitely if the services is down. (It also prevents configuring proxies, caching and other reqwest options).

There is also no fail over mechanism in case an oracle service is failing.

<!--
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

* Always pass explicit `reqwest::Client` to oracles.
* Add a `Median` aggregator oracle.

<!--
Summarize the solution and provide any necessary context needed to understand
the code change.
-->

To discuss:

1. Modifying constructor arguments is a breaking change. I could have added a new constructor. I opted not to, because one oracle already had a `client` argument. At least now it is more consistent.
2. Is it correct to take separate medians for max_fee and priority_fee for EIP1559? This could mix responses from two different oracles.
3. gasnow.org seems shut down. Should we remove it?
4. Etherchain recommends a new gasnow style mempool based API: https://etherchain.org/tools/gasnow should we add it?

To do:

- [ ] Add an example that constructs a gas oracle with all bells and whistles (multiple sources, timeouts, caching, etc).
- [x]  Add a cache layer that stores gas prices for some time.
- [x]  Maybe a failover aggregator instead of median? i.e. use the first source that doesn't fail.
- [ ] A `default_from_chain` function that creates sensible defaults. Maybe a Middleware that recognizes the chain from chainId and constructs this default.
- [x] https://gasstation-mainnet.matic.network/v2
- [x] https://gasstation-mumbai.matic.today/v2
- [ ] https://blockscout.com/xdai/mainnet/api/v1/gas-price-oracle
- [ ] https://community.optimism.io/docs/useful-tools/oracles/
- [ ] Oracle based on `eth_gasPrice`

## PR Checklist

- [ ] Added Tests
- [ ] Added Documentation
- [ ] Updated the changelog


recmo: > * Maybe a failover aggregator instead of median? i.e. use the first source that doesn't fail.

I've added a weighted median instead, which allows you to mimic failover by assigning high weights to prioritized sources. Also allows you to implement more complex strategies.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5def5468..810d78da 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -153,6 +153,8 @@
 
 ### Unreleased
 
+- Add retries to the pending transaction future
+  [1221](https://github.com/gakonst/ethers-rs/pull/1221)
 - Add support for basic and bearer authentication in http and non-wasm websockets.
   [829](https://github.com/gakonst/ethers-rs/pull/829)
 - Export `ethers_providers::IpcError` and `ethers_providers::QuorumError`
diff --git a/Cargo.lock b/Cargo.lock
index 6909794e..3d5770f6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1292,6 +1292,7 @@ dependencies = [
  "ethers-providers",
  "ethers-signers",
  "ethers-solc",
+ "futures-locks",
  "futures-util",
  "hex",
  "instant",
@@ -1588,6 +1589,17 @@ version = "0.3.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b"
 
+[[package]]
+name = "futures-locks"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eb42d4fb72227be5778429f9ef5240a38a358925a49f05b5cf702ce7c7e558a"
+dependencies = [
+ "futures-channel",
+ "futures-task",
+ "tokio",
+]
+
 [[package]]
 name = "futures-macro"
 version = "0.3.21"
diff --git a/ethers-contract/ethers-contract-abigen/src/contract.rs b/ethers-contract/ethers-contract-abigen/src/contract.rs
index 2dd4a4fa..288af663 100644
--- a/ethers-contract/ethers-contract-abigen/src/contract.rs
+++ b/ethers-contract/ethers-contract-abigen/src/contract.rs
@@ -133,7 +133,7 @@ impl Context {
         let contract = quote! {
                 #struct_decl
 
-                impl<'a, M: #ethers_providers::Middleware> #name<M> {
+                impl<M: #ethers_providers::Middleware> #name<M> {
                     /// Creates a new contract instance with the specified `ethers`
                     /// client at the given `Address`. The contract derefs to a `ethers::Contract`
                     /// object
diff --git a/ethers-contract/src/multicall/multicall_contract.rs b/ethers-contract/src/multicall/multicall_contract.rs
index c83753d0..33ccc625 100644
--- a/ethers-contract/src/multicall/multicall_contract.rs
+++ b/ethers-contract/src/multicall/multicall_contract.rs
@@ -33,7 +33,7 @@ mod multicallcontract_mod {
             f.debug_tuple(stringify!(MulticallContract)).field(&self.address()).finish()
         }
     }
-    impl<'a, M: Middleware> MulticallContract<M> {
+    impl<M: Middleware> MulticallContract<M> {
         #[doc = r" Creates a new contract instance with the specified `ethers`"]
         #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"]
         #[doc = r" object"]
diff --git a/ethers-core/src/types/mod.rs b/ethers-core/src/types/mod.rs
index 4c3b8a12..b19273ff 100644
--- a/ethers-core/src/types/mod.rs
+++ b/ethers-core/src/types/mod.rs
@@ -21,6 +21,9 @@ pub use address_or_bytes::AddressOrBytes;
 mod path_or_string;
 pub use path_or_string::PathOrString;
 
+mod u256;
+pub use u256::*;
+
 mod i256;
 pub use i256::{Sign, I256};
 
diff --git a/ethers-core/src/types/u256.rs b/ethers-core/src/types/u256.rs
new file mode 100644
index 00000000..6d3187ff
--- /dev/null
+++ b/ethers-core/src/types/u256.rs
@@ -0,0 +1,121 @@
+use ethabi::ethereum_types::U256;
+
+/// Convert a floating point value to its nearest f64 integer.
+///
+/// It is saturating, so values $\ge 2^{256}$ will be rounded
+/// to [`U256::max_value()`] and values $< 0$ to zero. This includes
+/// positive and negative infinity.
+///
+/// TODO: Move to ethabi::ethereum_types::U256.
+/// TODO: Add [`super::I256`] version.
+///
+/// # Panics
+///
+/// Panics if `f` is NaN.
+pub fn u256_from_f64_saturating(mut f: f64) -> U256 {
+    if f.is_nan() {
+        panic!("NaN is not a valid value for U256");
+    }
+    if f < 0.5 {
+        return U256::zero()
+    }
+    if f >= 1.157_920_892_373_162e77_f64 {
+        return U256::max_value()
+    }
+    // All non-normal cases should have been handled above
+    assert!(f.is_normal());
+    // Turn nearest rounding into truncated rounding
+    f += 0.5;
+
+    // Parse IEEE-754 double into U256
+    // Sign should be zero, exponent should be >= 0.
+    let bits = f.to_bits();
+    let sign = bits >> 63;
+    assert!(sign == 0);
+    let biased_exponent = (bits >> 52) & 0x7ff;
+    assert!(biased_exponent >= 1023);
+    let exponent = biased_exponent - 1023;
+    let fraction = bits & 0xfffffffffffff;
+    let mantissa = 0x10000000000000 | fraction;
+    if exponent > 255 {
+        U256::max_value()
+    } else if exponent < 52 {
+        // Truncate mantissa
+        U256([mantissa, 0, 0, 0]) >> (52 - exponent)
+    } else {
+        U256([mantissa, 0, 0, 0]) << (exponent - 52)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use std::f64;
+
+    #[test]
+    fn test_small_integers() {
+        for i in 0..=255 {
+            let f = i as f64;
+            let u = u256_from_f64_saturating(f);
+            assert_eq!(u, U256::from(i));
+        }
+    }
+
+    #[test]
+    fn test_small_integers_round_down() {
+        for i in 0..=255 {
+            let f = (i as f64) + 0.499;
+            let u = u256_from_f64_saturating(f);
+            assert_eq!(u, U256::from(i));
+        }
+    }
+
+    #[test]
+    fn test_small_integers_round_up() {
+        for i in 0..=255 {
+            let f = (i as f64) - 0.5;
+            let u = u256_from_f64_saturating(f);
+            assert_eq!(u, U256::from(i));
+        }
+    }
+
+    #[test]
+    fn test_infinities() {
+        assert_eq!(u256_from_f64_saturating(f64::INFINITY), U256::max_value());
+        assert_eq!(u256_from_f64_saturating(f64::NEG_INFINITY), U256::zero());
+    }
+
+    #[test]
+    fn test_saturating() {
+        assert_eq!(u256_from_f64_saturating(-1.0), U256::zero());
+        assert_eq!(u256_from_f64_saturating(1e90_f64), U256::max_value());
+    }
+
+    #[test]
+    fn test_large() {
+        // Check with e.g. `python3 -c 'print(int(1.0e36))'`
+        assert_eq!(
+            u256_from_f64_saturating(1.0e36_f64),
+            U256::from_dec_str("1000000000000000042420637374017961984").unwrap()
+        );
+        assert_eq!(
+            u256_from_f64_saturating(f64::consts::PI * 2.0e60_f64),
+            U256::from_dec_str("6283185307179586084560863929317662625677330590403879287914496")
+                .unwrap()
+        );
+        assert_eq!(
+            u256_from_f64_saturating(5.78960446186581e76_f64),
+            U256::from_dec_str(
+                "57896044618658097711785492504343953926634992332820282019728792003956564819968"
+            )
+            .unwrap()
+        );
+        assert_eq!(
+            u256_from_f64_saturating(1.157920892373161e77_f64),
+            U256::from_dec_str(
+                "115792089237316105435040506505232477503392813560534822796089932171514352762880"
+            )
+            .unwrap()
+        );
+    }
+}
diff --git a/ethers-core/src/utils/anvil.rs b/ethers-core/src/utils/anvil.rs
index fe75514a..37a15ebd 100644
--- a/ethers-core/src/utils/anvil.rs
+++ b/ethers-core/src/utils/anvil.rs
@@ -51,7 +51,7 @@ impl AnvilInstance {
 
 impl Drop for AnvilInstance {
     fn drop(&mut self) {
-        let _ = self.pid.kill().expect("could not kill anvil");
+        self.pid.kill().expect("could not kill anvil");
     }
 }
 
diff --git a/ethers-core/src/utils/ganache.rs b/ethers-core/src/utils/ganache.rs
index dfea1017..d591ee49 100644
--- a/ethers-core/src/utils/ganache.rs
+++ b/ethers-core/src/utils/ganache.rs
@@ -9,7 +9,7 @@ use std::{
     time::{Duration, Instant},
 };
 
-/// How long we will wait for ganache to indicate that it is ready.
+/// Default amount of time we will wait for ganache to indicate that it is ready.
 const GANACHE_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
 
 /// A ganache CLI instance. Will close the instance when dropped.
@@ -51,7 +51,7 @@ impl GanacheInstance {
 
 impl Drop for GanacheInstance {
     fn drop(&mut self) {
-        let _ = self.pid.kill().expect("could not kill ganache");
+        self.pid.kill().expect("could not kill ganache");
     }
 }
 
@@ -83,6 +83,7 @@ pub struct Ganache {
     mnemonic: Option<String>,
     fork: Option<String>,
     args: Vec<String>,
+    startup_timeout: Option<u64>,
 }
 
 impl Ganache {
@@ -92,6 +93,13 @@ impl Ganache {
         Self::default()
     }
 
+    /// Sets the startup timeout which will be used when the `ganache-cli` instance is launched in
+    /// miliseconds. 10_000 miliseconds by default).
+    pub fn startup_timeout_millis<T: Into<u64>>(mut self, timeout: T) -> Self {
+        self.startup_timeout = Some(timeout.into());
+        self
+    }
+
     /// Sets the port which will be used when the `ganache-cli` instance is launched.
     #[must_use]
     pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
@@ -176,8 +184,11 @@ impl Ganache {
         let mut private_keys = Vec::new();
         let mut addresses = Vec::new();
         let mut is_private_key = false;
+
+        let startup_timeout =
+            Duration::from_millis(self.startup_timeout.unwrap_or(GANACHE_STARTUP_TIMEOUT_MILLIS));
         loop {
-            if start + Duration::from_millis(GANACHE_STARTUP_TIMEOUT_MILLIS) <= Instant::now() {
+            if start + startup_timeout <= Instant::now() {
                 panic!("Timed out waiting for ganache to start. Is ganache-cli installed?")
             }
 
@@ -205,3 +216,18 @@ impl Ganache {
         GanacheInstance { pid: child, private_keys, addresses, port }
     }
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn configurable_startup_timeout() {
+        Ganache::new().startup_timeout_millis(100000_u64).spawn();
+    }
+
+    #[test]
+    fn default_startup_works() {
+        Ganache::new().spawn();
+    }
+}
diff --git a/ethers-core/src/utils/geth.rs b/ethers-core/src/utils/geth.rs
index 9016bc8f..2a65ab94 100644
--- a/ethers-core/src/utils/geth.rs
+++ b/ethers-core/src/utils/geth.rs
@@ -47,7 +47,7 @@ impl GethInstance {
 
 impl Drop for GethInstance {
     fn drop(&mut self) {
-        let _ = self.pid.kill().expect("could not kill geth");
+        self.pid.kill().expect("could not kill geth");
     }
 }
 
diff --git a/ethers-core/src/utils/moonbeam.rs b/ethers-core/src/utils/moonbeam.rs
index 67af8be7..761a6e4c 100644
--- a/ethers-core/src/utils/moonbeam.rs
+++ b/ethers-core/src/utils/moonbeam.rs
@@ -4,7 +4,7 @@ use std::collections::BTreeMap;
 
 use k256::SecretKey;
 
-/// Returns the private developer keys <https://docs.moonbeam.network/snippets/code/setting-up-node/dev-accounts/>
+/// Returns the private developer keys <https://docs.moonbeam.network/builders/get-started/networks/moonbeam-dev/#pre-funded-development-accounts>
 pub fn dev_keys() -> Vec<SecretKey> {
     MoonbeamDev::default().into_keys().collect()
 }
diff --git a/ethers-etherscan/src/contract.rs b/ethers-etherscan/src/contract.rs
index b390a5ab..b7bb7da7 100644
--- a/ethers-etherscan/src/contract.rs
+++ b/ethers-etherscan/src/contract.rs
@@ -305,14 +305,14 @@ impl Client {
         }
         if resp.result.starts_with("Contract source code not verified") {
             if let Some(ref cache) = self.cache {
-                let _ = cache.set_abi(address, None);
+                cache.set_abi(address, None);
             }
             return Err(EtherscanError::ContractCodeNotVerified(address))
         }
         let abi = serde_json::from_str(&resp.result)?;
 
         if let Some(ref cache) = self.cache {
-            let _ = cache.set_abi(address, Some(&abi));
+            cache.set_abi(address, Some(&abi));
         }
 
         Ok(abi)
@@ -350,14 +350,14 @@ impl Client {
         let response: Response<Vec<Metadata>> = self.get_json(&query).await?;
         if response.result.iter().any(|item| item.abi == "Contract source code not verified") {
             if let Some(ref cache) = self.cache {
-                let _ = cache.set_source(address, None);
+                cache.set_source(address, None);
             }
             return Err(EtherscanError::ContractCodeNotVerified(address))
         }
         let res = ContractMetadata { items: response.result };
 
         if let Some(ref cache) = self.cache {
-            let _ = cache.set_source(address, Some(&res));
+            cache.set_source(address, Some(&res));
         }
 
         Ok(res)
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index 5cf39a1d..e16d26ff 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -24,6 +24,7 @@ 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 }
 futures-util = { version = "^0.3" }
+futures-locks = { version = "0.7" }
 tracing = { version = "0.1.34", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false }
 
diff --git a/ethers-middleware/src/gas_oracle/blocknative.rs b/ethers-middleware/src/gas_oracle/blocknative.rs
index a7afb77c..11832c5a 100644
--- a/ethers-middleware/src/gas_oracle/blocknative.rs
+++ b/ethers-middleware/src/gas_oracle/blocknative.rs
@@ -1,12 +1,9 @@
 use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError, GWEI_TO_WEI};
 use async_trait::async_trait;
 use ethers_core::types::U256;
-use reqwest::{
-    header::{HeaderMap, HeaderValue, AUTHORIZATION},
-    Client, ClientBuilder,
-};
+use reqwest::{header::AUTHORIZATION, Client};
 use serde::Deserialize;
-use std::{collections::HashMap, convert::TryInto, iter::FromIterator};
+use std::{collections::HashMap, convert::TryInto};
 use url::Url;
 
 const BLOCKNATIVE_GAS_PRICE_ENDPOINT: &str = "https://api.blocknative.com/gasprices/blockprices";
@@ -26,6 +23,7 @@ fn gas_category_to_confidence(gas_category: &GasCategory) -> u64 {
 pub struct BlockNative {
     client: Client,
     url: Url,
+    api_key: String,
     gas_category: GasCategory,
 }
 
@@ -84,13 +82,16 @@ pub struct BaseFeeEstimate {
 }
 
 impl BlockNative {
-    /// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle
-    pub fn new(api_key: &str) -> Self {
-        let header_value = HeaderValue::from_str(api_key).unwrap();
-        let headers = HeaderMap::from_iter([(AUTHORIZATION, header_value)]);
-        let client = ClientBuilder::new().default_headers(headers).build().unwrap();
+    /// Creates a new [BlockNative](https://www.blocknative.com/gas-estimator) gas oracle.
+    pub fn new(api_key: String) -> Self {
+        Self::with_client(Client::new(), api_key)
+    }
+
+    /// Same as [`Self::new`] but with a custom [`Client`].
+    pub fn with_client(client: Client, api_key: String) -> Self {
         Self {
             client,
+            api_key,
             url: BLOCKNATIVE_GAS_PRICE_ENDPOINT.try_into().unwrap(),
             gas_category: GasCategory::Standard,
         }
@@ -105,7 +106,15 @@ impl BlockNative {
 
     /// Perform request to Blocknative, decode response
     pub async fn request(&self) -> Result<BlockNativeGasResponse, GasOracleError> {
-        Ok(self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?)
+        self.client
+            .get(self.url.as_ref())
+            .header(AUTHORIZATION, &self.api_key)
+            .send()
+            .await?
+            .error_for_status()?
+            .json()
+            .await
+            .map_err(GasOracleError::HttpClientError)
     }
 }
 
diff --git a/ethers-middleware/src/gas_oracle/cache.rs b/ethers-middleware/src/gas_oracle/cache.rs
new file mode 100644
index 00000000..6f8b162b
--- /dev/null
+++ b/ethers-middleware/src/gas_oracle/cache.rs
@@ -0,0 +1,70 @@
+use crate::gas_oracle::{GasOracle, GasOracleError};
+use async_trait::async_trait;
+use ethers_core::types::U256;
+use futures_locks::RwLock;
+use std::{
+    fmt::Debug,
+    future::Future,
+    time::{Duration, Instant},
+};
+
+#[derive(Debug)]
+pub struct Cache<T: GasOracle> {
+    inner: T,
+    validity: Duration,
+    fee: Cached<U256>,
+    eip1559: Cached<(U256, U256)>,
+}
+
+#[derive(Default, Debug)]
+struct Cached<T: Clone>(RwLock<Option<(Instant, T)>>);
+
+impl<T: Clone> Cached<T> {
+    async fn get<F, E, Fut>(&self, validity: Duration, fetch: F) -> Result<T, E>
+    where
+        F: FnOnce() -> Fut,
+        Fut: Future<Output = Result<T, E>>,
+    {
+        // Try with a read lock
+        {
+            let lock = self.0.read().await;
+            if let Some((last_fetch, value)) = lock.as_ref() {
+                if Instant::now().duration_since(*last_fetch) < validity {
+                    return Ok(value.clone())
+                }
+            }
+        }
+        // Acquire a write lock
+        {
+            let mut lock = self.0.write().await;
+            // Check again, a concurrent thread may have raced us to the write.
+            if let Some((last_fetch, value)) = lock.as_ref() {
+                if Instant::now().duration_since(*last_fetch) < validity {
+                    return Ok(value.clone())
+                }
+            }
+            // Set a fresh value
+            let value = fetch().await?;
+            *lock = Some((Instant::now(), value.clone()));
+            Ok(value)
+        }
+    }
+}
+
+impl<T: GasOracle> Cache<T> {
+    pub fn new(validity: Duration, inner: T) -> Self {
+        Self { inner, validity, fee: Cached::default(), eip1559: Cached::default() }
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<T: GasOracle> GasOracle for Cache<T> {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        self.fee.get(self.validity, || self.inner.fetch()).await
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        self.eip1559.get(self.validity, || self.inner.estimate_eip1559_fees()).await
+    }
+}
diff --git a/ethers-middleware/src/gas_oracle/eth_gas_station.rs b/ethers-middleware/src/gas_oracle/eth_gas_station.rs
index 6e2510c5..683caf37 100644
--- a/ethers-middleware/src/gas_oracle/eth_gas_station.rs
+++ b/ethers-middleware/src/gas_oracle/eth_gas_station.rs
@@ -61,15 +61,17 @@ pub struct EthGasStationResponse {
 
 impl EthGasStation {
     /// Creates a new [EthGasStation](https://docs.ethgasstation.info/) gas oracle
-    pub fn new(api_key: Option<&'static str>) -> Self {
-        let url = match api_key {
-            Some(key) => format!("{}?api-key={}", ETH_GAS_STATION_URL_PREFIX, key),
-            None => ETH_GAS_STATION_URL_PREFIX.to_string(),
-        };
-
-        let url = Url::parse(&url).expect("invalid url");
+    pub fn new(api_key: Option<&str>) -> Self {
+        Self::with_client(Client::new(), api_key)
+    }
 
-        EthGasStation { client: Client::new(), url, gas_category: GasCategory::Standard }
+    /// Same as [`Self::new`] but with a custom [`Client`].
+    pub fn with_client(client: Client, api_key: Option<&str>) -> Self {
+        let mut url = Url::parse(ETH_GAS_STATION_URL_PREFIX).expect("invalid url");
+        if let Some(key) = api_key {
+            url.query_pairs_mut().append_pair("api-key", key);
+        }
+        EthGasStation { client, url, gas_category: GasCategory::Standard }
     }
 
     /// Sets the gas price category to be used when fetching the gas price.
@@ -84,6 +86,12 @@ impl EthGasStation {
     }
 }
 
+impl Default for EthGasStation {
+    fn default() -> Self {
+        Self::new(None)
+    }
+}
+
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl GasOracle for EthGasStation {
diff --git a/ethers-middleware/src/gas_oracle/etherchain.rs b/ethers-middleware/src/gas_oracle/etherchain.rs
index f74187a8..3fc94e6a 100644
--- a/ethers-middleware/src/gas_oracle/etherchain.rs
+++ b/ethers-middleware/src/gas_oracle/etherchain.rs
@@ -18,12 +18,6 @@ pub struct Etherchain {
     gas_category: GasCategory,
 }
 
-impl Default for Etherchain {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 #[derive(Clone, Debug, Deserialize, PartialEq, PartialOrd)]
 #[serde(rename_all = "camelCase")]
 pub struct EtherchainResponse {
@@ -38,9 +32,14 @@ pub struct EtherchainResponse {
 impl Etherchain {
     /// Creates a new [Etherchain](https://etherchain.org/tools/gasPriceOracle) gas price oracle.
     pub fn new() -> Self {
+        Self::with_client(Client::new())
+    }
+
+    /// Same as [`Self::new`] but with a custom [`Client`].
+    pub fn with_client(client: Client) -> Self {
         let url = Url::parse(ETHERCHAIN_URL).expect("invalid url");
 
-        Etherchain { client: Client::new(), url, gas_category: GasCategory::Standard }
+        Etherchain { client, url, gas_category: GasCategory::Standard }
     }
 
     /// Sets the gas price category to be used when fetching the gas price.
@@ -55,6 +54,12 @@ impl Etherchain {
     }
 }
 
+impl Default for Etherchain {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl GasOracle for Etherchain {
diff --git a/ethers-middleware/src/gas_oracle/gas_now.rs b/ethers-middleware/src/gas_oracle/gas_now.rs
index 77e4b6aa..a1444ff1 100644
--- a/ethers-middleware/src/gas_oracle/gas_now.rs
+++ b/ethers-middleware/src/gas_oracle/gas_now.rs
@@ -7,9 +7,9 @@ use url::Url;
 
 use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
 
-const GAS_NOW_URL: &str = "https://www.gasnow.org/api/v3/gas/price";
+const GAS_NOW_URL: &str = "https://etherchain.org/api/gasnow";
 
-/// A client over HTTP for the [GasNow](https://www.gasnow.org/api/v1/gas/price) gas tracker API
+/// A client over HTTP for the [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas tracker API
 /// that implements the `GasOracle` trait
 #[derive(Clone, Debug)]
 pub struct GasNow {
@@ -18,12 +18,6 @@ pub struct GasNow {
     gas_category: GasCategory,
 }
 
-impl Default for GasNow {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
 #[derive(Deserialize)]
 struct GasNowResponseWrapper {
     data: GasNowResponse,
@@ -38,11 +32,16 @@ pub struct GasNowResponse {
 }
 
 impl GasNow {
-    /// Creates a new [GasNow](https://gasnow.org) gas price oracle.
+    /// Creates a new [Etherchain GasNow](https://etherchain.org/tools/gasnow) gas price oracle.
     pub fn new() -> Self {
+        Self::with_client(Client::new())
+    }
+
+    /// Same as [`Self::new`] but with a custom [`Client`].
+    pub fn with_client(client: Client) -> Self {
         let url = Url::parse(GAS_NOW_URL).expect("invalid url");
 
-        Self { client: Client::new(), url, gas_category: GasCategory::Standard }
+        Self { url, gas_category: GasCategory::Standard }
     }
 
     /// Sets the gas price category to be used when fetching the gas price.
@@ -63,6 +62,12 @@ impl GasNow {
     }
 }
 
+impl Default for GasNow {
+    fn default() -> Self {
+        Self::new(Client::new())
+    }
+}
+
 #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
 #[cfg_attr(not(target_arch = "wasm32"), async_trait)]
 impl GasOracle for GasNow {
diff --git a/ethers-middleware/src/gas_oracle/median.rs b/ethers-middleware/src/gas_oracle/median.rs
new file mode 100644
index 00000000..2149ce41
--- /dev/null
+++ b/ethers-middleware/src/gas_oracle/median.rs
@@ -0,0 +1,125 @@
+use crate::gas_oracle::{GasOracle, GasOracleError};
+use async_trait::async_trait;
+use ethers_core::types::U256;
+use futures_util::future::join_all;
+use std::{fmt::Debug, future::Future};
+use tracing::warn;
+
+#[derive(Default, Debug)]
+pub struct Median {
+    oracles: Vec<(f32, Box<dyn GasOracle>)>,
+}
+
+/// Computes the median gas price from a selection of oracles.
+///
+/// Don't forget to set a timeout on the source oracles. By default
+/// the reqwest based oracles will never time out.
+impl Median {
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    pub fn add<T: 'static + GasOracle>(&mut self, oracle: T) {
+        self.add_weighted(1.0, oracle)
+    }
+
+    pub fn add_weighted<T: 'static + GasOracle>(&mut self, weight: f32, oracle: T) {
+        assert!(weight > 0.0);
+        self.oracles.push((weight, Box::new(oracle)));
+    }
+
+    pub async fn query_all<'a, Fn, Fut, O>(
+        &'a self,
+        mut f: Fn,
+    ) -> Result<Vec<(f32, O)>, GasOracleError>
+    where
+        Fn: FnMut(&'a dyn GasOracle) -> Fut,
+        Fut: Future<Output = Result<O, GasOracleError>>,
+    {
+        // Process the oracles in parallel
+        let futures = self.oracles.iter().map(|(_, oracle)| f(oracle.as_ref()));
+        let results = join_all(futures).await;
+
+        // Filter out any errors
+        let values =
+            self.oracles.iter().zip(results).filter_map(
+                |((weight, oracle), result)| match result {
+                    Ok(value) => Some((*weight, value)),
+                    Err(err) => {
+                        warn!("Failed to fetch gas price from {:?}: {}", oracle, err);
+                        None
+                    }
+                },
+            );
+        let values = values.collect::<Vec<_>>();
+        if values.is_empty() {
+            return Err(GasOracleError::NoValues)
+        }
+        Ok(values)
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl GasOracle for Median {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        let mut values = self.query_all(|oracle| oracle.fetch()).await?;
+        // `query_all` guarantees `values` is not empty
+        Ok(*weighted_fractile_by_key(0.5, &mut values, |fee| fee).unwrap())
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        let mut values = self.query_all(|oracle| oracle.estimate_eip1559_fees()).await?;
+        // `query_all` guarantees `values` is not empty
+        Ok((
+            weighted_fractile_by_key(0.5, &mut values, |(max_fee, _)| max_fee).unwrap().0,
+            weighted_fractile_by_key(0.5, &mut values, |(_, priority_fee)| priority_fee).unwrap().1,
+        ))
+    }
+}
+
+/// Weighted fractile by key
+///
+/// Sort the values in place by key and return the weighted fractile value such
+/// that `fractile` fraction of the values by weight are less than or equal to
+/// the value.
+///
+/// Returns None if the values are empty.
+///
+/// Note: it doesn't handle NaNs or other special float values.
+///
+/// See <https://en.wikipedia.org/wiki/Percentile#The_weighted_percentile_method>
+///
+/// # Panics
+///
+/// Panics if [`fractile`] is not in the range $[0, 1]$.
+fn weighted_fractile_by_key<'a, T, F, K>(
+    fractile: f32,
+    values: &'a mut [(f32, T)],
+    mut key: F,
+) -> Option<&'a T>
+where
+    F: for<'b> FnMut(&'b T) -> &'b K,
+    K: Ord,
+{
+    assert!((0.0..=1.0).contains(&fractile));
+    if values.is_empty() {
+        return None
+    }
+    let weight_rank = fractile * values.iter().map(|(weight, _)| *weight).sum::<f32>();
+    values.sort_unstable_by(|a, b| key(&a.1).cmp(key(&b.1)));
+    let mut cumulative_weight = 0.0_f32;
+    for (weight, value) in values.iter() {
+        cumulative_weight += *weight;
+        if cumulative_weight >= weight_rank {
+            return Some(value)
+        }
+    }
+    // By the last element, cumulative_weight == weight_rank and we should have
+    // returned already. Assume there is a slight rounding error causing
+    // cumulative_weight to be slightly less than expected. In this case the last
+    // element is appropriate. (This is not exactly right, since the last
+    // elements may have zero weight.)
+    // `values` is not empty.
+    Some(&values.last().unwrap().1)
+}
diff --git a/ethers-middleware/src/gas_oracle/mod.rs b/ethers-middleware/src/gas_oracle/mod.rs
index 59170a39..155c277c 100644
--- a/ethers-middleware/src/gas_oracle/mod.rs
+++ b/ethers-middleware/src/gas_oracle/mod.rs
@@ -13,6 +13,15 @@ pub use etherscan::Etherscan;
 mod middleware;
 pub use middleware::{GasOracleMiddleware, MiddlewareError};
 
+mod median;
+pub use median::Median;
+
+mod cache;
+pub use cache::Cache;
+
+mod polygon;
+pub use polygon::Polygon;
+
 use ethers_core::types::U256;
 
 use async_trait::async_trait;
@@ -58,6 +67,12 @@ pub enum GasOracleError {
 
     #[error("EIP-1559 gas estimation not supported")]
     Eip1559EstimationNotSupported,
+
+    #[error("None of the oracles returned a value")]
+    NoValues,
+
+    #[error("Chain is not supported by the oracle")]
+    UnsupportedChain,
 }
 
 /// `GasOracle` is a trait that an underlying gas oracle needs to implement.
diff --git a/ethers-middleware/src/gas_oracle/polygon.rs b/ethers-middleware/src/gas_oracle/polygon.rs
new file mode 100644
index 00000000..a450a3ab
--- /dev/null
+++ b/ethers-middleware/src/gas_oracle/polygon.rs
@@ -0,0 +1,91 @@
+use crate::gas_oracle::{GasCategory, GasOracle, GasOracleError};
+use async_trait::async_trait;
+use ethers_core::types::{u256_from_f64_saturating, Chain, U256};
+use reqwest::Client;
+use serde::Deserialize;
+use url::Url;
+
+const GAS_PRICE_ENDPOINT: &str = "https://gasstation-mainnet.matic.network/v2";
+const MUMBAI_GAS_PRICE_ENDPOINT: &str = "https://gasstation-mumbai.matic.today/v2";
+
+/// The [Polygon](https://docs.polygon.technology/docs/develop/tools/polygon-gas-station/) gas station API
+/// Queries over HTTP and implements the `GasOracle` trait
+#[derive(Clone, Debug)]
+pub struct Polygon {
+    client: Client,
+    url: Url,
+    gas_category: GasCategory,
+}
+
+/// The response from the Polygon gas station API.
+/// Gas prices are in Gwei.
+#[derive(Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct Response {
+    estimated_base_fee: f64,
+    safe_low: GasEstimate,
+    standard: GasEstimate,
+    fast: GasEstimate,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq)]
+#[serde(rename_all = "camelCase")]
+pub struct GasEstimate {
+    max_priority_fee: f64,
+    max_fee: f64,
+}
+
+impl Polygon {
+    pub fn new(chain: Chain) -> Result<Self, GasOracleError> {
+        Self::with_client(Client::new(), chain)
+    }
+
+    pub fn with_client(client: Client, chain: Chain) -> Result<Self, GasOracleError> {
+        // TODO: Sniff chain from chain id.
+        let url = match chain {
+            Chain::Polygon => Url::parse(GAS_PRICE_ENDPOINT).unwrap(),
+            Chain::PolygonMumbai => Url::parse(MUMBAI_GAS_PRICE_ENDPOINT).unwrap(),
+            _ => return Err(GasOracleError::UnsupportedChain),
+        };
+        Ok(Self { client, url, gas_category: GasCategory::Standard })
+    }
+
+    /// Sets the gas price category to be used when fetching the gas price.
+    #[must_use]
+    pub fn category(mut self, gas_category: GasCategory) -> Self {
+        self.gas_category = gas_category;
+        self
+    }
+
+    /// Perform request to Blocknative, decode response
+    pub async fn request(&self) -> Result<(f64, GasEstimate), GasOracleError> {
+        let response: Response =
+            self.client.get(self.url.as_ref()).send().await?.error_for_status()?.json().await?;
+        let estimate = match self.gas_category {
+            GasCategory::SafeLow => response.safe_low,
+            GasCategory::Standard => response.standard,
+            GasCategory::Fast => response.fast,
+            GasCategory::Fastest => response.fast,
+        };
+        Ok((response.estimated_base_fee, estimate))
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl GasOracle for Polygon {
+    async fn fetch(&self) -> Result<U256, GasOracleError> {
+        let (base_fee, estimate) = self.request().await?;
+        let fee = base_fee + estimate.max_priority_fee;
+        Ok(from_gwei(fee))
+    }
+
+    async fn estimate_eip1559_fees(&self) -> Result<(U256, U256), GasOracleError> {
+        let (_, estimate) = self.request().await?;
+        Ok((from_gwei(estimate.max_fee), from_gwei(estimate.max_priority_fee)))
+    }
+}
+
+fn from_gwei(gwei: f64) -> U256 {
+    u256_from_f64_saturating(gwei * 1.0e18_f64)
+}
diff --git a/ethers-middleware/src/transformer/ds_proxy/factory.rs b/ethers-middleware/src/transformer/ds_proxy/factory.rs
index 4c5b05e2..8de0c88f 100644
--- a/ethers-middleware/src/transformer/ds_proxy/factory.rs
+++ b/ethers-middleware/src/transformer/ds_proxy/factory.rs
@@ -63,7 +63,7 @@ mod dsproxyfactory_mod {
             f.debug_tuple(stringify!(DsProxyFactory)).field(&self.address()).finish()
         }
     }
-    impl<'a, M: Middleware> DsProxyFactory<M> {
+    impl<M: Middleware> DsProxyFactory<M> {
         #[doc = r" Creates a new contract instance with the specified `ethers`"]
         #[doc = r" client at the given `Address`. The contract derefs to a `ethers::Contract`"]
         #[doc = r" object"]
diff --git a/ethers-providers/src/pending_escalator.rs b/ethers-providers/src/pending_escalator.rs
index b813f986..3dca65fb 100644
--- a/ethers-providers/src/pending_escalator.rs
+++ b/ethers-providers/src/pending_escalator.rs
@@ -190,7 +190,7 @@ where
                 poll_broadcast_fut!(cx, this, fut);
             }
             Sleeping(delay) => {
-                let _ready = futures_util::ready!(delay.as_mut().poll(cx));
+                futures_util::ready!(delay.as_mut().poll(cx));
                 // if broadcast timer has elapsed and if we have a TX to
                 // broadcast, broadcast it
                 if this.last.elapsed() > *this.broadcast_interval {
diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/pending_transaction.rs
index 0e1a8bef..68757aa8 100644
--- a/ethers-providers/src/pending_transaction.rs
+++ b/ethers-providers/src/pending_transaction.rs
@@ -62,8 +62,11 @@ pub struct PendingTransaction<'a, P> {
     provider: &'a Provider<P>,
     state: PendingTxState<'a>,
     interval: Box<dyn Stream<Item = ()> + Send + Unpin>,
+    retries_remaining: usize,
 }
 
+const DEFAULT_RETRIES: usize = 3;
+
 impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
     /// Creates a new pending transaction poller from a hash and a provider
     pub fn new(tx_hash: TxHash, provider: &'a Provider<P>) -> Self {
@@ -74,6 +77,7 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
             provider,
             state: PendingTxState::InitialDelay(delay),
             interval: Box::new(interval(DEFAULT_POLL_INTERVAL)),
+            retries_remaining: DEFAULT_RETRIES,
         }
     }
 
@@ -111,6 +115,13 @@ impl<'a, P: JsonRpcClient> PendingTransaction<'a, P> {
 
         self
     }
+
+    /// Set retries
+    #[must_use]
+    pub fn retries(mut self, retries: usize) -> Self {
+        self.retries_remaining = retries;
+        self
+    }
 }
 
 impl<'a, P> PendingTransaction<'a, P> {
@@ -161,7 +172,7 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
 
         match this.state {
             PendingTxState::InitialDelay(fut) => {
-                let _ready = futures_util::ready!(fut.as_mut().poll(ctx));
+                futures_util::ready!(fut.as_mut().poll(ctx));
                 tracing::debug!("Starting to poll pending tx {:?}", *this.tx_hash);
                 let fut = Box::pin(this.provider.get_transaction(*this.tx_hash));
                 rewake_with_new_state!(ctx, this, PendingTxState::GettingTx(fut));
@@ -188,9 +199,14 @@ impl<'a, P: JsonRpcClient> Future for PendingTransaction<'a, P> {
                 let tx_opt = tx_res.unwrap();
                 // If the tx is no longer in the mempool, return Ok(None)
                 if tx_opt.is_none() {
-                    tracing::debug!("Dropped from mempool, pending tx {:?}", *this.tx_hash);
-                    *this.state = PendingTxState::Completed;
-                    return Poll::Ready(Ok(None))
+                    if *this.retries_remaining == 0 {
+                        tracing::debug!("Dropped from mempool, pending tx {:?}", *this.tx_hash);
+                        *this.state = PendingTxState::Completed;
+                        return Poll::Ready(Ok(None))
+                    }
+
+                    *this.retries_remaining -= 1;
+                    rewake_with_new_state!(ctx, this, PendingTxState::PausedGettingTx);
                 }
 
                 // If it hasn't confirmed yet, poll again later
diff --git a/ethers-solc/src/artifact_output/configurable.rs b/ethers-solc/src/artifact_output/configurable.rs
index e4602ec5..fba1074b 100644
--- a/ethers-solc/src/artifact_output/configurable.rs
+++ b/ethers-solc/src/artifact_output/configurable.rs
@@ -9,7 +9,7 @@ use crate::{
             EwasmOutputSelection,
         },
         Ast, CompactContractBytecodeCow, DevDoc, Evm, Ewasm, FunctionDebugData, GasEstimates,
-        LosslessAbi, Metadata, Offsets, Settings, StorageLayout, UserDoc,
+        GeneratedSource, LosslessAbi, Metadata, Offsets, Settings, StorageLayout, UserDoc,
     },
     ArtifactOutput, SolcConfig, SolcError, SourceFile,
 };
@@ -29,11 +29,12 @@ pub struct ConfigurableContractArtifact {
     pub bytecode: Option<CompactBytecode>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub deployed_bytecode: Option<CompactDeployedBytecode>,
-
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub assembly: Option<String>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub method_identifiers: Option<BTreeMap<String, String>>,
+    #[serde(default, skip_serializing_if = "Vec::is_empty")]
+    pub generated_sources: Vec<GeneratedSource>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
     pub function_debug_data: Option<BTreeMap<String, FunctionDebugData>>,
     #[serde(default, skip_serializing_if = "Option::is_none")]
@@ -193,6 +194,14 @@ impl ConfigurableArtifacts {
         if self.additional_values.method_identifiers {
             selection.push(EvmOutputSelection::MethodIdentifiers.into());
         }
+        if self.additional_values.generated_sources {
+            selection.push(
+                EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources).into(),
+            );
+        }
+        if self.additional_values.source_map {
+            selection.push(EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap).into());
+        }
         selection
     }
 }
@@ -224,6 +233,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
         let mut artifact_method_identifiers = None;
         let mut artifact_assembly = None;
         let mut artifact_storage_layout = None;
+        let mut generated_sources = None;
 
         let Contract {
             abi,
@@ -262,7 +272,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
         if let Some(evm) = evm {
             let Evm {
                 assembly,
-                bytecode,
+                mut bytecode,
                 deployed_bytecode,
                 method_identifiers,
                 gas_estimates,
@@ -273,6 +283,10 @@ impl ArtifactOutput for ConfigurableArtifacts {
                 artifact_function_debug_data =
                     bytecode.as_ref().map(|b| b.function_debug_data.clone());
             }
+            if self.additional_values.generated_sources {
+                generated_sources =
+                    bytecode.as_mut().map(|code| std::mem::take(&mut code.generated_sources));
+            }
 
             artifact_bytecode = bytecode.map(Into::into);
             artifact_deployed_bytecode = deployed_bytecode.map(Into::into);
@@ -304,6 +318,7 @@ impl ArtifactOutput for ConfigurableArtifacts {
             ir_optimized: artifact_ir_optimized,
             ewasm: artifact_ewasm,
             ast: source_file.and_then(|s| s.ast.clone()),
+            generated_sources: generated_sources.unwrap_or_default(),
         }
     }
 }
@@ -324,6 +339,8 @@ pub struct ExtraOutputValues {
     pub ir_optimized: bool,
     pub ewasm: bool,
     pub function_debug_data: bool,
+    pub generated_sources: bool,
+    pub source_map: bool,
 
     /// PRIVATE: This structure may grow, As such, constructing this structure should
     /// _always_ be done using a public constructor or update syntax:
@@ -357,6 +374,8 @@ impl ExtraOutputValues {
             ir_optimized: true,
             ewasm: true,
             function_debug_data: true,
+            generated_sources: true,
+            source_map: true,
             __non_exhaustive: (),
         }
     }
@@ -391,6 +410,8 @@ impl ExtraOutputValues {
                         config.assembly = true;
                         config.gas_estimates = true;
                         config.method_identifiers = true;
+                        config.generated_sources = true;
+                        config.source_map = true;
                     }
                     EvmOutputSelection::Assembly => {
                         config.assembly = true;
@@ -404,6 +425,12 @@ impl ExtraOutputValues {
                     EvmOutputSelection::ByteCode(BytecodeOutputSelection::FunctionDebugData) => {
                         config.function_debug_data = true;
                     }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
+                        config.generated_sources = true;
+                    }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap) => {
+                        config.source_map = true;
+                    }
                     _ => {}
                 },
                 ContractOutputSelection::Ewasm(_) => {
@@ -425,6 +452,8 @@ pub struct ExtraOutputFiles {
     pub ir_optimized: bool,
     pub ewasm: bool,
     pub assembly: bool,
+    pub source_map: bool,
+    pub generated_sources: bool,
 
     /// PRIVATE: This structure may grow, As such, constructing this structure should
     /// _always_ be done using a public constructor or update syntax:
@@ -450,6 +479,8 @@ impl ExtraOutputFiles {
             ir_optimized: true,
             ewasm: true,
             assembly: true,
+            source_map: true,
+            generated_sources: true,
             __non_exhaustive: (),
         }
     }
@@ -473,10 +504,18 @@ impl ExtraOutputFiles {
                 ContractOutputSelection::Evm(evm) => match evm {
                     EvmOutputSelection::All => {
                         config.assembly = true;
+                        config.generated_sources = true;
+                        config.source_map = true;
                     }
                     EvmOutputSelection::Assembly => {
                         config.assembly = true;
                     }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::GeneratedSources) => {
+                        config.generated_sources = true;
+                    }
+                    EvmOutputSelection::ByteCode(BytecodeOutputSelection::SourceMap) => {
+                        config.source_map = true;
+                    }
                     _ => {}
                 },
                 ContractOutputSelection::Ewasm(_) => {
@@ -537,6 +576,27 @@ impl ExtraOutputFiles {
             }
         }
 
+        if self.generated_sources {
+            if let Some(ref evm) = contract.evm {
+                if let Some(ref bytecode) = evm.bytecode {
+                    let file = file.with_extension("gensources");
+                    fs::write(&file, serde_json::to_vec_pretty(&bytecode.generated_sources)?)
+                        .map_err(|err| SolcError::io(err, file))?;
+                }
+            }
+        }
+
+        if self.source_map {
+            if let Some(ref evm) = contract.evm {
+                if let Some(ref bytecode) = evm.bytecode {
+                    if let Some(ref sourcemap) = bytecode.source_map {
+                        let file = file.with_extension("sourcemap");
+                        fs::write(&file, sourcemap).map_err(|err| SolcError::io(err, file))?
+                    }
+                }
+            }
+        }
+
         Ok(())
     }
 }
diff --git a/ethers-solc/src/lib.rs b/ethers-solc/src/lib.rs
index 69ab0e6d..56ec5cbe 100644
--- a/ethers-solc/src/lib.rs
+++ b/ethers-solc/src/lib.rs
@@ -282,13 +282,20 @@ impl<T: ArtifactOutput> Project<T> {
     ///     ).unwrap();
     /// # }
     /// ```
-    #[cfg(all(feature = "svm-solc"))]
     pub fn compile_files<P, I>(&self, files: I) -> Result<ProjectCompileOutput<T>>
     where
         I: IntoIterator<Item = P>,
         P: Into<PathBuf>,
     {
-        project::ProjectCompiler::with_sources(self, Source::read_all(files)?)?.compile()
+        let sources = Source::read_all(files)?;
+
+        #[cfg(all(feature = "svm-solc"))]
+        if self.auto_detect {
+            return project::ProjectCompiler::with_sources(self, sources)?.compile()
+        }
+
+        let solc = self.configure_solc(self.solc.clone());
+        self.compile_with_version(&solc, sources)
     }
 
     /// Convenience function to compile only (re)compile files that match the provided [FileFilter].
@@ -321,16 +328,28 @@ impl<T: ArtifactOutput> Project<T> {
     ///     ).unwrap();
     /// # }
     /// ```
-    #[cfg(all(feature = "svm-solc"))]
     pub fn compile_sparse<F: FileFilter + 'static>(
         &self,
         filter: F,
     ) -> Result<ProjectCompileOutput<T>> {
         let sources =
             Source::read_all(self.paths.input_files().into_iter().filter(|p| filter.is_match(p)))?;
-
         let filter: Box<dyn FileFilter> = Box::new(filter);
-        project::ProjectCompiler::with_sources(self, sources)?.with_sparse_output(filter).compile()
+
+        #[cfg(all(feature = "svm-solc"))]
+        if self.auto_detect {
+            return project::ProjectCompiler::with_sources(self, sources)?
+                .with_sparse_output(filter)
+                .compile()
+        }
+
+        project::ProjectCompiler::with_sources_and_solc(
+            self,
+            sources,
+            self.configure_solc(self.solc.clone()),
+        )?
+        .with_sparse_output(filter)
+        .compile()
     }
 
     /// Compiles the given source files with the exact `Solc` executable
diff --git a/ethers-solc/src/remappings.rs b/ethers-solc/src/remappings.rs
index e82006ae..9ca337d5 100644
--- a/ethers-solc/src/remappings.rs
+++ b/ethers-solc/src/remappings.rs
@@ -1,3 +1,4 @@
+use crate::utils;
 use serde::{Deserialize, Serialize};
 use std::{
     collections::{hash_map::Entry, HashMap},
@@ -172,13 +173,15 @@ impl Remapping {
 
         let dir = dir.as_ref();
         let is_inside_node_modules = dir.ends_with("node_modules");
+
         // iterate over all dirs that are children of the root
         for dir in walkdir::WalkDir::new(dir)
             .follow_links(true)
             .min_depth(1)
             .max_depth(1)
             .into_iter()
-            .filter_map(std::result::Result::ok)
+            .filter_entry(|e| !is_hidden(e))
+            .filter_map(Result::ok)
             .filter(|e| e.file_type().is_dir())
         {
             let depth1_dir = dir.path();
@@ -487,6 +490,11 @@ fn is_lib_dir(dir: &Path) -> bool {
         .unwrap_or_default()
 }
 
+/// Returns true if the file is _hidden_
+fn is_hidden(entry: &walkdir::DirEntry) -> bool {
+    entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false)
+}
+
 /// Finds all remappings in the directory recursively
 fn find_remapping_candidates(
     current_dir: &Path,
@@ -506,8 +514,8 @@ fn find_remapping_candidates(
         .min_depth(1)
         .max_depth(1)
         .into_iter()
-        .filter_map(std::result::Result::ok)
-        .filter(|entry| !entry.file_name().to_str().map(|s| s.starts_with('.')).unwrap_or(false))
+        .filter_entry(|e| !is_hidden(e))
+        .filter_map(Result::ok)
     {
         let entry: walkdir::DirEntry = entry;
 
@@ -518,6 +526,26 @@ fn find_remapping_candidates(
         {
             is_candidate = true;
         } else if entry.file_type().is_dir() {
+            // if the dir is a symlink to a parent dir we short circuit here
+            // `walkdir` will catch symlink loops, but this check prevents that we end up scanning a
+            // workspace like
+            // ```text
+            // my-package/node_modules
+            // ├── dep/node_modules
+            //     ├── symlink to `my-package`
+            // ```
+            if entry.path_is_symlink() {
+                if let Ok(target) = utils::canonicalize(entry.path()) {
+                    // the symlink points to a parent dir of the current window
+                    if open.components().count() > target.components().count() &&
+                        utils::common_ancestor(open, &target).is_some()
+                    {
+                        // short-circuiting
+                        return Vec::new()
+                    }
+                }
+            }
+
             let subdir = entry.path();
             // we skip commonly used subdirs that should not be searched for recursively
             if !(subdir.ends_with("tests") || subdir.ends_with("test") || subdir.ends_with("demo"))
diff --git a/ethers-solc/src/sourcemap.rs b/ethers-solc/src/sourcemap.rs
index 6b47c65c..e1cf8a62 100644
--- a/ethers-solc/src/sourcemap.rs
+++ b/ethers-solc/src/sourcemap.rs
@@ -134,7 +134,7 @@ impl AsRef<str> for Jump {
     }
 }
 
-impl<'a> fmt::Display for Jump {
+impl fmt::Display for Jump {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         f.write_str(self.as_ref())
     }
@@ -176,7 +176,7 @@ pub struct SourceElement {
     pub modifier_depth: usize,
 }
 
-impl<'a> fmt::Display for SourceElement {
+impl fmt::Display for SourceElement {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         write!(
             f,
@@ -199,7 +199,7 @@ struct SourceElementBuilder {
     pub modifier_depth: Option<usize>,
 }
 
-impl<'a> fmt::Display for SourceElementBuilder {
+impl fmt::Display for SourceElementBuilder {
     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
         if self.offset.is_none() &&
             self.length.is_none() &&


Test patch:
diff --git a/ethers-middleware/tests/gas_oracle.rs b/ethers-middleware/tests/gas_oracle.rs
index c58f9e28..5967fa2d 100644
--- a/ethers-middleware/tests/gas_oracle.rs
+++ b/ethers-middleware/tests/gas_oracle.rs
@@ -57,7 +57,7 @@ async fn using_gas_oracle() {
 #[tokio::test]
 async fn eth_gas_station() {
     // initialize and fetch gas estimates from EthGasStation
-    let eth_gas_station_oracle = EthGasStation::new(None);
+    let eth_gas_station_oracle = EthGasStation::default();
     let data = eth_gas_station_oracle.fetch().await;
     assert!(data.is_ok());
 }
@@ -83,7 +83,7 @@ async fn etherscan() {
 #[tokio::test]
 async fn etherchain() {
     // initialize and fetch gas estimates from Etherchain
-    let etherchain_oracle = Etherchain::new().category(GasCategory::Fast);
+    let etherchain_oracle = Etherchain::default().category(GasCategory::Fast);
     let data = etherchain_oracle.fetch().await;
     assert!(data.is_ok());
 }