Skip to content

plugins.aea-ledger-ethereum.aea_ledger_ethereum.rpc_rotation

RPC rotation support for EthereumApi as a Web3 HTTPProvider subclass.

When multiple RPC endpoints are provided (comma-separated), the :class:RotatingHTTPProvider automatically fails over to healthy endpoints on rate-limit, connection, or quota errors. With a single RPC URL the provider retries on transport failures without rotation.

Implementing rotation as a provider (rather than a middleware) keeps the standard web3 middleware chain intact: every request runs through the full chain — defaults plus any user-injected middleware — and only the underlying transport changes when rotation occurs.

RETRY_DELAY

base delay between retries (exponential backoff)

MAX_RETRY_DELAY

cap on retry delay

ROTATION_COOLDOWN

min time between rotations to prevent cascade

parse_rpc_urls

def parse_rpc_urls(address: str) -> List[str]

Parse RPC URL(s) from an address string.

Supports a single URL or a comma-separated list. Returns a list with at least one URL.

Arguments:

  • address: single RPC URL or comma-separated list of URLs.

Returns:

list of parsed RPC URL strings.

classify_error

def classify_error(error: Exception) -> ErrorCategory

Classify an RPC error into a category.

Arguments:

  • error: the exception raised by the RPC call.

Returns:

one of "rate_limit", "connection", "quota", "server", "fd_exhaustion", "unknown".

RotatingHTTPProvider Objects

class RotatingHTTPProvider(HTTPProvider)

:class:~web3.HTTPProvider that rotates RPC endpoints on transport failures.

Manages a pool of :class:~web3.HTTPProvider instances with per-endpoint health tracking, automatic failover, and exponential-backoff retry logic. Because rotation happens at the transport layer (inside :meth:make_request) rather than as a web3 middleware, the standard middleware chain — defaults plus any user-injected middleware — runs untouched on every call.

For write operations (eth_sendRawTransaction, eth_sendTransaction) only clear pre-send connection failures are retried to prevent double-submission.

Concurrency note: :meth:make_request is synchronous and uses time.sleep between retries, which holds the calling thread for the duration of the sleep. When used through the valory/ledger connection, every call runs in that connection's dedicated thread pool sized by the max_thread_workers config (default 32). Under sustained RPC errors with many concurrent ledger calls, in-flight retries can occupy threads in this pool until they complete. Size max_thread_workers to comfortably exceed expected peak retry concurrency for the deployment's RPC topology.

Usage::

provider = RotatingHTTPProvider(
    rpc_urls=["https://rpc1.example.com", "https://rpc2.example.com"],
    request_kwargs={"timeout": 10},
    chain_id=100,
)
w3 = Web3(provider)

__init__

def __init__(rpc_urls: List[str],
             request_kwargs: Optional[Dict[str, Any]] = None,
             chain_id: Optional[int] = None) -> None

Initialize the rotating provider.

Arguments:

  • rpc_urls: list of RPC endpoint URL strings (required, non-empty).
  • request_kwargs: dict forwarded to each pooled :class:HTTPProvider.
  • chain_id: optional chain ID for Chainlist fallback enrichment.

Raises:

  • ValueError: if rpc_urls (after Chainlist enrichment) is empty.

current_rpc_url

@property
def current_rpc_url() -> str

Return the currently active RPC URL.

rpc_count

@property
def rpc_count() -> int

Return the number of configured RPC endpoints.

endpoint_uri

@property
def endpoint_uri() -> str

Return the URL of the currently active RPC endpoint.

Overrides :attr:HTTPProvider.endpoint_uri so that diagnostic tooling (metrics, logging, request IDs) reading w3.provider.endpoint_uri observes the URL we are currently dispatching to rather than the URL passed to super().__init__.

Returns:

the active RPC URL.

endpoint_uri

@endpoint_uri.setter
def endpoint_uri(value: str) -> None

No-op setter retained for parent-class compatibility.

Arguments:

  • value: ignored. HTTPProvider.__init__ assigns to endpoint_uri once at construction; we accept the write so the parent constructor does not raise, but the active endpoint is always derived from self._rpc_urls[self._current_index].

make_request

def make_request(method: RPCEndpoint, params: Any) -> RPCResponse

Dispatch a JSON-RPC call with rotation and retry across the pool.

Each attempt routes to the currently-active pooled provider. On a retryable transport failure the offending provider is marked unhealthy, rotation advances to the next healthy peer, and the call is retried (with exponential backoff) until the per-call retry budget is exhausted. Write methods are retried only on clear pre-send failures so a partially-submitted transaction is never re-broadcast.

Arguments:

  • method: JSON-RPC method name.
  • params: JSON-RPC parameters.

Returns:

the JSON-RPC response.