From ef6e7f41a10f8b027891a69f78926ff7ed8d6be2 Mon Sep 17 00:00:00 2001 From: James Prestwich <10149425+prestwich@users.noreply.github.com> Date: Mon, 20 Feb 2023 15:55:36 -0800 Subject: [PATCH] Refactor: organize ethers-providers (#2159) * feature: bubble up jsonrpc error response via trait * refactor: ClientError to TransportError * refactor: FromErr to MiddlewareError * tests: fix test with middlewareerror * fix: doctest * fix: fix custom middleware example * feature: as_serde_error * docs: for error traits * fix: custom example and unnecessary ref * refactor: in progress organization * refactor: continue cleaning up * refactor: finish changing crate layout * refactor: fix test imports * refactor: move convenience impl into toolbox * chore: changelog * docs: make them suck less * fix: remove deprecation * fix: DaniPopes's nits --- CHANGELOG.md | 1 + ethers-contract/tests/it/contract.rs | 15 +- ethers-middleware/src/gas_escalator/mod.rs | 15 +- .../src/gas_oracle/middleware.rs | 17 +- ethers-middleware/src/nonce_manager.rs | 24 +- ethers-middleware/src/policy.rs | 23 +- ethers-middleware/src/signer.rs | 23 +- ethers-middleware/src/timelag/mod.rs | 61 +- .../src/transformer/middleware.rs | 15 +- ethers-providers/src/errors.rs | 219 ++++ ethers-providers/src/{ => ext}/admin.rs | 10 +- ethers-providers/src/ext/dev_rpc.rs | 193 ++++ ethers-providers/src/{ => ext}/ens.rs | 0 ethers-providers/src/{ => ext}/erc.rs | 9 +- ethers-providers/src/ext/mod.rs | 13 + ethers-providers/src/lib.rs | 768 +------------- ethers-providers/src/middleware.rs | 962 ++++++++++++++++++ ethers-providers/src/rpc/connections.rs | 37 + ethers-providers/src/rpc/mod.rs | 11 + ethers-providers/src/{ => rpc}/provider.rs | 467 +-------- ethers-providers/src/{ => rpc}/pubsub.rs | 28 +- .../src/{ => rpc}/transports/common.rs | 4 + .../src/{ => rpc}/transports/http.rs | 28 +- .../src/{ => rpc}/transports/ipc.rs | 30 +- .../src/{ => rpc}/transports/mock.rs | 18 +- .../src/{ => rpc}/transports/mod.rs | 4 +- .../src/{ => rpc}/transports/quorum.rs | 28 +- .../src/{ => rpc}/transports/retry.rs | 27 +- .../src/{ => rpc}/transports/rw.rs | 28 +- .../src/{ => rpc}/transports/ws.rs | 29 +- ethers-providers/src/stream/mod.rs | 5 + .../src/{stream.rs => stream/tx_stream.rs} | 204 ++-- ethers-providers/src/stream/watcher.rs | 115 +++ .../src/{ => toolbox}/call_raw.rs | 11 +- .../src/{ => toolbox}/log_query.rs | 9 +- ethers-providers/src/toolbox/mod.rs | 11 + .../src/{ => toolbox}/pending_escalator.rs | 4 +- .../src/{ => toolbox}/pending_transaction.rs | 5 +- ethers-providers/src/utils.rs | 40 + ethers-providers/tests/ws_errors.rs | 2 +- .../examples/create_custom_middleware.rs | 17 +- examples/providers/examples/custom.rs | 42 +- 42 files changed, 2126 insertions(+), 1446 deletions(-) create mode 100644 ethers-providers/src/errors.rs rename ethers-providers/src/{ => ext}/admin.rs (97%) create mode 100644 ethers-providers/src/ext/dev_rpc.rs rename ethers-providers/src/{ => ext}/ens.rs (100%) rename ethers-providers/src/{ => ext}/erc.rs (92%) create mode 100644 ethers-providers/src/ext/mod.rs create mode 100644 ethers-providers/src/middleware.rs create mode 100644 ethers-providers/src/rpc/connections.rs create mode 100644 ethers-providers/src/rpc/mod.rs rename ethers-providers/src/{ => rpc}/provider.rs (79%) rename ethers-providers/src/{ => rpc}/pubsub.rs (83%) rename ethers-providers/src/{ => rpc}/transports/common.rs (98%) rename ethers-providers/src/{ => rpc}/transports/http.rs (89%) rename ethers-providers/src/{ => rpc}/transports/ipc.rs (96%) rename ethers-providers/src/{ => rpc}/transports/mock.rs (91%) rename ethers-providers/src/{ => rpc}/transports/mod.rs (88%) rename ethers-providers/src/{ => rpc}/transports/quorum.rs (96%) rename ethers-providers/src/{ => rpc}/transports/retry.rs (96%) rename ethers-providers/src/{ => rpc}/transports/rw.rs (79%) rename ethers-providers/src/{ => rpc}/transports/ws.rs (96%) create mode 100644 ethers-providers/src/stream/mod.rs rename ethers-providers/src/{stream.rs => stream/tx_stream.rs} (60%) create mode 100644 ethers-providers/src/stream/watcher.rs rename ethers-providers/src/{ => toolbox}/call_raw.rs (98%) rename ethers-providers/src/{ => toolbox}/log_query.rs (92%) create mode 100644 ethers-providers/src/toolbox/mod.rs rename ethers-providers/src/{ => toolbox}/pending_escalator.rs (98%) rename ethers-providers/src/{ => toolbox}/pending_transaction.rs (99%) create mode 100644 ethers-providers/src/utils.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 975a3c51..190fa258 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -238,6 +238,7 @@ ### Unreleased +- Re-organize the crate. #[2150](https://github.com/gakonst/ethers-rs/pull/2159) - Convert provider errors to arbitrary middleware errors [#1920](https://github.com/gakonst/ethers-rs/pull/1920) - Add a subset of the `admin` namespace diff --git a/ethers-contract/tests/it/contract.rs b/ethers-contract/tests/it/contract.rs index b5e85b03..4f16d827 100644 --- a/ethers-contract/tests/it/contract.rs +++ b/ethers-contract/tests/it/contract.rs @@ -13,7 +13,9 @@ mod eth_tests { utils::{keccak256, Anvil}, }; use ethers_derive_eip712::*; - use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt}; + use ethers_providers::{ + Http, Middleware, MiddlewareError, PendingTransaction, Provider, StreamExt, + }; use ethers_signers::{LocalWallet, Signer}; use std::{convert::TryFrom, iter::FromIterator, sync::Arc, time::Duration}; @@ -24,13 +26,20 @@ mod eth_tests { #[derive(Debug)] pub struct MwErr(M::Error); - impl ethers_providers::FromErr for MwErr + + impl MiddlewareError for MwErr where M: Middleware, { - fn from(src: M::Error) -> Self { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { Self(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + Some(&self.0) + } } impl std::fmt::Display for MwErr { diff --git a/ethers-middleware/src/gas_escalator/mod.rs b/ethers-middleware/src/gas_escalator/mod.rs index f3ab1840..384f0291 100644 --- a/ethers-middleware/src/gas_escalator/mod.rs +++ b/ethers-middleware/src/gas_escalator/mod.rs @@ -7,7 +7,7 @@ pub use linear::LinearGasPrice; use async_trait::async_trait; use ethers_core::types::{BlockId, TransactionRequest, TxHash, U256}; -use ethers_providers::{interval, FromErr, Middleware, PendingTransaction, StreamExt}; +use ethers_providers::{interval, Middleware, MiddlewareError, PendingTransaction, StreamExt}; use futures_util::lock::Mutex; use instant::Instant; use std::{pin::Pin, sync::Arc}; @@ -238,10 +238,19 @@ where } // Boilerplate -impl FromErr for GasEscalatorError { - fn from(src: M::Error) -> GasEscalatorError { +impl MiddlewareError for GasEscalatorError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> GasEscalatorError { GasEscalatorError::MiddlewareError(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + GasEscalatorError::MiddlewareError(e) => Some(e), + _ => None, + } + } } #[derive(Error, Debug)] diff --git a/ethers-middleware/src/gas_oracle/middleware.rs b/ethers-middleware/src/gas_oracle/middleware.rs index 918ec4b0..6835fb13 100644 --- a/ethers-middleware/src/gas_oracle/middleware.rs +++ b/ethers-middleware/src/gas_oracle/middleware.rs @@ -1,7 +1,7 @@ use super::{GasOracle, GasOracleError}; use async_trait::async_trait; use ethers_core::types::{transaction::eip2718::TypedTransaction, *}; -use ethers_providers::{FromErr, Middleware, PendingTransaction}; +use ethers_providers::{Middleware, MiddlewareError as METrait, PendingTransaction}; use thiserror::Error; /// Middleware used for fetching gas prices over an API instead of `eth_gasPrice`. @@ -33,10 +33,19 @@ pub enum MiddlewareError { UnsupportedTxType, } -impl FromErr for MiddlewareError { - fn from(src: M::Error) -> MiddlewareError { +impl METrait for MiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> MiddlewareError { MiddlewareError::MiddlewareError(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + MiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -86,7 +95,7 @@ where } }; - self.inner().fill_transaction(tx, block).await.map_err(FromErr::from) + self.inner().fill_transaction(tx, block).await.map_err(METrait::from_err) } async fn get_gas_price(&self) -> Result { diff --git a/ethers-middleware/src/nonce_manager.rs b/ethers-middleware/src/nonce_manager.rs index 0a72ddd0..1e59c57a 100644 --- a/ethers-middleware/src/nonce_manager.rs +++ b/ethers-middleware/src/nonce_manager.rs @@ -1,6 +1,6 @@ use async_trait::async_trait; use ethers_core::types::{transaction::eip2718::TypedTransaction, *}; -use ethers_providers::{FromErr, Middleware, PendingTransaction}; +use ethers_providers::{Middleware, MiddlewareError, PendingTransaction}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use thiserror::Error; @@ -40,7 +40,7 @@ where .inner .get_transaction_count(self.address, block) .await - .map_err(FromErr::from)?; + .map_err(MiddlewareError::from_err)?; self.nonce.store(nonce.as_u64(), Ordering::SeqCst); self.initialized.store(true, Ordering::SeqCst); Ok(nonce) @@ -60,7 +60,7 @@ where .inner .get_transaction_count(self.address, block) .await - .map_err(FromErr::from)?; + .map_err(MiddlewareError::from_err)?; self.nonce.store(nonce.as_u64(), Ordering::SeqCst); self.initialized.store(true, Ordering::SeqCst); } @@ -77,10 +77,18 @@ pub enum NonceManagerError { MiddlewareError(M::Error), } -impl FromErr for NonceManagerError { - fn from(src: M::Error) -> Self { +impl MiddlewareError for NonceManagerError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { NonceManagerError::MiddlewareError(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + NonceManagerError::MiddlewareError(e) => Some(e), + } + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -106,7 +114,7 @@ where tx.set_nonce(self.get_transaction_count_with_manager(block).await?); } - Ok(self.inner().fill_transaction(tx, block).await.map_err(FromErr::from)?) + Ok(self.inner().fill_transaction(tx, block).await.map_err(MiddlewareError::from_err)?) } /// Signs and broadcasts the transaction. The optional parameter `block` can be passed so that @@ -132,10 +140,10 @@ where // was a nonce mismatch self.nonce.store(nonce.as_u64(), Ordering::SeqCst); tx.set_nonce(nonce); - self.inner.send_transaction(tx, block).await.map_err(FromErr::from) + self.inner.send_transaction(tx, block).await.map_err(MiddlewareError::from_err) } else { // propagate the error otherwise - Err(FromErr::from(err)) + Err(MiddlewareError::from_err(err)) } } } diff --git a/ethers-middleware/src/policy.rs b/ethers-middleware/src/policy.rs index fc69d52e..3eef11cc 100644 --- a/ethers-middleware/src/policy.rs +++ b/ethers-middleware/src/policy.rs @@ -1,5 +1,5 @@ use ethers_core::types::{transaction::eip2718::TypedTransaction, BlockId}; -use ethers_providers::{FromErr, Middleware, PendingTransaction}; +use ethers_providers::{Middleware, MiddlewareError, PendingTransaction}; use async_trait::async_trait; use std::fmt::Debug; @@ -52,12 +52,6 @@ pub struct PolicyMiddleware { pub(crate) policy: P, } -impl FromErr for PolicyMiddlewareError { - fn from(src: M::Error) -> PolicyMiddlewareError { - PolicyMiddlewareError::MiddlewareError(src) - } -} - impl PolicyMiddleware where M: Middleware, @@ -80,6 +74,21 @@ pub enum PolicyMiddlewareError { MiddlewareError(M::Error), } +impl MiddlewareError for PolicyMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { + PolicyMiddlewareError::MiddlewareError(src) + } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + PolicyMiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Middleware for PolicyMiddleware diff --git a/ethers-middleware/src/signer.rs b/ethers-middleware/src/signer.rs index a6c79e15..1a58129f 100644 --- a/ethers-middleware/src/signer.rs +++ b/ethers-middleware/src/signer.rs @@ -2,7 +2,7 @@ use ethers_core::types::{ transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed}, Address, BlockId, Bytes, Chain, Signature, TransactionRequest, U256, }; -use ethers_providers::{maybe, FromErr, Middleware, PendingTransaction}; +use ethers_providers::{maybe, Middleware, MiddlewareError, PendingTransaction}; use ethers_signers::Signer; use std::convert::TryFrom; @@ -67,12 +67,6 @@ pub struct SignerMiddleware { pub(crate) address: Address, } -impl FromErr for SignerMiddlewareError { - fn from(src: M::Error) -> SignerMiddlewareError { - SignerMiddlewareError::MiddlewareError(src) - } -} - #[derive(Error, Debug)] /// Error thrown when the client interacts with the blockchain pub enum SignerMiddlewareError { @@ -101,6 +95,21 @@ pub enum SignerMiddlewareError { DifferentChainID, } +impl MiddlewareError for SignerMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { + SignerMiddlewareError::MiddlewareError(src) + } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + SignerMiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } +} + // Helper functions for locally signing transactions impl SignerMiddleware where diff --git a/ethers-middleware/src/timelag/mod.rs b/ethers-middleware/src/timelag/mod.rs index 6c1e90b8..258fc15a 100644 --- a/ethers-middleware/src/timelag/mod.rs +++ b/ethers-middleware/src/timelag/mod.rs @@ -6,7 +6,7 @@ use ethers_core::types::{ use std::sync::Arc; use thiserror::Error; -use ethers_providers::{FromErr, Middleware}; +use ethers_providers::{Middleware, MiddlewareError}; type TimeLagResult = Result>; @@ -25,12 +25,20 @@ where } // Boilerplate -impl FromErr for TimeLagError { - fn from(src: M::Error) -> TimeLagError { +impl MiddlewareError for TimeLagError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { TimeLagError::MiddlewareError(src) } -} + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + TimeLagError::MiddlewareError(e) => Some(e), + _ => None, + } + } +} /// TimeLag Provider #[derive(Debug)] pub struct TimeLag { @@ -115,7 +123,7 @@ where .get_block_number() .await .map(|num| num - self.lag) - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn send_transaction + Send + Sync>( @@ -123,7 +131,10 @@ where tx: T, block: Option, ) -> Result, Self::Error> { - self.inner().send_transaction(tx, block).await.map_err(ethers_providers::FromErr::from) + self.inner() + .send_transaction(tx, block) + .await + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_block + Send + Sync>( @@ -135,7 +146,10 @@ where .await? .expect("Cannot return None if Some is passed in"); - self.inner().get_block(block_hash_or_number).await.map_err(ethers_providers::FromErr::from) + self.inner() + .get_block(block_hash_or_number) + .await + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_block_with_txs + Send + Sync>( @@ -150,7 +164,7 @@ where self.inner() .get_block_with_txs(block_hash_or_number) .await - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_uncle_count + Send + Sync>( @@ -165,7 +179,7 @@ where self.inner() .get_uncle_count(block_hash_or_number) .await - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_uncle + Send + Sync>( @@ -181,7 +195,7 @@ where self.inner() .get_uncle(block_hash_or_number, idx) .await - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_transaction_count + Send + Sync>( @@ -194,7 +208,7 @@ where self.inner() .get_transaction_count(from, block) .await - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn call( @@ -204,7 +218,7 @@ where ) -> Result { let block = self.normalize_block_id(block).await?; - self.inner().call(tx, block).await.map_err(ethers_providers::FromErr::from) + self.inner().call(tx, block).await.map_err(ethers_providers::MiddlewareError::from_err) } async fn get_balance + Send + Sync>( @@ -213,7 +227,10 @@ where block: Option, ) -> Result { let block = self.normalize_block_id(block).await?; - self.inner().get_balance(from, block).await.map_err(ethers_providers::FromErr::from) + self.inner() + .get_balance(from, block) + .await + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_transaction_receipt>( @@ -224,7 +241,7 @@ where .inner() .get_transaction_receipt(transaction_hash) .await - .map_err(ethers_providers::FromErr::from)?; + .map_err(ethers_providers::MiddlewareError::from_err)?; if receipt.is_none() { return Ok(None) @@ -251,7 +268,7 @@ where ) -> Result { let block = self.normalize_block_id(block).await?; - self.inner().get_code(at, block).await.map_err(ethers_providers::FromErr::from) + self.inner().get_code(at, block).await.map_err(ethers_providers::MiddlewareError::from_err) } async fn get_storage_at + Send + Sync>( @@ -264,7 +281,7 @@ where self.inner() .get_storage_at(from, location, block) .await - .map_err(ethers_providers::FromErr::from) + .map_err(ethers_providers::MiddlewareError::from_err) } async fn fill_transaction( @@ -272,7 +289,10 @@ where tx: &mut TypedTransaction, block: Option, ) -> Result<(), Self::Error> { - self.inner().fill_transaction(tx, block).await.map_err(ethers_providers::FromErr::from) + self.inner() + .fill_transaction(tx, block) + .await + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_block_receipts + Send + Sync>( @@ -285,7 +305,10 @@ where .await? .expect("Cannot return None if Some is passed in"); - self.inner().get_block_receipts(block).await.map_err(ethers_providers::FromErr::from) + self.inner() + .get_block_receipts(block) + .await + .map_err(ethers_providers::MiddlewareError::from_err) } async fn get_logs( @@ -295,7 +318,7 @@ where let mut filter = filter.clone(); filter.block_option = self.normalize_filter_range(filter.block_option).await?; - self.inner().get_logs(&filter).await.map_err(ethers_providers::FromErr::from) + self.inner().get_logs(&filter).await.map_err(ethers_providers::MiddlewareError::from_err) } async fn new_filter( diff --git a/ethers-middleware/src/transformer/middleware.rs b/ethers-middleware/src/transformer/middleware.rs index 3847b941..39fddf38 100644 --- a/ethers-middleware/src/transformer/middleware.rs +++ b/ethers-middleware/src/transformer/middleware.rs @@ -1,7 +1,7 @@ use super::{Transformer, TransformerError}; use async_trait::async_trait; use ethers_core::types::{transaction::eip2718::TypedTransaction, *}; -use ethers_providers::{FromErr, Middleware, PendingTransaction}; +use ethers_providers::{Middleware, MiddlewareError, PendingTransaction}; use thiserror::Error; #[derive(Debug)] @@ -33,10 +33,19 @@ pub enum TransformerMiddlewareError { MiddlewareError(M::Error), } -impl FromErr for TransformerMiddlewareError { - fn from(src: M::Error) -> TransformerMiddlewareError { +impl MiddlewareError for TransformerMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { TransformerMiddlewareError::MiddlewareError(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + TransformerMiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] diff --git a/ethers-providers/src/errors.rs b/ethers-providers/src/errors.rs new file mode 100644 index 00000000..ea5d5052 --- /dev/null +++ b/ethers-providers/src/errors.rs @@ -0,0 +1,219 @@ +use std::{error::Error, fmt::Debug}; +use thiserror::Error; + +use crate::JsonRpcError; + +/// An `RpcError` is an abstraction over error types returned by a +/// [`crate::JsonRpcClient`]. +/// +/// All clients can return [`JsonRpcError`] responses, as +/// well as serde deserialization errors. However, because client errors are +/// typically type-erased via the [`ProviderError`], the error info can be +/// difficult to access. This trait provides convenient access to the +/// underlying error types. +/// +/// This trait deals only with behavior that is common to all clients. +/// Client-specific errorvariants cannot be accessed via this trait. +pub trait RpcError: Error + Debug + Send + Sync { + /// Access an underlying JSON-RPC error (if any) + /// + /// Attempts to access an underlying [`JsonRpcError`]. If the underlying + /// error is not a JSON-RPC error response, this function will return + /// `None`. + fn as_error_response(&self) -> Option<&JsonRpcError>; + + /// Returns `true` if the underlying error is a JSON-RPC error response + fn is_error_response(&self) -> bool { + self.as_error_response().is_some() + } + + /// Access an underlying `serde_json` error (if any) + /// + /// Attempts to access an underlying [`serde_json::Error`]. If the + /// underlying error is not a serde_json error, this function will return + /// `None`. + /// + /// ### Implementor's Note + /// + /// When writing a stacked [`crate::JsonRpcClient`] abstraction (e.g. a quorum + /// provider or retrying provider), be sure to account for `serde_json` + /// errors at your layer, as well as at lower layers. + fn as_serde_error(&self) -> Option<&serde_json::Error>; + + /// Returns `true` if the underlying error is a serde_json (de)serialization + /// error. This method can be used to identify + fn is_serde_error(&self) -> bool { + self.as_serde_error().is_some() + } +} + +/// [`MiddlewareError`] is a companion trait to [`crate::Middleware`]. It +/// describes error behavior that is common to all Middleware errors. +/// +/// Like [`crate::Middleware`], it allows moving down through layered errors. +/// +/// Like [`RpcError`] it exposes convenient accessors to useful underlying +/// error information. +/// +/// +/// ## Not to Devs: +/// While this trait includes the same methods as [`RpcError`], it is not a +/// supertrait. This is so that 3rd party developers do not need to learn and +/// implement both traits. We provide default methods that delegate to inner +/// middleware errors on the assumption that it will eventually reach a +/// [`ProviderError`], which has correct behavior. This allows Middleware devs +/// to ignore the methods' presence if they want. Middleware are already plenty +/// complicated and we don't need to make it worse :) +pub trait MiddlewareError: Error + Sized + Send + Sync { + /// The `Inner` type is the next lower middleware layer's error type. + type Inner: MiddlewareError; + + /// Convert the next lower middleware layer's error to this layer's error + fn from_err(e: Self::Inner) -> Self; + + /// Attempt to convert this error to the next lower middleware's error. + /// Conversion fails if the error is not from an inner layer (i.e. the + /// error originates at this middleware layer) + fn as_inner(&self) -> Option<&Self::Inner>; + + /// Returns `true` if the underlying error stems from a lower middleware + /// layer + fn is_inner(&self) -> bool { + self.as_inner().is_some() + } + + /// Access an underlying `serde_json` error (if any) + /// + /// Attempts to access an underlying [`serde_json::Error`]. If the + /// underlying error is not a serde_json error, this function will return + /// `None`. + /// + /// ### Implementor's Note: + /// + /// When writing a custom middleware, if your middleware uses `serde_json` + /// we recommend a custom implementation of this method. It should first + /// check your Middleware's error for local `serde_json` errors, and then + /// delegate to inner if none is found. Failing to implement this method may + /// result in missed `serde_json` errors. + fn as_serde_error(&self) -> Option<&serde_json::Error> { + self.as_inner()?.as_serde_error() + } + + /// Returns `true` if the underlying error is a serde_json (de)serialization + /// error. This method can be used to identify + fn is_serde_error(&self) -> bool { + self.as_serde_error().is_some() + } + + /// Attempts to access an underlying [`ProviderError`], usually by + /// traversing the entire middleware stack. Access fails if the underlying + /// error is not a [`ProviderError`] + fn as_provider_error(&self) -> Option<&ProviderError> { + self.as_inner()?.as_provider_error() + } + + /// Convert a [`ProviderError`] to this type, by successively wrapping it + /// in the error types of all lower middleware + fn from_provider_err(p: ProviderError) -> Self { + Self::from_err(Self::Inner::from_provider_err(p)) + } + + /// Access an underlying JSON-RPC error (if any) + /// + /// Attempts to access an underlying [`JsonRpcError`]. If the underlying + /// error is not a JSON-RPC error response, this function will return + /// `None`. + fn as_error_response(&self) -> Option<&JsonRpcError> { + self.as_inner()?.as_error_response() + } + + /// Returns `true` if the underlying error is a JSON-RPC error response + fn is_error_response(&self) -> bool { + self.as_error_response().is_some() + } +} + +#[derive(Debug, Error)] +/// An error thrown when making a call to the provider +pub enum ProviderError { + /// An internal error in the JSON RPC Client + #[error("{0}")] + JsonRpcClientError(Box), + + /// An error during ENS name resolution + #[error("ens name not found: {0}")] + EnsError(String), + + /// Invalid reverse ENS name + #[error("reverse ens name not pointing to itself: {0}")] + EnsNotOwned(String), + + /// Error in underlying lib `serde_json` + #[error(transparent)] + SerdeJson(#[from] serde_json::Error), + + /// Error in underlying lib `hex` + #[error(transparent)] + HexError(#[from] hex::FromHexError), + + /// Error in underlying lib `reqwest` + #[error(transparent)] + HTTPError(#[from] reqwest::Error), + + /// Custom error from unknown source + #[error("custom error: {0}")] + CustomError(String), + + /// RPC method is not supported by this provider + #[error("unsupported RPC")] + UnsupportedRPC, + + /// Node is not supported by this provider + #[error("unsupported node client")] + UnsupportedNodeClient, + + /// Signer is not available to this provider. + #[error("Attempted to sign a transaction with no available signer. Hint: did you mean to use a SignerMiddleware?")] + SignerUnavailable, +} + +impl RpcError for ProviderError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + if let ProviderError::JsonRpcClientError(err) = self { + err.as_error_response() + } else { + None + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + ProviderError::JsonRpcClientError(e) => e.as_serde_error(), + ProviderError::SerdeJson(e) => Some(e), + _ => None, + } + } +} + +// Do not change these implementations, they are critical to proper middleware +// error stack behavior. +impl MiddlewareError for ProviderError { + type Inner = Self; + + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + RpcError::as_error_response(self) + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + RpcError::as_serde_error(self) + } + + fn from_err(e: Self::Inner) -> Self { + e + } + + fn as_inner(&self) -> Option<&Self::Inner> { + // prevents infinite loops + None + } +} diff --git a/ethers-providers/src/admin.rs b/ethers-providers/src/ext/admin.rs similarity index 97% rename from ethers-providers/src/admin.rs rename to ethers-providers/src/ext/admin.rs index 3de1ed4e..2c99664d 100644 --- a/ethers-providers/src/admin.rs +++ b/ethers-providers/src/ext/admin.rs @@ -1,6 +1,8 @@ -use crate::{H256, U256}; use enr::{k256::ecdsa::SigningKey, Enr}; -use ethers_core::utils::{from_int_or_hex, ChainConfig}; +use ethers_core::{ + types::{H256, U256}, + utils::{from_int_or_hex, ChainConfig}, +}; use serde::{Deserialize, Serialize}; use std::net::{IpAddr, SocketAddr}; @@ -49,9 +51,11 @@ pub struct Ports { /// This contains protocol information reported by the connected RPC node. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct ProtocolInfo { + /// Details about the node's supported eth protocol. `None` if unsupported #[serde(default, skip_serializing_if = "Option::is_none")] pub eth: Option, + /// Details about the node's supported snap protocol. `None` if unsupported #[serde(default, skip_serializing_if = "Option::is_none")] pub snap: Option, } @@ -94,9 +98,11 @@ pub struct SnapProtocolInfo {} /// connected RPC node. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct PeerProtocolInfo { + /// Details about the peer's supported eth protocol. `None` if unsupported #[serde(default, skip_serializing_if = "Option::is_none")] pub eth: Option, + /// Details about the peer's supported snap protocol. `None` if unsupported #[serde(default, skip_serializing_if = "Option::is_none")] pub snap: Option, } diff --git a/ethers-providers/src/ext/dev_rpc.rs b/ethers-providers/src/ext/dev_rpc.rs new file mode 100644 index 00000000..6e72b485 --- /dev/null +++ b/ethers-providers/src/ext/dev_rpc.rs @@ -0,0 +1,193 @@ +//! A middleware supporting development-specific JSON RPC methods +//! +//! # Example +//! +//!``` +//! use ethers_providers::{Provider, Http, Middleware, DevRpcMiddleware}; +//! use ethers_core::types::TransactionRequest; +//! use ethers_core::utils::Anvil; +//! use std::convert::TryFrom; +//! +//! # #[tokio::main(flavor = "current_thread")] +//! # async fn main() -> Result<(), Box> { +//! let anvil = Anvil::new().spawn(); +//! let provider = Provider::::try_from(anvil.endpoint()).unwrap(); +//! let client = DevRpcMiddleware::new(provider); +//! +//! // snapshot the initial state +//! let block0 = client.get_block_number().await.unwrap(); +//! let snap_id = client.snapshot().await.unwrap(); +//! +//! // send a transaction +//! let accounts = client.get_accounts().await?; +//! let from = accounts[0]; +//! let to = accounts[1]; +//! let balance_before = client.get_balance(to, None).await?; +//! let tx = TransactionRequest::new().to(to).value(1000).from(from); +//! client.send_transaction(tx, None).await?.await?; +//! let balance_after = client.get_balance(to, None).await?; +//! assert_eq!(balance_after, balance_before + 1000); +//! +//! // revert to snapshot +//! client.revert_to_snapshot(snap_id).await.unwrap(); +//! let balance_after_revert = client.get_balance(to, None).await?; +//! assert_eq!(balance_after_revert, balance_before); +//! # Ok(()) +//! # } +//! ``` +use crate::{Middleware, MiddlewareError, ProviderError}; +use async_trait::async_trait; +use ethers_core::types::U256; +use thiserror::Error; + +use std::fmt::Debug; + +/// `DevRpcMiddleware` +#[derive(Clone, Debug)] +pub struct DevRpcMiddleware(M); + +/// DevRpcMiddleware Errors +#[derive(Error, Debug)] +pub enum DevRpcMiddlewareError { + /// Internal Middleware error + #[error("{0}")] + MiddlewareError(M::Error), + + /// Internal Provider error + #[error("{0}")] + ProviderError(ProviderError), + + /// Attempted to revert to unavailable snapshot + #[error("Could not revert to snapshot")] + NoSnapshot, +} + +#[async_trait] +impl Middleware for DevRpcMiddleware { + type Error = DevRpcMiddlewareError; + type Provider = M::Provider; + type Inner = M; + + fn inner(&self) -> &M { + &self.0 + } +} + +impl MiddlewareError for DevRpcMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> DevRpcMiddlewareError { + DevRpcMiddlewareError::MiddlewareError(src) + } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + DevRpcMiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } +} + +impl From for DevRpcMiddlewareError +where + M: Middleware, +{ + fn from(src: ProviderError) -> Self { + Self::ProviderError(src) + } +} + +impl DevRpcMiddleware { + /// Instantiate a new `DevRpcMiddleware` + pub fn new(inner: M) -> Self { + Self(inner) + } + + /// Create a new snapshot on the DevRpc node. Return the Snapshot ID + /// + /// ### Note + /// + /// Ganache, Hardhat and Anvil increment snapshot ID even if no state has changed + pub async fn snapshot(&self) -> Result> { + self.provider().request::<(), U256>("evm_snapshot", ()).await.map_err(From::from) + } + + /// Revert the state of the DevRpc node to the Snapshot, specified by its ID + pub async fn revert_to_snapshot(&self, id: U256) -> Result<(), DevRpcMiddlewareError> { + let ok = self + .provider() + .request::<[U256; 1], bool>("evm_revert", [id]) + .await + .map_err(DevRpcMiddlewareError::ProviderError)?; + if ok { + Ok(()) + } else { + Err(DevRpcMiddlewareError::NoSnapshot) + } + } +} + +#[cfg(test)] +// Celo blocks can not get parsed when used with Ganache +#[cfg(not(feature = "celo"))] +mod tests { + use super::*; + use crate::{Http, Provider}; + use ethers_core::utils::Anvil; + use std::convert::TryFrom; + + #[tokio::test] + async fn test_snapshot() { + let anvil = Anvil::new().spawn(); + let provider = Provider::::try_from(anvil.endpoint()).unwrap(); + let client = DevRpcMiddleware::new(provider); + + // snapshot initial state + let block0 = client.get_block_number().await.unwrap(); + let time0 = client.get_block(block0).await.unwrap().unwrap().timestamp; + let snap_id0 = client.snapshot().await.unwrap(); + + // mine a new block + client.provider().mine(1).await.unwrap(); + + // snapshot state + let block1 = client.get_block_number().await.unwrap(); + let time1 = client.get_block(block1).await.unwrap().unwrap().timestamp; + let snap_id1 = client.snapshot().await.unwrap(); + + // mine some blocks + client.provider().mine(5).await.unwrap(); + + // snapshot state + let block2 = client.get_block_number().await.unwrap(); + let time2 = client.get_block(block2).await.unwrap().unwrap().timestamp; + let snap_id2 = client.snapshot().await.unwrap(); + + // mine some blocks + client.provider().mine(5).await.unwrap(); + + // revert_to_snapshot should reset state to snap id + client.revert_to_snapshot(snap_id2).await.unwrap(); + let block = client.get_block_number().await.unwrap(); + let time = client.get_block(block).await.unwrap().unwrap().timestamp; + assert_eq!(block, block2); + assert_eq!(time, time2); + + client.revert_to_snapshot(snap_id1).await.unwrap(); + let block = client.get_block_number().await.unwrap(); + let time = client.get_block(block).await.unwrap().unwrap().timestamp; + assert_eq!(block, block1); + assert_eq!(time, time1); + + // revert_to_snapshot should throw given non-existent or + // previously used snapshot + let result = client.revert_to_snapshot(snap_id1).await; + assert!(result.is_err()); + + client.revert_to_snapshot(snap_id0).await.unwrap(); + let block = client.get_block_number().await.unwrap(); + let time = client.get_block(block).await.unwrap().unwrap().timestamp; + assert_eq!(block, block0); + assert_eq!(time, time0); + } +} diff --git a/ethers-providers/src/ens.rs b/ethers-providers/src/ext/ens.rs similarity index 100% rename from ethers-providers/src/ens.rs rename to ethers-providers/src/ext/ens.rs diff --git a/ethers-providers/src/erc.rs b/ethers-providers/src/ext/erc.rs similarity index 92% rename from ethers-providers/src/erc.rs rename to ethers-providers/src/ext/erc.rs index ac99b4f4..538ffe71 100644 --- a/ethers-providers/src/erc.rs +++ b/ethers-providers/src/ext/erc.rs @@ -15,8 +15,11 @@ const IPFS_GATEWAY: &str = "https://ipfs.io/ipfs/"; /// An ERC 721 or 1155 token pub struct ERCNFT { + /// Type of the NFT pub type_: ERCNFTType, + /// Address of the NFT contract pub contract: Address, + /// NFT ID in that contract pub id: [u8; 32], } @@ -56,7 +59,9 @@ impl FromStr for ERCNFT { /// Supported ERCs #[derive(PartialEq, Eq)] pub enum ERCNFTType { + /// ERC721 ERC721, + /// ERC1155 ERC1155, } @@ -72,7 +77,8 @@ impl FromStr for ERCNFTType { } impl ERCNFTType { - pub fn resolution_selector(&self) -> Selector { + /// Get the method selector + pub const fn resolution_selector(&self) -> Selector { match self { // tokenURI(uint256) ERCNFTType::ERC721 => [0xc8, 0x7b, 0x56, 0xdd], @@ -85,6 +91,7 @@ impl ERCNFTType { /// ERC-1155 and ERC-721 metadata document. #[derive(Deserialize)] pub struct Metadata { + /// The URL of the image for the NFT pub image: String, } diff --git a/ethers-providers/src/ext/mod.rs b/ethers-providers/src/ext/mod.rs new file mode 100644 index 00000000..210bfdfc --- /dev/null +++ b/ethers-providers/src/ext/mod.rs @@ -0,0 +1,13 @@ +/// Types for the admin api +pub mod admin; +pub use admin::{NodeInfo, PeerInfo}; + +pub mod ens; +pub use ens::*; + +pub mod erc; + +#[cfg(feature = "dev-rpc")] +pub mod dev_rpc; +#[cfg(feature = "dev-rpc")] +pub use dev_rpc::{DevRpcMiddleware, DevRpcMiddlewareError}; diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index ad7ce8c1..50c64bd2 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -3,772 +3,40 @@ #![deny(rustdoc::broken_intra_doc_links)] #![allow(clippy::type_complexity)] #![doc = include_str!("../README.md")] +#![warn(missing_docs)] -mod transports; -pub use transports::*; +mod ext; +pub use ext::*; -mod provider; -pub use provider::{is_local_endpoint, FilterKind, Provider, ProviderError, ProviderExt}; +mod rpc; +pub use rpc::*; -// types for the admin api -pub mod admin; -pub use admin::{NodeInfo, PeerInfo}; +mod toolbox; +pub use toolbox::*; -// ENS support -pub mod ens; +/// Crate utilities and type aliases +mod utils; +pub use utils::{interval, maybe, EscalationPolicy}; -mod pending_transaction; -pub use pending_transaction::PendingTransaction; - -mod pending_escalator; -pub use pending_escalator::EscalatingPending; - -mod log_query; -pub use log_query::{LogQuery, LogQueryError}; +/// Errors +mod errors; +pub use errors::{MiddlewareError, ProviderError, RpcError}; mod stream; pub use futures_util::StreamExt; pub use stream::{ - interval, FilterWatcher, TransactionStream, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL, + tx_stream::TransactionStream, FilterWatcher, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL, }; -mod pubsub; -pub use pubsub::{PubsubClient, SubscriptionStream}; - -pub mod call_raw; -pub mod erc; - -use async_trait::async_trait; -use auto_impl::auto_impl; -use ethers_core::types::{ - transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed}, - *, -}; -use futures_util::future::join_all; -use serde::{de::DeserializeOwned, Serialize}; -use std::{error::Error, fmt::Debug, future::Future, pin::Pin}; -use url::Url; - -// feature-enabled support for dev-rpc methods -#[cfg(feature = "dev-rpc")] -pub use provider::dev_rpc::DevRpcMiddleware; - -/// A simple gas escalation policy -pub type EscalationPolicy = Box U256 + Send + Sync>; - -// Helper type alias -#[cfg(target_arch = "wasm32")] -pub(crate) type PinBoxFut<'a, T> = Pin> + 'a>>; -#[cfg(not(target_arch = "wasm32"))] -pub(crate) type PinBoxFut<'a, T> = - Pin> + Send + 'a>>; - -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[auto_impl(&, Box, Arc)] -/// Trait which must be implemented by data transports to be used with the Ethereum -/// JSON-RPC provider. -pub trait JsonRpcClient: Debug + Send + Sync { - /// A JSON-RPC Error - type Error: Error + Into; - - /// Sends a request with the provided JSON-RPC and parameters serialized as JSON - async fn request(&self, method: &str, params: T) -> Result - where - T: Debug + Serialize + Send + Sync, - R: DeserializeOwned + Send; -} - -pub trait FromErr { - fn from(src: T) -> Self; -} - -/// Calls the future if `item` is None, otherwise returns a `futures::ok` -pub async fn maybe(item: Option, f: F) -> Result -where - F: Future>, -{ - if let Some(item) = item { - futures_util::future::ok(item).await - } else { - f.await - } -} - -/// A middleware allows customizing requests send and received from an ethereum node. -/// -/// Writing a middleware is as simple as: -/// 1. implementing the [`inner`](crate::Middleware::inner) method to point to the next layer in the -/// "middleware onion", 2. implementing the [`FromErr`](crate::FromErr) trait on your middleware's -/// error type 3. implementing any of the methods you want to override -/// -/// ```rust -/// use ethers_providers::{Middleware, FromErr}; -/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction, BlockId}; -/// use thiserror::Error; -/// use async_trait::async_trait; -/// -/// #[derive(Debug)] -/// struct MyMiddleware(M); -/// -/// #[derive(Error, Debug)] -/// pub enum MyError { -/// #[error("{0}")] -/// MiddlewareError(M::Error), -/// -/// // Add your middleware's specific errors here -/// } -/// -/// impl FromErr for MyError { -/// fn from(src: M::Error) -> MyError { -/// MyError::MiddlewareError(src) -/// } -/// } -/// -/// #[async_trait] -/// impl Middleware for MyMiddleware -/// where -/// M: Middleware, -/// { -/// type Error = MyError; -/// type Provider = M::Provider; -/// type Inner = M; -/// -/// fn inner(&self) -> &M { -/// &self.0 -/// } -/// -/// /// Overrides the default `get_block_number` method to always return 0 -/// async fn get_block_number(&self) -> Result { -/// Ok(U64::zero()) -/// } -/// -/// /// Overrides the default `estimate_gas` method to log that it was called, -/// /// before forwarding the call to the next layer. -/// async fn estimate_gas(&self, tx: &TypedTransaction, block: Option) -> Result { -/// println!("Estimating gas..."); -/// self.inner().estimate_gas(tx, block).await.map_err(FromErr::from) -/// } -/// } -/// ``` -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -#[auto_impl(&, Box, Arc)] -pub trait Middleware: Sync + Send + Debug { - type Error: Sync + Send + Error + FromErr<::Error>; - type Provider: JsonRpcClient; - type Inner: Middleware; - - /// The next middleware in the stack - fn inner(&self) -> &Self::Inner; - - /// Convert a provider error into the associated error type by successively - /// converting it to every intermediate middleware error - fn convert_err(p: ProviderError) -> Self::Error { - let e = ::Inner::convert_err(p); - FromErr::from(e) - } - - /// The HTTP or Websocket provider. - fn provider(&self) -> &Provider { - self.inner().provider() - } - - fn default_sender(&self) -> Option
{ - self.inner().default_sender() - } - - async fn client_version(&self) -> Result { - self.inner().client_version().await.map_err(FromErr::from) - } - - /// Fill necessary details of a transaction for dispatch - /// - /// This function is defined on providers to behave as follows: - /// 1. populate the `from` field with the default sender - /// 2. resolve any ENS names in the tx `to` field - /// 3. Estimate gas usage - /// 4. Poll and set legacy or 1559 gas prices - /// 5. Set the chain_id with the provider's, if not already set - /// - /// It does NOT set the nonce by default. - /// - /// Middleware are encouraged to override any values _before_ delegating - /// to the inner implementation AND/OR modify the values provided by the - /// default implementation _after_ delegating. - /// - /// E.g. a middleware wanting to double gas prices should consider doing so - /// _after_ delegating and allowing the default implementation to poll gas. - async fn fill_transaction( - &self, - tx: &mut TypedTransaction, - block: Option, - ) -> Result<(), Self::Error> { - self.inner().fill_transaction(tx, block).await.map_err(FromErr::from) - } - - async fn get_block_number(&self) -> Result { - self.inner().get_block_number().await.map_err(FromErr::from) - } - - async fn send_transaction + Send + Sync>( - &self, - tx: T, - block: Option, - ) -> Result, Self::Error> { - self.inner().send_transaction(tx, block).await.map_err(FromErr::from) - } - - /// Send a transaction with a simple escalation policy. - /// - /// `policy` should be a boxed function that maps `original_gas_price` - /// and `number_of_previous_escalations` -> `new_gas_price`. - /// - /// e.g. `Box::new(|start, escalation_index| start * 1250.pow(escalations) / - /// 1000.pow(escalations))` - async fn send_escalating<'a>( - &'a self, - tx: &TypedTransaction, - escalations: usize, - policy: EscalationPolicy, - ) -> Result, Self::Error> { - let mut original = tx.clone(); - self.fill_transaction(&mut original, None).await?; - - // set the nonce, if no nonce is found - if original.nonce().is_none() { - let nonce = - self.get_transaction_count(tx.from().copied().unwrap_or_default(), None).await?; - original.set_nonce(nonce); - } - - let gas_price = original.gas_price().expect("filled"); - let sign_futs: Vec<_> = (0..escalations) - .map(|i| { - let new_price = policy(gas_price, i); - let mut r = original.clone(); - r.set_gas_price(new_price); - r - }) - .map(|req| async move { - self.sign_transaction(&req, self.default_sender().unwrap_or_default()) - .await - .map(|sig| req.rlp_signed(&sig)) - }) - .collect(); - - // we reverse for convenience. Ensuring that we can always just - // `pop()` the next tx off the back later - let mut signed = join_all(sign_futs).await.into_iter().collect::, _>>()?; - signed.reverse(); - - Ok(EscalatingPending::new(self.provider(), signed)) - } - - async fn resolve_name(&self, ens_name: &str) -> Result { - self.inner().resolve_name(ens_name).await.map_err(FromErr::from) - } - - async fn lookup_address(&self, address: Address) -> Result { - self.inner().lookup_address(address).await.map_err(FromErr::from) - } - - async fn resolve_avatar(&self, ens_name: &str) -> Result { - self.inner().resolve_avatar(ens_name).await.map_err(FromErr::from) - } - - async fn resolve_nft(&self, token: erc::ERCNFT) -> Result { - self.inner().resolve_nft(token).await.map_err(FromErr::from) - } - - async fn resolve_field(&self, ens_name: &str, field: &str) -> Result { - self.inner().resolve_field(ens_name, field).await.map_err(FromErr::from) - } - - async fn get_block + Send + Sync>( - &self, - block_hash_or_number: T, - ) -> Result>, Self::Error> { - self.inner().get_block(block_hash_or_number).await.map_err(FromErr::from) - } - - async fn get_block_with_txs + Send + Sync>( - &self, - block_hash_or_number: T, - ) -> Result>, Self::Error> { - self.inner().get_block_with_txs(block_hash_or_number).await.map_err(FromErr::from) - } - - async fn get_uncle_count + Send + Sync>( - &self, - block_hash_or_number: T, - ) -> Result { - self.inner().get_uncle_count(block_hash_or_number).await.map_err(FromErr::from) - } - - async fn get_uncle + Send + Sync>( - &self, - block_hash_or_number: T, - idx: U64, - ) -> Result>, Self::Error> { - self.inner().get_uncle(block_hash_or_number, idx).await.map_err(FromErr::from) - } - - async fn get_transaction_count + Send + Sync>( - &self, - from: T, - block: Option, - ) -> Result { - self.inner().get_transaction_count(from, block).await.map_err(FromErr::from) - } - - async fn estimate_gas( - &self, - tx: &TypedTransaction, - block: Option, - ) -> Result { - self.inner().estimate_gas(tx, block).await.map_err(FromErr::from) - } - - async fn call( - &self, - tx: &TypedTransaction, - block: Option, - ) -> Result { - self.inner().call(tx, block).await.map_err(FromErr::from) - } - - async fn syncing(&self) -> Result { - self.inner().syncing().await.map_err(FromErr::from) - } - - async fn get_chainid(&self) -> Result { - self.inner().get_chainid().await.map_err(FromErr::from) - } - - async fn get_net_version(&self) -> Result { - self.inner().get_net_version().await.map_err(FromErr::from) - } - - async fn get_balance + Send + Sync>( - &self, - from: T, - block: Option, - ) -> Result { - self.inner().get_balance(from, block).await.map_err(FromErr::from) - } - - async fn get_transaction>( - &self, - transaction_hash: T, - ) -> Result, Self::Error> { - self.inner().get_transaction(transaction_hash).await.map_err(FromErr::from) - } - - async fn get_transaction_receipt>( - &self, - transaction_hash: T, - ) -> Result, Self::Error> { - self.inner().get_transaction_receipt(transaction_hash).await.map_err(FromErr::from) - } - - async fn get_block_receipts + Send + Sync>( - &self, - block: T, - ) -> Result, Self::Error> { - self.inner().get_block_receipts(block).await.map_err(FromErr::from) - } - - async fn get_gas_price(&self) -> Result { - self.inner().get_gas_price().await.map_err(FromErr::from) - } - - async fn estimate_eip1559_fees( - &self, - estimator: Option>) -> (U256, U256)>, - ) -> Result<(U256, U256), Self::Error> { - self.inner().estimate_eip1559_fees(estimator).await.map_err(FromErr::from) - } - - async fn get_accounts(&self) -> Result, Self::Error> { - self.inner().get_accounts().await.map_err(FromErr::from) - } - - async fn send_raw_transaction<'a>( - &'a self, - tx: Bytes, - ) -> Result, Self::Error> { - self.inner().send_raw_transaction(tx).await.map_err(FromErr::from) - } - - /// This returns true if either the middleware stack contains a `SignerMiddleware`, or the - /// JSON-RPC provider has an unlocked key that can sign using the `eth_sign` call. If none of - /// the above conditions are met, then the middleware stack is not capable of signing data. - async fn is_signer(&self) -> bool { - self.inner().is_signer().await - } - - async fn sign + Send + Sync>( - &self, - data: T, - from: &Address, - ) -> Result { - self.inner().sign(data, from).await.map_err(FromErr::from) - } - - /// Sign a transaction via RPC call - async fn sign_transaction( - &self, - tx: &TypedTransaction, - from: Address, - ) -> Result { - self.inner().sign_transaction(tx, from).await.map_err(FromErr::from) - } - - ////// Contract state - - async fn get_logs(&self, filter: &Filter) -> Result, Self::Error> { - self.inner().get_logs(filter).await.map_err(FromErr::from) - } - - /// Returns a stream of logs are loaded in pages of given page size - fn get_logs_paginated<'a>( - &'a self, - filter: &Filter, - page_size: u64, - ) -> LogQuery<'a, Self::Provider> { - self.inner().get_logs_paginated(filter, page_size) - } - - async fn new_filter(&self, filter: FilterKind<'_>) -> Result { - self.inner().new_filter(filter).await.map_err(FromErr::from) - } - - async fn uninstall_filter + Send + Sync>( - &self, - id: T, - ) -> Result { - self.inner().uninstall_filter(id).await.map_err(FromErr::from) - } - - async fn watch<'a>( - &'a self, - filter: &Filter, - ) -> Result, Self::Error> { - self.inner().watch(filter).await.map_err(FromErr::from) - } - - async fn watch_pending_transactions( - &self, - ) -> Result, Self::Error> { - self.inner().watch_pending_transactions().await.map_err(FromErr::from) - } - - async fn get_filter_changes(&self, id: T) -> Result, Self::Error> - where - T: Into + Send + Sync, - R: Serialize + DeserializeOwned + Send + Sync + Debug, - { - self.inner().get_filter_changes(id).await.map_err(FromErr::from) - } - - async fn watch_blocks(&self) -> Result, Self::Error> { - self.inner().watch_blocks().await.map_err(FromErr::from) - } - - async fn get_code + Send + Sync>( - &self, - at: T, - block: Option, - ) -> Result { - self.inner().get_code(at, block).await.map_err(FromErr::from) - } - - async fn get_storage_at + Send + Sync>( - &self, - from: T, - location: H256, - block: Option, - ) -> Result { - self.inner().get_storage_at(from, location, block).await.map_err(FromErr::from) - } - - async fn get_proof + Send + Sync>( - &self, - from: T, - locations: Vec, - block: Option, - ) -> Result { - self.inner().get_proof(from, locations, block).await.map_err(FromErr::from) - } - - /// Returns an indication if this node is currently mining. - async fn mining(&self) -> Result { - self.inner().mining().await.map_err(FromErr::from) - } - - // Personal namespace - - async fn import_raw_key( - &self, - private_key: Bytes, - passphrase: String, - ) -> Result { - self.inner().import_raw_key(private_key, passphrase).await.map_err(FromErr::from) - } - - async fn unlock_account + Send + Sync>( - &self, - account: T, - passphrase: String, - duration: Option, - ) -> Result { - self.inner().unlock_account(account, passphrase, duration).await.map_err(FromErr::from) - } - - // Admin namespace - - async fn add_peer(&self, enode_url: String) -> Result { - self.inner().add_peer(enode_url).await.map_err(FromErr::from) - } - - async fn add_trusted_peer(&self, enode_url: String) -> Result { - self.inner().add_trusted_peer(enode_url).await.map_err(FromErr::from) - } - - async fn node_info(&self) -> Result { - self.inner().node_info().await.map_err(FromErr::from) - } - - async fn peers(&self) -> Result, Self::Error> { - self.inner().peers().await.map_err(FromErr::from) - } - - async fn remove_peer(&self, enode_url: String) -> Result { - self.inner().remove_peer(enode_url).await.map_err(FromErr::from) - } - - async fn remove_trusted_peer(&self, enode_url: String) -> Result { - self.inner().remove_trusted_peer(enode_url).await.map_err(FromErr::from) - } - - // Miner namespace - - /// Starts the miner with the given number of threads. If threads is nil, the number of workers - /// started is equal to the number of logical CPUs that are usable by this process. If mining - /// is already running, this method adjust the number of threads allowed to use and updates the - /// minimum price required by the transaction pool. - async fn start_mining(&self, threads: Option) -> Result<(), Self::Error> { - self.inner().start_mining(threads).await.map_err(FromErr::from) - } - - /// Stop terminates the miner, both at the consensus engine level as well as at - /// the block creation level. - async fn stop_mining(&self) -> Result<(), Self::Error> { - self.inner().stop_mining().await.map_err(FromErr::from) - } - - // Mempool inspection for Geth's API - - async fn txpool_content(&self) -> Result { - self.inner().txpool_content().await.map_err(FromErr::from) - } - - async fn txpool_inspect(&self) -> Result { - self.inner().txpool_inspect().await.map_err(FromErr::from) - } - - async fn txpool_status(&self) -> Result { - self.inner().txpool_status().await.map_err(FromErr::from) - } - - // Geth `trace` support - - /// After replaying any previous transactions in the same block, - /// Replays a transaction, returning the traces configured with passed options - async fn debug_trace_transaction( - &self, - tx_hash: TxHash, - trace_options: GethDebugTracingOptions, - ) -> Result { - self.inner().debug_trace_transaction(tx_hash, trace_options).await.map_err(FromErr::from) - } - - /// Executes the given call and returns a number of possible traces for it - async fn debug_trace_call + Send + Sync>( - &self, - req: T, - block: Option, - trace_options: GethDebugTracingCallOptions, - ) -> Result { - self.inner().debug_trace_call(req, block, trace_options).await.map_err(FromErr::from) - } - - // Parity `trace` support - - /// Executes the given call and returns a number of possible traces for it - async fn trace_call + Send + Sync>( - &self, - req: T, - trace_type: Vec, - block: Option, - ) -> Result { - self.inner().trace_call(req, trace_type, block).await.map_err(FromErr::from) - } - - async fn trace_call_many + Send + Sync>( - &self, - req: Vec<(T, Vec)>, - block: Option, - ) -> Result, Self::Error> { - self.inner().trace_call_many(req, block).await.map_err(FromErr::from) - } - - /// Traces a call to `eth_sendRawTransaction` without making the call, returning the traces - async fn trace_raw_transaction( - &self, - data: Bytes, - trace_type: Vec, - ) -> Result { - self.inner().trace_raw_transaction(data, trace_type).await.map_err(FromErr::from) - } - - /// Replays a transaction, returning the traces - async fn trace_replay_transaction( - &self, - hash: H256, - trace_type: Vec, - ) -> Result { - self.inner().trace_replay_transaction(hash, trace_type).await.map_err(FromErr::from) - } - - /// Replays all transactions in a block returning the requested traces for each transaction - async fn trace_replay_block_transactions( - &self, - block: BlockNumber, - trace_type: Vec, - ) -> Result, Self::Error> { - self.inner().trace_replay_block_transactions(block, trace_type).await.map_err(FromErr::from) - } - - /// Returns traces created at given block - async fn trace_block(&self, block: BlockNumber) -> Result, Self::Error> { - self.inner().trace_block(block).await.map_err(FromErr::from) - } - - /// Return traces matching the given filter - async fn trace_filter(&self, filter: TraceFilter) -> Result, Self::Error> { - self.inner().trace_filter(filter).await.map_err(FromErr::from) - } - - /// Returns trace at the given position - async fn trace_get + Send + Sync>( - &self, - hash: H256, - index: Vec, - ) -> Result { - self.inner().trace_get(hash, index).await.map_err(FromErr::from) - } - - /// Returns all traces of a given transaction - async fn trace_transaction(&self, hash: H256) -> Result, Self::Error> { - self.inner().trace_transaction(hash).await.map_err(FromErr::from) - } - - // Parity namespace - - /// Returns all receipts for that block. Must be done on a parity node. - async fn parity_block_receipts + Send + Sync>( - &self, - block: T, - ) -> Result, Self::Error> { - self.inner().parity_block_receipts(block).await.map_err(FromErr::from) - } - - async fn subscribe( - &self, - params: T, - ) -> Result, Self::Error> - where - T: Debug + Serialize + Send + Sync, - R: DeserializeOwned + Send + Sync, - ::Provider: PubsubClient, - { - self.inner().subscribe(params).await.map_err(FromErr::from) - } - - async fn unsubscribe(&self, id: T) -> Result - where - T: Into + Send + Sync, - ::Provider: PubsubClient, - { - self.inner().unsubscribe(id).await.map_err(FromErr::from) - } - - async fn subscribe_blocks( - &self, - ) -> Result>, Self::Error> - where - ::Provider: PubsubClient, - { - self.inner().subscribe_blocks().await.map_err(FromErr::from) - } - - async fn subscribe_pending_txs( - &self, - ) -> Result, Self::Error> - where - ::Provider: PubsubClient, - { - self.inner().subscribe_pending_txs().await.map_err(FromErr::from) - } - - async fn subscribe_logs<'a>( - &'a self, - filter: &Filter, - ) -> Result, Self::Error> - where - ::Provider: PubsubClient, - { - self.inner().subscribe_logs(filter).await.map_err(FromErr::from) - } - - async fn fee_history + serde::Serialize + Send + Sync>( - &self, - block_count: T, - last_block: BlockNumber, - reward_percentiles: &[f64], - ) -> Result { - self.inner() - .fee_history(block_count, last_block, reward_percentiles) - .await - .map_err(FromErr::from) - } - - async fn create_access_list( - &self, - tx: &TypedTransaction, - block: Option, - ) -> Result { - self.inner().create_access_list(tx, block).await.map_err(FromErr::from) - } -} - +mod middleware; #[cfg(feature = "celo")] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -pub trait CeloMiddleware: Middleware { - async fn get_validators_bls_public_keys + Send + Sync>( - &self, - block_id: T, - ) -> Result, ProviderError> { - self.provider().get_validators_bls_public_keys(block_id).await.map_err(FromErr::from) - } -} +pub use middleware::CeloMiddleware; +pub use middleware::Middleware; #[allow(deprecated)] pub use test_provider::{GOERLI, MAINNET, ROPSTEN, SEPOLIA}; +#[allow(missing_docs)] /// Pre-instantiated Infura HTTP clients which rotate through multiple API keys /// to prevent rate limits pub mod test_provider { diff --git a/ethers-providers/src/middleware.rs b/ethers-providers/src/middleware.rs new file mode 100644 index 00000000..a5cc533a --- /dev/null +++ b/ethers-providers/src/middleware.rs @@ -0,0 +1,962 @@ +use async_trait::async_trait; +use auto_impl::auto_impl; +use ethers_core::types::{ + transaction::{eip2718::TypedTransaction, eip2930::AccessListWithGasUsed}, + *, +}; +use futures_util::future::join_all; +use serde::{de::DeserializeOwned, Serialize}; +use std::fmt::Debug; +use url::Url; + +use crate::{ + erc, EscalatingPending, EscalationPolicy, FilterKind, FilterWatcher, JsonRpcClient, LogQuery, + MiddlewareError, NodeInfo, PeerInfo, PendingTransaction, Provider, ProviderError, PubsubClient, + SubscriptionStream, +}; + +/// A middleware allows customizing requests send and received from an ethereum node. +/// +/// Writing a middleware is as simple as: +/// 1. implementing the [`inner`](crate::Middleware::inner) method to point to the next layer in the +/// "middleware onion", 2. implementing the +/// [`MiddlewareError`](crate::MiddlewareError) trait on your middleware's +/// error type 3. implementing any of the methods you want to override +/// +/// ``` +/// use ethers_providers::{Middleware, MiddlewareError}; +/// use ethers_core::types::{U64, TransactionRequest, U256, transaction::eip2718::TypedTransaction, BlockId}; +/// use thiserror::Error; +/// use async_trait::async_trait; +/// +/// #[derive(Debug)] +/// struct MyMiddleware(M); +/// +/// #[derive(Error, Debug)] +/// pub enum MyError { +/// #[error("{0}")] +/// MiddlewareError(M::Error), +/// +/// // Add your middleware's specific errors here +/// } +/// +/// impl MiddlewareError for MyError { +/// type Inner = M::Error; +/// +/// fn from_err(src: M::Error) -> MyError { +/// MyError::MiddlewareError(src) +/// } +/// +/// fn as_inner(&self) -> Option<&Self::Inner> { +/// match self { +/// MyError::MiddlewareError(e) => Some(e), +/// _ => None, +/// } +/// } +/// } +/// +/// #[async_trait] +/// impl Middleware for MyMiddleware +/// where +/// M: Middleware, +/// { +/// type Error = MyError; +/// type Provider = M::Provider; +/// type Inner = M; +/// +/// fn inner(&self) -> &M { +/// &self.0 +/// } +/// +/// /// Overrides the default `get_block_number` method to always return 0 +/// async fn get_block_number(&self) -> Result { +/// Ok(U64::zero()) +/// } +/// +/// /// Overrides the default `estimate_gas` method to log that it was called, +/// /// before forwarding the call to the next layer. +/// async fn estimate_gas(&self, tx: &TypedTransaction, block: Option) -> Result { +/// println!("Estimating gas..."); +/// self.inner().estimate_gas(tx, block).await.map_err(MiddlewareError::from_err) +/// } +/// } +/// ``` +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[auto_impl(&, Box, Arc)] +pub trait Middleware: Sync + Send + Debug { + /// Error type returned by most operations + type Error: MiddlewareError::Inner as Middleware>::Error>; + /// The JSON-RPC client type at the bottom of the stack + type Provider: JsonRpcClient; + /// The next-lower middleware in the middleware stack + type Inner: Middleware; + + /// Get a reference to the next-lower middleware in the middleware stack + fn inner(&self) -> &Self::Inner; + + /// Convert a provider error into the associated error type by successively + /// converting it to every intermediate middleware error + fn convert_err(p: ProviderError) -> Self::Error { + Self::Error::from_provider_err(p) + } + + /// The HTTP or Websocket provider. + fn provider(&self) -> &Provider { + self.inner().provider() + } + + /// Return the default sender (if any). This will typically be the + /// connected node's first address, or the address of a Signer in a lower + /// middleware stack + fn default_sender(&self) -> Option
{ + self.inner().default_sender() + } + + /// Returns the current client version using the `web3_clientVersion` RPC. + async fn client_version(&self) -> Result { + self.inner().client_version().await.map_err(MiddlewareError::from_err) + } + + /// Fill necessary details of a transaction for dispatch + /// + /// This function is defined on providers to behave as follows: + /// 1. populate the `from` field with the default sender + /// 2. resolve any ENS names in the tx `to` field + /// 3. Estimate gas usage + /// 4. Poll and set legacy or 1559 gas prices + /// 5. Set the chain_id with the provider's, if not already set + /// + /// It does NOT set the nonce by default. + /// + /// Middleware are encouraged to override any values _before_ delegating + /// to the inner implementation AND/OR modify the values provided by the + /// default implementation _after_ delegating. + /// + /// E.g. a middleware wanting to double gas prices should consider doing so + /// _after_ delegating and allowing the default implementation to poll gas. + async fn fill_transaction( + &self, + tx: &mut TypedTransaction, + block: Option, + ) -> Result<(), Self::Error> { + self.inner().fill_transaction(tx, block).await.map_err(MiddlewareError::from_err) + } + + /// Get the block number + async fn get_block_number(&self) -> Result { + self.inner().get_block_number().await.map_err(MiddlewareError::from_err) + } + + /// Sends the transaction to the entire Ethereum network and returns the + /// transaction's hash. This will consume gas from the account that signed + /// the transaction. This call will fail if no signer is available, and the + /// RPC node does not have an unlocked accounts + async fn send_transaction + Send + Sync>( + &self, + tx: T, + block: Option, + ) -> Result, Self::Error> { + self.inner().send_transaction(tx, block).await.map_err(MiddlewareError::from_err) + } + + /// Send a transaction with a simple escalation policy. + /// + /// `policy` should be a boxed function that maps `original_gas_price` + /// and `number_of_previous_escalations` -> `new_gas_price`. + /// + /// e.g. `Box::new(|start, escalation_index| start * 1250.pow(escalations) / + /// 1000.pow(escalations))` + async fn send_escalating<'a>( + &'a self, + tx: &TypedTransaction, + escalations: usize, + policy: EscalationPolicy, + ) -> Result, Self::Error> { + let mut original = tx.clone(); + self.fill_transaction(&mut original, None).await?; + + // set the nonce, if no nonce is found + if original.nonce().is_none() { + let nonce = + self.get_transaction_count(tx.from().copied().unwrap_or_default(), None).await?; + original.set_nonce(nonce); + } + + let gas_price = original.gas_price().expect("filled"); + let sign_futs: Vec<_> = (0..escalations) + .map(|i| { + let new_price = policy(gas_price, i); + let mut r = original.clone(); + r.set_gas_price(new_price); + r + }) + .map(|req| async move { + self.sign_transaction(&req, self.default_sender().unwrap_or_default()) + .await + .map(|sig| req.rlp_signed(&sig)) + }) + .collect(); + + // we reverse for convenience. Ensuring that we can always just + // `pop()` the next tx off the back later + let mut signed = join_all(sign_futs).await.into_iter().collect::, _>>()?; + signed.reverse(); + + Ok(EscalatingPending::new(self.provider(), signed)) + } + + ////// Ethereum Naming Service + // The Ethereum Naming Service (ENS) allows easy to remember and use names to + // be assigned to Ethereum addresses. Any provider operation which takes an address + // may also take an ENS name. + // + // ENS also provides the ability for a reverse lookup, which determines the name for an address + // if it has been configured. + + /// Returns the address that the `ens_name` resolves to (or None if not configured). + /// + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// an address. This should theoretically never happen. + async fn resolve_name(&self, ens_name: &str) -> Result { + self.inner().resolve_name(ens_name).await.map_err(MiddlewareError::from_err) + } + + /// Returns the ENS name the `address` resolves to (or None if not configured). + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// a string. This should theoretically never happen. + async fn lookup_address(&self, address: Address) -> Result { + self.inner().lookup_address(address).await.map_err(MiddlewareError::from_err) + } + + /// Returns the avatar HTTP link of the avatar that the `ens_name` resolves to (or None + /// if not configured) + /// + /// # Example + /// ```no_run + /// # use ethers_providers::{Provider, Http as HttpProvider, Middleware}; + /// # use std::convert::TryFrom; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// # let provider = Provider::::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap(); + /// let avatar = provider.resolve_avatar("parishilton.eth").await.unwrap(); + /// assert_eq!(avatar.to_string(), "https://i.imgur.com/YW3Hzph.jpg"); + /// # } + /// ``` + /// + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// a string. This should theoretically never happen. + async fn resolve_avatar(&self, ens_name: &str) -> Result { + self.inner().resolve_avatar(ens_name).await.map_err(MiddlewareError::from_err) + } + + /// Returns the URL (not necesserily HTTP) of the image behind a token. + /// + /// # Example + /// ```no_run + /// # use ethers_providers::{Provider, Http as HttpProvider, Middleware}; + /// # use std::{str::FromStr, convert::TryFrom}; + /// # #[tokio::main(flavor = "current_thread")] + /// # async fn main() { + /// # let provider = Provider::::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap(); + /// let token = ethers_providers::erc::ERCNFT::from_str("erc721:0xc92ceddfb8dd984a89fb494c376f9a48b999aafc/9018").unwrap(); + /// let token_image = provider.resolve_nft(token).await.unwrap(); + /// assert_eq!(token_image.to_string(), "https://creature.mypinata.cloud/ipfs/QmNwj3aUzXfG4twV3no7hJRYxLLAWNPk6RrfQaqJ6nVJFa/9018.jpg"); + /// # } + /// ``` + /// + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// a string. This should theoretically never happen. + async fn resolve_nft(&self, token: erc::ERCNFT) -> Result { + self.inner().resolve_nft(token).await.map_err(MiddlewareError::from_err) + } + + /// Fetch a field for the `ens_name` (no None if not configured). + /// + /// # Panics + /// + /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as + /// a string. This should theoretically never happen. + async fn resolve_field(&self, ens_name: &str, field: &str) -> Result { + self.inner().resolve_field(ens_name, field).await.map_err(MiddlewareError::from_err) + } + + /// Gets the block at `block_hash_or_number` (transaction hashes only) + async fn get_block + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, Self::Error> { + self.inner().get_block(block_hash_or_number).await.map_err(MiddlewareError::from_err) + } + + /// Gets the block at `block_hash_or_number` (full transactions included) + async fn get_block_with_txs + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result>, Self::Error> { + self.inner() + .get_block_with_txs(block_hash_or_number) + .await + .map_err(MiddlewareError::from_err) + } + + /// Gets the block uncle count at `block_hash_or_number` + async fn get_uncle_count + Send + Sync>( + &self, + block_hash_or_number: T, + ) -> Result { + self.inner().get_uncle_count(block_hash_or_number).await.map_err(MiddlewareError::from_err) + } + + /// Gets the block uncle at `block_hash_or_number` and `idx` + async fn get_uncle + Send + Sync>( + &self, + block_hash_or_number: T, + idx: U64, + ) -> Result>, Self::Error> { + self.inner().get_uncle(block_hash_or_number, idx).await.map_err(MiddlewareError::from_err) + } + + /// Returns the nonce of the address + async fn get_transaction_count + Send + Sync>( + &self, + from: T, + block: Option, + ) -> Result { + self.inner().get_transaction_count(from, block).await.map_err(MiddlewareError::from_err) + } + + /// Sends a transaction to a single Ethereum node and return the estimated amount of gas + /// required (as a U256) to send it This is free, but only an estimate. Providing too little + /// gas will result in a transaction being rejected (while still consuming all provided + /// gas). + async fn estimate_gas( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + self.inner().estimate_gas(tx, block).await.map_err(MiddlewareError::from_err) + } + + /// Sends the read-only (constant) transaction to a single Ethereum node and return the result + /// (as bytes) of executing it. This is free, since it does not change any state on the + /// blockchain. + async fn call( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + self.inner().call(tx, block).await.map_err(MiddlewareError::from_err) + } + + /// Return current client syncing status. If IsFalse sync is over. + async fn syncing(&self) -> Result { + self.inner().syncing().await.map_err(MiddlewareError::from_err) + } + + /// Returns the currently configured chain id, a value used in replay-protected + /// transaction signing as introduced by EIP-155. + async fn get_chainid(&self) -> Result { + self.inner().get_chainid().await.map_err(MiddlewareError::from_err) + } + + /// Returns the network version. + async fn get_net_version(&self) -> Result { + self.inner().get_net_version().await.map_err(MiddlewareError::from_err) + } + + /// Returns the account's balance + async fn get_balance + Send + Sync>( + &self, + from: T, + block: Option, + ) -> Result { + self.inner().get_balance(from, block).await.map_err(MiddlewareError::from_err) + } + + /// Gets the transaction with `transaction_hash` + async fn get_transaction>( + &self, + transaction_hash: T, + ) -> Result, Self::Error> { + self.inner().get_transaction(transaction_hash).await.map_err(MiddlewareError::from_err) + } + + /// Gets the transaction receipt with `transaction_hash` + async fn get_transaction_receipt>( + &self, + transaction_hash: T, + ) -> Result, Self::Error> { + self.inner() + .get_transaction_receipt(transaction_hash) + .await + .map_err(MiddlewareError::from_err) + } + + /// Returns all receipts for a block. + /// + /// Note that this uses the `eth_getBlockReceipts` RPC, which is + /// non-standard and currently supported by Erigon. + async fn get_block_receipts + Send + Sync>( + &self, + block: T, + ) -> Result, Self::Error> { + self.inner().get_block_receipts(block).await.map_err(MiddlewareError::from_err) + } + + /// Gets the current gas price as estimated by the node + async fn get_gas_price(&self) -> Result { + self.inner().get_gas_price().await.map_err(MiddlewareError::from_err) + } + + /// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for + /// EIP-1559 compatible transactions. + async fn estimate_eip1559_fees( + &self, + estimator: Option>) -> (U256, U256)>, + ) -> Result<(U256, U256), Self::Error> { + self.inner().estimate_eip1559_fees(estimator).await.map_err(MiddlewareError::from_err) + } + + /// Gets the accounts on the node + async fn get_accounts(&self) -> Result, Self::Error> { + self.inner().get_accounts().await.map_err(MiddlewareError::from_err) + } + + /// Send the raw RLP encoded transaction to the entire Ethereum network and returns the + /// transaction's hash This will consume gas from the account that signed the transaction. + async fn send_raw_transaction<'a>( + &'a self, + tx: Bytes, + ) -> Result, Self::Error> { + self.inner().send_raw_transaction(tx).await.map_err(MiddlewareError::from_err) + } + + /// This returns true if either the middleware stack contains a `SignerMiddleware`, or the + /// JSON-RPC provider has an unlocked key that can sign using the `eth_sign` call. If none of + /// the above conditions are met, then the middleware stack is not capable of signing data. + async fn is_signer(&self) -> bool { + self.inner().is_signer().await + } + + /// Signs data using a specific account. This account needs to be unlocked, + /// or the middleware stack must contain a `SignerMiddleware` + async fn sign + Send + Sync>( + &self, + data: T, + from: &Address, + ) -> Result { + self.inner().sign(data, from).await.map_err(MiddlewareError::from_err) + } + + /// Sign a transaction via RPC call + async fn sign_transaction( + &self, + tx: &TypedTransaction, + from: Address, + ) -> Result { + self.inner().sign_transaction(tx, from).await.map_err(MiddlewareError::from_err) + } + + ////// Contract state + + /// Returns an array (possibly empty) of logs that match the filter + async fn get_logs(&self, filter: &Filter) -> Result, Self::Error> { + self.inner().get_logs(filter).await.map_err(MiddlewareError::from_err) + } + + /// Returns a stream of logs are loaded in pages of given page size + fn get_logs_paginated<'a>( + &'a self, + filter: &Filter, + page_size: u64, + ) -> LogQuery<'a, Self::Provider> { + self.inner().get_logs_paginated(filter, page_size) + } + + /// Install a new filter on the node. + /// + /// This method is hidden because filter lifecycle should be managed by + /// the [`FilterWatcher`] + #[doc(hidden)] + async fn new_filter(&self, filter: FilterKind<'_>) -> Result { + self.inner().new_filter(filter).await.map_err(MiddlewareError::from_err) + } + + /// Uninstalls a filter. + /// + /// This method is hidden because filter lifecycle should be managed by + /// the [`FilterWatcher`] + #[doc(hidden)] + async fn uninstall_filter + Send + Sync>( + &self, + id: T, + ) -> Result { + self.inner().uninstall_filter(id).await.map_err(MiddlewareError::from_err) + } + + /// Streams event logs matching the filter. + /// + /// This function streams via a polling system, by repeatedly dispatching + /// RPC requests. If possible, prefer using a WS or IPC connection and the + /// `stream` interface + async fn watch<'a>( + &'a self, + filter: &Filter, + ) -> Result, Self::Error> { + self.inner().watch(filter).await.map_err(MiddlewareError::from_err) + } + + /// Streams pending transactions. + /// + /// This function streams via a polling system, by repeatedly dispatching + /// RPC requests. If possible, prefer using a WS or IPC connection and the + /// `stream` interface + async fn watch_pending_transactions( + &self, + ) -> Result, Self::Error> { + self.inner().watch_pending_transactions().await.map_err(MiddlewareError::from_err) + } + + /// Polling method for a filter, which returns an array of logs which occurred since last poll. + /// + /// This method must be called with one of the following return types, depending on the filter + /// type: + /// - `eth_newBlockFilter`: [`H256`], returns block hashes + /// - `eth_newPendingTransactionFilter`: [`H256`], returns transaction hashes + /// - `eth_newFilter`: [`Log`], returns raw logs + /// + /// If one of these types is not used, decoding will fail and the method will + /// return an error. + /// + /// [`H256`]: ethers_core::types::H256 + /// [`Log`]: ethers_core::types::Log + /// + /// This method is hidden because filter lifecycle should be managed by + /// the [`FilterWatcher`] + #[doc(hidden)] + async fn get_filter_changes(&self, id: T) -> Result, Self::Error> + where + T: Into + Send + Sync, + R: Serialize + DeserializeOwned + Send + Sync + Debug, + { + self.inner().get_filter_changes(id).await.map_err(MiddlewareError::from_err) + } + + /// Streams new block hashes + /// + /// This function streams via a polling system, by repeatedly dispatching + /// RPC requests. If possible, prefer using a WS or IPC connection and the + /// `stream` interface + async fn watch_blocks(&self) -> Result, Self::Error> { + self.inner().watch_blocks().await.map_err(MiddlewareError::from_err) + } + + /// Returns the deployed code at a given address + async fn get_code + Send + Sync>( + &self, + at: T, + block: Option, + ) -> Result { + self.inner().get_code(at, block).await.map_err(MiddlewareError::from_err) + } + + /// Get the storage of an address for a particular slot location + async fn get_storage_at + Send + Sync>( + &self, + from: T, + location: H256, + block: Option, + ) -> Result { + self.inner().get_storage_at(from, location, block).await.map_err(MiddlewareError::from_err) + } + + /// Returns the EIP-1186 proof response + /// + async fn get_proof + Send + Sync>( + &self, + from: T, + locations: Vec, + block: Option, + ) -> Result { + self.inner().get_proof(from, locations, block).await.map_err(MiddlewareError::from_err) + } + + /// Returns an indication if this node is currently mining. + async fn mining(&self) -> Result { + self.inner().mining().await.map_err(MiddlewareError::from_err) + } + + // Personal namespace + // NOTE: This will eventually need to be enabled by users explicitly because the personal + // namespace is being deprecated: + // Issue: https://github.com/ethereum/go-ethereum/issues/25948 + // PR: https://github.com/ethereum/go-ethereum/pull/26390 + + /// Sends the given key to the node to be encrypted with the provided + /// passphrase and stored. + /// + /// The key represents a secp256k1 private key and should be 32 bytes. + async fn import_raw_key( + &self, + private_key: Bytes, + passphrase: String, + ) -> Result { + self.inner() + .import_raw_key(private_key, passphrase) + .await + .map_err(MiddlewareError::from_err) + } + + /// Prompts the node to decrypt the given account from its keystore. + /// + /// If the duration provided is `None`, then the account will be unlocked + /// indefinitely. Otherwise, the account will be unlocked for the provided + /// number of seconds. + async fn unlock_account + Send + Sync>( + &self, + account: T, + passphrase: String, + duration: Option, + ) -> Result { + self.inner() + .unlock_account(account, passphrase, duration) + .await + .map_err(MiddlewareError::from_err) + } + + // Admin namespace + + /// Requests adding the given peer, returning a boolean representing + /// whether or not the peer was accepted for tracking. + async fn add_peer(&self, enode_url: String) -> Result { + self.inner().add_peer(enode_url).await.map_err(MiddlewareError::from_err) + } + + /// Requests adding the given peer as a trusted peer, which the node will + /// always connect to even when its peer slots are full. + async fn add_trusted_peer(&self, enode_url: String) -> Result { + self.inner().add_trusted_peer(enode_url).await.map_err(MiddlewareError::from_err) + } + + /// Returns general information about the node as well as information about the running p2p + /// protocols (e.g. `eth`, `snap`). + async fn node_info(&self) -> Result { + self.inner().node_info().await.map_err(MiddlewareError::from_err) + } + + /// Returns the list of peers currently connected to the node. + async fn peers(&self) -> Result, Self::Error> { + self.inner().peers().await.map_err(MiddlewareError::from_err) + } + + /// Requests to remove the given peer, returning true if the enode was successfully parsed and + /// the peer was removed. + async fn remove_peer(&self, enode_url: String) -> Result { + self.inner().remove_peer(enode_url).await.map_err(MiddlewareError::from_err) + } + + /// Requests to remove the given peer, returning a boolean representing whether or not the + /// enode url passed was validated. A return value of `true` does not necessarily mean that the + /// peer was disconnected. + async fn remove_trusted_peer(&self, enode_url: String) -> Result { + self.inner().remove_trusted_peer(enode_url).await.map_err(MiddlewareError::from_err) + } + + // Miner namespace + + /// Starts the miner with the given number of threads. If threads is nil, the number of workers + /// started is equal to the number of logical CPUs that are usable by this process. If mining + /// is already running, this method adjust the number of threads allowed to use and updates the + /// minimum price required by the transaction pool. + async fn start_mining(&self, threads: Option) -> Result<(), Self::Error> { + self.inner().start_mining(threads).await.map_err(MiddlewareError::from_err) + } + + /// Stop terminates the miner, both at the consensus engine level as well as at + /// the block creation level. + async fn stop_mining(&self) -> Result<(), Self::Error> { + self.inner().stop_mining().await.map_err(MiddlewareError::from_err) + } + + // Mempool inspection for Geth's API + + /// Returns the details of all transactions currently pending for inclusion in the next + /// block(s), as well as the ones that are being scheduled for future execution only. + /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content) + async fn txpool_content(&self) -> Result { + self.inner().txpool_content().await.map_err(MiddlewareError::from_err) + } + + /// Returns a summary of all the transactions currently pending for inclusion in the next + /// block(s), as well as the ones that are being scheduled for future execution only. + /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_inspect) + async fn txpool_inspect(&self) -> Result { + self.inner().txpool_inspect().await.map_err(MiddlewareError::from_err) + } + + /// Returns the number of transactions currently pending for inclusion in the next block(s), as + /// well as the ones that are being scheduled for future execution only. + /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_status) + async fn txpool_status(&self) -> Result { + self.inner().txpool_status().await.map_err(MiddlewareError::from_err) + } + + // Geth `trace` support + + /// After replaying any previous transactions in the same block, + /// Replays a transaction, returning the traces configured with passed options + async fn debug_trace_transaction( + &self, + tx_hash: TxHash, + trace_options: GethDebugTracingOptions, + ) -> Result { + self.inner() + .debug_trace_transaction(tx_hash, trace_options) + .await + .map_err(MiddlewareError::from_err) + } + + /// Executes the given call and returns a number of possible traces for it + async fn debug_trace_call + Send + Sync>( + &self, + req: T, + block: Option, + trace_options: GethDebugTracingCallOptions, + ) -> Result { + self.inner() + .debug_trace_call(req, block, trace_options) + .await + .map_err(MiddlewareError::from_err) + } + + // Parity `trace` support + + /// Executes the given call and returns a number of possible traces for it + async fn trace_call + Send + Sync>( + &self, + req: T, + trace_type: Vec, + block: Option, + ) -> Result { + self.inner().trace_call(req, trace_type, block).await.map_err(MiddlewareError::from_err) + } + + /// Executes given calls and returns a number of possible traces for each + /// call + async fn trace_call_many + Send + Sync>( + &self, + req: Vec<(T, Vec)>, + block: Option, + ) -> Result, Self::Error> { + self.inner().trace_call_many(req, block).await.map_err(MiddlewareError::from_err) + } + + /// Traces a call to `eth_sendRawTransaction` without making the call, returning the traces + async fn trace_raw_transaction( + &self, + data: Bytes, + trace_type: Vec, + ) -> Result { + self.inner() + .trace_raw_transaction(data, trace_type) + .await + .map_err(MiddlewareError::from_err) + } + + /// Replays a transaction, returning the traces + async fn trace_replay_transaction( + &self, + hash: H256, + trace_type: Vec, + ) -> Result { + self.inner() + .trace_replay_transaction(hash, trace_type) + .await + .map_err(MiddlewareError::from_err) + } + + /// Replays all transactions in a block returning the requested traces for each transaction + async fn trace_replay_block_transactions( + &self, + block: BlockNumber, + trace_type: Vec, + ) -> Result, Self::Error> { + self.inner() + .trace_replay_block_transactions(block, trace_type) + .await + .map_err(MiddlewareError::from_err) + } + + /// Returns traces created at given block + async fn trace_block(&self, block: BlockNumber) -> Result, Self::Error> { + self.inner().trace_block(block).await.map_err(MiddlewareError::from_err) + } + + /// Return traces matching the given filter + async fn trace_filter(&self, filter: TraceFilter) -> Result, Self::Error> { + self.inner().trace_filter(filter).await.map_err(MiddlewareError::from_err) + } + + /// Returns trace at the given position + async fn trace_get + Send + Sync>( + &self, + hash: H256, + index: Vec, + ) -> Result { + self.inner().trace_get(hash, index).await.map_err(MiddlewareError::from_err) + } + + /// Returns all traces of a given transaction + async fn trace_transaction(&self, hash: H256) -> Result, Self::Error> { + self.inner().trace_transaction(hash).await.map_err(MiddlewareError::from_err) + } + + // Parity namespace + + /// Returns all receipts for that block. Must be done on a parity node. + async fn parity_block_receipts + Send + Sync>( + &self, + block: T, + ) -> Result, Self::Error> { + self.inner().parity_block_receipts(block).await.map_err(MiddlewareError::from_err) + } + + /// Create a new subscription + /// + /// This method is hidden as subscription lifecycles are intended to be + /// handled by a [`SubscriptionStream`] object. + #[doc(hidden)] + async fn subscribe( + &self, + params: T, + ) -> Result, Self::Error> + where + T: Debug + Serialize + Send + Sync, + R: DeserializeOwned + Send + Sync, + ::Provider: PubsubClient, + { + self.inner().subscribe(params).await.map_err(MiddlewareError::from_err) + } + + /// Instruct the RPC to cancel a subscription by its ID + /// + /// This method is hidden as subscription lifecycles are intended to be + /// handled by a [`SubscriptionStream`] object + #[doc(hidden)] + async fn unsubscribe(&self, id: T) -> Result + where + T: Into + Send + Sync, + ::Provider: PubsubClient, + { + self.inner().unsubscribe(id).await.map_err(MiddlewareError::from_err) + } + + /// Subscribe to a stream of incoming blocks. + /// + /// This function is only available on pubsub clients, such as Websockets + /// or IPC. For a polling alternative available over HTTP, use + /// [`Middleware::watch_blocks`]. However, be aware that polling increases + /// RPC usage drastically. + async fn subscribe_blocks( + &self, + ) -> Result>, Self::Error> + where + ::Provider: PubsubClient, + { + self.inner().subscribe_blocks().await.map_err(MiddlewareError::from_err) + } + + /// Subscribe to a stream of pending transactions. + /// + /// This function is only available on pubsub clients, such as Websockets + /// or IPC. For a polling alternative available over HTTP, use + /// [`Middleware::watch_pending_transactions`]. However, be aware that + /// polling increases RPC usage drastically. + async fn subscribe_pending_txs( + &self, + ) -> Result, Self::Error> + where + ::Provider: PubsubClient, + { + self.inner().subscribe_pending_txs().await.map_err(MiddlewareError::from_err) + } + + /// Subscribe to a stream of event logs matchin the provided [`Filter`]. + /// + /// This function is only available on pubsub clients, such as Websockets + /// or IPC. For a polling alternative available over HTTP, use + /// [`Middleware::watch`]. However, be aware that polling increases + /// RPC usage drastically. + async fn subscribe_logs<'a>( + &'a self, + filter: &Filter, + ) -> Result, Self::Error> + where + ::Provider: PubsubClient, + { + self.inner().subscribe_logs(filter).await.map_err(MiddlewareError::from_err) + } + + /// Query the node for a [`FeeHistory`] object. This objct contains + /// information about the EIP-1559 base fee in past blocks, as well as gas + /// utilization within those blocks. + /// + /// See the + /// [EIP-1559 documentation](https://eips.ethereum.org/EIPS/eip-1559) for + /// details + async fn fee_history + serde::Serialize + Send + Sync>( + &self, + block_count: T, + last_block: BlockNumber, + reward_percentiles: &[f64], + ) -> Result { + self.inner() + .fee_history(block_count, last_block, reward_percentiles) + .await + .map_err(MiddlewareError::from_err) + } + + /// Querty the node for an EIP-2930 Access List. + /// + /// See the + /// [EIP-2930 documentation](https://eips.ethereum.org/EIPS/eip-2930) for + /// details + async fn create_access_list( + &self, + tx: &TypedTransaction, + block: Option, + ) -> Result { + self.inner().create_access_list(tx, block).await.map_err(MiddlewareError::from_err) + } +} + +#[cfg(feature = "celo")] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +/// Celo-specific extension trait +pub trait CeloMiddleware: Middleware { + /// Get validator BLS public keys + async fn get_validators_bls_public_keys + Send + Sync>( + &self, + block_id: T, + ) -> Result, ProviderError> { + self.provider() + .get_validators_bls_public_keys(block_id) + .await + .map_err(MiddlewareError::from_err) + } +} + +#[cfg(feature = "celo")] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl CeloMiddleware for T where T: Middleware {} diff --git a/ethers-providers/src/rpc/connections.rs b/ethers-providers/src/rpc/connections.rs new file mode 100644 index 00000000..e61b5e56 --- /dev/null +++ b/ethers-providers/src/rpc/connections.rs @@ -0,0 +1,37 @@ +use std::fmt::Debug; + +use async_trait::async_trait; +use auto_impl::auto_impl; +use ethers_core::types::U256; +use serde::{de::DeserializeOwned, Serialize}; +use serde_json::value::RawValue; + +use crate::{ProviderError, RpcError}; + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[auto_impl(&, Box, Arc)] +/// Trait which must be implemented by data transports to be used with the Ethereum +/// JSON-RPC provider. +pub trait JsonRpcClient: Debug + Send + Sync { + /// A JSON-RPC Error + type Error: Into + RpcError; + + /// Sends a request with the provided JSON-RPC and parameters serialized as JSON + async fn request(&self, method: &str, params: T) -> Result + where + T: Debug + Serialize + Send + Sync, + R: DeserializeOwned + Send; +} + +/// A transport implementation supporting pub sub subscriptions. +pub trait PubsubClient: JsonRpcClient { + /// The type of stream this transport returns + type NotificationStream: futures_core::Stream> + Send + Unpin; + + /// Add a subscription to this transport + fn subscribe>(&self, id: T) -> Result; + + /// Remove a subscription from this transport + fn unsubscribe>(&self, id: T) -> Result<(), Self::Error>; +} diff --git a/ethers-providers/src/rpc/mod.rs b/ethers-providers/src/rpc/mod.rs new file mode 100644 index 00000000..bb5311a7 --- /dev/null +++ b/ethers-providers/src/rpc/mod.rs @@ -0,0 +1,11 @@ +mod provider; +pub use provider::*; + +mod transports; +pub use transports::*; + +mod connections; +pub use connections::*; + +mod pubsub; +pub use pubsub::{PubsubClient, SubscriptionStream}; diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/rpc/provider.rs similarity index 79% rename from ethers-providers/src/provider.rs rename to ethers-providers/src/rpc/provider.rs index 0ade5eb3..df657258 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/rpc/provider.rs @@ -1,20 +1,25 @@ +use ethers_core::types::SyncingStatus; + use crate::{ call_raw::CallBuilder, - ens, erc, maybe, - pubsub::{PubsubClient, SubscriptionStream}, + errors::ProviderError, + ext::{ens, erc}, + rpc::pubsub::{PubsubClient, SubscriptionStream}, stream::{FilterWatcher, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL}, - FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, LogQuery, MockProvider, - NodeInfo, PeerInfo, PendingTransaction, QuorumProvider, RwClient, SyncingStatus, + utils::maybe, + Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, LogQuery, MiddlewareError, + MockProvider, NodeInfo, PeerInfo, PendingTransaction, QuorumProvider, RwClient, }; #[cfg(all(not(target_arch = "wasm32"), feature = "ws"))] -use crate::transports::Authorization; +use crate::Authorization; #[cfg(not(target_arch = "wasm32"))] -use crate::transports::{HttpRateLimitRetryPolicy, RetryClient}; +use crate::{HttpRateLimitRetryPolicy, RetryClient}; #[cfg(feature = "celo")] -use crate::CeloMiddleware; -use crate::Middleware; +pub use crate::CeloMiddleware; +pub use crate::Middleware; + use async_trait::async_trait; use ethers_core::{ @@ -35,17 +40,22 @@ use serde::{de::DeserializeOwned, Serialize}; use std::{ collections::VecDeque, convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration, }; -use thiserror::Error; use tracing::trace; use tracing_futures::Instrument; use url::{ParseError, Url}; +/// Node Clients #[derive(Copy, Clone)] pub enum NodeClient { + /// Geth Geth, + /// Erigon Erigon, + /// OpenEthereum OpenEthereum, + /// Nethermind Nethermind, + /// Besu Besu, } @@ -103,49 +113,6 @@ impl

AsRef

for Provider

{ } } -impl FromErr for ProviderError { - fn from(src: ProviderError) -> Self { - src - } -} - -#[derive(Debug, Error)] -/// An error thrown when making a call to the provider -pub enum ProviderError { - /// An internal error in the JSON RPC Client - #[error(transparent)] - JsonRpcClientError(#[from] Box), - - /// An error during ENS name resolution - #[error("ens name not found: {0}")] - EnsError(String), - - /// Invalid reverse ENS name - #[error("reverse ens name not pointing to itself: {0}")] - EnsNotOwned(String), - - #[error(transparent)] - SerdeJson(#[from] serde_json::Error), - - #[error(transparent)] - HexError(#[from] hex::FromHexError), - - #[error(transparent)] - HTTPError(#[from] reqwest::Error), - - #[error("custom error: {0}")] - CustomError(String), - - #[error("unsupported RPC")] - UnsupportedRPC, - - #[error("unsupported node client")] - UnsupportedNodeClient, - - #[error("Attempted to sign a transaction with no available signer. Hint: did you mean to use a SignerMiddleware?")] - SignerUnavailable, -} - /// Types of filters supported by the JSON-RPC. #[derive(Clone, Debug)] pub enum FilterKind<'a> { @@ -191,11 +158,13 @@ impl Provider

{ } #[must_use] + /// Set the default sender on the provider pub fn with_sender(mut self, address: impl Into

) -> Self { self.from = Some(address.into()); self } + /// Make an RPC request via the internal connection, and return the result. pub async fn request(&self, method: &str, params: T) -> Result where T: Debug + Serialize + Send + Sync, @@ -275,19 +244,6 @@ impl Provider

{ } } -#[cfg(feature = "celo")] -#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] -#[cfg_attr(not(target_arch = "wasm32"), async_trait)] -impl CeloMiddleware for Provider

{ - async fn get_validators_bls_public_keys + Send + Sync>( - &self, - block_id: T, - ) -> Result, ProviderError> { - let block_id = utils::serialize(&block_id.into()); - self.request("istanbul_getValidatorsBLSPublicKeys", [block_id]).await - } -} - #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl Middleware for Provider

{ @@ -312,11 +268,6 @@ impl Middleware for Provider

{ self.from } - ////// Blockchain Status - // - // Functions for querying the state of the blockchain - - /// Returns the current client version using the `web3_clientVersion` RPC. async fn client_version(&self) -> Result { self.request("web3_clientVersion", ()).await } @@ -377,12 +328,10 @@ impl Middleware for Provider

{ Ok(()) } - /// Gets the latest block number via the `eth_BlockNumber` API async fn get_block_number(&self) -> Result { self.request("eth_blockNumber", ()).await } - /// Gets the block at `block_hash_or_number` (transaction hashes only) async fn get_block + Send + Sync>( &self, block_hash_or_number: T, @@ -390,7 +339,6 @@ impl Middleware for Provider

{ self.get_block_gen(block_hash_or_number.into(), false).await } - /// Gets the block at `block_hash_or_number` (full transactions included) async fn get_block_with_txs + Send + Sync>( &self, block_hash_or_number: T, @@ -398,7 +346,6 @@ impl Middleware for Provider

{ self.get_block_gen(block_hash_or_number.into(), true).await } - /// Gets the block uncle count at `block_hash_or_number` async fn get_uncle_count + Send + Sync>( &self, block_hash_or_number: T, @@ -416,7 +363,6 @@ impl Middleware for Provider

{ }) } - /// Gets the block uncle at `block_hash_or_number` and `idx` async fn get_uncle + Send + Sync>( &self, block_hash_or_number: T, @@ -436,7 +382,6 @@ impl Middleware for Provider

{ }) } - /// Gets the transaction with `transaction_hash` async fn get_transaction>( &self, transaction_hash: T, @@ -445,7 +390,6 @@ impl Middleware for Provider

{ self.request("eth_getTransactionByHash", [hash]).await } - /// Gets the transaction receipt with `transaction_hash` async fn get_transaction_receipt>( &self, transaction_hash: T, @@ -454,10 +398,6 @@ impl Middleware for Provider

{ self.request("eth_getTransactionReceipt", [hash]).await } - /// Returns all receipts for a block. - /// - /// Note that this uses the `eth_getBlockReceipts` RPC, which is - /// non-standard and currently supported by Erigon. async fn get_block_receipts + Send + Sync>( &self, block: T, @@ -465,7 +405,6 @@ impl Middleware for Provider

{ self.request("eth_getBlockReceipts", [block.into()]).await } - /// Returns all receipts for that block. Must be done on a parity node. async fn parity_block_receipts + Send + Sync>( &self, block: T, @@ -473,13 +412,10 @@ impl Middleware for Provider

{ self.request("parity_getBlockReceipts", vec![block.into()]).await } - /// Gets the current gas price as estimated by the node async fn get_gas_price(&self) -> Result { self.request("eth_gasPrice", ()).await } - /// Gets a heuristic recommendation of max fee per gas and max priority fee per gas for - /// EIP-1559 compatible transactions. async fn estimate_eip1559_fees( &self, estimator: Option>) -> (U256, U256)>, @@ -509,12 +445,10 @@ impl Middleware for Provider

{ Ok((max_fee_per_gas, max_priority_fee_per_gas)) } - /// Gets the accounts on the node async fn get_accounts(&self) -> Result, ProviderError> { self.request("eth_accounts", ()).await } - /// Returns the nonce of the address async fn get_transaction_count + Send + Sync>( &self, from: T, @@ -530,7 +464,6 @@ impl Middleware for Provider

{ self.request("eth_getTransactionCount", [from, block]).await } - /// Returns the account's balance async fn get_balance + Send + Sync>( &self, from: T, @@ -546,29 +479,18 @@ impl Middleware for Provider

{ self.request("eth_getBalance", [from, block]).await } - /// Returns the currently configured chain id, a value used in replay-protected - /// transaction signing as introduced by EIP-155. async fn get_chainid(&self) -> Result { self.request("eth_chainId", ()).await } - /// Return current client syncing status. If IsFalse sync is over. async fn syncing(&self) -> Result { self.request("eth_syncing", ()).await } - /// Returns the network version. async fn get_net_version(&self) -> Result { self.request("net_version", ()).await } - ////// Contract Execution - // - // These are relatively low-level calls. The Contracts API should usually be used instead. - - /// Sends the read-only (constant) transaction to a single Ethereum node and return the result - /// (as bytes) of executing it. This is free, since it does not change any state on the - /// blockchain. async fn call( &self, tx: &TypedTransaction, @@ -579,10 +501,6 @@ impl Middleware for Provider

{ self.request("eth_call", [tx, block]).await } - /// Sends a transaction to a single Ethereum node and return the estimated amount of gas - /// required (as a U256) to send it This is free, but only an estimate. Providing too little - /// gas will result in a transaction being rejected (while still consuming all provided - /// gas). async fn estimate_gas( &self, tx: &TypedTransaction, @@ -609,8 +527,6 @@ impl Middleware for Provider

{ self.request("eth_createAccessList", [tx, block]).await } - /// Sends the transaction to the entire Ethereum network and returns the transaction's hash - /// This will consume gas from the account that signed the transaction. async fn send_transaction + Send + Sync>( &self, tx: T, @@ -623,8 +539,6 @@ impl Middleware for Provider

{ Ok(PendingTransaction::new(tx_hash, self)) } - /// Send the raw RLP encoded transaction to the entire Ethereum network and returns the - /// transaction's hash This will consume gas from the account that signed the transaction. async fn send_raw_transaction<'a>( &'a self, tx: Bytes, @@ -634,8 +548,6 @@ impl Middleware for Provider

{ Ok(PendingTransaction::new(tx_hash, self)) } - /// The JSON-RPC provider is at the bottom-most position in the middleware stack. Here we check - /// if it has the key for the sender address unlocked, as well as supports the `eth_sign` call. async fn is_signer(&self) -> bool { match self.from { Some(sender) => self.sign(vec![], &sender).await.is_ok(), @@ -643,7 +555,6 @@ impl Middleware for Provider

{ } } - /// Signs data using a specific account. This account needs to be unlocked. async fn sign + Send + Sync>( &self, data: T, @@ -668,12 +579,11 @@ impl Middleware for Provider

{ _tx: &TypedTransaction, _from: Address, ) -> Result { - Err(ProviderError::SignerUnavailable).map_err(FromErr::from) + Err(ProviderError::SignerUnavailable).map_err(MiddlewareError::from_err) } ////// Contract state - /// Returns an array (possibly empty) of logs that match the filter async fn get_logs(&self, filter: &Filter) -> Result, ProviderError> { self.request("eth_getLogs", [filter]).await } @@ -682,7 +592,6 @@ impl Middleware for Provider

{ LogQuery::new(self, filter).with_page_size(page_size) } - /// Streams matching filter logs async fn watch<'a>( &'a self, filter: &Filter, @@ -692,7 +601,6 @@ impl Middleware for Provider

{ Ok(filter) } - /// Streams new block hashes async fn watch_blocks(&self) -> Result, ProviderError> { let id = self.new_filter(FilterKind::NewBlocks).await?; let filter = FilterWatcher::new(id, self).interval(self.get_interval()); @@ -708,8 +616,6 @@ impl Middleware for Provider

{ Ok(filter) } - /// Creates a filter object, based on filter options, to notify when the state changes (logs). - /// To check if the state has changed, call `get_filter_changes` with the filter id. async fn new_filter(&self, filter: FilterKind<'_>) -> Result { let (method, args) = match filter { FilterKind::NewBlocks => ("eth_newBlockFilter", vec![]), @@ -720,7 +626,6 @@ impl Middleware for Provider

{ self.request(method, args).await } - /// Uninstalls a filter async fn uninstall_filter + Send + Sync>( &self, id: T, @@ -729,19 +634,6 @@ impl Middleware for Provider

{ self.request("eth_uninstallFilter", [id]).await } - /// Polling method for a filter, which returns an array of logs which occurred since last poll. - /// - /// This method must be called with one of the following return types, depending on the filter - /// type: - /// - `eth_newBlockFilter`: [`H256`], returns block hashes - /// - `eth_newPendingTransactionFilter`: [`H256`], returns transaction hashes - /// - `eth_newFilter`: [`Log`], returns raw logs - /// - /// If one of these types is not used, decoding will fail and the method will - /// return an error. - /// - /// [`H256`]: ethers_core::types::H256 - /// [`Log`]: ethers_core::types::Log async fn get_filter_changes(&self, id: T) -> Result, ProviderError> where T: Into + Send + Sync, @@ -751,7 +643,6 @@ impl Middleware for Provider

{ self.request("eth_getFilterChanges", [id]).await } - /// Get the storage of an address for a particular slot location async fn get_storage_at + Send + Sync>( &self, from: T, @@ -777,7 +668,6 @@ impl Middleware for Provider

{ Ok(H256::from_slice(&Vec::from_hex(value)?)) } - /// Returns the deployed code at a given address async fn get_code + Send + Sync>( &self, at: T, @@ -793,8 +683,6 @@ impl Middleware for Provider

{ self.request("eth_getCode", [at, block]).await } - /// Returns the EIP-1186 proof response - /// async fn get_proof + Send + Sync>( &self, from: T, @@ -818,15 +706,6 @@ impl Middleware for Provider

{ self.request("eth_mining", ()).await } - // Personal namespace - // NOTE: This will eventually need to be enabled by users explicitly because the personal - // namespace is being deprecated: - // Issue: https://github.com/ethereum/go-ethereum/issues/25948 - // PR: https://github.com/ethereum/go-ethereum/pull/26390 - - /// Sends the given key to the node to be encrypted with the provided passphrase and stored. - /// - /// The key represents a secp256k1 private key and should be 32 bytes. async fn import_raw_key( &self, private_key: Bytes, @@ -842,10 +721,6 @@ impl Middleware for Provider

{ self.request("personal_importRawKey", [private_key, passphrase]).await } - /// Prompts the node to decrypt the given account from its keystore. - /// - /// If the duration provided is `None`, then the account will be unlocked indefinitely. - /// Otherwise, the account will be unlocked for the provided number of seconds. async fn unlock_account + Send + Sync>( &self, account: T, @@ -858,88 +733,47 @@ impl Middleware for Provider

{ self.request("personal_unlockAccount", [account, passphrase, duration]).await } - // Admin namespace - - /// Requests adding the given peer, returning a boolean representing whether or not the peer - /// was accepted for tracking. async fn add_peer(&self, enode_url: String) -> Result { let enode_url = utils::serialize(&enode_url); self.request("admin_addPeer", [enode_url]).await } - /// Requests adding the given peer as a trusted peer, which the node will always connect to - /// even when its peer slots are full. async fn add_trusted_peer(&self, enode_url: String) -> Result { let enode_url = utils::serialize(&enode_url); self.request("admin_addTrustedPeer", [enode_url]).await } - /// Returns general information about the node as well as information about the running p2p - /// protocols (e.g. `eth`, `snap`). async fn node_info(&self) -> Result { self.request("admin_nodeInfo", ()).await } - /// Returns the list of peers currently connected to the node. async fn peers(&self) -> Result, Self::Error> { self.request("admin_peers", ()).await } - /// Requests to remove the given peer, returning true if the enode was successfully parsed and - /// the peer was removed. async fn remove_peer(&self, enode_url: String) -> Result { let enode_url = utils::serialize(&enode_url); self.request("admin_removePeer", [enode_url]).await } - /// Requests to remove the given peer, returning a boolean representing whether or not the - /// enode url passed was validated. A return value of `true` does not necessarily mean that the - /// peer was disconnected. async fn remove_trusted_peer(&self, enode_url: String) -> Result { let enode_url = utils::serialize(&enode_url); self.request("admin_removeTrustedPeer", [enode_url]).await } - // Miner namespace - - /// Starts the miner with the given number of threads. If threads is nil, the number of workers - /// started is equal to the number of logical CPUs that are usable by this process. If mining - /// is already running, this method adjust the number of threads allowed to use and updates the - /// minimum price required by the transaction pool. async fn start_mining(&self, threads: Option) -> Result<(), Self::Error> { let threads = utils::serialize(&threads); self.request("miner_start", [threads]).await } - /// Stop terminates the miner, both at the consensus engine level as well as at the block - /// creation level. async fn stop_mining(&self) -> Result<(), Self::Error> { self.request("miner_stop", ()).await } - ////// Ethereum Naming Service - // The Ethereum Naming Service (ENS) allows easy to remember and use names to - // be assigned to Ethereum addresses. Any provider operation which takes an address - // may also take an ENS name. - // - // ENS also provides the ability for a reverse lookup, which determines the name for an address - // if it has been configured. - - /// Returns the address that the `ens_name` resolves to (or None if not configured). - /// - /// # Panics - /// - /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as - /// an address. This should theoretically never happen. async fn resolve_name(&self, ens_name: &str) -> Result { self.query_resolver(ParamType::Address, ens_name, ens::ADDR_SELECTOR).await } - /// Returns the ENS name the `address` resolves to (or None if not configured). - /// # Panics - /// - /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as - /// a string. This should theoretically never happen. async fn lookup_address(&self, address: Address) -> Result { let ens_name = ens::reverse_address(address); let domain: String = @@ -952,25 +786,6 @@ impl Middleware for Provider

{ } } - /// Returns the avatar HTTP link of the avatar that the `ens_name` resolves to (or None - /// if not configured) - /// - /// # Example - /// ```no_run - /// # use ethers_providers::{Provider, Http as HttpProvider, Middleware}; - /// # use std::convert::TryFrom; - /// # #[tokio::main(flavor = "current_thread")] - /// # async fn main() { - /// # let provider = Provider::::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap(); - /// let avatar = provider.resolve_avatar("parishilton.eth").await.unwrap(); - /// assert_eq!(avatar.to_string(), "https://i.imgur.com/YW3Hzph.jpg"); - /// # } - /// ``` - /// - /// # Panics - /// - /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as - /// a string. This should theoretically never happen. async fn resolve_avatar(&self, ens_name: &str) -> Result { let (field, owner) = try_join!(self.resolve_field(ens_name, "avatar"), self.resolve_name(ens_name))?; @@ -1030,25 +845,6 @@ impl Middleware for Provider

{ } } - /// Returns the URL (not necesserily HTTP) of the image behind a token. - /// - /// # Example - /// ```no_run - /// # use ethers_providers::{Provider, Http as HttpProvider, Middleware}; - /// # use std::{str::FromStr, convert::TryFrom}; - /// # #[tokio::main(flavor = "current_thread")] - /// # async fn main() { - /// # let provider = Provider::::try_from("https://mainnet.infura.io/v3/c60b0bb42f8a4c6481ecd229eddaca27").unwrap(); - /// let token = ethers_providers::erc::ERCNFT::from_str("erc721:0xc92ceddfb8dd984a89fb494c376f9a48b999aafc/9018").unwrap(); - /// let token_image = provider.resolve_nft(token).await.unwrap(); - /// assert_eq!(token_image.to_string(), "https://creature.mypinata.cloud/ipfs/QmNwj3aUzXfG4twV3no7hJRYxLLAWNPk6RrfQaqJ6nVJFa/9018.jpg"); - /// # } - /// ``` - /// - /// # Panics - /// - /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as - /// a string. This should theoretically never happen. async fn resolve_nft(&self, token: erc::ERCNFT) -> Result { let selector = token.type_.resolution_selector(); let tx = TransactionRequest { @@ -1070,12 +866,6 @@ impl Middleware for Provider

{ Url::parse(&metadata.image).map_err(|e| ProviderError::CustomError(e.to_string())) } - /// Fetch a field for the `ens_name` (no None if not configured). - /// - /// # Panics - /// - /// If the bytes returned from the ENS registrar/resolver cannot be interpreted as - /// a string. This should theoretically never happen. async fn resolve_field(&self, ens_name: &str, field: &str) -> Result { let field: String = self .query_resolver_parameters( @@ -1088,28 +878,18 @@ impl Middleware for Provider

{ Ok(field) } - /// Returns the details of all transactions currently pending for inclusion in the next - /// block(s), as well as the ones that are being scheduled for future execution only. - /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_content) async fn txpool_content(&self) -> Result { self.request("txpool_content", ()).await } - /// Returns a summary of all the transactions currently pending for inclusion in the next - /// block(s), as well as the ones that are being scheduled for future execution only. - /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_inspect) async fn txpool_inspect(&self) -> Result { self.request("txpool_inspect", ()).await } - /// Returns the number of transactions currently pending for inclusion in the next block(s), as - /// well as the ones that are being scheduled for future execution only. - /// Ref: [Here](https://geth.ethereum.org/docs/rpc/ns-txpool#txpool_status) async fn txpool_status(&self) -> Result { self.request("txpool_status", ()).await } - /// Executes the given call and returns a number of possible traces for it async fn debug_trace_transaction( &self, tx_hash: TxHash, @@ -1120,7 +900,6 @@ impl Middleware for Provider

{ self.request("debug_traceTransaction", [tx_hash, trace_options]).await } - /// Executes the given call and returns a number of possible traces for it async fn debug_trace_call + Send + Sync>( &self, req: T, @@ -1134,7 +913,6 @@ impl Middleware for Provider

{ self.request("debug_traceCall", [req, block, trace_options]).await } - /// Executes the given call and returns a number of possible traces for it async fn trace_call + Send + Sync>( &self, req: T, @@ -1148,7 +926,6 @@ impl Middleware for Provider

{ self.request("trace_call", [req, trace_type, block]).await } - /// Executes given calls and returns a number of possible traces for each call async fn trace_call_many + Send + Sync>( &self, req: Vec<(T, Vec)>, @@ -1161,7 +938,6 @@ impl Middleware for Provider

{ self.request("trace_callMany", [req, block]).await } - /// Traces a call to `eth_sendRawTransaction` without making the call, returning the traces async fn trace_raw_transaction( &self, data: Bytes, @@ -1172,7 +948,6 @@ impl Middleware for Provider

{ self.request("trace_rawTransaction", [data, trace_type]).await } - /// Replays a transaction, returning the traces async fn trace_replay_transaction( &self, hash: H256, @@ -1183,7 +958,6 @@ impl Middleware for Provider

{ self.request("trace_replayTransaction", [hash, trace_type]).await } - /// Replays all transactions in a block returning the requested traces for each transaction async fn trace_replay_block_transactions( &self, block: BlockNumber, @@ -1194,19 +968,16 @@ impl Middleware for Provider

{ self.request("trace_replayBlockTransactions", [block, trace_type]).await } - /// Returns traces created at given block async fn trace_block(&self, block: BlockNumber) -> Result, ProviderError> { let block = utils::serialize(&block); self.request("trace_block", [block]).await } - /// Return traces matching the given filter async fn trace_filter(&self, filter: TraceFilter) -> Result, ProviderError> { let filter = utils::serialize(&filter); self.request("trace_filter", vec![filter]).await } - /// Returns trace at the given position async fn trace_get + Send + Sync>( &self, hash: H256, @@ -1218,7 +989,6 @@ impl Middleware for Provider

{ self.request("trace_get", vec![hash, index]).await } - /// Returns all traces of a given transaction async fn trace_transaction(&self, hash: H256) -> Result, ProviderError> { let hash = utils::serialize(&hash); self.request("trace_transaction", vec![hash]).await @@ -1458,6 +1228,14 @@ impl Provider { Ok(Self::new(ws)) } + /// Direct connection to a websocket endpoint + #[cfg(target_arch = "wasm32")] + pub async fn connect(url: &str) -> Result { + let ws = crate::Ws::connect(url).await?; + Ok(Self::new(ws)) + } + + /// Connect to a WS RPC provider with authentication details #[cfg(not(target_arch = "wasm32"))] pub async fn connect_with_auth( url: impl tokio_tungstenite::tungstenite::client::IntoClientRequest + Unpin, @@ -1466,13 +1244,6 @@ impl Provider { let ws = crate::Ws::connect_with_auth(url, auth).await?; Ok(Self::new(ws)) } - - /// Direct connection to a websocket endpoint - #[cfg(target_arch = "wasm32")] - pub async fn connect(url: &str) -> Result { - let ws = crate::Ws::connect(url).await?; - Ok(Self::new(ws)) - } } #[cfg(all(feature = "ipc", any(unix, windows)))] @@ -1587,6 +1358,8 @@ impl<'a> TryFrom<&'a String> for Provider { #[cfg(not(target_arch = "wasm32"))] impl Provider> { + /// Create a new [`RetryClient`] by connecting to the provided URL. Errors + /// if `src` is not a valid URL pub fn new_client(src: &str, max_retry: u32, initial_backoff: u64) -> Result { Ok(Provider::new(RetryClient::new( HttpProvider::new(Url::parse(src)?), @@ -1711,182 +1484,6 @@ pub fn is_local_endpoint(url: &str) -> bool { url.contains("127.0.0.1") || url.contains("localhost") } -/// A middleware supporting development-specific JSON RPC methods -/// -/// # Example -/// -///``` -/// use ethers_providers::{Provider, Http, Middleware, DevRpcMiddleware}; -/// use ethers_core::types::TransactionRequest; -/// use ethers_core::utils::Anvil; -/// use std::convert::TryFrom; -/// -/// # #[tokio::main(flavor = "current_thread")] -/// # async fn main() -> Result<(), Box> { -/// let anvil = Anvil::new().spawn(); -/// let provider = Provider::::try_from(anvil.endpoint()).unwrap(); -/// let client = DevRpcMiddleware::new(provider); -/// -/// // snapshot the initial state -/// let block0 = client.get_block_number().await.unwrap(); -/// let snap_id = client.snapshot().await.unwrap(); -/// -/// // send a transaction -/// let accounts = client.get_accounts().await?; -/// let from = accounts[0]; -/// let to = accounts[1]; -/// let balance_before = client.get_balance(to, None).await?; -/// let tx = TransactionRequest::new().to(to).value(1000).from(from); -/// client.send_transaction(tx, None).await?.await?; -/// let balance_after = client.get_balance(to, None).await?; -/// assert_eq!(balance_after, balance_before + 1000); -/// -/// // revert to snapshot -/// client.revert_to_snapshot(snap_id).await.unwrap(); -/// let balance_after_revert = client.get_balance(to, None).await?; -/// assert_eq!(balance_after_revert, balance_before); -/// # Ok(()) -/// # } -/// ``` -#[cfg(feature = "dev-rpc")] -pub mod dev_rpc { - use crate::{FromErr, Middleware, ProviderError}; - use async_trait::async_trait; - use ethers_core::types::U256; - use thiserror::Error; - - use std::fmt::Debug; - - #[derive(Clone, Debug)] - pub struct DevRpcMiddleware(M); - - #[derive(Error, Debug)] - pub enum DevRpcMiddlewareError { - #[error("{0}")] - MiddlewareError(M::Error), - - #[error("{0}")] - ProviderError(ProviderError), - - #[error("Could not revert to snapshot")] - NoSnapshot, - } - - #[async_trait] - impl Middleware for DevRpcMiddleware { - type Error = DevRpcMiddlewareError; - type Provider = M::Provider; - type Inner = M; - - fn inner(&self) -> &M { - &self.0 - } - } - - impl FromErr for DevRpcMiddlewareError { - fn from(src: M::Error) -> DevRpcMiddlewareError { - DevRpcMiddlewareError::MiddlewareError(src) - } - } - - impl From for DevRpcMiddlewareError - where - M: Middleware, - { - fn from(src: ProviderError) -> Self { - Self::ProviderError(src) - } - } - - impl DevRpcMiddleware { - pub fn new(inner: M) -> Self { - Self(inner) - } - - // Ganache, Hardhat and Anvil increment snapshot ID even if no state has changed - pub async fn snapshot(&self) -> Result> { - self.provider().request::<(), U256>("evm_snapshot", ()).await.map_err(From::from) - } - - pub async fn revert_to_snapshot(&self, id: U256) -> Result<(), DevRpcMiddlewareError> { - let ok = self - .provider() - .request::<[U256; 1], bool>("evm_revert", [id]) - .await - .map_err(DevRpcMiddlewareError::ProviderError)?; - if ok { - Ok(()) - } else { - Err(DevRpcMiddlewareError::NoSnapshot) - } - } - } - #[cfg(test)] - // Celo blocks can not get parsed when used with Ganache - #[cfg(not(feature = "celo"))] - mod tests { - use super::*; - use crate::{Http, Provider}; - use ethers_core::utils::Anvil; - use std::convert::TryFrom; - - #[tokio::test] - async fn test_snapshot() { - let anvil = Anvil::new().spawn(); - let provider = Provider::::try_from(anvil.endpoint()).unwrap(); - let client = DevRpcMiddleware::new(provider); - - // snapshot initial state - let block0 = client.get_block_number().await.unwrap(); - let time0 = client.get_block(block0).await.unwrap().unwrap().timestamp; - let snap_id0 = client.snapshot().await.unwrap(); - - // mine a new block - client.provider().mine(1).await.unwrap(); - - // snapshot state - let block1 = client.get_block_number().await.unwrap(); - let time1 = client.get_block(block1).await.unwrap().unwrap().timestamp; - let snap_id1 = client.snapshot().await.unwrap(); - - // mine some blocks - client.provider().mine(5).await.unwrap(); - - // snapshot state - let block2 = client.get_block_number().await.unwrap(); - let time2 = client.get_block(block2).await.unwrap().unwrap().timestamp; - let snap_id2 = client.snapshot().await.unwrap(); - - // mine some blocks - client.provider().mine(5).await.unwrap(); - - // revert_to_snapshot should reset state to snap id - client.revert_to_snapshot(snap_id2).await.unwrap(); - let block = client.get_block_number().await.unwrap(); - let time = client.get_block(block).await.unwrap().unwrap().timestamp; - assert_eq!(block, block2); - assert_eq!(time, time2); - - client.revert_to_snapshot(snap_id1).await.unwrap(); - let block = client.get_block_number().await.unwrap(); - let time = client.get_block(block).await.unwrap().unwrap().timestamp; - assert_eq!(block, block1); - assert_eq!(time, time1); - - // revert_to_snapshot should throw given non-existent or - // previously used snapshot - let result = client.revert_to_snapshot(snap_id1).await; - assert!(result.is_err()); - - client.revert_to_snapshot(snap_id0).await.unwrap(); - let block = client.get_block_number().await.unwrap(); - let time = client.get_block(block).await.unwrap().unwrap().timestamp; - assert_eq!(block, block0); - assert_eq!(time, time0); - } - } -} - #[cfg(test)] #[cfg(not(target_arch = "wasm32"))] mod tests { diff --git a/ethers-providers/src/pubsub.rs b/ethers-providers/src/rpc/pubsub.rs similarity index 83% rename from ethers-providers/src/pubsub.rs rename to ethers-providers/src/rpc/pubsub.rs index a31c0dcb..690606d6 100644 --- a/ethers-providers/src/pubsub.rs +++ b/ethers-providers/src/rpc/pubsub.rs @@ -1,6 +1,6 @@ -use crate::{JsonRpcClient, Middleware, Provider, TransactionStream}; +use crate::{JsonRpcClient, Middleware, Provider}; -use ethers_core::types::{TxHash, U256}; +use ethers_core::types::U256; use futures_util::stream::Stream; use pin_project::{pin_project, pinned_drop}; @@ -35,7 +35,7 @@ pub struct SubscriptionStream<'a, P: PubsubClient, R: DeserializeOwned> { loaded_elements: VecDeque, - provider: &'a Provider

, + pub(crate) provider: &'a Provider

, #[pin] rx: P::NotificationStream, @@ -66,6 +66,13 @@ where self.provider.unsubscribe(self.id).await } + /// Set the loaded elements buffer. This buffer contains logs waiting for + /// the consumer to read. Setting the buffer can be used to add logs + /// without receiving them from the RPC node + /// + /// ### Warning + /// + /// Setting the buffer will drop any logs in the current buffer. pub fn set_loaded_elements(&mut self, loaded_elements: VecDeque) { self.loaded_elements = loaded_elements; } @@ -114,18 +121,3 @@ where let _ = (*self.provider).as_ref().unsubscribe(self.id); } } - -impl<'a, P> SubscriptionStream<'a, P, TxHash> -where - P: PubsubClient, -{ - /// Returns a stream that yields the `Transaction`s for the transaction hashes this stream - /// yields. - /// - /// This internally calls `Provider::get_transaction` with every new transaction. - /// No more than n futures will be buffered at any point in time, and less than n may also be - /// buffered depending on the state of each future. - pub fn transactions_unordered(self, n: usize) -> TransactionStream<'a, P, Self> { - TransactionStream::new(self.provider, self, n) - } -} diff --git a/ethers-providers/src/transports/common.rs b/ethers-providers/src/rpc/transports/common.rs similarity index 98% rename from ethers-providers/src/transports/common.rs rename to ethers-providers/src/rpc/transports/common.rs index a23b68d5..02591928 100644 --- a/ethers-providers/src/transports/common.rs +++ b/ethers-providers/src/rpc/transports/common.rs @@ -186,11 +186,14 @@ impl<'de: 'a, 'a> Deserialize<'de> for Response<'a> { /// Use to inject username and password or an auth token into requests #[derive(Clone, Debug)] pub enum Authorization { + /// HTTP Basic Auth Basic(String), + /// Bearer Auth Bearer(String), } impl Authorization { + /// Make a new basic auth pub fn basic(username: impl AsRef, password: impl AsRef) -> Self { let username = username.as_ref(); let password = password.as_ref(); @@ -198,6 +201,7 @@ impl Authorization { Self::Basic(auth_secret) } + /// Make a new bearer auth pub fn bearer(token: impl Into) -> Self { Self::Bearer(token.into()) } diff --git a/ethers-providers/src/transports/http.rs b/ethers-providers/src/rpc/transports/http.rs similarity index 89% rename from ethers-providers/src/transports/http.rs rename to ethers-providers/src/rpc/transports/http.rs index b110c66f..6198bf2f 100644 --- a/ethers-providers/src/transports/http.rs +++ b/ethers-providers/src/rpc/transports/http.rs @@ -1,7 +1,7 @@ // Code adapted from: https://github.com/althea-net/guac_rs/tree/master/web3/src/jsonrpc use super::common::{Authorization, JsonRpcError, Request, Response}; -use crate::{provider::ProviderError, JsonRpcClient}; +use crate::{errors::ProviderError, JsonRpcClient}; use async_trait::async_trait; use reqwest::{header::HeaderValue, Client, Error as ReqwestError}; use serde::{de::DeserializeOwned, Serialize}; @@ -46,7 +46,12 @@ pub enum ClientError { #[error("Deserialization Error: {err}. Response: {text}")] /// Serde JSON Error - SerdeJson { err: serde_json::Error, text: String }, + SerdeJson { + /// Underlying error + err: serde_json::Error, + /// The contents of the HTTP response that could not be deserialized + text: String, + }, } impl From for ProviderError { @@ -58,13 +63,28 @@ impl From for ProviderError { } } +impl crate::RpcError for ClientError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + if let ClientError::JsonRpcError(err) = self { + Some(err) + } else { + None + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + ClientError::SerdeJson { err, .. } => Some(err), + _ => None, + } + } +} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl JsonRpcClient for Provider { type Error = ClientError; - /// Sends a POST request with the provided method and the params serialized as JSON - /// over HTTP async fn request( &self, method: &str, diff --git a/ethers-providers/src/transports/ipc.rs b/ethers-providers/src/rpc/transports/ipc.rs similarity index 96% rename from ethers-providers/src/transports/ipc.rs rename to ethers-providers/src/rpc/transports/ipc.rs index 8f3c597b..8d892257 100644 --- a/ethers-providers/src/transports/ipc.rs +++ b/ethers-providers/src/rpc/transports/ipc.rs @@ -1,9 +1,4 @@ use super::common::Params; -use crate::{ - provider::ProviderError, - transports::common::{JsonRpcError, Request, Response}, - JsonRpcClient, PubsubClient, -}; use async_trait::async_trait; use bytes::{Buf, BytesMut}; use ethers_core::types::U256; @@ -31,6 +26,9 @@ use tokio::{ sync::oneshot::{self, error::RecvError}, }; +use super::common::{JsonRpcError, Request, Response}; +use crate::{errors::ProviderError, JsonRpcClient, PubsubClient}; + type FxHashMap = std::collections::HashMap>; type Pending = oneshot::Sender, JsonRpcError>>; @@ -471,16 +469,19 @@ pub enum IpcError { #[error(transparent)] IoError(#[from] io::Error), + /// Server responded to the request with a valid JSON-RPC error response #[error(transparent)] - /// Thrown if the response could not be parsed JsonRpcError(#[from] JsonRpcError), + /// Internal channel failed #[error("{0}")] ChannelError(String), + /// Listener for request result is gone #[error(transparent)] RequestCancelled(#[from] RecvError), + /// IPC server exited #[error("The IPC server has exited")] ServerExit, } @@ -491,6 +492,23 @@ impl From for ProviderError { } } +impl crate::RpcError for IpcError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + if let IpcError::JsonRpcError(err) = self { + Some(err) + } else { + None + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + IpcError::JsonError(err) => Some(err), + _ => None, + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/ethers-providers/src/transports/mock.rs b/ethers-providers/src/rpc/transports/mock.rs similarity index 91% rename from ethers-providers/src/transports/mock.rs rename to ethers-providers/src/rpc/transports/mock.rs index 0e0cedb1..f32c69ea 100644 --- a/ethers-providers/src/transports/mock.rs +++ b/ethers-providers/src/rpc/transports/mock.rs @@ -97,16 +97,32 @@ impl MockProvider { #[derive(Error, Debug)] /// Errors for the `MockProvider` pub enum MockError { + /// (De)Serialization error #[error(transparent)] SerdeJson(#[from] serde_json::Error), - #[error("empty responses array, please push some requests")] + /// Empty requests array + #[error("empty requests array, please push some requests")] EmptyRequests, + /// Empty responses array #[error("empty responses array, please push some responses")] EmptyResponses, } +impl crate::RpcError for MockError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + None + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + MockError::SerdeJson(e) => Some(e), + _ => None, + } + } +} + impl From for ProviderError { fn from(src: MockError) -> Self { ProviderError::JsonRpcClientError(Box::new(src)) diff --git a/ethers-providers/src/transports/mod.rs b/ethers-providers/src/rpc/transports/mod.rs similarity index 88% rename from ethers-providers/src/transports/mod.rs rename to ethers-providers/src/rpc/transports/mod.rs index feb4ed37..6655cdcc 100644 --- a/ethers-providers/src/transports/mod.rs +++ b/ethers-providers/src/rpc/transports/mod.rs @@ -1,5 +1,5 @@ -mod common; -pub use common::Authorization; +pub(crate) mod common; +pub use common::{Authorization, JsonRpcError}; mod http; pub use self::http::{ClientError as HttpClientError, Provider as Http}; diff --git a/ethers-providers/src/transports/quorum.rs b/ethers-providers/src/rpc/transports/quorum.rs similarity index 96% rename from ethers-providers/src/transports/quorum.rs rename to ethers-providers/src/rpc/transports/quorum.rs index 23702374..127419fc 100644 --- a/ethers-providers/src/transports/quorum.rs +++ b/ethers-providers/src/rpc/transports/quorum.rs @@ -1,4 +1,4 @@ -use crate::{provider::ProviderError, JsonRpcClient, PubsubClient}; +use crate::{errors::ProviderError, JsonRpcClient, PubsubClient}; use async_trait::async_trait; use ethers_core::types::{U256, U64}; use futures_core::Stream; @@ -100,10 +100,13 @@ impl QuorumProvider { QuorumProviderBuilder::default() } + /// Instantiate a new `QuorumProvider` from a [`Quorum`] and a set of + /// providers pub fn new(quorum: Quorum, providers: impl IntoIterator>) -> Self { Self::builder().add_providers(providers).quorum(quorum).build() } + /// Return a reference to the weighted providers pub fn providers(&self) -> &[WeightedProvider] { &self.providers } @@ -113,6 +116,7 @@ impl QuorumProvider { self.quorum_weight } + /// Add a provider to the set pub fn add_provider(&mut self, provider: WeightedProvider) { self.providers.push(provider); self.quorum_weight = self.quorum.weight(&self.providers) @@ -342,6 +346,7 @@ impl WeightedProvider { Self::with_weight(inner, 1) } + /// Instantiate a `WeightedProvider` with a set weight pub fn with_weight(inner: T, weight: u64) -> Self { assert!(weight > 0); Self { inner, weight } @@ -352,7 +357,23 @@ impl WeightedProvider { /// Error thrown when sending an HTTP request pub enum QuorumError { #[error("No Quorum reached.")] - NoQuorumReached { values: Vec, errors: Vec }, + /// NoQuorumReached + NoQuorumReached { + /// Returned responses + values: Vec, + /// Returned errors + errors: Vec, + }, +} + +impl crate::RpcError for QuorumError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + None + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + None + } } impl From for ProviderError { @@ -361,9 +382,12 @@ impl From for ProviderError { } } +/// Wrapper trait for [`crate::JsonRpcClient`] that erases generics and is +/// object-safe. This trait is not intended for outside implementation #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait JsonRpcClientWrapper: Send + Sync + Debug { + /// Make a request, as [`crate::JsonRpcClient`] async fn request(&self, method: &str, params: QuorumParams) -> Result; } type NotificationStream = diff --git a/ethers-providers/src/transports/retry.rs b/ethers-providers/src/rpc/transports/retry.rs similarity index 96% rename from ethers-providers/src/transports/retry.rs rename to ethers-providers/src/rpc/transports/retry.rs index 8b51573c..9af8c7a0 100644 --- a/ethers-providers/src/transports/retry.rs +++ b/ethers-providers/src/rpc/transports/retry.rs @@ -2,7 +2,7 @@ //! with an exponential backoff. use super::{common::JsonRpcError, http::ClientError}; -use crate::{provider::ProviderError, JsonRpcClient}; +use crate::{errors::ProviderError, JsonRpcClient}; use async_trait::async_trait; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ @@ -50,7 +50,7 @@ pub trait RetryPolicy: Send + Sync + Debug { pub struct RetryClient where T: JsonRpcClient, - T::Error: Sync + Send + 'static, + T::Error: crate::RpcError + Sync + Send + 'static, { inner: T, requests_enqueued: AtomicU32, @@ -114,6 +114,7 @@ where } } +/// Builder for a [`RetryClient`] #[derive(Debug, Clone, Eq, PartialEq)] pub struct RetryClientBuilder { /// How many connection `TimedOut` should be retried. @@ -201,14 +202,36 @@ impl Default for RetryClientBuilder { /// 3. Request timed out i.e. max retries were already made. #[derive(Error, Debug)] pub enum RetryClientError { + /// Internal provider error #[error(transparent)] ProviderError(ProviderError), + /// Timeout while making requests TimeoutError, + /// (De)Serialization error #[error(transparent)] SerdeJson(serde_json::Error), + /// TimerError (wasm only) TimerError, } +impl crate::RpcError for RetryClientError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + if let RetryClientError::ProviderError(err) = self { + err.as_error_response() + } else { + None + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + RetryClientError::ProviderError(e) => e.as_serde_error(), + RetryClientError::SerdeJson(e) => Some(e), + _ => None, + } + } +} + impl std::fmt::Display for RetryClientError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{self:?}") diff --git a/ethers-providers/src/transports/rw.rs b/ethers-providers/src/rpc/transports/rw.rs similarity index 79% rename from ethers-providers/src/transports/rw.rs rename to ethers-providers/src/rpc/transports/rw.rs index 945c2c52..ed8fdda3 100644 --- a/ethers-providers/src/transports/rw.rs +++ b/ethers-providers/src/rpc/transports/rw.rs @@ -1,7 +1,7 @@ //! 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 crate::{errors::ProviderError, JsonRpcClient}; use async_trait::async_trait; use serde::{de::DeserializeOwned, Serialize}; use thiserror::Error; @@ -68,9 +68,9 @@ impl RwClient { pub enum RwClientError where Read: JsonRpcClient, - ::Error: Sync + Send + 'static, + ::Error: crate::RpcError + Sync + Send + 'static, Write: JsonRpcClient, - ::Error: Sync + Send + 'static, + ::Error: crate::RpcError + Sync + Send + 'static, { /// Thrown if the _read_ request failed #[error(transparent)] @@ -80,6 +80,28 @@ where Write(Write::Error), } +impl crate::RpcError for RwClientError +where + Read: JsonRpcClient, + ::Error: crate::RpcError + Sync + Send + 'static, + Write: JsonRpcClient, + ::Error: crate::RpcError + Sync + Send + 'static, +{ + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + match self { + RwClientError::Read(e) => e.as_error_response(), + RwClientError::Write(e) => e.as_error_response(), + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + RwClientError::Read(e) => e.as_serde_error(), + RwClientError::Write(e) => e.as_serde_error(), + } + } +} + impl From> for ProviderError where Read: JsonRpcClient + 'static, diff --git a/ethers-providers/src/transports/ws.rs b/ethers-providers/src/rpc/transports/ws.rs similarity index 96% rename from ethers-providers/src/transports/ws.rs rename to ethers-providers/src/rpc/transports/ws.rs index a8865a8e..1b59741b 100644 --- a/ethers-providers/src/transports/ws.rs +++ b/ethers-providers/src/rpc/transports/ws.rs @@ -1,9 +1,9 @@ -use super::common::{Params, Response}; use crate::{ - provider::ProviderError, - transports::common::{JsonRpcError, Request}, + errors::ProviderError, + rpc::transports::common::{JsonRpcError, Params, Request, Response}, JsonRpcClient, PubsubClient, }; + use async_trait::async_trait; use ethers_core::types::U256; use futures_channel::{mpsc, oneshot}; @@ -461,9 +461,11 @@ pub enum ClientError { TungsteniteError(#[from] WsError), #[error("{0}")] + /// Error in internal mpsc channel ChannelError(String), - #[error(transparent)] + #[error("{0}")] + /// Error in internal oneshot channel Canceled(#[from] oneshot::Canceled), /// Remote server sent a Close message @@ -472,7 +474,7 @@ pub enum ClientError { WsClosed(CloseFrame<'static>), /// Remote server sent a Close message - #[error("Websocket closed with info")] + #[error("Websocket closed")] #[cfg(target_arch = "wasm32")] WsClosed, @@ -496,6 +498,23 @@ pub enum ClientError { RequestError(#[from] http::Error), } +impl crate::RpcError for ClientError { + fn as_error_response(&self) -> Option<&super::JsonRpcError> { + if let ClientError::JsonRpcError(err) = self { + Some(err) + } else { + None + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + ClientError::JsonError(err) => Some(err), + _ => None, + } + } +} + impl From for ProviderError { fn from(src: ClientError) -> Self { ProviderError::JsonRpcClientError(Box::new(src)) diff --git a/ethers-providers/src/stream/mod.rs b/ethers-providers/src/stream/mod.rs new file mode 100644 index 00000000..6b213097 --- /dev/null +++ b/ethers-providers/src/stream/mod.rs @@ -0,0 +1,5 @@ +pub mod tx_stream; +pub use tx_stream::*; + +pub mod watcher; +pub use watcher::*; diff --git a/ethers-providers/src/stream.rs b/ethers-providers/src/stream/tx_stream.rs similarity index 60% rename from ethers-providers/src/stream.rs rename to ethers-providers/src/stream/tx_stream.rs index 1d3a0efe..b4fb723e 100644 --- a/ethers-providers/src/stream.rs +++ b/ethers-providers/src/stream/tx_stream.rs @@ -1,143 +1,22 @@ -#![allow(clippy::return_self_not_must_use)] - -use crate::{JsonRpcClient, Middleware, PinBoxFut, Provider, ProviderError}; -use ethers_core::types::{Transaction, TxHash, U256}; -use futures_core::{stream::Stream, Future}; -use futures_util::{stream, stream::FuturesUnordered, FutureExt, StreamExt}; -use pin_project::pin_project; -use serde::{de::DeserializeOwned, Serialize}; use std::{ collections::VecDeque, - fmt::Debug, pin::Pin, task::{Context, Poll}, - time::Duration, - vec::IntoIter, }; -#[cfg(not(target_arch = "wasm32"))] -use futures_timer::Delay; -#[cfg(target_arch = "wasm32")] -use wasm_timer::Delay; +use futures_core::{stream::Stream, Future}; +use futures_util::{ + self, + stream::{FuturesUnordered, StreamExt}, + FutureExt, +}; -// https://github.com/tomusdrw/rust-web3/blob/befcb2fb8f3ca0a43e3081f68886fa327e64c8e6/src/api/eth_filter.rs#L20 -pub fn interval(duration: Duration) -> impl Stream + Send + Unpin { - stream::unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop) -} +use ethers_core::types::{Transaction, TxHash}; -/// The default polling interval for filters and pending transactions -pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(7000); - -/// The polling interval to use for local endpoints, See [`crate::is_local_endpoint()`] -pub const DEFAULT_LOCAL_POLL_INTERVAL: Duration = Duration::from_millis(100); - -enum FilterWatcherState<'a, R> { - WaitForInterval, - GetFilterChanges(PinBoxFut<'a, Vec>), - NextItem(IntoIter), -} - -#[must_use = "filters do nothing unless you stream them"] -/// Streams data from an installed filter via `eth_getFilterChanges` -#[pin_project] -pub struct FilterWatcher<'a, P, R> { - /// The filter's installed id on the ethereum node - pub id: U256, - - provider: &'a Provider

, - - // The polling interval - interval: Box + Send + Unpin>, - /// statemachine driven by the Stream impl - state: FilterWatcherState<'a, R>, -} - -impl<'a, P, R> FilterWatcher<'a, P, R> -where - P: JsonRpcClient, - R: Send + Sync + DeserializeOwned, -{ - /// Creates a new watcher with the provided factory and filter id. - pub fn new>(id: T, provider: &'a Provider

) -> Self { - Self { - id: id.into(), - interval: Box::new(interval(DEFAULT_POLL_INTERVAL)), - state: FilterWatcherState::WaitForInterval, - provider, - } - } - - /// Sets the stream's polling interval - pub fn interval(mut self, duration: Duration) -> Self { - self.interval = Box::new(interval(duration)); - self - } - - /// Alias for Box::pin, must be called in order to pin the stream and be able - /// to call `next` on it. - pub fn stream(self) -> Pin> { - Box::pin(self) - } -} - -// Advances the filter's state machine -impl<'a, P, R> Stream for FilterWatcher<'a, P, R> -where - P: JsonRpcClient, - R: Serialize + Send + Sync + DeserializeOwned + Debug + 'a, -{ - type Item = R; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - let mut this = self.project(); - let id = *this.id; - - loop { - *this.state = match &mut this.state { - FilterWatcherState::WaitForInterval => { - // Wait the polling period - let _ready = futures_util::ready!(this.interval.poll_next_unpin(cx)); - let fut = Box::pin(this.provider.get_filter_changes(id)); - FilterWatcherState::GetFilterChanges(fut) - } - FilterWatcherState::GetFilterChanges(fut) => { - // NOTE: If the provider returns an error, this will return an empty - // vector. Should we make this return a Result instead? Ideally if we're - // in a streamed loop we wouldn't want the loop to terminate if an error - // is encountered (since it might be a temporary error). - let items: Vec = - futures_util::ready!(fut.as_mut().poll(cx)).unwrap_or_default(); - FilterWatcherState::NextItem(items.into_iter()) - } - // Consume 1 element from the vector. If more elements are in the vector, - // the next call will immediately go to this branch instead of trying to get - // filter changes again. Once the whole vector is consumed, it will poll again - // for new logs - FilterWatcherState::NextItem(iter) => { - if let item @ Some(_) = iter.next() { - return Poll::Ready(item) - } - FilterWatcherState::WaitForInterval - } - }; - } - } -} - -impl<'a, P> FilterWatcher<'a, P, TxHash> -where - P: JsonRpcClient, -{ - /// Returns a stream that yields the `Transaction`s for the transaction hashes this stream - /// yields. - /// - /// This internally calls `Provider::get_transaction` with every new transaction. - /// No more than n futures will be buffered at any point in time, and less than n may also be - /// buffered depending on the state of each future. - pub fn transactions_unordered(self, n: usize) -> TransactionStream<'a, P, Self> { - TransactionStream::new(self.provider, self, n) - } -} +use crate::{ + FilterWatcher, JsonRpcClient, Middleware, Provider, ProviderError, PubsubClient, + SubscriptionStream, +}; /// Errors `TransactionStream` can throw #[derive(Debug, thiserror::Error)] @@ -159,25 +38,26 @@ impl From for ProviderError { } #[cfg(not(target_arch = "wasm32"))] -type TransactionFut<'a> = Pin + Send + 'a>>; -#[cfg(target_arch = "wasm32")] -type TransactionFut<'a> = Pin + 'a>>; +pub(crate) type TransactionFut<'a> = Pin + Send + 'a>>; -type TransactionResult = Result; +#[cfg(target_arch = "wasm32")] +pub(crate) type TransactionFut<'a> = Pin + 'a>>; + +pub(crate) type TransactionResult = Result; /// Drains a stream of transaction hashes and yields entire `Transaction`. #[must_use = "streams do nothing unless polled"] pub struct TransactionStream<'a, P, St> { /// Currently running futures pending completion. - pending: FuturesUnordered>, + pub(crate) pending: FuturesUnordered>, /// Temporary buffered transaction that get started as soon as another future finishes. - buffered: VecDeque, + pub(crate) buffered: VecDeque, /// The provider that gets the transaction - provider: &'a Provider

, + pub(crate) provider: &'a Provider

, /// A stream of transaction hashes. - stream: St, + pub(crate) stream: St, /// max allowed futures to execute at once. - max_concurrent: usize, + pub(crate) max_concurrent: usize, } impl<'a, P: JsonRpcClient, St> TransactionStream<'a, P, St> { @@ -193,7 +73,7 @@ impl<'a, P: JsonRpcClient, St> TransactionStream<'a, P, St> { } /// Push a future into the set - fn push_tx(&mut self, tx: TxHash) { + pub(crate) fn push_tx(&mut self, tx: TxHash) { let fut = self.provider.get_transaction(tx).then(move |res| match res { Ok(Some(tx)) => futures_util::future::ok(tx), Ok(None) => futures_util::future::err(GetTransactionError::NotFound(tx)), @@ -254,17 +134,47 @@ where } } +impl<'a, P> FilterWatcher<'a, P, TxHash> +where + P: JsonRpcClient, +{ + /// Returns a stream that yields the `Transaction`s for the transaction hashes this stream + /// yields. + /// + /// This internally calls `Provider::get_transaction` with every new transaction. + /// No more than n futures will be buffered at any point in time, and less than n may also be + /// buffered depending on the state of each future. + pub fn transactions_unordered(self, n: usize) -> TransactionStream<'a, P, Self> { + TransactionStream::new(self.provider, self, n) + } +} + +impl<'a, P> SubscriptionStream<'a, P, TxHash> +where + P: PubsubClient, +{ + /// Returns a stream that yields the `Transaction`s for the transaction hashes this stream + /// yields. + /// + /// This internally calls `Provider::get_transaction` with every new transaction. + /// No more than n futures will be buffered at any point in time, and less than n may also be + /// buffered depending on the state of each future. + pub fn transactions_unordered(self, n: usize) -> TransactionStream<'a, P, Self> { + TransactionStream::new(self.provider, self, n) + } +} + #[cfg(test)] #[cfg(not(target_arch = "wasm32"))] mod tests { use super::*; - use crate::{Http, Ws}; + use crate::{stream::tx_stream, Http, Ws}; use ethers_core::{ - types::{TransactionReceipt, TransactionRequest}, + types::{Transaction, TransactionReceipt, TransactionRequest}, utils::Anvil, }; use futures_util::{FutureExt, StreamExt}; - use std::{collections::HashSet, convert::TryFrom}; + use std::{collections::HashSet, convert::TryFrom, time::Duration}; #[tokio::test] async fn can_stream_pending_transactions() { @@ -342,9 +252,9 @@ mod tests { })) .await; - let stream = TransactionStream::new( + let stream = tx_stream::TransactionStream::new( &provider, - stream::iter(txs.iter().cloned().map(|tx| tx.unwrap().transaction_hash)), + futures_util::stream::iter(txs.iter().cloned().map(|tx| tx.unwrap().transaction_hash)), 10, ); let res = diff --git a/ethers-providers/src/stream/watcher.rs b/ethers-providers/src/stream/watcher.rs new file mode 100644 index 00000000..8c37ec26 --- /dev/null +++ b/ethers-providers/src/stream/watcher.rs @@ -0,0 +1,115 @@ +use crate::{ + utils::{interval, PinBoxFut}, + JsonRpcClient, Middleware, Provider, +}; +use ethers_core::types::U256; +use futures_core::stream::Stream; +use futures_util::StreamExt; +use pin_project::pin_project; +use serde::{de::DeserializeOwned, Serialize}; +use std::{ + fmt::Debug, + pin::Pin, + task::{Context, Poll}, + time::Duration, + vec::IntoIter, +}; + +/// The default polling interval for filters and pending transactions +pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(7000); + +/// The polling interval to use for local endpoints, See [`crate::is_local_endpoint()`] +pub const DEFAULT_LOCAL_POLL_INTERVAL: Duration = Duration::from_millis(100); + +enum FilterWatcherState<'a, R> { + WaitForInterval, + GetFilterChanges(PinBoxFut<'a, Vec>), + NextItem(IntoIter), +} + +#[must_use = "filters do nothing unless you stream them"] +/// Streams data from an installed filter via `eth_getFilterChanges` +#[pin_project] +pub struct FilterWatcher<'a, P, R> { + /// The filter's installed id on the ethereum node + pub id: U256, + + pub(crate) provider: &'a Provider

, + + // The polling interval + interval: Box + Send + Unpin>, + /// statemachine driven by the Stream impl + state: FilterWatcherState<'a, R>, +} + +impl<'a, P, R> FilterWatcher<'a, P, R> +where + P: JsonRpcClient, + R: Send + Sync + DeserializeOwned, +{ + /// Creates a new watcher with the provided factory and filter id. + pub fn new>(id: T, provider: &'a Provider

) -> Self { + Self { + id: id.into(), + interval: Box::new(interval(DEFAULT_POLL_INTERVAL)), + state: FilterWatcherState::WaitForInterval, + provider, + } + } + + /// Sets the stream's polling interval + pub fn interval(mut self, duration: Duration) -> Self { + self.interval = Box::new(interval(duration)); + self + } + + /// Alias for Box::pin, must be called in order to pin the stream and be able + /// to call `next` on it. + pub fn stream(self) -> Pin> { + Box::pin(self) + } +} + +// Advances the filter's state machine +impl<'a, P, R> Stream for FilterWatcher<'a, P, R> +where + P: JsonRpcClient, + R: Serialize + Send + Sync + DeserializeOwned + Debug + 'a, +{ + type Item = R; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context) -> Poll> { + let mut this = self.project(); + let id = *this.id; + + loop { + *this.state = match &mut this.state { + FilterWatcherState::WaitForInterval => { + // Wait the polling period + let _ready = futures_util::ready!(this.interval.poll_next_unpin(cx)); + let fut = Box::pin(this.provider.get_filter_changes(id)); + FilterWatcherState::GetFilterChanges(fut) + } + FilterWatcherState::GetFilterChanges(fut) => { + // NOTE: If the provider returns an error, this will return an empty + // vector. Should we make this return a Result instead? Ideally if we're + // in a streamed loop we wouldn't want the loop to terminate if an error + // is encountered (since it might be a temporary error). + let items: Vec = + futures_util::ready!(fut.as_mut().poll(cx)).unwrap_or_default(); + FilterWatcherState::NextItem(items.into_iter()) + } + // Consume 1 element from the vector. If more elements are in the vector, + // the next call will immediately go to this branch instead of trying to get + // filter changes again. Once the whole vector is consumed, it will poll again + // for new logs + FilterWatcherState::NextItem(iter) => { + if let item @ Some(_) = iter.next() { + return Poll::Ready(item) + } + FilterWatcherState::WaitForInterval + } + }; + } + } +} diff --git a/ethers-providers/src/call_raw.rs b/ethers-providers/src/toolbox/call_raw.rs similarity index 98% rename from ethers-providers/src/call_raw.rs rename to ethers-providers/src/toolbox/call_raw.rs index 24e19b76..a3f0b771 100644 --- a/ethers-providers/src/call_raw.rs +++ b/ethers-providers/src/toolbox/call_raw.rs @@ -1,6 +1,6 @@ //! Overrides for the `eth_call` rpc method -use crate::{JsonRpcClient, PinBoxFut, Provider, ProviderError}; +use crate::{utils::PinBoxFut, JsonRpcClient, Provider, ProviderError}; use ethers_core::{ types::{ transaction::eip2718::TypedTransaction, Address, BlockId, BlockNumber, Bytes, H256, U256, @@ -59,6 +59,7 @@ impl fmt::Debug for CallBuilder<'_, P> { } impl<'a, P> CallBuilder<'a, P> { + /// Instantiate a new call builder based on `tx` pub fn new(provider: &'a Provider

, tx: &'a TypedTransaction) -> Self { Self::Build(Caller::new(provider, tx)) } @@ -125,6 +126,7 @@ pub struct Caller<'a, P> { } impl<'a, P> Caller<'a, P> { + /// Instantiate a new `Caller` based on `tx` pub fn new(provider: &'a Provider

, tx: &'a TypedTransaction) -> Self { Self { provider, input: CallInput::new(tx) } } @@ -192,6 +194,7 @@ impl fmt::Debug for Map { } impl Map { + /// Instantiate a new map pub fn new(inner: T, f: F) -> Self { Self { inner, f } } @@ -236,12 +239,16 @@ pub mod spoof { /// The state elements to override for a particular account. #[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub struct Account { + /// Account nonce #[serde(skip_serializing_if = "Option::is_none")] pub nonce: Option, + /// Account balance #[serde(skip_serializing_if = "Option::is_none")] pub balance: Option, + /// Account code #[serde(skip_serializing_if = "Option::is_none")] pub code: Option, + /// Account storage #[serde(flatten, skip_serializing_if = "Option::is_none")] pub storage: Option, } @@ -275,8 +282,10 @@ pub mod spoof { /// as a diff on the existing state. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum Storage { + /// State Diff #[serde(rename = "stateDiff")] Diff(HashMap), + /// State override #[serde(rename = "state")] Replace(HashMap), } diff --git a/ethers-providers/src/log_query.rs b/ethers-providers/src/toolbox/log_query.rs similarity index 92% rename from ethers-providers/src/log_query.rs rename to ethers-providers/src/toolbox/log_query.rs index 43cf7086..b7b2f799 100644 --- a/ethers-providers/src/log_query.rs +++ b/ethers-providers/src/toolbox/log_query.rs @@ -1,4 +1,4 @@ -use super::{JsonRpcClient, Middleware, PinBoxFut, Provider, ProviderError}; +use crate::{utils::PinBoxFut, JsonRpcClient, Middleware, Provider, ProviderError}; use ethers_core::types::{Filter, Log, U64}; use futures_core::stream::Stream; use std::{ @@ -8,6 +8,9 @@ use std::{ }; use thiserror::Error; +/// A log query provides streaming access to historical logs via a paginated +/// request. For streaming access to future logs, use [`Middleware::watch`] or +/// [`Middleware::subscribe_logs`] pub struct LogQuery<'a, P> { provider: &'a Provider

, filter: Filter, @@ -29,6 +32,7 @@ impl<'a, P> LogQuery<'a, P> where P: JsonRpcClient, { + /// Instantiate a new `LogQuery` pub fn new(provider: &'a Provider

, filter: &Filter) -> Self { Self { provider, @@ -56,10 +60,13 @@ macro_rules! rewake_with_new_state { }; } +/// Errors while querying for logs #[derive(Error, Debug)] pub enum LogQueryError { + /// Error loading latest block #[error(transparent)] LoadLastBlockError(E), + /// Error loading logs from block range #[error(transparent)] LoadLogsError(E), } diff --git a/ethers-providers/src/toolbox/mod.rs b/ethers-providers/src/toolbox/mod.rs new file mode 100644 index 00000000..5d31172f --- /dev/null +++ b/ethers-providers/src/toolbox/mod.rs @@ -0,0 +1,11 @@ +mod pending_transaction; +pub use pending_transaction::PendingTransaction; + +mod pending_escalator; +pub use pending_escalator::EscalatingPending; + +mod log_query; +pub use log_query::{LogQuery, LogQueryError}; + +pub mod call_raw; +pub use call_raw::*; diff --git a/ethers-providers/src/pending_escalator.rs b/ethers-providers/src/toolbox/pending_escalator.rs similarity index 98% rename from ethers-providers/src/pending_escalator.rs rename to ethers-providers/src/toolbox/pending_escalator.rs index 5b77b60d..77803ba4 100644 --- a/ethers-providers/src/pending_escalator.rs +++ b/ethers-providers/src/toolbox/pending_escalator.rs @@ -15,7 +15,9 @@ use futures_timer::Delay; #[cfg(target_arch = "wasm32")] use wasm_timer::Delay; -use crate::{JsonRpcClient, Middleware, PendingTransaction, PinBoxFut, Provider, ProviderError}; +use crate::{ + utils::PinBoxFut, JsonRpcClient, Middleware, PendingTransaction, Provider, ProviderError, +}; /// States for the EscalatingPending future enum EscalatorStates<'a, P> { diff --git a/ethers-providers/src/pending_transaction.rs b/ethers-providers/src/toolbox/pending_transaction.rs similarity index 99% rename from ethers-providers/src/pending_transaction.rs rename to ethers-providers/src/toolbox/pending_transaction.rs index 92a89be8..74bead5a 100644 --- a/ethers-providers/src/pending_transaction.rs +++ b/ethers-providers/src/toolbox/pending_transaction.rs @@ -1,4 +1,7 @@ -use crate::{stream::interval, JsonRpcClient, Middleware, PinBoxFut, Provider, ProviderError}; +use crate::{ + utils::{interval, PinBoxFut}, + JsonRpcClient, Middleware, Provider, ProviderError, +}; use ethers_core::types::{Transaction, TransactionReceipt, TxHash, U64}; use futures_core::stream::Stream; use futures_util::stream::StreamExt; diff --git a/ethers-providers/src/utils.rs b/ethers-providers/src/utils.rs new file mode 100644 index 00000000..ec53ec2a --- /dev/null +++ b/ethers-providers/src/utils.rs @@ -0,0 +1,40 @@ +use ethers_core::types::U256; +use futures_util::{stream, FutureExt, StreamExt}; +use std::{future::Future, pin::Pin}; + +#[cfg(not(target_arch = "wasm32"))] +use futures_timer::Delay; +#[cfg(target_arch = "wasm32")] +use wasm_timer::Delay; + +use crate::ProviderError; + +/// A simple gas escalation policy +pub type EscalationPolicy = Box U256 + Send + Sync>; + +// Helper type alias +#[cfg(target_arch = "wasm32")] +pub(crate) type PinBoxFut<'a, T> = Pin> + 'a>>; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) type PinBoxFut<'a, T> = + Pin> + Send + 'a>>; + +/// Calls the future if `item` is None, otherwise returns a `futures::ok` +pub async fn maybe(item: Option, f: F) -> Result +where + F: Future>, +{ + if let Some(item) = item { + futures_util::future::ok(item).await + } else { + f.await + } +} + +// https://github.com/tomusdrw/rust-web3/blob/befcb2fb8f3ca0a43e3081f68886fa327e64c8e6/src/api/eth_filter.rs#L20 +/// Create a stream that emits items at a fixed interval. Used for rate control +pub fn interval( + duration: std::time::Duration, +) -> impl futures_core::stream::Stream + Send + Unpin { + stream::unfold((), move |_| Delay::new(duration).map(|_| Some(((), ())))).map(drop) +} diff --git a/ethers-providers/tests/ws_errors.rs b/ethers-providers/tests/ws_errors.rs index 6ae85213..170771a8 100644 --- a/ethers-providers/tests/ws_errors.rs +++ b/ethers-providers/tests/ws_errors.rs @@ -25,7 +25,7 @@ async fn graceful_disconnect_on_ws_errors() { spawn_ws_server().await; // Connect to the fake server - let (ws, _) = connect_async(format!("ws://{}", WS_ENDPOINT)).await.unwrap(); + let (ws, _) = connect_async(format!("ws://{WS_ENDPOINT}")).await.unwrap(); let provider = Provider::new(Ws::new(ws)); let filter = Filter::new().event("Transfer(address,address,uint256)"); let mut stream = provider.subscribe_logs(&filter).await.unwrap(); diff --git a/examples/middleware/examples/create_custom_middleware.rs b/examples/middleware/examples/create_custom_middleware.rs index 56a4a21d..170dcfd1 100644 --- a/examples/middleware/examples/create_custom_middleware.rs +++ b/examples/middleware/examples/create_custom_middleware.rs @@ -5,7 +5,7 @@ use ethers::{ utils::{parse_units, Anvil}, }, middleware::MiddlewareBuilder, - providers::{FromErr, Http, Middleware, PendingTransaction, Provider}, + providers::{Http, Middleware, MiddlewareError, PendingTransaction, Provider}, signers::{LocalWallet, Signer}, }; use thiserror::Error; @@ -98,7 +98,7 @@ where println!("Raised transaction gas: {raised_gas:?} wei"); // Dispatch the call to the inner layer - self.inner().send_transaction(tx, block).await.map_err(FromErr::from) + self.inner().send_transaction(tx, block).await.map_err(MiddlewareError::from_err) } } @@ -122,10 +122,19 @@ pub enum GasMiddlewareError { NoGasSetForTransaction, } -impl FromErr for GasMiddlewareError { - fn from(src: M::Error) -> Self { +impl MiddlewareError for GasMiddlewareError { + type Inner = M::Error; + + fn from_err(src: M::Error) -> Self { GasMiddlewareError::MiddlewareError(src) } + + fn as_inner(&self) -> Option<&Self::Inner> { + match self { + GasMiddlewareError::MiddlewareError(e) => Some(e), + _ => None, + } + } } #[tokio::main] diff --git a/examples/providers/examples/custom.rs b/examples/providers/examples/custom.rs index 450731e6..f21678d0 100644 --- a/examples/providers/examples/custom.rs +++ b/examples/providers/examples/custom.rs @@ -7,12 +7,14 @@ use std::fmt::Debug; use thiserror::Error; use url::Url; -/// First we must create an error type, and implement [`From`] for [`ProviderError`]. +/// First we must create an error type, and implement [`From`] for +/// [`ProviderError`]. /// -/// Here we are using [`thiserror`](https://docs.rs/thiserror) to wrap [`WsClientError`] -/// and [`IpcError`]. -/// This also provides a conversion implementation ([`From`]) for both, so we can use -/// the [question mark operator](https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html) +/// Here we are using [`thiserror`](https://docs.rs/thiserror) to wrap +/// [`WsClientError`] and [`IpcError`]. +/// +/// This also provides a conversion implementation ([`From`]) for both, so we +/// can use the [question mark operator](https://doc.rust-lang.org/rust-by-example/std/result/question_mark.html) /// later on in our implementations. #[derive(Debug, Error)] pub enum WsOrIpcError { @@ -23,6 +25,36 @@ pub enum WsOrIpcError { Ipc(#[from] IpcError), } +/// In order to use our `WsOrIpcError` in the RPC client, we have to implement +/// this trait. +/// +/// [`RpcError`] helps other parts off the stack get access to common provider +/// error cases. For example, any RPC connection may have a `serde_json` error, +/// so we want to make those easily accessible, so we implement +/// `as_serde_error()` +/// +/// In addition, RPC requests may return JSON errors from the node, describing +/// why the request failed. In order to make these accessible, we implement +/// `as_error_response()`. +impl RpcError for WsOrIpcError { + fn as_error_response(&self) -> Option<ðers::providers::JsonRpcError> { + match self { + WsOrIpcError::Ws(e) => e.as_error_response(), + WsOrIpcError::Ipc(e) => e.as_error_response(), + } + } + + fn as_serde_error(&self) -> Option<&serde_json::Error> { + match self { + WsOrIpcError::Ws(WsClientError::JsonError(e)) => Some(e), + WsOrIpcError::Ipc(IpcError::JsonError(e)) => Some(e), + _ => None, + } + } +} + +/// This implementation helps us convert our Error to the library's +/// [`ProviderError`] so that we can use the `?` operator impl From for ProviderError { fn from(value: WsOrIpcError) -> Self { Self::JsonRpcClientError(Box::new(value))