Repository: gakonst/ethers-rs
Pull request #541 (2021-10-29 18:38:17)

feat(providers): unify get_block_receipts for eth/parity RPCs
## Motivation

Resolve https://github.com/gakonst/ethers-rs/issues/367

## Solution

Unify `get_block_receipts` for erigon/parity nodes into one method

## PR Checklist

- [x] Introduce `NodeClient` enum w/ `TryFrom` implementation for parsing from `web3_clientVersion`
- [x] Store provider's node client type under mutex on first `Provider::node_client()` call
- [x] Decide which RPC to use for `get_block_receipts` depending on node client type



Patch:
diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs
index 91525c73..009f74fb 100644
--- a/ethers-providers/src/lib.rs
+++ b/ethers-providers/src/lib.rs
@@ -557,14 +557,6 @@ pub trait Middleware: Sync + Send + Debug {
 
     // Parity namespace
 
-    /// Returns all receipts for that block. Must be done on a parity node.
-    async fn parity_block_receipts<T: Into<BlockNumber> + Send + Sync>(
-        &self,
-        block: T,
-    ) -> Result<Vec<TransactionReceipt>, Self::Error> {
-        self.inner().parity_block_receipts(block).await.map_err(FromErr::from)
-    }
-
     async fn subscribe<T, R>(
         &self,
         params: T,
diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs
index ac875440..1d60845e 100644
--- a/ethers-providers/src/provider.rs
+++ b/ethers-providers/src/provider.rs
@@ -25,10 +25,35 @@ use serde::{de::DeserializeOwned, Serialize};
 use thiserror::Error;
 use url::{ParseError, Url};
 
-use std::{convert::TryFrom, fmt::Debug, time::Duration};
+use futures_util::lock::Mutex;
+use std::{convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration};
 use tracing::trace;
 use tracing_futures::Instrument;
 
+#[derive(Copy, Clone)]
+pub enum NodeClient {
+    Geth,
+    Erigon,
+    OpenEthereum,
+    Nethermind,
+    Besu,
+}
+
+impl FromStr for NodeClient {
+    type Err = ProviderError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        match s.split('/').next().unwrap() {
+            "Geth" => Ok(NodeClient::Geth),
+            "Erigon" => Ok(NodeClient::Erigon),
+            "OpenEthereum" => Ok(NodeClient::OpenEthereum),
+            "Nethermind" => Ok(NodeClient::Nethermind),
+            "besu" => Ok(NodeClient::Besu),
+            _ => Err(ProviderError::UnsupportedNodeClient),
+        }
+    }
+}
+
 /// An abstract provider for interacting with the [Ethereum JSON RPC
 /// API](https://github.com/ethereum/wiki/wiki/JSON-RPC). Must be instantiated
 /// with a data transport which implements the [`JsonRpcClient`](trait@crate::JsonRpcClient) trait
@@ -56,6 +81,10 @@ pub struct Provider<P> {
     ens: Option<Address>,
     interval: Option<Duration>,
     from: Option<Address>,
+    /// Node client hasn't been checked yet = `None`
+    /// Unsupported node client = `Some(None)`
+    /// Supported node client = `Some(Some(NodeClient))`
+    _node_client: Arc<Mutex<Option<NodeClient>>>,
 }
 
 impl<P> AsRef<P> for Provider<P> {
@@ -89,6 +118,12 @@ pub enum ProviderError {
 
     #[error("custom error: {0}")]
     CustomError(String),
+
+    #[error("unsupported RPC")]
+    UnsupportedRPC,
+
+    #[error("unsupported node client")]
+    UnsupportedNodeClient,
 }
 
 /// Types of filters supported by the JSON-RPC.
@@ -108,7 +143,31 @@ pub enum FilterKind<'a> {
 impl<P: JsonRpcClient> Provider<P> {
     /// Instantiate a new provider with a backend.
     pub fn new(provider: P) -> Self {
-        Self { inner: provider, ens: None, interval: None, from: None }
+        Self {
+            inner: provider,
+            ens: None,
+            interval: None,
+            from: None,
+            _node_client: Arc::new(Mutex::new(None)),
+        }
+    }
+
+    /// Returns the type of node we're connected to, while also caching the value for use
+    /// in other node-specific API calls, such as the get_block_receipts call.
+    pub async fn node_client(&self) -> Result<NodeClient, ProviderError> {
+        let mut node_client = self._node_client.lock().await;
+
+        if let Some(node_client) = *node_client {
+            Ok(node_client)
+        } else {
+            let client_version = self.client_version().await?;
+            let client_version = match client_version.parse::<NodeClient>() {
+                Ok(res) => res,
+                Err(_) => return Err(ProviderError::UnsupportedNodeClient),
+            };
+            *node_client = Some(client_version);
+            Ok(client_version)
+        }
     }
 
     pub fn with_sender(mut self, address: impl Into<Address>) -> Self {
@@ -275,13 +334,19 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
 
     /// Returns all receipts for a block.
     ///
-    /// Note that this uses the `eth_getBlockReceipts` RPC, which is
-    /// non-standard and currently supported by Erigon.
+    /// Note that this uses the `eth_getBlockReceipts` or `parity_getBlockReceipts` RPC, which is
+    /// non-standard and currently supported by Erigon, OpenEthereum and Nethermind.
     async fn get_block_receipts<T: Into<BlockNumber> + Send + Sync>(
         &self,
         block: T,
     ) -> Result<Vec<TransactionReceipt>, Self::Error> {
-        self.request("eth_getBlockReceipts", [block.into()]).await
+        let method = match self.node_client().await? {
+            NodeClient::Erigon => "eth_getBlockReceipts",
+            NodeClient::OpenEthereum | NodeClient::Nethermind => "parity_getBlockReceipts",
+            _ => return Err(ProviderError::UnsupportedRPC),
+        };
+
+        self.request(method, [block.into()]).await
     }
 
     /// Gets the current gas price as estimated by the node
@@ -712,14 +777,6 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
         self.request("trace_transaction", vec![hash]).await
     }
 
-    /// Returns all receipts for that block. Must be done on a parity node.
-    async fn parity_block_receipts<T: Into<BlockNumber> + Send + Sync>(
-        &self,
-        block: T,
-    ) -> Result<Vec<TransactionReceipt>, Self::Error> {
-        self.request("parity_getBlockReceipts", vec![block.into()]).await
-    }
-
     async fn subscribe<T, R>(
         &self,
         params: T,
@@ -1085,7 +1142,7 @@ mod tests {
             _ => return,
         };
         let provider = Provider::<Http>::try_from(url.as_str()).unwrap();
-        let receipts = provider.parity_block_receipts(10657200).await.unwrap();
+        let receipts = provider.get_block_receipts(10657200).await.unwrap();
         assert!(!receipts.is_empty());
     }
 


Test patch:
diff --git a/ethers-middleware/tests/nonce_manager.rs b/ethers-middleware/tests/nonce_manager.rs
index bec24666..e61759b0 100644
--- a/ethers-middleware/tests/nonce_manager.rs
+++ b/ethers-middleware/tests/nonce_manager.rs
@@ -14,7 +14,8 @@ async fn nonce_manager() {
             .interval(Duration::from_millis(2000u64));
     let chain_id = provider.get_chainid().await.unwrap().as_u64();
 
-    let wallet = "fa4a1a79e869a96fcb42727f75e3232d6865a82ea675bb95de967a7fe6a773b2"
+    let wallet = std::env::var("RINKEBY_PRIVATE_KEY")
+        .unwrap()
         .parse::<LocalWallet>()
         .unwrap()
         .with_chain_id(chain_id);