Skip to content

Wrapping a Provider

A common pattern is to create abstractions over the Provider type to add additional functionality or to bake in a more user-friendly API on top. The goal of this is to have the more verbose part API or error handling done under the hood.

For example, creating a Deployer struct that ingests the Provider and the bytecode to deploy contracts and interact with them. More on this in the example snippets below.

There are multiple ways in which a provider can be wrapped:

  1. Using the P: Provider.
  2. Wrapping the DynProvider when you're okay with erasing the type information.

Using generics P: Provider

The ideal way is by using the P: Provider generic on the encapsulating type. This is depicted by the following example.

wrapped_provider.rs
//! Example demonstrating how to wrap the [`Provider`] in a struct and pass it through free
//! functions.
 
use alloy::{
    network::EthereumWallet,
    node_bindings::Anvil,
    primitives::address,
    providers::{Provider, ProviderBuilder},
    rpc::types::TransactionReceipt,
    signers::local::PrivateKeySigner,
    sol,
    transports::{TransportErrorKind, TransportResult},
};
use eyre::Result;
use Counter::CounterInstance;
 
// Codegen from embedded Solidity code and precompiled bytecode.
sol! {
    #[allow(missing_docs)]
    // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
    #[sol(rpc, bytecode="6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033")]
    contract Counter {
        uint256 public number;
 
        function setNumber(uint256 newNumber) public {
            number = newNumber;
        }
 
        function increment() public {
            number++;
        }
    }
}
 
/// Deployer that ingests a [`Provider`] and [`EthereumWallet`] and deploys [`Counter`]
struct Deployer<P: Provider> {
    provider: P,
    wallet: EthereumWallet,
}
 
impl<P: Provider> Deployer<P> {
    /// Create a new instance of [`Deployer`].
    fn new(provider: P, private_key: PrivateKeySigner) -> Self {
        let wallet = EthereumWallet::new(private_key);
        Self { provider, wallet }
    }
 
    /// Deploys [`Counter`] using the given [`EthereumWallet`] and returns [`CounterInstance`]
    async fn deploy(&self) -> Result<CounterInstance<&P>> {
        let addr = CounterInstance::deploy_builder(&self.provider)
            .from(self.wallet.default_signer().address())
            .deploy()
            .await?;
 
        Ok(CounterInstance::new(addr, &self.provider))
    }
}
 
struct CounterContract<P: Provider> {
    provider: P,
    counter: CounterInstance<P>,
}
 
impl<P: Provider> CounterContract<P> {
    /// Create a new instance of [`CounterContract`].
    const fn new(provider: P, counter: CounterInstance<P>) -> Self {
        Self { provider, counter }
    }
 
    /// Returns the current number stored in the [`Counter`].
    async fn number(&self) -> TransportResult<u64> {
        let number = self.counter.number().call().await.map_err(TransportErrorKind::custom)?;
        Ok(number.to::<u64>())
    }
 
    /// Increments the number stored in the [`Counter`].
    async fn increment(&self) -> TransportResult<TransactionReceipt> {
        self.counter
            .increment()
            .from(address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")) // Default anvil signer
            .send()
            .await
            .map_err(TransportErrorKind::custom)?
            .get_receipt()
            .await
            .map_err(TransportErrorKind::custom)
    }
 
    /// Returns the inner provider.
    fn provider(&self) -> &impl Provider {
        &self.provider
    }
}
 
#[tokio::main]
async fn main() -> Result<()> {
    // Spin up a local Anvil node.
    // Ensure `anvil` is available in $PATH.
    let anvil = Anvil::new().spawn();
 
    let provider = ProviderBuilder::new().connect(anvil.endpoint().as_str()).await?;
 
    let signer_pk = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse()?;
    let deployer = Deployer::new(provider.clone(), signer_pk);
    let counter_instance = deployer.deploy().await?;
 
    println!("Deployed `Counter` at {}", counter_instance.address());
 
    let counter = CounterContract::new(&provider, counter_instance);
    let num = counter.number().await?;
 
    println!("Current number: {num}");
 
    counter.increment().await?;
 
    let num = counter.number().await?;
 
    println!("Incremented number: {num}");
 
    let block_num = counter.provider().get_block_number().await?;
 
    println!("Current block number: {}", block_num);
 
    Ok(())
}

DynProvider

Sometimes, you're okay with type erasure and don't want to use generics. In those cases, one should use the DynProvider.

DynProvider erases the type of a provider while maintaining its core functionality.

dyn_provider.rs
//! Demonstrates how to obtain a `DynProvider` from a Provider.
 
use alloy::{
    node_bindings::Anvil,
    providers::{Provider, ProviderBuilder},
    signers::local::PrivateKeySigner,
    sol,
};
 
// Codegen from embedded Solidity code and precompiled bytecode.
sol! {
    #[allow(missing_docs)]
    // solc v0.8.26; solc Counter.sol --via-ir --optimize --bin
    #[sol(rpc, bytecode="6080806040523460135760df908160198239f35b600080fdfe6080806040526004361015601257600080fd5b60003560e01c9081633fb5c1cb1460925781638381f58a146079575063d09de08a14603c57600080fd5b3460745760003660031901126074576000546000198114605e57600101600055005b634e487b7160e01b600052601160045260246000fd5b600080fd5b3460745760003660031901126074576020906000548152f35b34607457602036600319011260745760043560005500fea2646970667358221220e978270883b7baed10810c4079c941512e93a7ba1cd1108c781d4bc738d9090564736f6c634300081a0033")]
    contract Counter {
        uint256 public number;
 
        function setNumber(uint256 newNumber) public {
            number = newNumber;
        }
 
        function increment() public {
            number++;
        }
    }
}
 
#[tokio::main]
async fn main() -> eyre::Result<()> {
    // Spin up a local Anvil node.
    // Ensure `anvil` is available in $PATH.
    let anvil = Anvil::new().spawn();
 
    let signer_pk: PrivateKeySigner =
        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80".parse()?;
 
    let from = signer_pk.address();
 
    // Provider with verbose types.
    let regular_provider =
        ProviderBuilder::new().wallet(signer_pk).connect(anvil.endpoint().as_str()).await?;
 
    // One can use the erased method to obtain a DynProvider from a Provider.
    let dyn_provider = regular_provider.erased();
 
    // Note that the fillers set while building provider are still available, only the types have
    // been erased OR boxed under the hood.
    // This enables us to use the DynProvider as one would use a regular Provider with verbose
    // types.
    let counter = Counter::deploy(&dyn_provider).await?;
 
    println!("Counter deployed at {}", counter.address());
 
    // Sends a transaction with required properties such as gas, nonce, from filled.
    let incr = counter.increment().send().await?;
    let receipt = incr.get_receipt().await?;
    assert_eq!(receipt.from, from);
 
    let number = counter.number().call().await?;
 
    println!("New number: {}", number);
 
    Ok(())
}

One might be tempted to Arc a Provider to enable Clone on the struct.

#[derive(Clone)]
struct MyProvider<P: Provider> {
    inner: Arc<P>, // Not required
}

However, this is not required as the inner provider is already Arc-ed, and one should just add the Clone bound to the Provider when needed.

struct MyProvider<P: Provider + Clone> {
    inner: P,
}