Skip to content

Add full pending-transaction subscription support (closes #14)#53

Merged
koko1123 merged 3 commits into
mainfrom
feat/pending-tx-full
Jun 10, 2026
Merged

Add full pending-transaction subscription support (closes #14)#53
koko1123 merged 3 commits into
mainfrom
feat/pending-tx-full

Conversation

@koko1123

@koko1123 koko1123 commented May 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements geth-style eth_subscribe(\"newPendingTransactions\", true) so MEV bots can read pending transactions as full RpcTransaction objects instead of just hashes -- saves the round-trip eth_getTransactionByHash call per pending tx.

API changes

  • SubscriptionParams.new_pending_transactions changes from void to PendingTxParams { full: bool = false }. Slight API break (no public consumers yet -- the WsClient was just merged in Add WsClient: resilient WebSocket subscriptions with auto-reconnect #51). Migration:
    - .{ .new_pending_transactions = {} }
    + .{ .new_pending_transactions = .{} }                  // hashes only (default)
    + .{ .new_pending_transactions = .{ .full = true } }    // full Transaction objects
  • buildSubscribeParams emits [\"newPendingTransactions\"] or [\"newPendingTransactions\", true].

New types and parsers

  • src/rpc_transaction.zig -- RpcTransaction flat struct covering all four tx types (legacy / EIP-2930 / EIP-1559 / EIP-4844). Distinct from the canonical signing-time Transaction union; captures the RPC-only fields (hash, recovered from, optional block-position fields). v1 omits access_list and blob_versioned_hashes array parsing -- both are rare in mempool sniping and can be added later without a breaking change.
  • provider.parseSingleTransaction(allocator, obj) -- mirrors parseSingleLog. Handles both input and the legacy data alias. Caller owns tx.input; release with rpc_transaction.freeRpcTransaction.
  • subscription.parseTransactionFromNotification(allocator, raw) -- wraps the above for use with WsClient.next() events.

Test plan

  • make ci passes (1300+ unit tests)
  • zig build integration-test passes against Anvil: 19/19 (one new test sends a tx, then verifies the streamed RpcTransaction matches by hash, recipient, value, and pending state)
  • coderabbit review --prompt-only --type committed --base origin/main: no findings

Out of scope

  • access_list and blob_versioned_hashes array parsing (additive follow-up)
  • A typed client.nextPendingTx() convenience helper -- the existing parseTransactionFromNotification is enough; we can add sugar after we see real usage patterns
  • Pending-tx filter helpers (to/from/value-threshold). Issue Pending transaction subscription (newPendingTransactions full tx) #14 mentions these as a follow-up

Closes #14.

Summary by CodeRabbit

  • New Features
    • WebSocket subscriptions can deliver full pending transaction payloads; notifications are parsed into a structured transaction type that accepts legacy and modern fee formats and normalizes calldata.
  • Bug Fixes
    • Reconnect/resubscribe now preserves original pending-transaction parameters.
  • Tests
    • Added unit and integration tests for parsing, notification handling, pending/full variants, and memory cleanup.

@vercel

vercel Bot commented May 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eth-zig Ready Ready Preview, Comment Jun 10, 2026 12:41am

Request Review

@coderabbitai

coderabbitai Bot commented May 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c0783f99-127d-4ae0-b1e4-2ca0c55963dd

📥 Commits

Reviewing files that changed from the base of the PR and between e8f0c8b and 0748b09.

📒 Files selected for processing (1)
  • src/provider.zig
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/provider.zig

📝 Walkthrough

Walkthrough

Adds end-to-end support for subscribing to full pending transactions: new RpcTransaction type, provider parsing for single transactions, subscription-notification parsing, and tests covering parsing and notification handling.

Changes

Full pending transaction parsing

Layer / File(s) Summary
Provider import & parseSingleTransaction
src/provider.zig
Adds rpc_transaction_mod import and pub fn parseSingleTransaction(allocator, obj) !rpc_transaction_mod.RpcTransaction that decodes transaction JSON (fees legacy/EIP-1559/EIP-4844, input/data alias, v/yParity, optional block/position/chainId) and allocates input bytes with proper cleanup.
Provider parse tests
src/provider.zig (tests)
Unit tests for pending EIP-1559, mined legacy, contract creation (to: null), data alias fallback, yParity handling, invalid-field rejection, and end-to-end eth_subscription pending-transaction notification parsing.
sequenceDiagram
    actor User
    participant WS as "WsClient"
    participant Sub as "SubscriptionParser"
    participant Prov as "Provider.parseSingleTransaction"
    participant Type as "RpcTransaction"

    User->>WS: subscribePendingTransactions(full=true)
    WS->>WS: send eth_subscribe("newPendingTransactions", true)
    Note over WS: Node emits subscription notifications
    WS->>Sub: receive raw notification (params.result)
    Sub->>Prov: parseSingleTransaction(raw_obj)
    Prov->>Type: allocate & populate RpcTransaction
    Type-->>Prov: RpcTransaction
    Prov-->>Sub: RpcTransaction
    Sub-->>WS: parsed RpcTransaction
    WS-->>User: deliver RpcTransaction
    User->>Type: freeRpcTransaction(allocator, tx)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble mempool crumbs and hop with glee,
Full txs arrive — not just a hash to see,
Bytes decoded, fees and sigs in tow,
I free the input when the bunnies go,
Hooray for websockets streaming mempool flow!

🚥 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 Title clearly and concisely summarizes the main objective: adding support for full pending-transaction subscriptions, with a direct reference to the issue it closes.
Linked Issues check ✅ Passed PR implements all core coding requirements from #14: eth_subscribe support for full pending transactions, RpcTransaction parsing from notifications, WebSocket integration, and elimination of per-tx round-trips.
Out of Scope Changes check ✅ Passed All changes align with #14 objectives. New modules (rpc_transaction.zig, provider parsing, subscription notification parsing) and API updates (SubscriptionParams structure) directly support full pending-tx subscriptions as specified.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/pending-tx-full

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

@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: 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 `@tests/integration_tests.zig`:
- Around line 444-455: The test currently assumes subscription notifications
contain full tx objects and calls
eth.subscription.parseTransactionFromNotification which fails for hash-only
notifications; update the test around client.next()/ev.payload to detect and
skip hash-only pending notifications (e.g., inspect ev.payload for a JSON
string/hash or catch the parse error) so that when the node emits a plain tx
hash you continue the loop instead of failing; keep the existing defer
eth.rpc_transaction.freeRpcTransaction(allocator, tx) for the successful parse
path and only call it when parseTransactionFromNotification returns a non-error
tx.
🪄 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: d5334343-fd66-434f-909e-a53ff4a7cc8c

📥 Commits

Reviewing files that changed from the base of the PR and between 6cf28b5 and 5ce6c60.

📒 Files selected for processing (7)
  • README.md
  • src/provider.zig
  • src/root.zig
  • src/rpc_transaction.zig
  • src/subscription.zig
  • src/ws_client.zig
  • tests/integration_tests.zig

Comment thread tests/integration_tests.zig
koko1123 added 2 commits May 1, 2026 16:20
Implements the geth-style eth_subscribe("newPendingTransactions", true)
variant so MEV bots can read pending transactions as full Transaction
objects without the extra eth_getTransactionByHash round-trip per tx.

API changes:
- SubscriptionParams.new_pending_transactions changes from `void` to
  `PendingTxParams { full: bool = false }`. Callers writing
  `.{ .new_pending_transactions = {} }` must update to
  `.{ .new_pending_transactions = .{} }`.
- buildSubscribeParams emits `["newPendingTransactions"]` (hashes only)
  or `["newPendingTransactions", true]` (full tx).

New types and parsers:
- src/rpc_transaction.zig: RpcTransaction flat struct covering all four
  transaction types (legacy, EIP-2930, EIP-1559, EIP-4844). Captures the
  RPC-only fields (hash, recovered from, optional block-position fields)
  that the canonical signing-time Transaction union does not. v1 omits
  access_list and blob_versioned_hashes parsing -- both are rare in
  mempool sniping and can be added without a breaking change.
- provider.parseSingleTransaction(allocator, obj) -> RpcTransaction.
  Mirrors parseSingleLog. Handles both `input` and the legacy `data`
  alias. Caller owns the input slice; release with
  rpc_transaction.freeRpcTransaction.
- subscription.parseTransactionFromNotification(allocator, raw) wraps
  the above for use with WsClient.next() events.

Tests:
- 5 new unit tests in provider.zig (pending EIP-1559, mined legacy,
  contract creation, data alias, full notification round-trip).
- 2 new struct/free helper tests in rpc_transaction.zig.
- 1 new buildSubscribeParams test for the `full` variant.
- 1 new integration test against Anvil that subscribes, sends a tx via
  the wallet, and verifies the parsed RpcTransaction matches what was
  sent (hash, recipient, value, pending state).

All 1300+ unit tests pass; 19/19 integration tests pass against Anvil.

Closes #14.
Some nodes (and even some Anvil builds) ignore the `full = true`
parameter on `eth_subscribe("newPendingTransactions", true)` and stream
plain transaction hashes (JSON strings) instead of full objects. The
integration test previously called parseTransactionFromNotification
unconditionally, which would error on those payloads and surface as
test failure rather than the intended skip-and-keep-reading behavior.

Catch error.InvalidNotification from the parser and `continue` so the
loop drains until either the matching tx is observed or the attempt
budget is exhausted (which already returns early).

@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: 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 `@src/provider.zig`:
- Around line 851-876: The code currently silently coerces malformed fields
(tx_index via parseHexU32, type_val via parseHexU8 with catch 0, and v via
parseHexU256 with orelse "0x0"), which hides invalid RPC payloads; update the
parsing of transactionIndex (tx_index) to propagate failures instead of mapping
parseHexU32 errors to null (use try or orelse return error.InvalidResponse),
change type parsing (type_val) to return error.InvalidResponse on parse failures
rather than catch 0, and require v to be present/parsable by replacing the
orelse "0x0" with either try or orelse return error.InvalidResponse; use
jsonGetString, parseHexU32, parseHexU8, parseHexU256 and error.InvalidResponse
to locate and fix these spots so malformed fields cause immediate
error.InvalidResponse instead of silent coercion.
🪄 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: 886bcce9-7736-4719-bcd0-3932e9193620

📥 Commits

Reviewing files that changed from the base of the PR and between 5ce6c60 and e8f0c8b.

📒 Files selected for processing (7)
  • README.md
  • src/provider.zig
  • src/root.zig
  • src/rpc_transaction.zig
  • src/subscription.zig
  • src/ws_client.zig
  • tests/integration_tests.zig
✅ Files skipped from review due to trivial changes (1)
  • src/root.zig
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/ws_client.zig
  • tests/integration_tests.zig
  • src/rpc_transaction.zig
  • src/subscription.zig

Comment thread src/provider.zig
Propagate parse errors for transactionIndex and type instead of
coercing to null/0, and require a v (or spec-canonical yParity)
signature field instead of defaulting to 0x0.
@koko1123
koko1123 merged commit 46f8dfc into main Jun 10, 2026
12 of 14 checks passed
@koko1123
koko1123 deleted the feat/pending-tx-full branch June 10, 2026 00:49
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.

Pending transaction subscription (newPendingTransactions full tx)

1 participant