feat(transaction-introspection): add txn introspection package - #1611
Conversation
🦋 Changeset detectedLatest commit: 92c66d6 The changes in this PR will be included in the next version bump. This PR includes changesets to release 48 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
2fd5630 to
c635106
Compare
BundleMonFiles added (3)
Files updated (10)
Unchanged files (137)
Total files change +11.11KB +2.1% Final result: ✅ View report in BundleMon website ➡️ |
mcintyre94
left a comment
There was a problem hiding this comment.
Thanks for updating this! I made a few comments, the most important is that I think there is still a v1 gap in decodeFromJson.
I'll also kick off an AI review to see what I missed
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
This PR adds a new @solana/transaction-introspection package that closes the gap between a getTransaction response and the auto-generated @solana-program/* parseXInstruction / identifyXInstruction clients. It decodes base64/base58/json responses into a CompiledTransactionMessage (+ wire Transaction for the binary encodings), resolves account indices against static + ALT-loaded addresses with proper signer/writable roles, normalises inner instructions from meta.innerInstructions, and exposes walkInstructions to enumerate every outer + inner instruction with a trace recording its location. Each returned item is itself a ResolvedInstruction, so it flows straight into isInstructionForProgram and the auto-generated identify/parse helpers.
It also tidies packages/rpc-api/src/getTransaction.ts by hoisting the four inline non-null response shapes (Base64, Base58, Json, JsonParsed) into named exported types, so the new package can consume them directly without re-deriving them. The underlying overload shapes look unchanged — purely an extraction.
Nice quality bar overall: thorough JSDoc, a real README with a quickstart and per-symbol docs, type tests for both the decodeTransactionFromRpcResponse overload narrowing and TracedInstruction interop with isInstructionForProgram, and unit tests covering legacy/v0/v1 + the JSON path + the ALT-loaded v0 walk. The _exhaustiveCheck: never in normalizeCompiledInstructions is a nice touch — a future CompiledTransactionMessage variant will fail to typecheck rather than silently misbehaving.
Key things to address
Echoing Callum's point — the v1 gap in decodeFromJson is the headline issue and I think it needs to be addressed before this lands. Details inline. A few smaller things alongside it (error-code semantics, .gitignore additions, PR-description / changeset drift). Everything else is healthy.
Notes for subsequent reviewers
- The biggest semantic question is how strict the JSON path should be about transaction versions it does not understand. Today, anything other than
'legacy'is forced to0(rpcTx.version as 0), which silently mis-shapes v1+ responses. Worth deciding before this is published. - Worth a sanity-check on whether
@solana/rpc-apishould appear in the changeset. It now exports four new named types (GetTransactionApiResponseBase58/Base64/Json/JsonParsed). All publishable packages are version-locked viafixed, so the bump will propagate regardless, but the changeset entry as-is doesn't list rpc-api as user-facing; if you'd rather the release notes call out the new exports, the changeset should be expanded. - The
rpc-apiextraction is a structural refactor of a public type. I scanned the diff and the four hoisted aliases look line-for-line equivalent to the previous inline shapes, but worth a second pair of eyes to confirm nothing widens or narrows (especially theTMaxSupportedTransactionVersion extends voidbranches). - Inner-instructions normalisation in
get-inner-instructions.tsreusesSOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUNDfor both program-index and account-index out-of-range cases. Same code is reused inget-instructions.tsfor account indices. See inline. - The PR description mentions
filterInstructionsForProgram, but it isn't insrc/index.tsor the README — the description is stale and the README correctly says you can useisInstructionForProgramdirectly. Worth tidying the description before merge.
4362a6a to
adfa25e
Compare
… transactions Adds @solana/transaction-introspection, a new package that bridges a getTransaction response and the auto-generated @solana-program/* parseXInstruction clients. Decodes responses encoded as base64, base58, or json; resolves account indices against static and ALT-loaded addresses; normalizes inner instructions from meta.innerInstructions; and exposes walkInstructions and filterInstructionsForProgram for streaming traversal of every instruction (outer and inner). Re-exported from @solana/kit. Also hoists the inline GetTransactionApi response shapes in @solana/rpc-api into named exported types (Base64/Base58/Json/JsonParsed) so the new package can consume them directly.
…rim public surface Drops the synthesized Transaction for json responses — the empty-messageBytes fake was a lie and required reconstructing signatures from a parallel array. Now DecodedRpcTransaction.transaction is optional, and the base64/base58 overloads narrow it to a guaranteed Transaction so callers using those encodings get static guarantees. Adds a typetest to lock the narrowing in. Restores lifetimeToken parity across all three encodings (the prior refactor silently dropped it from the json path), and asserts it on both the wire-decoder and json paths so the asymmetry can't regress. Trims the public API: switches src/index.ts from export * to explicit named exports, marks getInstructionsFromCompiledTransactionMessageWithMetas @internal, and updates the README for the walkInnerInstructionsFromMeta rename. docs(transaction-introspection): align decoder docs with optional `transaction` and widen `compiledMessage` to guarantee `lifetimeToken` Updates the docblocks on decodeTransactionFromRpcResponse and DecodedRpcTransaction (and the matching README sections) so they describe the post-refactor shape: `transaction` is omitted for json responses, not empty-bytes; base64/base58 overloads statically guarantee a re-encodable Transaction. Widens DecodedRpcTransaction.compiledMessage to `CompiledTransactionMessage & CompiledTransactionMessageWithLifetime` — every path now sets `lifetimeToken`, so callers can read `.lifetimeToken` without their own narrowing. Drops the corresponding cast in the tests.
…filter
Address PR feedback by making `TracedInstruction` a `ResolvedInstruction<T> & { trace }` rather than a `{ instruction, trace }` wrapper. Each walked item is now itself an `IInstruction` and can be passed directly to `isInstructionForProgram` from `@solana/instructions` and to the auto-generated `identifyXInstruction` / `parseXInstruction` helpers, removing the need for our own `filterInstructionsForProgram` (deleted).
`walkInstructions` and the renamed `getInnerInstructionsFromMeta` (was `walkInnerInstructionsFromMeta`) now return arrays rather than generators. A transaction is capped at 64 total instructions, so laziness wasn't buying anything, and arrays compose with native `.filter` / `.find` / `.map` for the common cases.
The typetest is renamed to `traced-instruction-typetest.ts` and rewritten to exercise narrowing via `isInstructionForProgram`.
V1 is rolling out on testnet and the rest of Kit already supports it, so the introspection helpers shouldn't be the odd ones out.
A new `normalizeCompiledInstructions` reduces `legacy`, `v0`, and `v1` to a single internal shape — for `v1` it zips `instructionHeaders` and `instructionPayloads` into the `{ programAddressIndex, accountIndices, data }` form the resolver already consumed. Inner instructions and ALT-loaded addresses are version-agnostic on the RPC side, so the rest of the package needed no change.
The response-type generics widen from `0 | void` to `TransactionVersion | void` to match upstream `@solana/rpc-api`, and the README notes that callers still need to pass `maxSupportedTransactionVersion` on `getTransaction` to actually receive `v0` or `v1`. A `_exhaustiveCheck: never` assignment in `normalizeCompiledInstructions` ensures a future variant of `CompiledTransactionMessage` won't silently slip through the version check.
…nsupported getTransaction shapes Adds SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE so account-index lookups no longer reuse the program-address error code, and a new TRANSACTION_INTROSPECTION domain (5664xxx) with CANNOT_DECODE_JSON_PARSED_TRANSACTION and UNRECOGNIZED_GET_TRANSACTION_RESPONSE so decodeTransactionFromRpcResponse no longer throws MALFORMED_MESSAGE_BYTES with empty bytes for inputs that have no message bytes at all. jsonParsed responses are now detected explicitly and rejected with their own error.
…nknown versions decodeFromJson previously collapsed every versioned response to v0 (rpcTx.version as 0), so a v1 'json' response silently produced a mis-shaped compiled message. The version dispatch is now an exhaustive switch: v1 responses synthesize a V1CompiledTransactionMessage (the 'json' encoding carries no v1 transaction config, so the config is reported empty), and any version outside TransactionVersion throws SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED — the same error surface as getInstructionsFromCompiledTransactionMessage. Also documents that message.recentBlockhash carries the nonce value for durable-nonce transactions, and aligns the JSON path with the wire decoder by omitting accountIndices/data when empty.
…nse types directly Drops the Base64/Base58/JsonGetTransactionResponse aliases in favor of GetTransactionApiResponseBase64/Base58/Json from @solana/rpc-api. The aliases were pure renames, and their widened default type parameter (TransactionVersion | void vs rpc-api's void) meant an unparameterised alias was not the same type as its rpc-api counterpart.
…TransactionMessage The flat-address list is derivable from getAccountMetasFromCompiledTransactionMessage by mapping each meta to its address, so the standalone helper added no capability worth a public API surface. The LoadedAddresses type moves to its own module.
…m resolved instructions ResolvedInstruction no longer forces InstructionWithAccounts/InstructionWithData. Resolved outer and inner instructions now attach accounts and data only when non-empty, matching the kit Instruction conventions and the wire decoder's behavior, so isInstructionWithAccounts and isInstructionWithData from @solana/instructions behave as expected on the results.
…kInstructions output walkInstructions now returns instructions in display order — each outer instruction followed immediately by its inner instructions — instead of all outer instructions followed by all inner ones. This matches how explorers present a transaction and makes positional iteration line up with execution structure.
The base64 test that claimed v0 coverage only differed from the legacy one by a type cast. It now encodes an actual v0 message with addressTableLookups and asserts the decoded version and lookups.
The changeset now includes a usage example and lists @solana/rpc-api (new named getTransaction response types) and @solana/errors (new error codes) as affected packages. The .gitignore entries for personal agent configs were moved to anza-xyz#1737.
…ing message header The previous sniff inspected the first instruction, so a jsonParsed response for a transaction with no instructions (e.g. fee-only) passed as 'json' and crashed on the missing header with a raw TypeError. The 'json' encoding always carries the compiled-message header while jsonParsed never does, so the header is a reliable discriminator independent of instruction count.
…SON-derived v0 messages The wire decoder drops the field when the message has no lookups, so the JSON path now does too — a lookup-free v0 transaction decodes to the same shape on both encodings. Also defers compiled-instruction construction into the legacy/v0 branches so the v1 path no longer base58-decodes every instruction's data twice.
… before identify/parse The auto-generated identifyXInstruction / parseXInstruction helpers require data (and parse also accounts), which are optional on ResolvedInstruction, so every README and docblock example now narrows with isInstructionWithData / isInstructionWithAccounts first. The typetest also exercises those narrows over TracedInstruction so the examples' pattern is checked at compile time.
…peScript peer with the workspace Bumps the stale 6.8.0 to 6.9.0 to match the fixed changesets group, moves @solana/rpc-types to devDependencies since only tests import it, and raises the TypeScript peer to >=5.4.0 like every other package.
…cases normalizeCompiledInstructions now throws SOLANA_ERROR__TRANSACTION__INSTRUCTION_HEADERS_PAYLOADS_MISMATCH for v1 messages whose headers and payloads disagree in length, matching the transaction-messages decompile path, instead of failing with a raw TypeError. walkInstructions appends inner groups whose index matches no outer instruction instead of silently dropping them. Also adds wire-path (base64) coverage for v1 transaction decoding.
adfa25e to
96dc50c
Compare
|
@mcintyre94 @trevor-cortex thx for the feedback
|
mcintyre94
left a comment
There was a problem hiding this comment.
This looks really good to me now, thanks for making all those changes!
To flag a relevant PR, #1776 adds getTransactionForAddress to the Kit RPC. If my PR to add version to that API is merged (solana-rpc/superbank#20) then AFAICT this would also be compatible with transactions from getTransactionsForAddress which would be super nice.
I'd suggest that we handle that as a follow up though, rather than blocking this until that's added.
Will do another AI review in case I missed anything!
trevor-cortex
left a comment
There was a problem hiding this comment.
Took a fresh pass over the diff against my previous notes. All the substantive points from the last round are addressed and the package looks ready to ship.
What's been resolved since the last review
- v1 gap in
decodeFromJson(the headline) is closed.versionis now read from the envelope and dispatched through aswitchwith a_exhaustiveCheck: neverdefault that throwsSOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED. The legacy / v0 / v1 branches each build the correct compiled-message shape, and there's a dedicated test covering an unknownversion: 99. - Error-code overloading for account-index lookups is fixed with the new
SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_ACCOUNT_INDEX_OUT_OF_RANGE(5663038), plus a newTRANSACTION_INTROSPECTIONdomain at 5664xxx withCANNOT_DECODE_JSON_PARSED_TRANSACTIONandUNRECOGNIZED_GET_TRANSACTION_RESPONSE.jsonParsednow has its own dedicated, message-clear error rather than a generic decode failure. .gitignoreis cleaned up — only.docs/anddist/remain in the package-local file, no personal tooling state.- JSON instruction shape consistency is now explicit: the JSON path's
getCompiledInstructionsomitsaccountIndices/datawhen empty, with a comment noting it matches the wire decoder. - Default type-parameter widening is consistent — every overload defaults to
TransactionVersion | void. - Changeset now correctly lists
@solana/errors,@solana/rpc-api,@solana/kit, and@solana/transaction-introspection, and the description matches the README (no stalefilterInstructionsForProgram). loaded-addresses.tsextracted cleanly as a tiny module — nice.
The new tests are a notable jump in coverage: v0 + v1 JSON paths, unknown version, unrecognized shape, ALT-loaded v0 walking with mixed static/ALT-writable/ALT-readonly indices, and the v1 INSTRUCTION_HEADERS_PAYLOADS_MISMATCH case. The two type tests (decode-rpc-transaction-typetest.ts and traced-instruction-typetest.ts) lock down both the overload narrowing of transaction and the isInstructionForProgram / isInstructionWithData ergonomics for TracedInstruction.
Remaining observations (all minor)
None block merge — these are polish-level. See inline.
Notes for subsequent reviewers
- The
isJsonResponse/isJsonParsedResponsediscrimination relies on the presence ofmessage.header. That's a reasonable heuristic given the RPC contract, but it's worth being aware of: any future change to thejsonParsedshape that re-introduced aheaderfield onmessagewould silently route into the JSON path. The conditional is currently the only thing distinguishing the two encodings structurally — there's noencodingdiscriminator on the response itself. - Worth double-checking that
getAccountMetasFromCompiledTransactionMessageproduces the same role assignments as the kitdecompileTransactionMessagehelper for v0 messages with ALT-loaded addresses — the order (static → ALT writable → ALT readonly) matches the runtime, and thewalk-instructions-test.tsALT case exercises it, but if there's a canonical decoder elsewhere in the kit it would be reassuring to confirm shape parity once. - Callum already mentioned this in their approval, but flagging here for posterity: the related PR #1611 discussion of follow-up compatibility with
getTransactionsForAddress(onceversionlands there) is a reasonable follow-up rather than a blocker.
Replace the custom `getThrownError` helper in `get-instructions-test.ts` with `expect(() => ...).toThrow(new SolanaError(...))`, matching the pattern in `get-inner-instructions-test.ts` and asserting error context alongside the code. Rename the shadowed `meta` locals to `accountMeta` in the account-index lookups, and document that `decodeTransactionFromRpcResponse` can throw `SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED`.
Your organization requires reapproval when changes are made, so Graphite has dismissed approvals. No previous SHAs found; treated as if diff has changed at https://github.com/anza-xyz/kit/actions/runs/28134143740
|
@mcintyre94 updated that test and a couple of nits from trevor |
mcintyre94
left a comment
There was a problem hiding this comment.
I think we're good to ship this! Thankyou! 🚀
|
🔎💬 Inkeep AI search and chat service is syncing content for source 'Solana Kit Docs' |
|
Because there has been no activity on this PR for 14 days since it was merged, it has been automatically locked. Please open a new issue if it requires a follow up. |
For consideration/discussion.
Adds
@solana/transaction-introspection, a new package that bridges a getTransaction response and the auto-generated @solana-program/* parseXInstruction clients. Decodes responses encoded as base64, base58, or json; resolves account indices against static and ALT-loaded addresses; normalizes inner instructions from meta.innerInstructions; and exposes walkInstructions for streaming traversal of every instruction (outer and inner). Re-exported from @solana/kit.Also hoists the inline GetTransactionApi response shapes in @solana/rpc-api into named exported types (Base64/Base58/Json/JsonParsed) so the new package can consume them directly.