Skip to content
31 changes: 28 additions & 3 deletions src/macros/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,22 @@
/// fn method_name<T: SomeTrait>(filter_id: U256) -> Vec<T>
/// );
/// ```
///
/// ## With where clause
/// ```ignore
/// robust_rpc!(
/// doc_args = [(tx, "The transaction request.")]
/// @clone [tx]
/// fn method_name(tx: TransactionRequest) -> ReturnType
/// where [SomeType: SomeTrait]
/// );
/// ```
macro_rules! robust_rpc {
// Main pattern: optional doc_errors, optional doc_args, zero or more fn doc_args, optional error variant
(
$(doc_include_error = [$($error_doc:tt)+])?
$(doc_args = [$(($arg_name:ident, $arg_desc:literal)),* $(,)?])?
$(doc_alias = $alias:tt)?
fn $method:ident $(<$generic:ident: $bound:path>)? ($($($arg:ident: $arg_ty:ty),+)?) -> $ret:ty $(; or $err:ident)?
) => {
#[doc = concat!("This is a wrapper function for [`Provider::", stringify!($method), "`].")]
Expand All @@ -76,6 +87,9 @@ macro_rules! robust_rpc {
$(
#[doc = $($error_doc)+]
)?
$(
#[doc(alias = $alias)]
)?
pub async fn $method $(<$generic: $bound>)? (&self $(, $($arg: $arg_ty),+)?) -> Result<$ret, Error> {
let result = self
.try_operation_with_failover(
Expand All @@ -89,15 +103,21 @@ macro_rules! robust_rpc {
}
};

// Arguments with cloning use with @clone
// Arguments with cloning and optional where clause
(
$(#[doc = $doc:literal])*
$(doc_include_error = [$($error_doc:tt)+])?
$(doc_args = [$(($arg_name:ident, $arg_desc:literal)),* $(,)?])?
$(doc_alias = $alias:tt)?
@clone [$($clone_arg:ident),+]
fn $method:ident $(<$generic:ident: $bound:path>)? (
$($arg:ident: $arg_ty:ty),+
) -> $ret:ty $(; or $err:ident)?
) -> $ret:ty
$(where [$($where_ty:ty: $where_bound:path),+ $(,)?])?
$(; or $err:ident)?
) => {
$(#[doc = $doc])*
///
#[doc = concat!("This is a wrapper function for [`Provider::", stringify!($method), "`].")]
$($(
///
Expand All @@ -115,7 +135,12 @@ macro_rules! robust_rpc {
$(
#[doc = $($error_doc)+]
)?
pub async fn $method $(<$generic: $bound>)? (&self, $($arg: $arg_ty),+) -> Result<$ret, Error> {
$(
#[doc(alias = $alias)]
)?
pub async fn $method $(<$generic: $bound>)? (&self, $($arg: $arg_ty),+) -> Result<$ret, Error>
$(where $($where_ty: $where_bound),+)?
{
let result = self
.try_operation_with_failover(
move |provider| {
Expand Down
112 changes: 102 additions & 10 deletions src/robust_provider/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,28 @@

use std::{fmt::Debug, future::Future, time::Duration};

use alloy::{
network::Network,
providers::{Provider, RootProvider},
transports::{RpcError, TransportErrorKind},
};
use backon::{ExponentialBuilder, Retryable};
use tokio::time::timeout;

use super::errors::{CoreError, is_retryable_error};
use alloy::{
consensus::TrieAccount,
eips::{BlockId, BlockNumberOrTag},
network::Ethereum,
eips::{BlockId, BlockNumberOrTag, eip1559::Eip1559Estimation},
network::{Ethereum, Network},
primitives::{
Address, B256, BlockHash, BlockNumber, Bytes, StorageKey, StorageValue, TxHash, U256,
},
providers::PendingTransactionBuilder,
providers::{PendingTransactionBuilder, Provider, RootProvider},
rpc::{
json_rpc::RpcRecv,
types::{
Bundle, EIP1186AccountProofResponse, EthCallResponse, FeeHistory, Filter, Log,
SyncStatus,
AccessListResult, AccountInfo, Bundle, EIP1186AccountProofResponse, EthCallResponse,
FeeHistory, FillTransaction, Filter, Log, SyncStatus,
erc4337::TransactionConditional,
simulate::{SimulatePayload, SimulatedBlock},
},
},
transports::{RpcError, TransportErrorKind},
};

use crate::{Error, block_not_found_doc, robust_provider::RobustSubscription};
Expand Down Expand Up @@ -89,12 +86,21 @@ impl<N: Network> RobustProvider<N> {

robust_rpc!(fn get_chain_id() -> u64);

robust_rpc!(
doc_args = [(request, "The transaction request to create an access list for.")]
fn create_access_list(request: &N::TransactionRequest) -> AccessListResult
);

robust_rpc!(
doc_args = [(tx, "The transaction request to estimate gas for.")]
@clone [tx]
fn estimate_gas(tx: N::TransactionRequest) -> u64
);

robust_rpc!(
fn estimate_eip1559_fees() -> Eip1559Estimation
);

robust_rpc!(
doc_args = [
(block_count, "The number of blocks to include in the fee history."),
Expand All @@ -107,6 +113,11 @@ impl<N: Network> RobustProvider<N> {
robust_rpc!(fn get_gas_price() -> u128);
robust_rpc!(fn get_max_priority_fee_per_gas() -> u128);

robust_rpc!(
doc_args = [(address, "The address for which to get the account info.")]
fn get_account_info(address: Address) -> AccountInfo
);

robust_rpc!(
doc_args = [(address, "The address to get the account for.")]
fn get_account(address: Address) -> TrieAccount
Expand Down Expand Up @@ -201,6 +212,15 @@ impl<N: Network> RobustProvider<N> {
fn sign_transaction(tx: N::TransactionRequest) -> Bytes
);

robust_rpc!(
doc_args = [
(tx, "The transaction request to fill.")
]
@clone [tx]
fn fill_transaction(tx: N::TransactionRequest) -> FillTransaction<N::TxEnvelope>
where [N::TxEnvelope: RpcRecv]
);

robust_rpc!(
doc_args = [
(address, "The address of the account."),
Expand Down Expand Up @@ -259,6 +279,14 @@ impl<N: Network> RobustProvider<N> {
fn get_uncle_count(block: BlockId) -> u64
);

robust_rpc!(
doc_args = [
(tag, "The block identifier (hash or number)."),
(idx, "The uncle index position.")
]
fn get_uncle(tag: BlockId, idx: u64) -> Option<N::BlockResponse>
);

robust_rpc!(
doc_args = [(request, "The simulation request")]
fn simulate(request: &SimulatePayload) -> Vec<SimulatedBlock<N::BlockResponse>>
Expand All @@ -276,6 +304,70 @@ impl<N: Network> RobustProvider<N> {
fn send_raw_transaction(encoded_tx: &[u8]) -> PendingTransactionBuilder<N>
);

robust_rpc!(
doc_args = [(encoded_tx, "The RLP-encoded signed transaction bytes")]
fn send_raw_transaction_sync(encoded_tx: &[u8]) -> N::ReceiptResponse
);

robust_rpc!(
doc_args = [
(encoded_tx, "The RLP-encoded signed transaction bytes"),
(conditional, "The transaction conditional to apply")
]
@clone [conditional]
fn send_raw_transaction_conditional(encoded_tx: &[u8], conditional: TransactionConditional) -> PendingTransactionBuilder<N>
);

robust_rpc!(
doc_args = [(tx, "The transaction request to send")]
@clone [tx]
fn send_transaction(tx: N::TransactionRequest) -> PendingTransactionBuilder<N>
);

robust_rpc!(
doc_args = [
(tx, "The signed transaction envelope to send.")
]
@clone [tx]
fn send_tx_envelope(tx: N::TxEnvelope) -> PendingTransactionBuilder<N>
where [N::TxEnvelope: Clone]
);

robust_rpc!(
doc_args = [(tx, "The transaction request to send synchronously")]
@clone [tx]
fn send_transaction_sync(tx: N::TransactionRequest) -> N::ReceiptResponse
);

robust_rpc!(
doc_args = [
(sender, "The sender address"),
(nonce, "The nonce of the transaction")
]
fn get_transaction_by_sender_nonce(sender: Address, nonce: u64) -> Option<N::TransactionResponse>
);

robust_rpc!(
doc_args = [
(block_hash, "The hash of the block"),
(index, "The transaction index position")
]
fn get_raw_transaction_by_block_hash_and_index(block_hash: B256, index: usize) -> Option<Bytes>
);

robust_rpc!(
doc_args = [
(block_number, "The block number or tag"),
(index, "The transaction index position")
]
fn get_raw_transaction_by_block_number_and_index(block_number: BlockNumberOrTag, index: usize) -> Option<Bytes>
);

robust_rpc!(
doc_alias = "web3_client_version"
fn get_client_version() -> String
);

/// Subscribe to new block headers with automatic failover and reconnection.
///
/// Returns a `RobustSubscription` that automatically:
Expand Down
20 changes: 16 additions & 4 deletions tests/eth_namespace/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ async fn test_get_accounts_succeeds() -> anyhow::Result<()> {
// eth_getAccount
// ============================================================================

#[tokio::test]
async fn test_get_account_info_succeeds() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil().await?;

let accounts = alloy_provider.get_accounts().await?;
let address = accounts[0];

let robust_account = robust.get_account_info(address).await?;
let alloy_account = alloy_provider.get_account_info(address).await?;

assert_eq!(robust_account, alloy_account);

Ok(())
}

#[tokio::test]
async fn test_get_account_succeeds() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil().await?;
Expand All @@ -36,10 +51,7 @@ async fn test_get_account_succeeds() -> anyhow::Result<()> {
let robust_account = robust.get_account(address).await?;
let alloy_account = alloy_provider.get_account(address).await?;

assert_eq!(robust_account.nonce, alloy_account.nonce);
assert_eq!(robust_account.balance, alloy_account.balance);
assert_eq!(robust_account.storage_root, alloy_account.storage_root);
assert_eq!(robust_account.code_hash, alloy_account.code_hash);
assert_eq!(robust_account, alloy_account);

Ok(())
}
Expand Down
72 changes: 70 additions & 2 deletions tests/eth_namespace/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,6 @@ async fn test_get_uncle_count_by_block_number_succeeds() -> anyhow::Result<()> {
let alloy_count = alloy_provider.get_uncle_count(tag).await?;

assert_eq!(robust_count, alloy_count);
// Anvil doesn't produce uncles
assert_eq!(robust_count, 0);
}

Expand All @@ -419,8 +418,77 @@ async fn test_get_uncle_count_by_block_hash_succeeds() -> anyhow::Result<()> {
let alloy_count = alloy_provider.get_uncle_count(BlockId::hash(block_hash)).await?;

assert_eq!(robust_count, alloy_count);
// Anvil doesn't produce uncles
assert_eq!(robust_count, 0);

Ok(())
}

// ============================================================================
// eth_getUncleByBlockHashAndIndex / eth_getUncleByBlockNumberAndIndex
// ============================================================================

#[tokio::test]
async fn test_get_uncle_by_block_number_returns_none() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(10).await?;

// Anvil doesn't produce uncles, so get_uncle should return None
let robust_uncle = robust.get_uncle(BlockId::number(5), 0).await?;
let alloy_uncle = alloy_provider.get_uncle(BlockId::number(5), 0).await?;

assert!(robust_uncle.is_none());
assert_eq!(robust_uncle, alloy_uncle);

Ok(())
}

#[tokio::test]
async fn test_get_uncle_by_block_hash_returns_none() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(10).await?;

let block = alloy_provider
.get_block_by_number(BlockNumberOrTag::Number(5))
.await?
.expect("block should exist");
let block_hash = block.header.hash;

// Anvil doesn't produce uncles, so get_uncle should return None
let robust_uncle = robust.get_uncle(BlockId::hash(block_hash), 0).await?;
let alloy_uncle = alloy_provider.get_uncle(BlockId::hash(block_hash), 0).await?;

assert!(robust_uncle.is_none());
assert_eq!(robust_uncle, alloy_uncle);

Ok(())
}

#[tokio::test]
async fn test_get_uncle_with_various_block_tags() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(10).await?;

let tags = [BlockId::latest(), BlockId::earliest(), BlockId::safe(), BlockId::finalized()];

for tag in tags {
let robust_uncle = robust.get_uncle(tag, 0).await?;
let alloy_uncle = alloy_provider.get_uncle(tag, 0).await?;

assert!(robust_uncle.is_none());
assert_eq!(robust_uncle, alloy_uncle);
}

Ok(())
}

#[tokio::test]
async fn test_get_uncle_with_various_indices() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil_with_blocks(10).await?;

for idx in [0, 1, 2, 10, 100] {
let robust_uncle = robust.get_uncle(BlockId::number(5), idx).await?;
let alloy_uncle = alloy_provider.get_uncle(BlockId::number(5), idx).await?;

assert!(robust_uncle.is_none());
assert_eq!(robust_uncle, alloy_uncle);
}

Ok(())
}
20 changes: 19 additions & 1 deletion tests/eth_namespace/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,25 @@ async fn test_get_chain_id_succeeds() -> anyhow::Result<()> {
}

// ============================================================================
// eth_estimateGas
// eth_createAccessList
// ============================================================================

#[tokio::test]
async fn test_create_access_list() -> anyhow::Result<()> {
let (_anvil, robust, alloy_provider) = setup_anvil().await?;

let tx = TransactionRequest::default();

let robust_access_list = robust.create_access_list(&tx).await?;
let alloy_access_list = alloy_provider.create_access_list(&tx).await?;

assert_eq!(robust_access_list, alloy_access_list);

Ok(())
}

// ============================================================================
// eth_estimate_eip1559Fees
// ============================================================================

#[tokio::test]
Expand Down
Loading