diff --git a/ethers-core/src/types/chain.rs b/ethers-core/src/types/chain.rs index 794c04aa..b366bccd 100644 --- a/ethers-core/src/types/chain.rs +++ b/ethers-core/src/types/chain.rs @@ -4,6 +4,7 @@ use std::{ convert::{TryFrom, TryInto}, fmt, str::FromStr, + time::Duration, }; use strum::EnumVariantNames; use thiserror::Error; @@ -60,6 +61,39 @@ pub enum Chain { AuroraTestnet = 1313161555, } +// === impl Chain === + +impl Chain { + /// The blocktime varies from chain to chain + /// + /// It can be beneficial to know the average blocktime to adjust the polling of an Http provider + /// for example. + /// + /// **Note:** this will not return the accurate average depending on the time but is rather a + /// sensible default derived from blocktime charts like + /// + pub fn average_blocktime_hint(&self) -> Option { + let ms = match self { + Chain::Arbitrum | Chain::ArbitrumTestnet => 1_300, + Chain::Mainnet | Chain::Optimism => 13_000, + Chain::Polygon | Chain::PolygonMumbai => 2_100, + Chain::Moonbeam | Chain::Moonriver => 12_500, + Chain::BinanceSmartChain | Chain::BinanceSmartChainTestnet => 3_000, + Chain::Avalanche | Chain::AvalancheFuji => 2_000, + Chain::Fantom | Chain::FantomTestnet => 1_200, + Chain::Cronos | Chain::CronosTestnet => 5_700, + Chain::Evmos | Chain::EvmosTestnet => 1_900, + Chain::Aurora | Chain::AuroraTestnet => 1_100, + Chain::Oasis => 5_500, + Chain::Emerald => 6_000, + Chain::Dev | Chain::AnvilHardhat => 200, + _ => return None, + }; + + Some(Duration::from_millis(ms)) + } +} + impl fmt::Display for Chain { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { let chain = match self { diff --git a/ethers-providers/src/lib.rs b/ethers-providers/src/lib.rs index da7cfa84..9e7ee807 100644 --- a/ethers-providers/src/lib.rs +++ b/ethers-providers/src/lib.rs @@ -8,6 +8,7 @@ use futures_util::future::join_all; pub use transports::*; mod provider; +pub use provider::{is_local_endpoint, FilterKind, Provider, ProviderError, ProviderExt}; // ENS support pub mod ens; @@ -23,7 +24,9 @@ pub use log_query::LogQuery; mod stream; pub use futures_util::StreamExt; -pub use stream::{interval, FilterWatcher, TransactionStream, DEFAULT_POLL_INTERVAL}; +pub use stream::{ + interval, FilterWatcher, TransactionStream, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL, +}; mod pubsub; pub use pubsub::{PubsubClient, SubscriptionStream}; @@ -38,8 +41,6 @@ use serde::{de::DeserializeOwned, Serialize}; use std::{error::Error, fmt::Debug, future::Future, pin::Pin}; use url::Url; -pub use provider::{FilterKind, Provider, ProviderError}; - // feature-enabled support for dev-rpc methods #[cfg(feature = "dev-rpc")] pub use provider::dev_rpc::DevRpcMiddleware; diff --git a/ethers-providers/src/provider.rs b/ethers-providers/src/provider.rs index 5348d670..dc07c8fe 100644 --- a/ethers-providers/src/provider.rs +++ b/ethers-providers/src/provider.rs @@ -2,7 +2,7 @@ use crate::{ call_raw::CallBuilder, ens, erc, maybe, pubsub::{PubsubClient, SubscriptionStream}, - stream::{FilterWatcher, DEFAULT_POLL_INTERVAL}, + stream::{FilterWatcher, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL}, FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, LogQuery, MockProvider, PendingTransaction, QuorumProvider, RwClient, SyncingStatus, }; @@ -33,6 +33,7 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use thiserror::Error; use url::{ParseError, Url}; +use ethers_core::types::Chain; use futures_util::{lock::Mutex, try_join}; use std::{ collections::VecDeque, convert::TryFrom, fmt::Debug, str::FromStr, sync::Arc, time::Duration, @@ -1453,6 +1454,120 @@ impl Provider> { } } +mod sealed { + use crate::{Http, Provider}; + /// private trait to ensure extension trait is not implement outside of this crate + pub trait Sealed {} + impl Sealed for Provider {} +} + +/// Extension trait for `Provider` +/// +/// **Note**: this is currently sealed until is finalized +/// +/// # Example +/// +/// Automatically configure poll interval via `eth_getChainId` +/// +/// Note that this will send an RPC to retrieve the chain id. +/// +/// ``` +/// # use ethers_providers::{Http, Provider, ProviderExt}; +/// # async fn t() { +/// let http_provider = Provider::::connect("https://eth-mainnet.alchemyapi.io/v2/API_KEY").await; +/// # } +/// ``` +/// +/// This is essentially short for +/// +/// ``` +/// use std::convert::TryFrom; +/// use ethers_core::types::Chain; +/// use ethers_providers::{Http, Provider, ProviderExt}; +/// let http_provider = Provider::::try_from("https://eth-mainnet.alchemyapi.io/v2/API_KEY").unwrap().set_chain(Chain::Mainnet); +/// ``` +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait ProviderExt: sealed::Sealed { + /// The error type that can occur when creating a provider + type Error: Debug; + + /// Creates a new instance connected to the given `url`, exit on error + async fn connect(url: &str) -> Self + where + Self: Sized, + { + Self::try_connect(url).await.unwrap() + } + + /// Try to create a new `Provider` + async fn try_connect(url: &str) -> Result + where + Self: Sized; + + /// Customize `Provider` settings for chain. + /// + /// E.g. [`Chain::average_blocktime_hint()`] returns the average block time which can be used to + /// tune the polling interval. + /// + /// Returns the customized `Provider` + fn for_chain(mut self, chain: impl Into) -> Self + where + Self: Sized, + { + self.set_chain(chain); + self + } + + /// Customized `Provider` settings for chain + fn set_chain(&mut self, chain: impl Into) -> &mut Self; +} + +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +impl ProviderExt for Provider { + type Error = ParseError; + + async fn try_connect(url: &str) -> Result + where + Self: Sized, + { + let mut provider = Provider::try_from(url)?; + if is_local_endpoint(url) { + provider.set_interval(DEFAULT_LOCAL_POLL_INTERVAL); + } else if let Some(chain) = + provider.get_chainid().await.ok().and_then(|id| Chain::try_from(id).ok()) + { + provider.set_chain(chain); + } + + Ok(provider) + } + + fn set_chain(&mut self, chain: impl Into) -> &mut Self { + let chain = chain.into(); + if let Some(blocktime) = chain.average_blocktime_hint() { + // use half of the block time + self.set_interval(blocktime / 2); + } + self + } +} + +/// Returns true if the endpoint is local +/// +/// # Example +/// +/// ``` +/// use ethers_providers::is_local_endpoint; +/// assert!(is_local_endpoint("http://localhost:8545")); +/// assert!(is_local_endpoint("http://127.0.0.1:8545")); +/// ``` +#[inline] +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 diff --git a/ethers-providers/src/stream.rs b/ethers-providers/src/stream.rs index 6cd73c7d..1d3a0efe 100644 --- a/ethers-providers/src/stream.rs +++ b/ethers-providers/src/stream.rs @@ -28,6 +28,9 @@ pub fn interval(duration: Duration) -> impl Stream + Send + Unpin { /// 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>),