RPC Provider Abstractions
Alloy offers Provider Wrapping as a design pattern that lets you extend or customize the behavior of a Provider by encapsulating it inside another object.
There are several reasons why you would want create your own RPC provider abstractions, for example to simplify complex workflows, or build more intuitive interfaces for particular use cases (like deployment, data indexing, or trading).
Let's dive into an example: Imagine you have multiple contracts that need to be deployed and monitored. Rather than repeating the same boilerplate code throughout your application, you can create specialized abstractions that wrap the Provider. Your Deployer
struct ingests the Provider
and the bytecode to deploy contracts and interact with them. More on this in the example snippets below.
There are two ways to ways to implement provider wrapping, both offer different trade-offs depending on your use case:
- Using Generics (
P: Provider
): Preserves type information and enables static dispatch - Using Type Erasure (
DynProvider
): Simplifies types at the cost of some runtime overhead
1. Using generics P: Provider
The ideal way is by using the P: Provider
generic on the encapsulating type. This approach Preserves full type information and static dispatch, though can lead to complex type signatures and handling generics.
This is depicted by the following example. Use generics when you need maximum performance and type safety, especially in library code.
//! 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(())
}
During this approach the compiler creates a unique Deployer
struct for each Provider type you use static dispatch with slightly better runtime overhead.
Use this approach when performance is critical, type information is valuable e.g. when creating library code or working with embedded systems.
Type information is valuable: You need to know the exact Provider type for specialized behavior
2. Using Type Erasure DynProvider
Use DynProvider when you prioritize simplicity and flexibility, such as in application code where the performance difference is negligible.
DynProvider
erases the type of a provider while maintaining its core functionality.
//! 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(())
}
With DynProvider
we use dynamic dispatch, accept a slightly slower runtime overhead but can avoid dealing with generics.
Use this approach when you prefer simplicity over speed speed, minimise compile and binary size or want to create heterogeneous collections.
Provider
does not require Arc
You might be tempted to wrap a Provider
in Arc
to enable sharing and cloning:
#[derive(Clone)]
struct MyProvider<P: Provider> {
inner: Arc<P>, // Unnecssary
}
This is actually unnecessary because Alloy's Providers already implement internal reference counting. Instead, simply add the Clone
bound when needed:
struct MyProvider<P: Provider + Clone> {
inner: P,
}
This eliminates common boilerplate and prevents potential performance issues from double Arc-ing.