Skip to content

feat(evm-rpc): Resilient JSON-RPC client for EVM chains #218

Description

@Emmyt24

Depends on: #217. Blocks: #219, #221, #225.

Description

Octo talks to Stellar through two purpose-built clients —
crates/ingest/src/horizon.rs for deposit polling and
crates/api/src/horizon.rs for API-side reads/submits — both
wrapped in octo-resilience. EVM needs the equivalent over
JSON-RPC 2.0.

Build crates/evm-rpc (octo-evm-rpc) as a typed client covering the methods this epic needs,
with the same resilience posture.

Requirements and context

  • Honour the submit-asymmetry rule (AD-5). eth_call, eth_getLogs, eth_blockNumber,
    eth_getTransactionReceipt are CallKind::Read and retryable.
    eth_sendRawTransaction must not be transparently retried — see the header comment in
    crates/ingest/src/horizon.rs for the established reasoning.
  • JSON-RPC error handling is a trap. A JSON-RPC error is HTTP 200 with an error member. A
    client that only checks HTTP status will treat every failure as success. Handle the error object
    explicitly and distinguish transport failure from protocol error from execution revert.
  • Providers differ in real, breaking ways: eth_getLogs block-range caps (Alchemy 2k, Infura 10k,
    others unbounded), rate-limit responses, and inconsistent revert-data encoding. The client must
    surface a typed RangeTooLarge so feat(ingest): EVM ingest worker — ERC-20 Transfer log scanning #221 can adaptively bisect rather than stall.
  • All quantities are hex-encoded strings (0x-prefixed, minimal-length). Parse into the U256
    type chosen in feat(evm-core): secp256k1 keys, BIP-44 derivation, and EIP-55 addresses #217never into u64 or f64.
  • Security: RPC URLs contain API keys — never log the URL (see feat(config): Per-chain configuration and runtime chain registry #216). Cap response body size;
    a malicious or compromised RPC returning a multi-GB response must not OOM the worker.

Suggested execution

Branch: feat/evm-rpc-client

Implement changes

  • Add crates/evm-rpc using the workspace reqwest (already rustls-tls, no OpenSSL).
  • Implement typed wrappers: eth_blockNumber, eth_getBlockByNumber, eth_getLogs,
    eth_getTransactionReceipt, eth_getTransactionCount, eth_call, eth_estimateGas,
    eth_feeHistory, eth_chainId, eth_sendRawTransaction.
  • Wire execute(CallKind::Read, ..) with per-chain RetryPolicy/CircuitBreaker from feat(config): Per-chain configuration and runtime chain registry #216 for reads
    only; document at the call site why the submit path is excluded.
  • Add a startup assertion that eth_chainId matches the configured CAIP-2 chain id — a
    misconfigured RPC pointing at the wrong chain is a fund-loss bug
    , and this one-line check
    prevents it.
  • Typed errors: Transport, JsonRpc { code, message }, Revert { data }, RangeTooLarge,
    RateLimited { retry_after }, CircuitOpen.

Test and commit

  • Mock-server tests in the style of
    crates/ingest/tests/horizon_mock_tests.rs and
    crates/api/tests/horizon_resilience_tests.rs.
  • A test that HTTP 200 with a JSON-RPC error member is treated as an error, not success.
  • Test retry/backoff on 5xx and the circuit opening after failure_threshold.
  • A test asserting eth_sendRawTransaction is called exactly once on failure — no silent retry.
  • Test hex parsing edge cases: 0x0, values above u64::MAX, malformed input, and a response body
    exceeding the size cap.
  • Test the chain-id mismatch guard rejects at startup.

Example commit message

feat(evm-rpc): typed JSON-RPC client with per-chain resilience

Reads go through octo-resilience retry + circuit breaker;
eth_sendRawTransaction deliberately does not, preserving the
submit-asymmetry rule.

JSON-RPC errors arrive as HTTP 200 with an error member, so status-only
checking would read every failure as success — handled explicitly.

Startup asserts eth_chainId matches the configured CAIP-2 id; an RPC
pointed at the wrong chain is a fund-loss bug.

Refs #218

Guidelines

Do not pull in a full EVM SDK for this. A focused client keeps the dependency audit small and the
cargo deny surface reviewable.


Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaignarea/backendBackend crates: api, store, ingest, webhooks, bin/serverdifficulty/mediumMedium difficultytype/epicTracking issue for a multi-issue epic

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions