Skip to content

Add eth_getLogs support to EvmRpcClient with event decoding - #1360

Merged
DZakh merged 6 commits into
mainfrom
claude/rpc-rast-logs-migration-tiop1v
Jun 30, 2026
Merged

Add eth_getLogs support to EvmRpcClient with event decoding#1360
DZakh merged 6 commits into
mainfrom
claude/rpc-rast-logs-migration-tiop1v

Conversation

@DZakh

@DZakh DZakh commented Jun 29, 2026

Copy link
Copy Markdown
Member

Summary

Adds eth_getLogs RPC method support to the Rust-based EvmRpcClient, with integrated event parameter decoding. This replaces the previous JavaScript-based Rpc.getLogs approach with a native implementation that decodes event logs using the same DecoderCore infrastructure as HyperSync.

Key Changes

  • Rust EvmRpcClient (packages/cli/src/evm_rpc_source/mod.rs):

    • Added GetLogsParams struct to define filter parameters (block range, addresses, topics)
    • Added RpcLog struct to represent decoded log fields (hex quantities converted to i64)
    • Added RpcEventItem struct pairing logs with their decoded params
    • Implemented get_logs() async method that:
      • Calls eth_getLogs via JSON-RPC
      • Decodes hex quantities (blockNumber, transactionIndex, logIndex) to integers
      • Spawns blocking task to decode event parameters using DecoderCore
      • Returns logs with optional decoded params keyed by contract name
    • Updated EvmRpcClient::new() to accept event_params and checksum_addresses for decoder initialization
  • ReScript bindings (packages/envio/src/sources/EvmRpcClient.res):

    • Added getLogsParams type matching the Rust struct
    • Added rpcEventItem type with log and decoded params
    • Updated t type to include getLogs method
    • Updated classNew binding to pass event params and checksum flag to Rust constructor
    • Added error coercion for JSON-RPC errors in getLogs calls
  • RpcSource integration (packages/envio/src/sources/RpcSource.res):

    • Replaced Rpc.getLogs call with rpcClient.getLogs()
    • Removed separate HyperSync decoder initialization and log-to-event conversion
    • Simplified event processing by using pre-decoded params from RpcEventItem
    • Updated getNextPage to use EvmRpcClient.t instead of generic Rpc.client
  • Test infrastructure:

    • Replaced JavaScript fetch stub with MockJsonRpcServer module that runs a real local HTTP server (required because Rust client uses its own HTTP stack)
    • Added comprehensive tests for EvmRpcClient.getLogs() covering:
      • Event parameter decoding with indexed/non-indexed fields
      • Hex field parsing (blockNumber, transactionIndex, logIndex)
      • Null params when no registered signature matches
      • JSON-RPC error propagation

Implementation Details

  • Hex quantity parsing uses existing parse_hex_u64 utility and validates against i64::MAX
  • Event decoding runs on a tokio blocking task to avoid blocking the async runtime
  • Topic filter format matches the flattened structure from JavaScript (null for any, Some(vec) for specific values)
  • Decoded params are keyed by contract name, allowing callers to route by address then select the appropriate contract's params

https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g

Summary by CodeRabbit

  • New Features

    • Added EVM log retrieval via the RPC client, with block range plus topic/address filtering and ABI-decoded event parameters returned per log.
    • Updated client initialization to accept event parameter definitions and optional checksum-address behavior.
  • Bug Fixes

    • Improved decoding flow and surfaced JSON-RPC errors and decoding failures more consistently during log fetching.
  • Refactor

    • Reworked the public decoder/addon bindings to use separate hypersync vs RPC client interfaces.
  • Tests

    • Expanded scenario coverage for getLogs, decoding correctness, and retry/error behavior using a dedicated mock RPC server.

eth_getLogs now runs through the Rust EvmRpcClient and decodes event
params with the shared DecoderCore, returning logs already paired with
their decoded params. RpcSource no longer converts logs to the HyperSync
shape or runs the JS decoder; block/transaction/receipt enrichment and
the block-interval/error-classification logic stay on the ReScript side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 10 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f04842bf-5f29-47a7-a45b-1f4d0301ea53

📥 Commits

Reviewing files that changed from the base of the PR and between 34d8b35 and 49ce0e0.

📒 Files selected for processing (7)
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/evm_rpc_source/mod.rs
  • packages/envio/src/sources/EvmRpcClient.res
  • packages/envio/src/sources/Rpc.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/helpers/NativeDecoder.res
  • scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res
📝 Walkthrough

Walkthrough

Removes the standalone EvmDecoder and Rpc.getLogs paths, adds get_logs to EvmRpcClient with embedded decoding, updates ReScript bindings and RpcSource to use decoded rpcEventItem results, and rewires tests through MockRpcServer and NativeDecoder helpers.

Changes

ABI decoding consolidated into EvmRpcClient

Layer / File(s) Summary
Remove old decoder exports
packages/cli/src/evm_hypersync_source/decode.rs, packages/cli/src/evm_hypersync_source/mod.rs, packages/cli/src/evm_hypersync_source/types.rs
Deletes the exported EvmDecoder wrapper, its methods, the Event wrapper, and Log::from_simple; makes decode pub(crate) and narrows imports.
Add EvmRpcClient log fetching and decoding
packages/cli/src/evm_rpc_source/mod.rs
Adds GetLogsParams, RpcLog, and RpcEventItem; stores DecoderCore; extends constructor inputs; implements get_logs to fetch eth_getLogs, decode logs, and return decoded items.
Update ReScript bindings and remove old RPC helpers
packages/envio/src/Core.res, packages/envio/src/sources/EvmRpcClient.res, packages/envio/src/sources/HyperSyncClient.res, packages/envio/src/sources/Rpc.res
Replaces the EvmDecoder constructor binding with separate EvmHypersyncClient and EvmRpcClient bindings, adds getLogs typing and constructor wiring, and removes the old HyperSync decoder and Rpc.getLogs surface.
Switch RpcSource to rpcEventItem inputs
packages/envio/src/sources/RpcSource.res
Uses EvmRpcClient.getLogs, carries items with embedded params, constructs the rpc client with allEventParams and checksum mode, and updates reorg tracking to iterate over items.
Add HTTP mock server and native decoder helper
scenarios/test_codegen/test/helpers/MockRpcServer.res, scenarios/test_codegen/test/helpers/NativeDecoder.res
Adds a real HTTP JSON-RPC test server and a helper that decodes log tuples by routing them through EvmRpcClient.getLogs.
Update decoder and RPC tests
scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res, scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res, scenarios/test_codegen/test/lib_tests/RenamedEventDecode_test.res, scenarios/test_codegen/test/lib_tests/SameSignatureEventDecode_test.res, scenarios/test_codegen/test/lib_tests/Rpc_Test.res, scenarios/test_codegen/test/RpcSource_test.res
Migrates mocked RPC tests to MockRpcServer, adds getLogs coverage, and rewrites decoder fixtures to use NativeDecoder.decodeLogs with tuple-based logs.

Sequence Diagram(s)

sequenceDiagram
  participant RpcSource
  participant EvmRpcClient
  participant JsonRpcClient
  participant DecoderCore
  RpcSource->>EvmRpcClient: getLogs(GetLogsParams)
  EvmRpcClient->>JsonRpcClient: eth_getLogs(filter)
  JsonRpcClient-->>EvmRpcClient: RawLog list
  EvmRpcClient->>DecoderCore: spawn_blocking decode each log
  DecoderCore-->>EvmRpcClient: decoded params
  EvmRpcClient-->>RpcSource: RpcEventItem list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • enviodev/hyperindex#1246: Refactors ABI decoding around the native decoder/DecoderCore path and removes the standalone decoder-style flow.
  • enviodev/hyperindex#1286: Changes the native decoder parameter/result shapes that this RPC decoding path consumes.
  • enviodev/hyperindex#1335: Also updates packages/envio/src/sources/RpcSource.res in the RPC log processing path.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: native eth_getLogs support in EvmRpcClient with event decoding.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

eth_getLogs now runs through the Rust client, so drop the unused ReScript
route, request/result schemas, and Rpc.getLogs, along with the live tests
that exercised them. The ReScript↔Rust boundary is covered by
EvmRpcClient_test's getLogs cases (decode, no-match, error round-trip).

Kept the GetLogs.log type and topic-query helpers, still used by the
source's field parsing and selection config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g
Both data sources now decode internally (HyperSync via get_event_items,
RPC via get_logs), so the EvmDecoder napi class and its ReScript binding
no longer have a production caller. Drop it, along with the now-orphaned
Event type and Log::from_simple converter on the Rust side and the
ResponseTypes.log/event shapes on the ReScript side; DecoderCore stays.

The decoder unit tests (renamed events #1285, same-signature contracts,
tuple/struct/empty params, the #1353 raw_events regression) now decode
through EvmRpcClient.getLogs against a mock server via a shared
NativeDecoder helper, preserving their coverage. The per-file mock
servers are unified into a MockRpcServer test helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res (1)

108-161: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the outgoing eth_getLogs payload in this happy-path test.

Right now this only proves response parsing/decoding. Because the mock ignores the request body, a regression in filter serialization (fromBlock/toBlock hex encoding or topics nesting) would still pass. Adding mock.requests to the expectation would lock down the new Rust request contract too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res` around lines 108
- 161, This test only verifies response decoding and does not assert the
outgoing eth_getLogs request shape, so a regression in filter serialization
could slip through. Update EvmRpcClient_test.res in the Async.it("Decodes event
params and parses hex log fields") case to also inspect mock.requests after
client.getLogs, and assert the serialized request body includes the correct
fromBlock/toBlock hex values and topics nesting. Use the existing
EvmRpcClient.make and MockRpcServer.makeRaw setup to verify the request contract
alongside the current response assertions.
scenarios/test_codegen/test/helpers/MockRpcServer.res (1)

58-76: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Echo the incoming JSON-RPC id here.

make() always returns "id": 1, so this helper won't catch regressions if the client starts varying ids or pipelines multiple requests. Reflecting the request id back keeps the mock aligned with the actual JSON-RPC contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scenarios/test_codegen/test/helpers/MockRpcServer.res` around lines 58 - 76,
The mock response in MockRpcServer.start currently hardcodes the JSON-RPC id, so
it should echo the incoming request id instead. Update the request handler to
parse the "id" from requestBody alongside "method", then pass that parsed id
into the JSON.Object built for the response in start so the mock reflects the
actual JSON-RPC contract. Keep getResult(method) unchanged and use the existing
start/~handler flow to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/evm_rpc_source/mod.rs`:
- Around line 151-160: The `RpcSource` event decoding path is hiding real ABI
decode failures by collapsing `decoder.decode_napi(...)` with `.ok().flatten()`,
so malformed topics/data or stale metadata look the same as an unmatched event.
Update the logic in `evm_rpc_source::mod` inside the `spawn_blocking` mapping so
`decode_napi` errors are preserved and surfaced separately from the “no match”
case, using the `params` construction around `RpcEventItem` to distinguish
successful decode, no decode result, and actual decode failure.
- Around line 135-143: The get_logs method is hex-encoding from_block and
to_block before validating them, which allows negative i64 values to turn into
unintended two’s-complement block numbers. In get_logs, reject any negative
params.from_block or params.to_block up front and return an error before
constructing the JSON filter, so only valid non-negative block bounds reach
eth_getLogs.

---

Nitpick comments:
In `@scenarios/test_codegen/test/helpers/MockRpcServer.res`:
- Around line 58-76: The mock response in MockRpcServer.start currently
hardcodes the JSON-RPC id, so it should echo the incoming request id instead.
Update the request handler to parse the "id" from requestBody alongside
"method", then pass that parsed id into the JSON.Object built for the response
in start so the mock reflects the actual JSON-RPC contract. Keep
getResult(method) unchanged and use the existing start/~handler flow to locate
the fix.

In `@scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res`:
- Around line 108-161: This test only verifies response decoding and does not
assert the outgoing eth_getLogs request shape, so a regression in filter
serialization could slip through. Update EvmRpcClient_test.res in the
Async.it("Decodes event params and parses hex log fields") case to also inspect
mock.requests after client.getLogs, and assert the serialized request body
includes the correct fromBlock/toBlock hex values and topics nesting. Use the
existing EvmRpcClient.make and MockRpcServer.makeRaw setup to verify the request
contract alongside the current response assertions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2431ba46-8f0e-4512-aa47-2de0c98bc728

📥 Commits

Reviewing files that changed from the base of the PR and between 0d4e4e6 and b88cacb.

📒 Files selected for processing (17)
  • packages/cli/src/evm_hypersync_source/decode.rs
  • packages/cli/src/evm_hypersync_source/mod.rs
  • packages/cli/src/evm_hypersync_source/types.rs
  • packages/cli/src/evm_rpc_source/mod.rs
  • packages/envio/src/Core.res
  • packages/envio/src/sources/EvmRpcClient.res
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/Rpc.res
  • packages/envio/src/sources/RpcSource.res
  • scenarios/test_codegen/test/RpcSource_test.res
  • scenarios/test_codegen/test/helpers/MockRpcServer.res
  • scenarios/test_codegen/test/helpers/NativeDecoder.res
  • scenarios/test_codegen/test/lib_tests/EvmRpcClient_test.res
  • scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res
  • scenarios/test_codegen/test/lib_tests/RenamedEventDecode_test.res
  • scenarios/test_codegen/test/lib_tests/Rpc_Test.res
  • scenarios/test_codegen/test/lib_tests/SameSignatureEventDecode_test.res
💤 Files with no reviewable changes (5)
  • scenarios/test_codegen/test/lib_tests/Rpc_Test.res
  • packages/cli/src/evm_hypersync_source/types.rs
  • packages/envio/src/Core.res
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/Rpc.res

Comment thread packages/cli/src/evm_rpc_source/mod.rs
Comment on lines +151 to +160
let decoder = self.decoder.clone();
// Decoding is CPU-bound ABI work; keep it off the libuv async thread.
tokio::task::spawn_blocking(move || {
raw_logs
.into_iter()
.map(|raw| {
let params = decoder.decode_napi(&raw.to_decoder_log()).ok().flatten();
Ok(RpcEventItem {
log: raw.into_rpc_log()?,
params,

@coderabbitai coderabbitai Bot Jun 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Don’t collapse decode errors into “no match.”

Line 157 drops every decode_napi failure with .ok().flatten(). For a registered event with malformed topics/data or stale ABI metadata, params becomes None, and downstream RpcSource will treat that the same as an unmatched signature and skip the event. Please surface decode failures separately instead of silently discarding them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/evm_rpc_source/mod.rs` around lines 151 - 160, The
`RpcSource` event decoding path is hiding real ABI decode failures by collapsing
`decoder.decode_napi(...)` with `.ok().flatten()`, so malformed topics/data or
stale metadata look the same as an unmatched event. Update the logic in
`evm_rpc_source::mod` inside the `spawn_blocking` mapping so `decode_napi`
errors are preserved and surfaced separately from the “no match” case, using the
`params` construction around `RpcEventItem` to distinguish successful decode, no
decode result, and actual decode failure.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is currently intentional: it mirrors the HyperSync decoder's existing behavior (its decode_logs also used .ok().flatten()), so RPC and HyperSync now treat a genuine decode failure the same way — drop the event rather than fail the batch. The prior RPC-only path raised a non-retryable ImpossibleForTheQuery ("check your ABI"). Whether to restore that louder signal (and reintroduce the RPC/HyperSync divergence) is a deliberate design call I've flagged to the maintainer; leaving this thread open pending that decision.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DZakh Thanks for the clarification — that makes sense.

Keeping decode failures collapsed to None in packages/cli/src/evm_rpc_source/mod.rs for parity with the existing HyperSync path is a reasonable intentional tradeoff, and I agree this is better treated as a maintainer/product decision than a must-fix within this PR. Happy to leave the thread open for that call rather than push further here.

(ᵔᴥᵔ)੭ 🐇


✏️ Learnings added
Learnt from: DZakh
Repo: enviodev/hyperindex PR: 1360
File: packages/cli/src/evm_rpc_source/mod.rs:160-169
Timestamp: 2026-06-29T14:54:40.926Z
Learning: In the Rust/ReScript EVM log decoding flow for this repository, `packages/cli/src/evm_rpc_source/mod.rs` `EvmRpcClient::get_logs` intentionally collapses `DecoderCore::decode_napi` failures to `None` to mirror the existing HyperSync decoder behavior, so both RPC and HyperSync drop undecodable events instead of failing the batch. The older RPC-only path surfaced these as a non-retryable `ImpossibleForTheQuery` error, and changing back would be an explicit maintainer decision because it reintroduces RPC/HyperSync behavioral divergence.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

- get_logs rejects negative from_block/to_block before hex-encoding, so a
  bad bound can't become a two's-complement quantity that widens the range.
- The getLogs decode test now also asserts the outgoing eth_getLogs request
  body (hex block bounds + topic nesting), locking the request contract.
- MockRpcServer.make echoes the request's JSON-RPC id instead of hardcoding 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g
- RpcLog no longer carries `data` (consumed only by the Rust decoder) or
  `removed` (unused on the ReScript side), so neither crosses the napi
  boundary. RawLog keeps `data` for decoding and drops `removed`.
- EvmRpcClient::new takes checksum_addresses as a required bool instead of
  Option<bool>; the ReScript make/binding now require ~checksumAddresses so
  callers pick decode-time address casing explicitly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014HcYHxx8g1htYJS1G8Wq4g
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants