Skip to content

Added output (returndata) decoding APIs to CallDecoder#59

Open
Pratham6392 wants to merge 4 commits intoenviodev:mainfrom
Pratham6392:feat/call-decoder-decode-output
Open

Added output (returndata) decoding APIs to CallDecoder#59
Pratham6392 wants to merge 4 commits intoenviodev:mainfrom
Pratham6392:feat/call-decoder-decode-output

Conversation

@Pratham6392
Copy link
Copy Markdown

@Pratham6392 Pratham6392 commented Apr 2, 2026

Summary

This PR exposes the upstream hypersync_client::CallDecoder::decode_output capability in the Python client by adding PyO3 bindings and Python wrapper methods. It enables users to decode ABI-encoded return data (returndata) from raw hex blobs and from trace outputs.

Key Changes

New Rust bindings in src/decode_call.rs:

  • decode_outputs_sync() / decode_outputs() for batch decoding of raw output hex + signatures
  • decode_traces_output_sync() / decode_traces_output() for decoding Trace.output using per-trace signatures
  • Internal helper decode_output_impl() to mirror the existing decode_impl() input path

Python SDK additions in hypersync/init.py:

  • CallDecoder.decode_outputs() / decode_outputs_sync()
  • CallDecoder.decode_traces_output() / decode_traces_output_sync()

Tests:

  • Adds fake-inner wrapper tests covering correct argument forwarding and return passthrough for the new methods in tests/test_wrappers.py
  • Example:
  • Adds/updates examples/trace_call_watch.py to demonstrate offline decode_outputs_sync usage (and/or trace-based usage if available)

Implementation Details

Mirrors existing async/sync patterns:

  • Async methods use future_into_py + tokio::spawn_blocking to avoid blocking the event loop.
  • Sync methods return List[Optional[List[DecodedSolValue]]] (or Rust equivalent) matching the “decode may fail → None” contract.
    Error behavior matches existing decoder conventions:
  • Malformed hex will error/panic similarly to existing decode helpers (via .unwrap() in Rust helper).
  • Signature mismatch yields None rather than raising, matching upstream behavior.
    No dependency or config changes are required (uses existing hypersync-client API).

This closes a feature gap where Python users can decode calldata (decode_input) but not returndata (decode_output), even though the upstream Rust client already supports it. Decoding outputs is essential for interpreting trace results and eth_call return values in indexing/debugging pipelines, and the implementation follows the established wrapper/binding patterns in this repo.

Summary by CodeRabbit

  • New Features

    • Added output decoding for contract call return data and trace outputs (sync/async).
    • Added a new example script demonstrating trace querying, filtering, and input/output decoding.
  • Bug Fixes

    • Fixed parameter forwarding in input decoding APIs.
    • Corrected synchronous decode return types to allow absent/failed decodes.
    • Restored stream receive methods to return underlying results.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

📝 Walkthrough

Walkthrough

Adds a new example script demonstrating trace call input/output decoding, fixes input-decoding forwarding bugs, introduces async/sync output-decoding APIs in CallDecoder (Python surface), updates stream recv wrappers, and implements Rust-side output-decoding helpers.

Changes

Cohort / File(s) Summary
Example script
examples/trace_call_watch.py
New executable example that builds a HypersyncClient, queries recent trace calls for a USDC balanceOf(address) sighash, decodes inputs/outputs (sync and async paths exercised), filters by target address, and prints transaction/call metadata with decoded values.
Python API wrapper
hypersync/__init__.py
Fixed CallDecoder.decode_inputs/decode_inputs_sync to forward inputs correctly; added decode_outputs/decode_outputs_sync and decode_traces_output/decode_traces_output_sync; updated ArrowStream.recv and EventStream.recv to return inner results.
Rust implementation
src/decode_call.rs
Implemented output-decoding backend: decode_outputs_sync/decode_traces_output_sync, async Python wrappers using spawn_blocking, and decode_output_impl helper to hex-decode and map results to DecodedSolValue (preserving checksummed addresses).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Example as Example Script
  participant Client as HypersyncClient
  participant Envio as Envio API
  participant DecoderPy as CallDecoder (Python)
  participant DecoderRs as Decoder (Rust)

  rect rgba(200,230,201,0.5)
    Example->>Client: build client (ENVIO_API_TOKEN)
    Example->>Client: send Query (trace call filter)
    Client->>Envio: fetch traces
    Envio-->>Client: traces list
    Client-->>Example: QueryResponse (traces)
  end

  rect rgba(187,222,251,0.5)
    Example->>DecoderPy: decode_inputs / decode_traces_input (inputs)
    DecoderPy->>DecoderRs: spawn_blocking decode_inputs
    DecoderRs-->>DecoderPy: decoded inputs
    DecoderPy-->>Example: decoded input results
  end

  rect rgba(255,224,178,0.5)
    Example->>DecoderPy: decode_outputs / decode_traces_output (outputs + signatures)
    DecoderPy->>DecoderRs: spawn_blocking decode_outputs
    DecoderRs-->>DecoderPy: decoded outputs (or None)
    DecoderPy-->>Example: decoded output results
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • enviodev/hypersync-client-python#55: Fixes the same CallDecoder.decode_inputs / decode_inputs_sync argument/typo issues referenced in that issue.

Poem

🐰
I nibbled bytes beneath the moon,
Traces traced and decodes tuned,
Inputs matched and outputs found,
Carrots of logs strewn all around,
Hypersync hops — delighted tune!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding output decoding APIs to CallDecoder. It directly matches the core functionality introduced across all modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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: 4

🧹 Nitpick comments (1)
hypersync/__init__.py (1)

221-237: Return type annotations inconsistent with decode_inputs_sync.

The newly fixed decode_inputs_sync has return type list[Optional[list[DecodedSolValue]]], but these existing methods still use list[list[DecodedSolValue]]. The Rust implementations return Vec<Option<Vec<DecodedSolValue>>> for all of them.

Consider updating the type hints for consistency:

📝 Proposed type annotation fix
     def decode_transactions_input_sync(
         self, txs: list[Transaction]
-    ) -> list[list[DecodedSolValue]]:
+    ) -> list[Optional[list[DecodedSolValue]]]:
         """Parse log and return decoded event. Returns None if topic0 not found."""
         return self.inner.decode_transactions_input_sync(txs)
 
     def decode_traces_input_sync(
         self, traces: list[Trace]
-    ) -> list[list[DecodedSolValue]]:
+    ) -> list[Optional[list[DecodedSolValue]]]:
         """Parse log and return decoded event. Returns None if topic0 not found."""
         return self.inner.decode_traces_input_sync(traces)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hypersync/__init__.py` around lines 221 - 237, Update the return type
annotations for decode_transactions_input_sync, decode_traces_input, and
decode_traces_input_sync to match decode_inputs_sync and the Rust
implementation: change from list[list[DecodedSolValue]] to
list[Optional[list[DecodedSolValue]]]; ensure any docstrings or comments remain
accurate and that callers handle Optional entries accordingly so the Python
signatures align with Vec<Option<Vec<DecodedSolValue>>> semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/trace_call_watch.py`:
- Around line 16-25: The function make_client currently hardcodes the API token;
change it to read from an environment variable (e.g., ENVIO_API_TOKEN) instead
of the literal string, remove the dead-code check caused by the hardcoded value,
and raise a ValueError if the env var is missing; keep the rest of the client
construction (hypersync.HypersyncClient and hypersync.ClientConfig with url and
api_token) unchanged so make_client fails fast when the token is not provided
via the environment.

In `@hypersync/__init__.py`:
- Around line 896-899: The recv method in QueryResponseStream currently awaits
self.inner.recv() but doesn't return its result; change QueryResponseStream.recv
to return await self.inner.recv() (i.e., return the value from
self.inner.recv()) and fix the extra leading space/indentation so the return is
properly aligned within the async def recv(self) -> Optional[QueryResponse]:
block.

In `@src/decode_call.rs`:
- Around line 184-200: The function decode_traces_output_sync currently zips
traces and signatures which silently truncates when their lengths differ; add an
explicit length check at the start of decode_traces_output_sync (e.g.,
assert_eq!(traces.len(), signatures.len(), "mismatched traces and signatures
lengths") or return/raise a clear error) so the mismatch is detected instead of
truncating, then proceed to call decode_output_impl as before; reference
decode_traces_output_sync and decode_output_impl when making the change.
- Around line 171-182: The current decode_outputs_sync uses zip which silently
truncates when outputs and signatures differ; add an explicit length check at
the start of decode_outputs_sync (compare outputs.len() and signatures.len())
and return a clear failure if they differ (e.g., panic! or return an Err if you
change the signature) so mismatches are not silently dropped, then iterate by
index or enumerate and call decode_output_impl(output.as_str(), sig.as_str(),
py) for each pair to preserve full alignment between outputs and signatures.

---

Nitpick comments:
In `@hypersync/__init__.py`:
- Around line 221-237: Update the return type annotations for
decode_transactions_input_sync, decode_traces_input, and
decode_traces_input_sync to match decode_inputs_sync and the Rust
implementation: change from list[list[DecodedSolValue]] to
list[Optional[list[DecodedSolValue]]]; ensure any docstrings or comments remain
accurate and that callers handle Optional entries accordingly so the Python
signatures align with Vec<Option<Vec<DecodedSolValue>>> semantics.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c501c1fc-8ff1-430f-8c73-5a506435d5ba

📥 Commits

Reviewing files that changed from the base of the PR and between cddabcf and 4eae58b.

📒 Files selected for processing (3)
  • examples/trace_call_watch.py
  • hypersync/__init__.py
  • src/decode_call.rs

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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/trace_call_watch.py`:
- Line 71: The code constructs target = "0x" + ADDR.lower() which
double-prefixes when ADDR already starts with "0x"; update the logic that builds
target (the target variable using ADDR) to first normalize ADDR by stripping any
leading "0x"/"0X" and whitespace, then lowercasing and prefixing a single "0x"
(e.g., normalize ADDR before using it where target is created) so valid traces
are not incorrectly filtered; locate the target assignment in
examples/trace_call_watch.py and change it to use the normalized ADDR value.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3afc4a1a-034b-4a69-8345-248e0790a9ac

📥 Commits

Reviewing files that changed from the base of the PR and between 4eae58b and 02627d0.

📒 Files selected for processing (1)
  • examples/trace_call_watch.py

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.

♻️ Duplicate comments (1)
src/decode_call.rs (1)

189-205: ⚠️ Potential issue | 🟡 Minor

Missing length validation for traces and signatures.

Unlike decode_outputs_sync which now validates lengths via assert_eq!, this method still uses zip() without any length check. Mismatched lengths will silently discard trailing elements.

🛡️ Proposed fix to add length validation
 pub fn decode_traces_output_sync(
     &self,
     traces: Vec<Trace>,
     signatures: Vec<String>,
     py: Python,
 ) -> Vec<Option<Vec<DecodedSolValue>>> {
+    assert_eq!(
+        traces.len(),
+        signatures.len(),
+        "traces and signatures must have the same length"
+    );
     traces
         .into_iter()
         .zip(signatures.into_iter())
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/decode_call.rs` around lines 189 - 205, The function
decode_traces_output_sync uses zip on traces and signatures which silently drops
trailing items when lengths differ; add an explicit length check (e.g.,
assert_eq! or a Result-returning check) comparing traces.len() and
signatures.len() at the start of decode_traces_output_sync to ensure they match
(mirroring the validation used in decode_outputs_sync), so that mismatched
lengths are detected instead of silently truncating; keep the rest of the logic
using trace.output and decode_output_impl unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/decode_call.rs`:
- Around line 189-205: The function decode_traces_output_sync uses zip on traces
and signatures which silently drops trailing items when lengths differ; add an
explicit length check (e.g., assert_eq! or a Result-returning check) comparing
traces.len() and signatures.len() at the start of decode_traces_output_sync to
ensure they match (mirroring the validation used in decode_outputs_sync), so
that mismatched lengths are detected instead of silently truncating; keep the
rest of the logic using trace.output and decode_output_impl unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b523dc71-1140-4752-89f5-55a7c5f597a7

📥 Commits

Reviewing files that changed from the base of the PR and between 02627d0 and f4a627c.

📒 Files selected for processing (2)
  • hypersync/__init__.py
  • src/decode_call.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • hypersync/init.py

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