use crate::{
fillers::{
BlobGasEstimator, BlobGasFiller, CachedNonceManager, ChainIdFiller, FillerControlFlow,
GasFiller, JoinFill, NonceFiller, NonceManager, RecommendedFillers, SimpleNonceManager,
TxFiller, WalletFiller,
},
layers::{BlockIdLayer, CallBatchLayer, ChainLayer},
provider::SendableTx,
utils::Eip1559Estimator,
Provider, RootProvider,
};
use alloy_chains::NamedChain;
use alloy_network::{Ethereum, IntoWallet, Network};
use alloy_primitives::ChainId;
use alloy_rpc_client::{ClientBuilder, ConnectionConfig, RpcClient};
use alloy_transport::{TransportConnect, TransportError, TransportResult};
use std::marker::PhantomData;
pub trait ProviderLayer<P: Provider<N>, N: Network = Ethereum> {
type Provider: Provider<N>;
fn layer(&self, inner: P) -> Self::Provider;
}
#[derive(Clone, Copy, Debug)]
pub struct Identity;
impl<N> TxFiller<N> for Identity
where
N: Network,
{
type Fillable = ();
fn status(&self, _tx: &<N as Network>::TransactionRequest) -> FillerControlFlow {
FillerControlFlow::Finished
}
fn fill_sync(&self, _tx: &mut SendableTx<N>) {}
async fn prepare<P>(
&self,
_provider: &P,
_tx: &N::TransactionRequest,
) -> TransportResult<Self::Fillable> {
Ok(())
}
async fn fill(
&self,
_to_fill: Self::Fillable,
tx: SendableTx<N>,
) -> TransportResult<SendableTx<N>> {
Ok(tx)
}
}
impl<P, N> ProviderLayer<P, N> for Identity
where
N: Network,
P: Provider<N>,
{
type Provider = P;
fn layer(&self, inner: P) -> Self::Provider {
inner
}
}
#[derive(Debug)]
pub struct Stack<Inner, Outer> {
inner: Inner,
outer: Outer,
}
impl<Inner, Outer> Stack<Inner, Outer> {
pub const fn new(inner: Inner, outer: Outer) -> Self {
Self { inner, outer }
}
}
impl<P, N, Inner, Outer> ProviderLayer<P, N> for Stack<Inner, Outer>
where
N: Network,
P: Provider<N>,
Inner: ProviderLayer<P, N>,
Outer: ProviderLayer<Inner::Provider, N>,
{
type Provider = Outer::Provider;
fn layer(&self, provider: P) -> Self::Provider {
let inner = self.inner.layer(provider);
self.outer.layer(inner)
}
}
#[derive(Debug)]
pub struct ProviderBuilder<L, F, N = Ethereum> {
layer: L,
filler: F,
network: PhantomData<fn() -> N>,
}
impl
ProviderBuilder<
Identity,
JoinFill<Identity, <Ethereum as RecommendedFillers>::RecommendedFillers>,
Ethereum,
>
{
pub fn new() -> Self {
ProviderBuilder::default().with_recommended_fillers()
}
pub fn disable_recommended_fillers(self) -> ProviderBuilder<Identity, Identity, Ethereum> {
ProviderBuilder { layer: self.layer, filler: Identity, network: self.network }
}
}
impl<N> Default for ProviderBuilder<Identity, Identity, N> {
fn default() -> Self {
Self { layer: Identity, filler: Identity, network: PhantomData }
}
}
impl ProviderBuilder<Identity, Identity, Ethereum> {
pub fn new_with_network<Net: RecommendedFillers>(
) -> ProviderBuilder<Identity, JoinFill<Identity, Net::RecommendedFillers>, Net> {
ProviderBuilder {
layer: Identity,
filler: JoinFill::new(Identity, Net::recommended_fillers()),
network: PhantomData,
}
}
}
impl<L, N: Network> ProviderBuilder<L, Identity, N> {
pub fn with_recommended_fillers(
self,
) -> ProviderBuilder<L, JoinFill<Identity, N::RecommendedFillers>, N>
where
N: RecommendedFillers,
{
self.filler(N::recommended_fillers())
}
}
impl<L, F, N> ProviderBuilder<L, F, N> {
pub fn apply<T>(self, f: impl FnOnce(Self) -> T) -> T {
f(self)
}
pub fn map_layer<L2>(self, f: impl FnOnce(L) -> L2) -> ProviderBuilder<L2, F, N> {
ProviderBuilder { layer: f(self.layer), filler: self.filler, network: PhantomData }
}
pub fn map_filler<F2>(self, f: impl FnOnce(F) -> F2) -> ProviderBuilder<L, F2, N> {
ProviderBuilder { layer: self.layer, filler: f(self.filler), network: PhantomData }
}
pub fn layer<Inner>(self, layer: Inner) -> ProviderBuilder<Stack<Inner, L>, F, N> {
self.map_layer(|current| Stack::new(layer, current))
}
pub fn filler<F2>(self, filler: F2) -> ProviderBuilder<L, JoinFill<F, F2>, N> {
self.map_filler(|current| JoinFill::new(current, filler))
}
pub fn network<Net: RecommendedFillers>(
self,
) -> ProviderBuilder<L, JoinFill<Identity, Net::RecommendedFillers>, Net> {
ProviderBuilder {
layer: self.layer,
filler: JoinFill::new(Identity, Net::recommended_fillers()),
network: PhantomData,
}
}
pub fn with_chain(self, chain: NamedChain) -> ProviderBuilder<Stack<ChainLayer, L>, F, N> {
self.layer(ChainLayer::new(chain))
}
pub fn with_blob_gas_estimation(self) -> ProviderBuilder<L, JoinFill<F, BlobGasFiller>, N> {
self.filler(BlobGasFiller::default())
}
pub fn with_blob_gas_estimator(
self,
estimator: BlobGasEstimator,
) -> ProviderBuilder<L, JoinFill<F, BlobGasFiller>, N> {
self.filler(BlobGasFiller { estimator })
}
pub fn with_gas_estimation(self) -> ProviderBuilder<L, JoinFill<F, GasFiller>, N> {
self.filler(GasFiller::default())
}
pub fn with_eip1559_estimator(
self,
estimator: Eip1559Estimator,
) -> ProviderBuilder<L, JoinFill<F, GasFiller>, N> {
self.filler(GasFiller { estimator })
}
pub fn with_nonce_management<M: NonceManager>(
self,
nonce_manager: M,
) -> ProviderBuilder<L, JoinFill<F, NonceFiller<M>>, N> {
self.filler(NonceFiller::new(nonce_manager))
}
pub fn with_simple_nonce_management(
self,
) -> ProviderBuilder<L, JoinFill<F, NonceFiller<SimpleNonceManager>>, N> {
self.with_nonce_management(SimpleNonceManager::default())
}
pub fn with_cached_nonce_management(
self,
) -> ProviderBuilder<L, JoinFill<F, NonceFiller<CachedNonceManager>>, N> {
self.with_nonce_management(CachedNonceManager::default())
}
pub fn fetch_chain_id(self) -> ProviderBuilder<L, JoinFill<F, ChainIdFiller>, N> {
self.filler(ChainIdFiller::default())
}
pub fn with_chain_id(
self,
chain_id: ChainId,
) -> ProviderBuilder<L, JoinFill<F, ChainIdFiller>, N> {
self.filler(ChainIdFiller::new(Some(chain_id)))
}
pub fn wallet<W: IntoWallet<N>>(
self,
wallet: W,
) -> ProviderBuilder<L, JoinFill<F, WalletFiller<W::NetworkWallet>>, N>
where
N: Network,
{
self.filler(WalletFiller::new(wallet.into_wallet()))
}
pub fn with_call_batching(self) -> ProviderBuilder<Stack<CallBatchLayer, L>, F, N> {
self.layer(CallBatchLayer::new())
}
pub fn with_arbitrum_call_batching(self) -> ProviderBuilder<Stack<CallBatchLayer, L>, F, N> {
self.layer(CallBatchLayer::new().arbitrum_compat())
}
#[cfg(not(target_family = "wasm"))]
pub fn with_caching(
self,
max_items: u32,
) -> ProviderBuilder<Stack<crate::layers::CacheLayer, L>, F, N> {
self.layer(crate::layers::CacheLayer::new(max_items))
}
#[cfg(not(target_family = "wasm"))]
pub fn with_default_caching(
self,
) -> ProviderBuilder<Stack<crate::layers::CacheLayer, L>, F, N> {
self.with_caching(100)
}
pub fn with_default_block(
self,
block_id: alloy_eips::BlockId,
) -> ProviderBuilder<Stack<BlockIdLayer, L>, F, N> {
self.layer(BlockIdLayer::new(block_id))
}
pub fn connect_provider<P>(self, provider: P) -> F::Provider
where
L: ProviderLayer<P, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
P: Provider<N>,
N: Network,
{
let Self { layer, filler, network: PhantomData } = self;
let stack = Stack::new(layer, filler);
stack.layer(provider)
}
pub fn connect_client(self, client: RpcClient) -> F::Provider
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
self.connect_provider(RootProvider::new(client))
}
pub fn connect_mocked_client(self, asserter: alloy_transport::mock::Asserter) -> F::Provider
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
self.connect_client(RpcClient::mocked(asserter))
}
#[doc(alias = "on_builtin")]
pub async fn connect(self, s: &str) -> Result<F::Provider, TransportError>
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().connect(s).await?;
Ok(self.connect_client(client))
}
pub async fn connect_with_config(
self,
s: &str,
config: ConnectionConfig,
) -> Result<F::Provider, TransportError>
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().connect_with_config(s, config).await?;
Ok(self.connect_client(client))
}
pub async fn connect_with<C>(self, connect: &C) -> Result<F::Provider, TransportError>
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
C: TransportConnect,
{
connect
.get_transport()
.await
.map(|t| RpcClient::new(t, connect.is_local()))
.map(|client| self.connect_client(client))
}
#[cfg(feature = "pubsub")]
pub async fn connect_pubsub_with<C>(self, connect: C) -> Result<F::Provider, TransportError>
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
C: alloy_pubsub::PubSubConnect,
{
ClientBuilder::default().pubsub(connect).await.map(|client| self.connect_client(client))
}
#[cfg(feature = "ws-base")]
pub async fn connect_ws(
self,
connect: alloy_transport_ws::WsConnect,
) -> Result<F::Provider, TransportError>
where
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().ws(connect).await?;
Ok(self.connect_client(client))
}
#[cfg(feature = "ipc")]
pub async fn connect_ipc<T>(
self,
connect: alloy_transport_ipc::IpcConnect<T>,
) -> Result<F::Provider, TransportError>
where
alloy_transport_ipc::IpcConnect<T>: alloy_pubsub::PubSubConnect,
L: ProviderLayer<RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().ipc(connect).await?;
Ok(self.connect_client(client))
}
#[cfg(any(test, all(feature = "reqwest", not(all(target_os = "wasi", target_env = "p1")))))]
pub fn connect_http(self, url: reqwest::Url) -> F::Provider
where
L: ProviderLayer<crate::RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().http(url);
self.connect_client(client)
}
#[cfg(any(test, all(feature = "reqwest", not(all(target_os = "wasi", target_env = "p1")))))]
pub fn connect_reqwest<C>(self, client: C, url: reqwest::Url) -> F::Provider
where
L: ProviderLayer<crate::RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
C: Into<reqwest::Client>,
{
let client = ClientBuilder::default().http_with_client(client.into(), url);
self.connect_client(client)
}
#[cfg(any(test, all(feature = "reqwest", not(all(target_os = "wasi", target_env = "p1")))))]
pub fn with_reqwest<B>(self, url: reqwest::Url, builder: B) -> F::Provider
where
L: ProviderLayer<crate::RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
B: FnOnce(reqwest::ClientBuilder) -> reqwest::Client,
{
self.connect_reqwest(builder(reqwest::ClientBuilder::default()), url)
}
#[cfg(feature = "hyper")]
pub fn connect_hyper_http(self, url: url::Url) -> F::Provider
where
L: ProviderLayer<crate::RootProvider<N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
N: Network,
{
let client = ClientBuilder::default().hyper_http(url);
self.connect_client(client)
}
}
#[cfg(any(test, feature = "anvil-node"))]
type JoinedEthereumWalletFiller<F> = JoinFill<F, WalletFiller<alloy_network::EthereumWallet>>;
#[cfg(any(test, feature = "anvil-node"))]
type AnvilProviderResult<T> = Result<T, alloy_node_bindings::NodeError>;
#[cfg(any(test, feature = "anvil-node"))]
impl<L, F, N: Network> ProviderBuilder<L, F, N> {
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
pub fn connect_anvil(self) -> F::Provider
where
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
L: crate::builder::ProviderLayer<
crate::layers::AnvilProvider<crate::provider::RootProvider<N>, N>,
N,
>,
{
self.connect_anvil_with_config(std::convert::identity)
}
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
pub fn connect_anvil_with_wallet(
self,
) -> <JoinedEthereumWalletFiller<F> as ProviderLayer<L::Provider, N>>::Provider
where
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
L: crate::builder::ProviderLayer<
crate::layers::AnvilProvider<crate::provider::RootProvider<N>, N>,
N,
>,
alloy_network::EthereumWallet: alloy_network::NetworkWallet<N>,
{
self.connect_anvil_with_wallet_and_config(std::convert::identity)
.expect("failed to build provider")
}
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
pub fn connect_anvil_with_config(
self,
f: impl FnOnce(alloy_node_bindings::Anvil) -> alloy_node_bindings::Anvil,
) -> F::Provider
where
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
L: crate::builder::ProviderLayer<
crate::layers::AnvilProvider<crate::provider::RootProvider<N>, N>,
N,
>,
{
let anvil_layer = crate::layers::AnvilLayer::from(f(Default::default()));
let url = anvil_layer.endpoint_url();
let rpc_client = ClientBuilder::default().http(url);
self.layer(anvil_layer).connect_client(rpc_client)
}
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
#[deprecated(since = "0.12.6", note = "use `connect_anvil_with_config` instead")]
pub fn on_anvil_with_config(
self,
f: impl FnOnce(alloy_node_bindings::Anvil) -> alloy_node_bindings::Anvil,
) -> F::Provider
where
L: ProviderLayer<crate::layers::AnvilProvider<RootProvider<N>, N>, N>,
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
{
let anvil_layer = crate::layers::AnvilLayer::from(f(Default::default()));
let url = anvil_layer.endpoint_url();
let rpc_client = ClientBuilder::default().http(url);
self.layer(anvil_layer).connect_client(rpc_client)
}
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
pub fn connect_anvil_with_wallet_and_config(
self,
f: impl FnOnce(alloy_node_bindings::Anvil) -> alloy_node_bindings::Anvil,
) -> AnvilProviderResult<
<JoinedEthereumWalletFiller<F> as ProviderLayer<L::Provider, N>>::Provider,
>
where
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
L: crate::builder::ProviderLayer<
crate::layers::AnvilProvider<crate::provider::RootProvider<N>, N>,
N,
>,
alloy_network::EthereumWallet: alloy_network::NetworkWallet<N>,
{
let anvil_layer = crate::layers::AnvilLayer::from(f(Default::default()));
let url = anvil_layer.endpoint_url();
let wallet = anvil_layer
.instance()
.wallet()
.ok_or(alloy_node_bindings::NodeError::NoKeysAvailable)?;
let rpc_client = ClientBuilder::default().http(url);
Ok(self.wallet(wallet).layer(anvil_layer).connect_client(rpc_client))
}
#[cfg_attr(docsrs, doc(cfg(feature = "anvil-node")))]
#[deprecated(since = "0.12.6", note = "use `connect_anvil_with_wallet_and_config` instead")]
pub fn on_anvil_with_wallet_and_config(
self,
f: impl FnOnce(alloy_node_bindings::Anvil) -> alloy_node_bindings::Anvil,
) -> AnvilProviderResult<
<JoinedEthereumWalletFiller<F> as ProviderLayer<L::Provider, N>>::Provider,
>
where
F: TxFiller<N> + ProviderLayer<L::Provider, N>,
L: crate::builder::ProviderLayer<
crate::layers::AnvilProvider<crate::provider::RootProvider<N>, N>,
N,
>,
alloy_network::EthereumWallet: alloy_network::NetworkWallet<N>,
{
let anvil_layer = crate::layers::AnvilLayer::from(f(Default::default()));
let url = anvil_layer.endpoint_url();
let wallet = anvil_layer
.instance()
.wallet()
.ok_or(alloy_node_bindings::NodeError::NoKeysAvailable)?;
let rpc_client = ClientBuilder::default().http(url);
Ok(self.wallet(wallet).layer(anvil_layer).connect_client(rpc_client))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Provider;
use alloy_network::AnyNetwork;
#[tokio::test]
async fn basic() {
let provider = ProviderBuilder::new()
.with_cached_nonce_management()
.with_call_batching()
.connect_http("http://localhost:8545".parse().unwrap());
let _ = provider.get_account(Default::default());
let provider = provider.erased();
let _ = provider.get_account(Default::default());
}
#[tokio::test]
#[cfg(feature = "reqwest")]
async fn test_connect_reqwest() {
let provider = ProviderBuilder::new()
.with_cached_nonce_management()
.with_call_batching()
.connect_reqwest(
reqwest::Client::new(),
reqwest::Url::parse("http://localhost:8545").unwrap(),
);
let _ = provider.get_account(Default::default());
let provider = provider.erased();
let _ = provider.get_account(Default::default());
}
#[tokio::test]
#[cfg(feature = "reqwest")]
async fn test_with_reqwest() {
let provider = ProviderBuilder::new()
.with_cached_nonce_management()
.with_call_batching()
.with_reqwest(reqwest::Url::parse("http://localhost:8545").unwrap(), |builder| {
builder
.user_agent("alloy/test")
.timeout(std::time::Duration::from_secs(10))
.build()
.expect("failed to build reqwest client")
});
let _ = provider.get_account(Default::default());
let provider = provider.erased();
let _ = provider.get_account(Default::default());
}
#[tokio::test]
async fn compile_with_network() {
let p = ProviderBuilder::new_with_network::<AnyNetwork>().connect_anvil();
let num = p.get_block_number().await.unwrap();
assert_eq!(num, 0);
}
#[test]
fn network_replaces_fillers() {
let builder = ProviderBuilder::new().filler(GasFiller::default()).network::<AnyNetwork>();
let _: ProviderBuilder<
Identity,
JoinFill<Identity, <AnyNetwork as RecommendedFillers>::RecommendedFillers>,
AnyNetwork,
> = builder;
}
#[test]
fn apply_transforms_builder() {
let builder = ProviderBuilder::new()
.apply(|builder| builder.disable_recommended_fillers().with_gas_estimation());
let _: ProviderBuilder<Identity, JoinFill<Identity, GasFiller>, Ethereum> = builder;
}
#[test]
fn map_filler_replaces_fillers() {
let builder = ProviderBuilder::new().map_filler(|_| GasFiller::default());
let _: ProviderBuilder<Identity, GasFiller, Ethereum> = builder;
}
#[test]
fn map_layer_replaces_layers() {
let builder = ProviderBuilder::<Identity, Identity>::default()
.map_layer(|_| ChainLayer::new(NamedChain::Mainnet));
let _: ProviderBuilder<ChainLayer, Identity, Ethereum> = builder;
}
#[tokio::test]
async fn network_swap_works_at_runtime() {
let p = ProviderBuilder::new().network::<AnyNetwork>().connect_anvil();
let num = p.get_block_number().await.unwrap();
assert_eq!(num, 0);
}
}