|
| 1 | +//! Helper for `eth_getLogs` with automatic retry on "max results exceeded" errors. |
| 2 | +
|
| 3 | +use alloy_primitives::BlockNumber; |
| 4 | +use alloy_provider::{network::AnyNetwork, Provider}; |
| 5 | +use alloy_rpc_types::{Filter, Log}; |
| 6 | + |
| 7 | +/// The result type returned by `get_logs`. |
| 8 | +pub type GetLogsResult<T> = |
| 9 | + Result<T, alloy_json_rpc::RpcError<alloy_transport::TransportErrorKind>>; |
| 10 | + |
| 11 | +/// Fetches logs with automatic retry when the RPC returns a "max results exceeded" error. |
| 12 | +/// |
| 13 | +/// Some RPC providers limit the number of logs returned in a single request. When exceeded, |
| 14 | +/// they return an error like: |
| 15 | +/// `"query exceeds max results 20000, retry with the range 24383075-24383096"` |
| 16 | +/// |
| 17 | +/// This function parses such errors and retries with the suggested narrower block range. |
| 18 | +pub async fn get_logs_with_retry<P: Provider<AnyNetwork>>( |
| 19 | + provider: &P, |
| 20 | + filter: &Filter, |
| 21 | +) -> GetLogsResult<Vec<Log>> { |
| 22 | + match provider.get_logs(filter).await { |
| 23 | + Ok(logs) => Ok(logs), |
| 24 | + Err(e) => { |
| 25 | + if let Some((from, to)) = parse_max_results_error(&e) { |
| 26 | + let narrowed_filter = filter.clone().from_block(from).to_block(to); |
| 27 | + provider.get_logs(&narrowed_filter).await |
| 28 | + } else { |
| 29 | + Err(e) |
| 30 | + } |
| 31 | + } |
| 32 | + } |
| 33 | +} |
| 34 | + |
| 35 | +/// Parses an error to extract the suggested block range from "max results exceeded" errors. |
| 36 | +/// |
| 37 | +/// Expected format: "query exceeds max results N, retry with the range FROM-TO" |
| 38 | +fn parse_max_results_error<E: std::fmt::Display>(error: &E) -> Option<(BlockNumber, BlockNumber)> { |
| 39 | + let msg = error.to_string(); |
| 40 | + |
| 41 | + if !msg.contains("max results") { |
| 42 | + return None; |
| 43 | + } |
| 44 | + |
| 45 | + // Look for pattern like "range 24383075-24383096" |
| 46 | + let range_prefix = "range "; |
| 47 | + let range_start = msg.find(range_prefix)?; |
| 48 | + let range_part = &msg[range_start + range_prefix.len()..]; |
| 49 | + |
| 50 | + // Parse "FROM-TO" (stop at first non-numeric, non-dash char) |
| 51 | + let range_end = |
| 52 | + range_part.find(|c: char| !c.is_ascii_digit() && c != '-').unwrap_or(range_part.len()); |
| 53 | + let range_str = &range_part[..range_end]; |
| 54 | + |
| 55 | + let mut parts = range_str.split('-'); |
| 56 | + let from: BlockNumber = parts.next()?.parse().ok()?; |
| 57 | + let to: BlockNumber = parts.next()?.parse().ok()?; |
| 58 | + |
| 59 | + Some((from, to)) |
| 60 | +} |
| 61 | + |
| 62 | +#[cfg(test)] |
| 63 | +mod tests { |
| 64 | + use super::*; |
| 65 | + |
| 66 | + #[test] |
| 67 | + fn test_parse_max_results_error_message() { |
| 68 | + let error_msg = "query exceeds max results 20000, retry with the range 24383075-24383096"; |
| 69 | + let result = parse_max_results_error(&error_msg); |
| 70 | + assert_eq!(result, Some((24383075, 24383096))); |
| 71 | + } |
| 72 | + |
| 73 | + #[test] |
| 74 | + fn test_parse_non_matching_error() { |
| 75 | + let error_msg = "some other error"; |
| 76 | + let result = parse_max_results_error(&error_msg); |
| 77 | + assert_eq!(result, None); |
| 78 | + } |
| 79 | + |
| 80 | + #[test] |
| 81 | + fn test_parse_with_trailing_text() { |
| 82 | + let error_msg = "query exceeds max results 20000, retry with the range 100-200, extra info"; |
| 83 | + let result = parse_max_results_error(&error_msg); |
| 84 | + assert_eq!(result, Some((100, 200))); |
| 85 | + } |
| 86 | +} |
0 commit comments