Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/rpc-tester/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ alloy-primitives.workspace = true
alloy-rpc-types.workspace = true
alloy-rpc-types-trace.workspace = true
alloy-provider = { workspace = true, features = ["trace-api", "debug-api"] }
alloy-json-rpc = "0.11.1"
alloy-transport = "0.11.1"

assert-json-diff.workspace = true
eyre.workspace = true
Expand Down
86 changes: 86 additions & 0 deletions crates/rpc-tester/src/get_logs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
//! Helper for `eth_getLogs` with automatic retry on "max results exceeded" errors.

use alloy_primitives::BlockNumber;
use alloy_provider::{network::AnyNetwork, Provider};
use alloy_rpc_types::{Filter, Log};

/// The result type returned by `get_logs`.
pub type GetLogsResult<T> =
Result<T, alloy_json_rpc::RpcError<alloy_transport::TransportErrorKind>>;

/// Fetches logs with automatic retry when the RPC returns a "max results exceeded" error.
///
/// Some RPC providers limit the number of logs returned in a single request. When exceeded,
/// they return an error like:
/// `"query exceeds max results 20000, retry with the range 24383075-24383096"`
///
/// This function parses such errors and retries with the suggested narrower block range.
pub async fn get_logs_with_retry<P: Provider<AnyNetwork>>(
provider: &P,
filter: &Filter,
) -> GetLogsResult<Vec<Log>> {
match provider.get_logs(filter).await {
Ok(logs) => Ok(logs),
Err(e) => {
if let Some((from, to)) = parse_max_results_error(&e) {
let narrowed_filter = filter.clone().from_block(from).to_block(to);
provider.get_logs(&narrowed_filter).await
} else {
Err(e)
}
}
}
}

/// Parses an error to extract the suggested block range from "max results exceeded" errors.
///
/// Expected format: "query exceeds max results N, retry with the range FROM-TO"
fn parse_max_results_error<E: std::fmt::Display>(error: &E) -> Option<(BlockNumber, BlockNumber)> {
let msg = error.to_string();

if !msg.contains("max results") {
return None;
}

// Look for pattern like "range 24383075-24383096"
let range_prefix = "range ";
let range_start = msg.find(range_prefix)?;
let range_part = &msg[range_start + range_prefix.len()..];

// Parse "FROM-TO" (stop at first non-numeric, non-dash char)
let range_end =
range_part.find(|c: char| !c.is_ascii_digit() && c != '-').unwrap_or(range_part.len());
let range_str = &range_part[..range_end];

let mut parts = range_str.split('-');
let from: BlockNumber = parts.next()?.parse().ok()?;
let to: BlockNumber = parts.next()?.parse().ok()?;

Some((from, to))
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_parse_max_results_error_message() {
let error_msg = "query exceeds max results 20000, retry with the range 24383075-24383096";
let result = parse_max_results_error(&error_msg);
assert_eq!(result, Some((24383075, 24383096)));
}

#[test]
fn test_parse_non_matching_error() {
let error_msg = "some other error";
let result = parse_max_results_error(&error_msg);
assert_eq!(result, None);
}

#[test]
fn test_parse_with_trailing_text() {
let error_msg = "query exceeds max results 20000, retry with the range 100-200, extra info";
let result = parse_max_results_error(&error_msg);
assert_eq!(result, Some((100, 200)));
}
}
7 changes: 6 additions & 1 deletion crates/rpc-tester/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! rpc-tester library
#![cfg_attr(not(test), warn(unused_crate_dependencies))]

#[doc(hidden)]
pub mod get_logs;
mod tester;
pub use tester::RpcTester;
mod report;
Expand Down Expand Up @@ -49,6 +51,9 @@ macro_rules! rpc_with_block {
}

/// Macro to call the `get_logs` rpc method and box the future result.
///
/// Uses [`get_logs::get_logs_with_retry`] to automatically handle "max results exceeded" errors
/// by retrying with the narrower block range suggested in the error message.
#[macro_export]
macro_rules! get_logs {
($self:expr, $arg:expr) => {{
Expand All @@ -58,7 +63,7 @@ macro_rules! get_logs {
$self
.test_rpc_call(stringify!(get_logs), args_str, move |provider: &P| {
let filter = filter.clone();
async move { provider.get_logs(&filter).await }
async move { $crate::get_logs::get_logs_with_retry(provider, &filter).await }
})
.await
}) as Pin<Box<dyn Future<Output = (MethodName, Result<(), TestError>)> + Send>>
Expand Down