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 { } } +impl Provider> +where + Read: JsonRpcClient + 'static, + ::Error: Sync + Send + 'static, + Write: JsonRpcClient + 'static, + ::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 Provider> { /// Provider that uses a quorum pub fn quorum(inner: QuorumProvider) -> 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 { + /// client used to read + r: Read, + /// client used to write + w: Write, +} + +impl RwClient { + /// 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 { + 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 { + 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 +where + Read: JsonRpcClient, + ::Error: Sync + Send + 'static, + Write: 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 From> for ProviderError +where + Read: JsonRpcClient + 'static, + ::Error: Sync + Send + 'static, + Write: JsonRpcClient + 'static, + ::Error: Sync + Send + 'static, +{ + fn from(src: RwClientError) -> Self { + ProviderError::JsonRpcClientError(Box::new(src)) + } +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl JsonRpcClient for RwClient +where + Read: JsonRpcClient + 'static, + ::Error: Sync + Send + 'static, + Write: JsonRpcClient + 'static, + ::Error: Sync + Send + 'static, +{ + type Error = RwClientError; + + /// Sends a POST request with the provided method and the params serialized as JSON + /// over HTTP + async fn request( + &self, + method: &str, + params: T, + ) -> Result + 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/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(()) +}