Repository: gakonst/ethers-rs
Pull request #1143 (2022-04-13 19:57:40)

test: add type check test for abi array tuples
<!--
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
Followup #1140

@meetmangukiya highlighted that `AbiArrayType` should be implemented for `Vec<T:AbiArrayType>` and tuples are `AbiArrayType`. This PR confirms that, so whatever is causing #1140 is related to a derive macro I think
<!--
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



Patch:
diff --git a/Cargo.lock b/Cargo.lock
index dac6a7a5..cd255bce 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2848,9 +2848,9 @@ dependencies = [
 
 [[package]]
 name = "rayon"
-version = "1.5.1"
+version = "1.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90"
+checksum = "fd249e82c21598a9a426a4e00dd7adc1d640b22445ec8545feef801d1a74c221"
 dependencies = [
  "autocfg",
  "crossbeam-deque",
@@ -2860,14 +2860,13 @@ dependencies = [
 
 [[package]]
 name = "rayon-core"
-version = "1.9.1"
+version = "1.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
+checksum = "9f51245e1e62e1f1629cbfec37b5793bbabcaeb90f30e94d2ba03564687353e4"
 dependencies = [
  "crossbeam-channel",
  "crossbeam-deque",
  "crossbeam-utils",
- "lazy_static",
  "num_cpus",
 ]
 
@@ -3522,9 +3521,9 @@ dependencies = [
 
 [[package]]
 name = "solang-parser"
-version = "0.1.11"
+version = "0.1.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90f49c1bb28a8814fdac132b1ec1f2f65c656186d8dfbc411db8bcaba45cdc06"
+checksum = "e6b2ad9c159bd02219a59368133301e6195fdaa2b5d55c628ccdcd611d49235f"
 dependencies = [
  "lalrpop",
  "lalrpop-util",
@@ -3585,7 +3584,7 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
 [[package]]
 name = "svm-rs"
 version = "0.2.9"
-source = "git+https://github.com/roynalnaruto/svm-rs#9c86111689147842dd76f5fe8db7cf472e729118"
+source = "git+https://github.com/roynalnaruto/svm-rs#1eb7f4e2ccb549096885ded598f6c05b2d96dccf"
 dependencies = [
  "anyhow",
  "cfg-if 1.0.0",
@@ -3614,7 +3613,7 @@ dependencies = [
 [[package]]
 name = "svm-rs-builds"
 version = "0.1.0"
-source = "git+https://github.com/roynalnaruto/svm-rs#9c86111689147842dd76f5fe8db7cf472e729118"
+source = "git+https://github.com/roynalnaruto/svm-rs#1eb7f4e2ccb549096885ded598f6c05b2d96dccf"
 dependencies = [
  "build_const",
  "hex",
@@ -3898,9 +3897,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
 
 [[package]]
 name = "tracing"
-version = "0.1.33"
+version = "0.1.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "80b9fa4360528139bc96100c160b7ae879f5567f49f1782b0b02035b0358ebf3"
+checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
 dependencies = [
  "cfg-if 1.0.0",
  "pin-project-lite",
diff --git a/ethers-contract/ethers-contract-derive/src/display.rs b/ethers-contract/ethers-contract-derive/src/display.rs
index 45918207..8fc613f1 100644
--- a/ethers-contract/ethers-contract-derive/src/display.rs
+++ b/ethers-contract/ethers-contract-derive/src/display.rs
@@ -42,7 +42,7 @@ pub(crate) fn derive_eth_display_impl(input: DeriveInput) -> Result<TokenStream,
                 }
                 ParamType::Bytes => {
                     quote! {
-                         write!(f, "0x{}", #hex_encode(self.#ident))?;
+                         write!(f, "0x{}", #hex_encode(&self.#ident))?;
                     }
                 }
                 ParamType::Bool | ParamType::String => {
diff --git a/ethers-contract/ethers-contract-derive/src/utils.rs b/ethers-contract/ethers-contract-derive/src/utils.rs
index 066cbbfa..abef0999 100644
--- a/ethers-contract/ethers-contract-derive/src/utils.rs
+++ b/ethers-contract/ethers-contract-derive/src/utils.rs
@@ -111,10 +111,26 @@ pub fn find_parameter_type(ty: &Type) -> Result<ParamType, Error> {
             Err(Error::new(ty.span(), "Failed to derive proper ABI from array field"))
         }
         Type::Path(ty) => {
-            if let Some(ident) = ty.path.get_ident() {
+            // check for `Vec`
+            if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" {
+                if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments {
+                    if args.args.len() == 1 {
+                        if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() {
+                            let kind = find_parameter_type(ty)?;
+                            return Ok(ParamType::Array(Box::new(kind)))
+                        }
+                    }
+                }
+            }
+            let mut ident = ty.path.get_ident();
+            if ident.is_none() {
+                ident = ty.path.segments.last().map(|s| &s.ident);
+            }
+            if let Some(ident) = ident {
                 let ident = ident.to_string().to_lowercase();
                 return match ident.as_str() {
                     "address" => Ok(ParamType::Address),
+                    "bytes" => Ok(ParamType::Bytes),
                     "string" => Ok(ParamType::String),
                     "bool" => Ok(ParamType::Bool),
                     "int" | "uint" => Ok(ParamType::Uint(256)),
@@ -126,18 +142,6 @@ pub fn find_parameter_type(ty: &Type) -> Result<ParamType, Error> {
                     }),
                 }
             }
-            // check for `Vec`
-            if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" {
-                if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments {
-                    if args.args.len() == 1 {
-                        if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() {
-                            let kind = find_parameter_type(ty)?;
-                            return Ok(ParamType::Array(Box::new(kind)))
-                        }
-                    }
-                }
-            }
-
             Err(Error::new(ty.span(), "Failed to derive proper ABI from fields"))
         }
         Type::Tuple(ty) => {
diff --git a/ethers-core/src/abi/mod.rs b/ethers-core/src/abi/mod.rs
index b2d50839..b4db858a 100644
--- a/ethers-core/src/abi/mod.rs
+++ b/ethers-core/src/abi/mod.rs
@@ -270,4 +270,22 @@ mod tests {
             <[u16; 32]>::param_type()
         );
     }
+
+    #[test]
+    fn abi_type_tuples_work() {
+        fn assert_abitype<T: AbiType>() {}
+        fn assert_abiarraytype<T: AbiArrayType>() {}
+
+        assert_abitype::<(u64, u64)>();
+        assert_abiarraytype::<(u64, u64)>();
+
+        assert_abitype::<(u8, u8)>();
+        assert_abiarraytype::<(u8, u8)>();
+
+        assert_abitype::<Vec<(u64, u64)>>();
+        assert_abiarraytype::<Vec<(u64, u64)>>();
+
+        assert_abitype::<Vec<(u8, u8)>>();
+        assert_abiarraytype::<Vec<(u8, u8)>>();
+    }
 }
diff --git a/ethers-core/src/types/log.rs b/ethers-core/src/types/log.rs
index 934ef466..dd70fa79 100644
--- a/ethers-core/src/types/log.rs
+++ b/ethers-core/src/types/log.rs
@@ -65,6 +65,15 @@ pub struct Log {
     pub removed: Option<bool>,
 }
 
+impl rlp::Encodable for Log {
+    fn rlp_append(&self, s: &mut rlp::RlpStream) {
+        s.begin_list(3);
+        s.append(&self.address);
+        s.append_list(&self.topics);
+        s.append(&self.data.0);
+    }
+}
+
 #[derive(Copy, Clone, Debug, PartialEq)]
 pub enum FilterBlockOption {
     Range { from_block: Option<BlockNumber>, to_block: Option<BlockNumber> },
diff --git a/ethers-core/src/types/transaction/response.rs b/ethers-core/src/types/transaction/response.rs
index 1c4fc76c..2968dc71 100644
--- a/ethers-core/src/types/transaction/response.rs
+++ b/ethers-core/src/types/transaction/response.rs
@@ -414,6 +414,16 @@ pub struct TransactionReceipt {
     pub effective_gas_price: Option<U256>,
 }
 
+impl rlp::Encodable for TransactionReceipt {
+    fn rlp_append(&self, s: &mut RlpStream) {
+        s.begin_list(4);
+        s.append(&self.status);
+        s.append(&self.cumulative_gas_used);
+        s.append(&self.logs_bloom);
+        s.append_list(&self.logs);
+    }
+}
+
 #[cfg(test)]
 #[cfg(not(feature = "celo"))]
 mod tests {
diff --git a/ethers-middleware/Cargo.toml b/ethers-middleware/Cargo.toml
index afdb013a..6c0b3167 100644
--- a/ethers-middleware/Cargo.toml
+++ b/ethers-middleware/Cargo.toml
@@ -24,7 +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.30", default-features = false }
 futures-util = { version = "^0.3" }
-tracing = { version = "0.1.33", default-features = false }
+tracing = { version = "0.1.34", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false }
 
 # for gas oracles
diff --git a/ethers-middleware/src/timelag/mod.rs b/ethers-middleware/src/timelag/mod.rs
index 09541eab..6c1e90b8 100644
--- a/ethers-middleware/src/timelag/mod.rs
+++ b/ethers-middleware/src/timelag/mod.rs
@@ -57,6 +57,7 @@ where
             Some(BlockId::Number(n)) => {
                 Ok(self.normalize_block_number(Some(n)).await?.map(Into::into))
             }
+            None => Ok(self.normalize_block_number(None).await?.map(Into::into)),
             _ => Ok(id),
         }
     }
@@ -65,16 +66,17 @@ where
         &self,
         number: Option<BlockNumber>,
     ) -> TimeLagResult<Option<BlockNumber>, M> {
-        let tip = self.get_block_number().await?;
+        let lag_tip = self.get_block_number().await?;
         match number {
-            Some(BlockNumber::Latest) => Ok(Some(BlockNumber::Number(tip))),
+            Some(BlockNumber::Latest) => Ok(Some(BlockNumber::Number(lag_tip))),
             Some(BlockNumber::Number(n)) => {
-                if n > tip {
-                    Ok(Some(BlockNumber::Latest))
+                if n < lag_tip {
+                    Ok(Some(BlockNumber::Number(n)))
                 } else {
-                    Ok(number)
+                    Ok(Some(BlockNumber::Number(lag_tip)))
                 }
             }
+            None => Ok(Some(BlockNumber::Number(lag_tip))),
             _ => Ok(number),
         }
     }
@@ -121,7 +123,6 @@ where
         tx: T,
         block: Option<BlockId>,
     ) -> Result<ethers_providers::PendingTransaction<'_, Self::Provider>, Self::Error> {
-        let block = self.normalize_block_id(block).await?;
         self.inner().send_transaction(tx, block).await.map_err(ethers_providers::FromErr::from)
     }
 
@@ -271,7 +272,6 @@ where
         tx: &mut TypedTransaction,
         block: Option<BlockId>,
     ) -> Result<(), Self::Error> {
-        let block = self.normalize_block_id(block).await?;
         self.inner().fill_transaction(tx, block).await.map_err(ethers_providers::FromErr::from)
     }
 
diff --git a/ethers-providers/Cargo.toml b/ethers-providers/Cargo.toml
index 1a437f83..0e9eb75c 100644
--- a/ethers-providers/Cargo.toml
+++ b/ethers-providers/Cargo.toml
@@ -35,7 +35,7 @@ futures-channel = { version = "0.3.16", default-features = false, optional = tru
 pin-project = { version = "1.0.7", default-features = false }
 
 # tracing
-tracing = { version = "0.1.33", default-features = false }
+tracing = { version = "0.1.34", default-features = false }
 tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
 
 bytes = { version  = "1.1.0", default-features = false, optional = true }
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index bb51c120..1bdfe200 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -3,7 +3,7 @@ use crate::{
     pubsub::{PubsubClient, SubscriptionStream},
     stream::{FilterWatcher, DEFAULT_POLL_INTERVAL},
     FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, MockProvider,
-    PendingTransaction, QuorumProvider, SyncingStatus,
+    PendingTransaction, QuorumProvider, RwClient, SyncingStatus,
 };
 
 #[cfg(feature = "celo")]
@@ -1235,6 +1235,19 @@ impl Provider<crate::Ipc> {
     }
 }
 
+impl<Read, Write> Provider<RwClient<Read, Write>>
+where
+    Read: JsonRpcClient + 'static,
+    <Read as JsonRpcClient>::Error: Sync + Send + 'static,
+    Write: JsonRpcClient + 'static,
+    <Write as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    /// Creates a new [Provider] with a [RwClient]
+    pub fn rw(r: Read, w: Write) -> Self {
+        Self::new(RwClient::new(r, w))
+    }
+}
+
 impl<T: JsonRpcClientWrapper> Provider<QuorumProvider<T>> {
     /// Provider that uses a quorum
     pub fn quorum(inner: QuorumProvider<T>) -> Self {
diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/transports/mod.rs
index 58233ab4..1a369b40 100644
--- a/ethers-providers/src/transports/mod.rs
+++ b/ethers-providers/src/transports/mod.rs
@@ -36,5 +36,8 @@ mod quorum;
 pub(crate) use quorum::JsonRpcClientWrapper;
 pub use quorum::{Quorum, QuorumError, QuorumProvider, WeightedProvider};
 
+mod rw;
+pub use rw::{RwClient, RwClientError};
+
 mod mock;
 pub use mock::{MockError, MockProvider};
diff --git a/ethers-providers/src/transports/rw.rs b/ethers-providers/src/transports/rw.rs
new file mode 100644
index 00000000..f54a96eb
--- /dev/null
+++ b/ethers-providers/src/transports/rw.rs
@@ -0,0 +1,127 @@
+//! A [JsonRpcClient] implementation that serves as a wrapper around two different [JsonRpcClient]
+//! and uses a dedicated client for read and the other for write operations
+
+use crate::{provider::ProviderError, JsonRpcClient};
+
+use async_trait::async_trait;
+
+use serde::{de::DeserializeOwned, Serialize};
+
+use thiserror::Error;
+
+/// A client contains two clients.
+///
+/// One is used for _read_ operations
+/// One is used for _write_ operations that consume gas `["eth_sendTransaction",
+/// "eth_sendRawTransaction"]`
+///
+/// **Note**: if the method is unknown this client falls back to the _read_ client
+// # Example
+#[derive(Debug, Clone)]
+pub struct RwClient<Read, Write> {
+    /// client used to read
+    r: Read,
+    /// client used to write
+    w: Write,
+}
+
+impl<Read, Write> RwClient<Read, Write> {
+    /// Creates a new client using two different clients
+    ///
+    /// # Example
+    ///
+    /// ```no_run
+    /// # use url::Url;
+    ///  async fn t(){
+    /// use ethers_providers::{Http, RwClient, Ws};
+    /// let http = Http::new(Url::parse("http://localhost:8545").unwrap());
+    /// let ws = Ws::connect("ws://localhost:8545").await.unwrap();
+    /// let rw = RwClient::new(http, ws);
+    /// # }
+    /// ```
+    pub fn new(r: Read, w: Write) -> RwClient<Read, Write> {
+        Self { r, w }
+    }
+
+    /// Returns the client used for read operations
+    pub fn read_client(&self) -> &Read {
+        &self.r
+    }
+
+    /// Returns the client used for read operations
+    pub fn write_client(&self) -> &Write {
+        &self.w
+    }
+
+    /// Returns a new `RwClient` with transposed clients
+    pub fn transpose(self) -> RwClient<Write, Read> {
+        let RwClient { r, w } = self;
+        RwClient::new(w, r)
+    }
+
+    /// Consumes the client and returns the underlying clients
+    pub fn split(self) -> (Read, Write) {
+        let RwClient { r, w } = self;
+        (r, w)
+    }
+}
+
+#[derive(Error, Debug)]
+/// Error thrown when using either read or write client
+pub enum RwClientError<Read, Write>
+where
+    Read: JsonRpcClient,
+    <Read as JsonRpcClient>::Error: Sync + Send + 'static,
+    Write: JsonRpcClient,
+    <Write as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    /// Thrown if the _read_ request failed
+    #[error(transparent)]
+    Read(Read::Error),
+    #[error(transparent)]
+    /// Thrown if the _write_ request failed
+    Write(Write::Error),
+}
+
+impl<Read, Write> From<RwClientError<Read, Write>> for ProviderError
+where
+    Read: JsonRpcClient + 'static,
+    <Read as JsonRpcClient>::Error: Sync + Send + 'static,
+    Write: JsonRpcClient + 'static,
+    <Write as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    fn from(src: RwClientError<Read, Write>) -> Self {
+        ProviderError::JsonRpcClientError(Box::new(src))
+    }
+}
+
+#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
+#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
+impl<Read, Write> JsonRpcClient for RwClient<Read, Write>
+where
+    Read: JsonRpcClient + 'static,
+    <Read as JsonRpcClient>::Error: Sync + Send + 'static,
+    Write: JsonRpcClient + 'static,
+    <Write as JsonRpcClient>::Error: Sync + Send + 'static,
+{
+    type Error = RwClientError<Read, Write>;
+
+    /// Sends a POST request with the provided method and the params serialized as JSON
+    /// over HTTP
+    async fn request<T: Serialize + Send + Sync, R: DeserializeOwned>(
+        &self,
+        method: &str,
+        params: T,
+    ) -> Result<R, Self::Error>
+    where
+        T: std::fmt::Debug + Serialize + Send + Sync,
+        R: DeserializeOwned,
+    {
+        match method {
+            "eth_sendTransaction" | "eth_sendRawTransaction" => {
+                self.w.request(method, params).await.map_err(RwClientError::Write)
+            }
+            _ => self.r.request(method, params).await.map_err(RwClientError::Read),
+        }
+    }
+}
diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml
index fc4eda84..8714080b 100644
--- a/ethers-signers/Cargo.toml
+++ b/ethers-signers/Cargo.toml
@@ -33,7 +33,7 @@ trezor-client = { version = "0.0.5", optional = true, default-features = false,
 # aws
 rusoto_core = { version = "0.47.0", optional = true }
 rusoto_kms = { version = "0.47.0", optional = true }
-tracing = { version = "0.1.33", optional = true }
+tracing = { version = "0.1.34", optional = true }
 spki = { version = "0.5.4", optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
diff --git a/ethers-solc/Cargo.toml b/ethers-solc/Cargo.toml
index e832d5b8..fe62ee72 100644
--- a/ethers-solc/Cargo.toml
+++ b/ethers-solc/Cargo.toml
@@ -28,15 +28,15 @@ thiserror = "1.0.30"
 hex = "0.4.3"
 colored = "2.0.0"
 glob = "0.3.0"
-tracing = "0.1.33"
+tracing = "0.1.34"
 num_cpus = "1.13.1"
 tiny-keccak = { version = "2.0.2", default-features = false }
 tempfile = { version = "3.3.0", optional = true }
 fs_extra = { version = "1.2.0", optional = true }
 sha2 = { version = "0.9.8", default-features = false, optional = true }
 dunce = "1.0.2"
-solang-parser = { default-features = false, version = "0.1.11" }
-rayon = "1.5.1"
+solang-parser = { default-features = false, version = "0.1.12" }
+rayon = "1.5.2"
 rand = { version = "0.8.5", optional = true }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
diff --git a/ethers-solc/src/cache.rs b/ethers-solc/src/cache.rs
index f87c4bd4..7dbfa5ec 100644
--- a/ethers-solc/src/cache.rs
+++ b/ethers-solc/src/cache.rs
@@ -630,8 +630,6 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
     /// so that their [OutputSelection] can be optimized in the [CompilerOutput] and their (empty)
     /// artifacts ignored.
     fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources {
-        self.fill_hashes(&sources);
-
         // all files that are not dirty themselves, but are pulled from a dirty file
         let mut imports_of_dirty = HashSet::new();
 
@@ -728,6 +726,8 @@ impl<'a, T: ArtifactOutput> ArtifactsCacheInner<'a, T> {
                 return false
             }
             tracing::trace!("Missing cache entry for {}", file.display());
+        } else {
+            tracing::trace!("Missing content hash for {}", file.display());
         }
         true
     }
@@ -833,6 +833,14 @@ impl<'a, T: ArtifactOutput> ArtifactsCache<'a, T> {
         }
     }
 
+    /// Adds the file's hashes to the set if not set yet
+    pub fn fill_content_hashes(&mut self, sources: &Sources) {
+        match self {
+            ArtifactsCache::Ephemeral(_, _) => {}
+            ArtifactsCache::Cached(cache) => cache.fill_hashes(sources),
+        }
+    }
+
     /// Filters out those sources that don't need to be compiled
     pub fn filter(&mut self, sources: Sources, version: &Version) -> FilteredSources {
         match self {
diff --git a/ethers-solc/src/compile/project.rs b/ethers-solc/src/compile/project.rs
index c831afd3..49b5eb37 100644
--- a/ethers-solc/src/compile/project.rs
+++ b/ethers-solc/src/compile/project.rs
@@ -331,6 +331,11 @@ impl CompilerSources {
             sources: VersionedSources,
             cache: &mut ArtifactsCache<T>,
         ) -> VersionedFilteredSources {
+            // fill all content hashes first so they're available for all source sets
+            sources.iter().for_each(|(_, (_, sources))| {
+                cache.fill_content_hashes(sources);
+            });
+
             sources
                 .into_iter()
                 .map(|(solc, (version, sources))| {
diff --git a/ethers-solc/src/config.rs b/ethers-solc/src/config.rs
index d39db454..3b67de83 100644
--- a/ethers-solc/src/config.rs
+++ b/ethers-solc/src/config.rs
@@ -311,7 +311,7 @@ impl ProjectPathsConfig {
         for import in imports.iter() {
             let import_path = self.resolve_import(target_dir, import.data())?;
             let s = self.flatten_node(&import_path, graph, imported, true, true, true)?;
-            let import_content = s.trim().as_bytes();
+            let import_content = s.as_bytes();
             let import_content_len = import_content.len() as isize;
             let (start, end) = import.loc_by_offset(offset);
             content.splice(start..end, import_content.iter().copied());
@@ -321,6 +321,7 @@ impl ProjectPathsConfig {
         let result = String::from_utf8(content).map_err(|err| {
             SolcError::msg(format!("failed to convert extended bytes to string: {}", err))
         })?;
+        let result = utils::RE_THREE_OR_MORE_NEWLINES.replace_all(&result, "\n\n").into_owned();
 
         Ok(result)
     }
diff --git a/ethers-solc/src/utils.rs b/ethers-solc/src/utils.rs
index cbc42d51..bd354228 100644
--- a/ethers-solc/src/utils.rs
+++ b/ethers-solc/src/utils.rs
@@ -32,6 +32,9 @@ pub static RE_SOL_PRAGMA_VERSION: Lazy<Regex> =
 pub static RE_SOL_SDPX_LICENSE_IDENTIFIER: Lazy<Regex> =
     Lazy::new(|| Regex::new(r"///?\s*SPDX-License-Identifier:\s*(?P<license>.+)").unwrap());
 
+/// A regex used to remove extra lines in flatenned files
+pub static RE_THREE_OR_MORE_NEWLINES: Lazy<Regex> = Lazy::new(|| Regex::new("\n{3,}").unwrap());
+
 /// Returns all path parts from any solidity import statement in a string,
 /// `import "./contracts/Contract.sol";` -> `"./contracts/Contract.sol"`.
 ///
diff --git a/examples/rw.rs b/examples/rw.rs
new file mode 100644
index 00000000..fea6ae80
--- /dev/null
+++ b/examples/rw.rs
@@ -0,0 +1,19 @@
+//! Example usage for the `RwClinet` that uses a didicated client to send transaction and nother one
+//! for read ops
+
+use ethers::{prelude::*, utils::Ganache};
+use std::{str::FromStr, time::Duration};
+
+#[tokio::main]
+async fn main() -> eyre::Result<()> {
+    let ganache = Ganache::new().spawn();
+
+    let http = Http::from_str(&ganache.endpoint())?;
+    let ws = Ws::connect(ganache.ws_endpoint()).await?;
+
+    let provider = Provider::rw(http, ws).interval(Duration::from_millis(10u64));
+
+    dbg!(provider.get_accounts().await?);
+
+    Ok(())
+}


Test patch:
diff --git a/ethers-contract/tests/common/derive.rs b/ethers-contract/tests/common/derive.rs
index 65f07ca7..25dd7aaf 100644
--- a/ethers-contract/tests/common/derive.rs
+++ b/ethers-contract/tests/common/derive.rs
@@ -583,3 +583,16 @@ fn can_derive_array_tuples() {
         pub calldata: Bytes,
     }
 }
+
+#[test]
+fn eth_display_works_on_ethers_bytes() {
+    #[derive(Clone, Debug, Default, Eq, PartialEq, EthCall, EthDisplay)]
+    #[ethcall(name = "logBytes", abi = "logBytes(bytes)")]
+    pub struct LogBytesCall {
+        pub p_0: ethers_core::types::Bytes,
+    }
+    let call = LogBytesCall { p_0: hex::decode(b"aaaaaa").unwrap().into() };
+
+    let s = format!("{}", call);
+    assert_eq!(s, "0xaaaaaa");
+}
diff --git a/ethers-solc/tests/project.rs b/ethers-solc/tests/project.rs
index 86939884..2b360c9e 100644
--- a/ethers-solc/tests/project.rs
+++ b/ethers-solc/tests/project.rs
@@ -492,8 +492,11 @@ contract C { }
         result,
         r#"
 pragma solidity ^0.8.10;
+
 contract C { }
+
 contract B { }
+
 contract A { }
 "#
     );
@@ -547,8 +550,11 @@ contract C { }
         r#"
 pragma solidity ^0.8.10;
 pragma experimental ABIEncoderV2;
+
 contract C { }
+
 contract B { }
+
 contract A { }
 "#
     );
@@ -572,6 +578,7 @@ fn can_flatten_file_with_duplicates() {
 pragma solidity >=0.6.0;
 
 contract Bar {}
+
 contract Foo {}
 
 contract FooBar {}
@@ -598,6 +605,7 @@ fn can_flatten_on_solang_failure() {
 pragma solidity ^0.8.10;
 
 library Lib {}
+
 // Intentionally erroneous code
 contract Contract {
     failure();
@@ -650,9 +658,64 @@ contract C { }
     assert_eq!(
         result.trim(),
         r#"pragma solidity ^0.8.10;
+
 contract C { }
+
 error IllegalArgument();
 error IllegalState();
+
+contract A { }"#
+    );
+}
+
+#[test]
+fn can_flatten_remove_extra_spacing() {
+    let project = TempProject::dapptools().unwrap();
+
+    let f = project
+        .add_source(
+            "A",
+            r#"pragma solidity ^0.8.10;
+import "./C.sol";
+import "./B.sol";
+contract A { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "B",
+            r#"// This is a B Contract
+pragma solidity ^0.8.10;
+
+import "./C.sol";
+
+contract B { }
+"#,
+        )
+        .unwrap();
+
+    project
+        .add_source(
+            "C",
+            r#"pragma solidity ^0.8.10;
+contract C { }
+"#,
+        )
+        .unwrap();
+
+    let result = project.flatten(&f).unwrap();
+    assert_eq!(
+        result.trim(),
+        r#"pragma solidity ^0.8.10;
+
+contract C { }
+
+// This is a B Contract
+
+contract B { }
+
 contract A { }"#
     );
 }