Skip to content

feat: read a transaction's decoded input#164

Merged
MicBun merged 4 commits into
mainfrom
feat/decode-transaction-payload
Jul 6, 2026
Merged

feat: read a transaction's decoded input#164
MicBun merged 4 commits into
mainfrom
feat/decode-transaction-payload

Conversation

@MicBun

@MicBun MicBun commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

resolves: https://github.com/trufnetwork/trufscan/issues/168

What this does

Adds a way to read what an execute transaction actually did — the action it
called and its arguments (the deployed stream IDs from a deploy, the records
from an insert) — from a transaction hash or a raw payload. Previously only
attestations exposed their decoded contents; a deploy or insert surfaced only
an opaque length-prefixed payload from txInfo.

  • Bumps @trufnetwork/kwil-js to 0.9.13 and decodes the payload with its
    canonical Utils.decodeActionExecution / Utils.decodeValue, so the wire
    layout has a single source of truth.
  • New decodeTransactionPayload(payload){ namespace, action, arguments },
    each argument decoded to a native value (text→string, integer→bigint, bool,
    bytea→Uint8Array, uuid→string, numeric→string, null). Exported publicly.
  • New TransactionAction.getTransactionInput({ txId }) — fetches via txInfo
    and decodes; complements getTransactionEvent (which reads fee-ledger data).

Usage

const txAction = client.loadTransactionAction();
const { action, arguments: args } = await txAction.getTransactionInput({
  txId: "0xabcdef123456...",
});
// action === "insert_records"; args is one array of native values per batched call

Also fixes (value-decode correctness)

An adversarial review of the shared value decoder (AttestationEncoding, used by
attestation and orderbook decoding) surfaced three confirmed bugs, now fixed with
regression tests:

  • UUID arguments were UTF-8-decoded from their 16 raw bytes (mojibake) instead of
    formatted as a canonical 8-4-4-4-12 string.
  • An array whose first element is SQL NULL collapsed to a scalar null.
  • An empty array decoded to null instead of [].

Tests

  • TransactionPayload.ts inline tests decode a real
    ActionExecution.MarshalBinary payload from kwil-db byte-for-byte, including a
    negative int64.
  • AttestationEncoding.ts gains 6 regression tests round-tripping through the
    kwil-js encoder (uuid, leading/trailing-null arrays, empty array, scalar null).

Full unit suite (276 tests) green against kwil-js 0.9.13; typecheck and build
(ESM/CJS/types) pass.

Related

Summary by CodeRabbit

  • New Features

    • Added support for viewing decoded transaction inputs for action calls, making transaction details easier to inspect.
    • Expanded support for decoding transaction payload data in more environments.
  • Bug Fixes

    • Improved handling of UUID values so they display in standard UUID format.
    • Fixed null handling in arrays and scalar values for more accurate decoded results.
    • Strengthened validation for transaction payloads and unsupported payload formats.

@MicBun MicBun self-assigned this Jul 6, 2026
@holdex

holdex Bot commented Jul 6, 2026

Copy link
Copy Markdown

Time Submission Status

Member # Time Running Total Status Last Update
MicBun 4h 30min ✅ Submitted Jul 6, 2026, 2:54 PM

Submit or update total time with:

@holdex pr submit-time 2h

Add time on top of previous submission with:

@holdex pr add-time 1h30m

See available commands to help comply with our Guidelines.

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@MicBun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: e91aea6e-d03e-49c8-b4a3-18b28552ee30

📥 Commits

Reviewing files that changed from the base of the PR and between 03778f9 and 88ed1a8.

📒 Files selected for processing (5)
  • src/contracts-api/transactionAction.test.ts
  • src/contracts-api/transactionAction.ts
  • src/util/AttestationEncoding.ts
  • src/util/TransactionPayload.test.ts
  • src/util/TransactionPayload.ts
📝 Walkthrough

Walkthrough

This PR bumps the kwil-js dependency, adds a new decodeTransactionPayload utility and TransactionAction.getTransactionInput method for decoding execute-type transaction payloads, re-exports these from internal.ts, and fixes UUID string formatting and per-element NULL handling in decoded array values.

Changes

Transaction Payload Decoding

Layer / File(s) Summary
Payload decoding contract and implementation
src/util/TransactionPayload.ts, src/internal.ts
Adds DecodedTransactionPayload interface and decodeTransactionPayload function decoding execute payloads via Utils.decodeActionExecution/decodeValue, with inline tests, then re-exports both from internal.ts.
getTransactionInput integration
src/contracts-api/transactionAction.ts
Adds base64ToBytes helper and TransactionAction.getTransactionInput, which fetches tx info, validates execute payload type, converts payload bytes, and returns the decoded payload.

UUID Formatting and Array NULL Fidelity

Layer / File(s) Summary
UUID and array decoding fixes
src/util/AttestationEncoding.ts
Adds bytesToUuidString helper used for canonical UUID formatting, refactors decodedValueToJS to handle array elements independently before scalar NULL checks, and extends inline tests for fidelity.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant TransactionAction
  participant KwilClient
  participant TransactionPayload

  Caller->>TransactionAction: getTransactionInput(txId)
  TransactionAction->>KwilClient: txInfo(hash)
  KwilClient-->>TransactionAction: tx response with payload
  TransactionAction->>TransactionAction: base64ToBytes(payload)
  TransactionAction->>TransactionPayload: decodeTransactionPayload(bytes)
  TransactionPayload-->>TransactionAction: DecodedTransactionPayload
  TransactionAction-->>Caller: DecodedTransactionPayload
Loading

Suggested labels: enhancement

Suggested reviewers: outerlook, williamrusdyputra

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main addition: reading a transaction's decoded input.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/decode-transaction-payload

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.

@MicBun

MicBun commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@holdex pr submit-time 4h30m

@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.

🧹 Nitpick comments (3)
src/contracts-api/transactionAction.ts (2)

3-3: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use import type for the type-only import.

DecodedTransactionPayload is only used as a type but is imported alongside the value import. Splitting it out avoids relying on the bundler/tsc to elide the type-only binding under stricter isolatedModules/verbatimModuleSyntax configs.

♻️ Proposed fix
-import { decodeTransactionPayload, DecodedTransactionPayload } from "../util/TransactionPayload";
+import { decodeTransactionPayload } from "../util/TransactionPayload";
+import type { DecodedTransactionPayload } from "../util/TransactionPayload";
🤖 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 `@src/contracts-api/transactionAction.ts` at line 3, The import in
transactionAction should separate the type-only symbol from the runtime value
import. Update the existing import so decodeTransactionPayload remains a normal
import, while DecodedTransactionPayload is brought in with import type to keep
the module compatible with stricter isolatedModules/verbatimModuleSyntax
settings.

253-258: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add tx context when payload decoding fails.

If decodeTransactionPayload throws (truncated/unsupported-version payload), the raised error won't reference which transaction failed, making production troubleshooting harder — especially since this method is designed to be called generically across arbitrary tx hashes.

♻️ Proposed fix
-    return decodeTransactionPayload(payloadBytes);
+    try {
+      return decodeTransactionPayload(payloadBytes);
+    } catch (error) {
+      throw new Error(
+        `Failed to decode transaction input for ${input.txId}: ${(error as Error).message}`
+      );
+    }
🤖 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 `@src/contracts-api/transactionAction.ts` around lines 253 - 258, The payload
decoding path in transactionAction.ts does not add any transaction-specific
context when decodeTransactionPayload fails, so update the decode flow to catch
and rethrow with identifying tx details from the current body/receipt object.
Keep the existing payloadBytes conversion logic, but wrap the
decodeTransactionPayload(payloadBytes) call in a try/catch and include the
transaction hash or other available transaction identifier in the thrown error
message so failures can be traced to the exact tx.
src/util/AttestationEncoding.ts (1)

391-399: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract shared null-flag decode helper.

The item.length === 0 || item[0] === 0 null check is duplicated between the array branch and the scalar branch (with decodeSingleValue(typeName, x.slice(1)) following in both). A small shared helper (e.g. decodeFlaggedValue(typeName, bytes)) would remove the duplication and make future changes to the null-flag convention safer to keep in sync.

♻️ Possible refactor
+function decodeFlaggedValue(typeName: string, item: Uint8Array): any {
+  return item.length === 0 || item[0] === 0 ? null : decodeSingleValue(typeName, item.slice(1));
+}
+
 export function decodedValueToJS(decoded: DecodedEncodedValue): any {
   const typeName = decoded.type.name.toLowerCase();

   if (decoded.type.is_array) {
-    return decoded.data.map((item) =>
-      item.length === 0 || item[0] === 0 ? null : decodeSingleValue(typeName, item.slice(1))
-    );
+    return decoded.data.map((item) => decodeFlaggedValue(typeName, item));
   }

   if (decoded.data.length === 0) {
     return null;
   }
-  const firstItem = decoded.data[0];
-  if (firstItem.length === 0 || firstItem[0] === 0) {
-    return null;
-  }
-
-  return decodeSingleValue(typeName, firstItem.slice(1));
+  return decodeFlaggedValue(typeName, decoded.data[0]);
 }

Also applies to: 401-408

🤖 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 `@src/util/AttestationEncoding.ts` around lines 391 - 399, The null-flag
decoding logic is duplicated in the array handling path and the scalar branch in
AttestationEncoding’s decode flow. Extract the repeated “flagged value” logic
into a shared helper (for example, a helper used by decodeSingleValue and the
array branch) that checks the null marker and then decodes the payload, so both
branches use the same implementation and stay in sync.
🤖 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.

Nitpick comments:
In `@src/contracts-api/transactionAction.ts`:
- Line 3: The import in transactionAction should separate the type-only symbol
from the runtime value import. Update the existing import so
decodeTransactionPayload remains a normal import, while
DecodedTransactionPayload is brought in with import type to keep the module
compatible with stricter isolatedModules/verbatimModuleSyntax settings.
- Around line 253-258: The payload decoding path in transactionAction.ts does
not add any transaction-specific context when decodeTransactionPayload fails, so
update the decode flow to catch and rethrow with identifying tx details from the
current body/receipt object. Keep the existing payloadBytes conversion logic,
but wrap the decodeTransactionPayload(payloadBytes) call in a try/catch and
include the transaction hash or other available transaction identifier in the
thrown error message so failures can be traced to the exact tx.

In `@src/util/AttestationEncoding.ts`:
- Around line 391-399: The null-flag decoding logic is duplicated in the array
handling path and the scalar branch in AttestationEncoding’s decode flow.
Extract the repeated “flagged value” logic into a shared helper (for example, a
helper used by decodeSingleValue and the array branch) that checks the null
marker and then decodes the payload, so both branches use the same
implementation and stay in sync.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9259095a-d3c5-4e9d-a90d-80ac64ef42fc

📥 Commits

Reviewing files that changed from the base of the PR and between aabfb5e and 03778f9.

📒 Files selected for processing (5)
  • package.json
  • src/contracts-api/transactionAction.ts
  • src/internal.ts
  • src/util/AttestationEncoding.ts
  • src/util/TransactionPayload.ts

@MicBun
MicBun merged commit 5172346 into main Jul 6, 2026
4 checks passed
@MicBun
MicBun deleted the feat/decode-transaction-payload branch July 6, 2026 15:13
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.

1 participant