Skip to content

Fix same-signature event decoding across contracts with different param names#1286

Merged
DZakh merged 3 commits into
mainfrom
claude/brave-mendel-6ZCNd
Jun 5, 2026
Merged

Fix same-signature event decoding across contracts with different param names#1286
DZakh merged 3 commits into
mainfrom
claude/brave-mendel-6ZCNd

Conversation

@DZakh
Copy link
Copy Markdown
Member

@DZakh DZakh commented Jun 5, 2026

Problem

When two contracts emit the same-signature event but name its params differently (e.g. OpenZeppelin Transfer(from, to, value) vs WETH Transfer(src, dst, wad)), the second contract's handler reads undefined for its params.

EvmChain.collectEventParams built the native decoder's input by deduping on (sighash, topicCount), first-contract-wins. Only the first contract's param metadata reached the decoder, so every matching log — including the second contract's — decoded under the first contract's names. The router still resolved the correct contract by address, but the params object already had the wrong keys baked in.

Worked on 3.0.0-alpha.20 (per-contract decoding via convertHyperSyncEventArgs); regressed in 3.1.0 (PR #1235) when naming moved into the native decoder keyed only by (sighash, topicCount).

Fix

The native decoder now returns params keyed by contract name (paramsByContractName):

  • The expensive inner ABI decode stays single — keyed by signature, run once per log.
  • Each contract sharing that signature re-applies its own names (including nested struct field names) over the shared positional values — cheap, and N=1 in the common case.
  • collectEventParams no longer dedupes; it emits one entry per contract event with a contractName.
  • After the router resolves a log to its contract by address, both HyperSyncSource and RpcSource pick params[contractName].

No new dynamic state, no rollback coupling, no indexingAddresses plumbing — routing stays in JS, the decoder stays static.

Rust cleanup

param_meta: HashMap<MetaKey, Vec<ParamMeta>>variants: HashMap<MetaKey, Vec<EventVariant>> with a named MetaKey struct (both parse and from_topics constructors) and an extracted apply_names helper shared across variants.

Tests

  • New high-level reproduction SameSignatureEventDecode_test.res drives the full path (collectEventParams → native decoder) and asserts each contract decodes under its own names.
  • Updated existing decoder/client tests for the by-contract shape.
  • Rust unit tests, all 301 ReScript lib/source tests, cargo fmt, and tsc --noEmit pass.

Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Event decoding now returns decoded parameters keyed by contract name so identically-signed events preserve each contract's own parameter names.
    • Event input now includes contract identity so per-contract parameter mappings are retained.
  • Behavior Change

    • Event collection no longer deduplicates by signature/topic count; all matching event entries are preserved.
  • Tests

    • Updated tests and fixtures to validate multi-contract decoding and the new contract-keyed decoding shape.

claude added 2 commits June 5, 2026 13:29
Two contracts emitting the same-signature event with differently named
params share one native decoder entry (collectEventParams dedupes by
sighash+topicCount, first-contract-wins). The second contract's events
then decode under the first contract's param names, so its handler reads
undefined.

https://claude.ai/code/session_01MyjtCSDfE2XybnkeEa9q9y
Two contracts emitting the same-signature event with differently named
params shared one native decoder entry: collectEventParams deduped by
(sighash, topicCount) first-contract-wins, so the second contract's events
decoded under the first contract's names and its handler read undefined.

The native decoder now returns params keyed by contract name. The inner
ABI decode stays single (keyed by signature); each contract sharing the
signature re-applies its own names over the shared positional values. After
the router resolves a log to its contract by address, the source picks
params[contractName].

https://claude.ai/code/session_01MyjtCSDfE2XybnkeEa9q9y
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 29ea1732-0a1c-40d6-b79d-d615a79e97e8

📥 Commits

Reviewing files that changed from the base of the PR and between 868bfc6 and f6e4a7e.

📒 Files selected for processing (3)
  • packages/cli/src/hypersync_source/decode.rs
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/envio/src/sources/RpcSource.res
  • packages/envio/src/sources/HyperSyncSource.res

📝 Walkthrough

Walkthrough

This PR refactors the Rust decoder to support multiple contract-specific naming variants for identical event signatures and propagates contract-name-keyed decoded params through collection, types, routing, and tests.

Changes

Multi-contract event decoding with per-contract parameter naming

Layer / File(s) Summary
Decode core refactoring
packages/cli/src/hypersync_source/decode.rs
MetaKey struct with parse/from_topics, EventVariant added, and DecoderCore now stores variants: MetaKey -> Vec<EventVariant>; decoding decodes once and applies per-variant names to produce dict<contractName, params>.
Input type extension for contract names
packages/cli/src/hypersync_source/types.rs
EventParamsInput gains contract_name: String to identify which contract an event params entry belongs to.
Event parameter collection changes
packages/envio/src/sources/EvmChain.res
collectEventParams no longer deduplicates by sighash+topicCount; it appends each contract's eventParams entry so multiple contracts with the same signature are collected.
HyperSync type signature updates
packages/envio/src/sources/HyperSyncClient.res
Decoder.eventParamsInput includes contractName; decodeLogs returns Promise<array<Nullable.t<dict<Internal.eventParams>>>>; EventItems.item.params uses nullable dict shape.
Event routing implementation
packages/envio/src/sources/HyperSyncSource.res, packages/envio/src/sources/RpcSource.res
Routing now looks up decoded params by eventConfig.contractName from the returned dict; missing per-contract params route to handleDecodeFailure.
Test updates and validation
scenarios/test_codegen/test/HyperSyncClient_test.res, scenarios/test_codegen/test/SourceBlockHashes_test.res, scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res, scenarios/test_codegen/test/lib_tests/SameSignatureEventDecode_test.res
Fixtures updated to include contractName; assertions updated to extract per-contract params via dict lookup; new test validates same-signature events decode to distinct per-contract param names.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI Rust
  participant Rescript as ReScript types
  participant Collector as EvmChain.collectEventParams
  participant Decoder as DecoderCore
  participant Router as HyperSyncSource / RpcSource
  participant Queue as makeEventBatchQueueItem / Internal.Event

  CLI->>Collector: supply eventParamsInput entries (with contractName)
  Collector->>Rescript: eventParamsInput list
  Rescript->>Decoder: build Decoder variants map from params
  Decoder->>Decoder: decode log once (inner decoder)
  Decoder->>Decoder: apply_names per EventVariant -> dict<contractName, params>
  Decoder-->>Router: return dict<contractName, params>
  Router->>Router: lookup dict[eventConfig.contractName]
  Router->>Queue: construct queue/event with contract-specific params
Loading

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing same-signature event decoding across contracts with different parameter names. It directly corresponds to the PR's core objective of supporting contract-specific naming variants for identical decoded signatures.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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/hypersync_source/decode.rs`:
- Around line 74-88: The current grouping uses MetaKey (sighash + topic_count)
to pick a single decoder signature via reconstruct_signature, which incorrectly
collapses events that differ only by which params are indexed; change the
grouping key used for signatures so it includes an indexed-layout fingerprint
(e.g., a bitmap derived from ep.params.iter().map(|p| p.indexed) or similar)
instead of or in addition to MetaKey: update the signatures HashMap to use a
composite key (MetaKey + indexed bitmap) or compute/compare the indexed bitmap
before reusing a signature, ensure variants still group by MetaKey (for naming
variants) but decoders are chosen by the composite key, and preserve use of
reconstruct_signature and apply_names when creating/assigning signatures.

In `@packages/envio/src/sources/HyperSyncSource.res`:
- Around line 355-363: The code currently calls Dict.getUnsafe on
Value(paramsByContractName) with eventConfig.contractName inside the
parsedQueueItems -> Array.push call, which will throw if the contract key is
missing; update the mapping so you first safely look up the contract params (use
Dict.get or pattern-match Value(paramsByContractName) to extract the dict and
then Dict.get) and only call makeEventBatchQueueItem when the key exists,
otherwise route the item into the decode-failure handling path (e.g., push a
failure queue item or log/return an error) so decoding errors are preserved;
locate the lookup near parsedQueueItems, Dict.getUnsafe,
eventConfig.contractName and makeEventBatchQueueItem to implement this guard.

In `@packages/envio/src/sources/RpcSource.res`:
- Around line 1136-1138: The code currently uses
Dict.getUnsafe(eventConfig.contractName) in the Value(paramsByContractName)
branch which can throw if the contract key is missing; change this to a safe
lookup using Dict.get and pattern-match the option (e.g., switch on None/Some)
so a missing key yields a safe None/skip for that event rather than throwing;
update the surrounding logic in the Value(...) handling (where decoded is used)
to propagate None or handle the miss gracefully instead of relying on getUnsafe.
🪄 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: ec65d69d-13ff-4c5b-933f-cffc98ac63d3

📥 Commits

Reviewing files that changed from the base of the PR and between 2afcb2f and 868bfc6.

📒 Files selected for processing (10)
  • packages/cli/src/hypersync_source/decode.rs
  • packages/cli/src/hypersync_source/types.rs
  • packages/envio/src/sources/EvmChain.res
  • packages/envio/src/sources/HyperSyncClient.res
  • packages/envio/src/sources/HyperSyncSource.res
  • packages/envio/src/sources/RpcSource.res
  • scenarios/test_codegen/test/HyperSyncClient_test.res
  • scenarios/test_codegen/test/SourceBlockHashes_test.res
  • scenarios/test_codegen/test/lib_tests/HyperSyncDecoder_test.res
  • scenarios/test_codegen/test/lib_tests/SameSignatureEventDecode_test.res

Comment thread packages/cli/src/hypersync_source/decode.rs
Comment thread packages/envio/src/sources/HyperSyncSource.res Outdated
Comment thread packages/envio/src/sources/RpcSource.res Outdated
Replace Dict.getUnsafe with a safe lookup at both source pick sites. A
missing contract key now folds into each source's existing decode-miss
path (HyperSyncSource raises via handleDecodeFailure for non-wildcard
events; RpcSource skips) instead of silently returning undefined params.

Document why decoder signatures are keyed by MetaKey alone: the upstream
decoder collapses by (topic0, topic count) too, so an indexed-layout
fingerprint can't be distinguished at this layer regardless.

https://claude.ai/code/session_01MyjtCSDfE2XybnkeEa9q9y
@DZakh DZakh enabled auto-merge (squash) June 5, 2026 15:11
@DZakh DZakh merged commit ea4e258 into main Jun 5, 2026
8 checks passed
@DZakh DZakh deleted the claude/brave-mendel-6ZCNd branch June 5, 2026 15:15
@DZakh DZakh restored the claude/brave-mendel-6ZCNd branch June 5, 2026 15:17
@DZakh DZakh deleted the claude/brave-mendel-6ZCNd branch June 5, 2026 15:18
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