Skip to content

fix: instruction decoder account parsing and naming for light token a…#2201

Merged
ananas-block merged 2 commits intomainfrom
jorrit/fix-instruction-decoder-account-parsing
Jan 22, 2026
Merged

fix: instruction decoder account parsing and naming for light token a…#2201
ananas-block merged 2 commits intomainfrom
jorrit/fix-instruction-decoder-account-parsing

Conversation

@ananas-block
Copy link
Contributor

@ananas-block ananas-block commented Jan 22, 2026

…nd system program

Summary by CodeRabbit

  • New Features

    • Dynamic account-name resolution for decoded instructions
    • Account state tracking with pre/post lamport and data-length snapshots
    • Thousand-separator formatting for numeric displays
  • Improvements

    • Enhanced instruction/account table rendering when state data is available
    • Richer transaction logging that includes account-state context
    • Updated program naming for clearer UI labels

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 22, 2026

📝 Walkthrough

Walkthrough

Adds dynamic account-name resolvers for instruction variants and captures per-account pre/post state snapshots to enrich enhanced transaction logs and formatter output.

Changes

Cohort / File(s) Summary
Derive macro changes
sdk-libs/instruction-decoder-derive/src/attribute_impl.rs, sdk-libs/instruction-decoder-derive/src/builder.rs, sdk-libs/instruction-decoder-derive/src/parsing.rs
Adds account_names_resolver_from_params attribute support, centralizes match-arm body generation to choose dynamic resolver vs static names, and uses real parameter field names for DecodedField output.
Core types & formatter
sdk-libs/instruction-decoder/src/types.rs, sdk-libs/instruction-decoder/src/formatter.rs, sdk-libs/instruction-decoder/src/lib.rs
Introduces AccountStateSnapshot and EnhancedTransactionLog.account_states; threads optional account_states through formatting to render 7-column outer account tables and adds numeric formatting helpers; re-exports snapshot type.
CToken program updates
sdk-libs/instruction-decoder/src/programs/ctoken.rs
Replaces static packed-account offset with calculate_packed_accounts_start; adds resolve_*_account_names resolvers and matching formatters (format_transfer2, format_mint_action); wires resolvers into CTokenInstruction attributes.
Light System program updates
sdk-libs/instruction-decoder/src/programs/light_system.rs
Adds resolvers resolve_invoke_cpi_readonly_account_names and resolve_invoke_cpi_account_info_account_names; updates static account name lists (noop_program → log_program) and wires resolvers for CPI variants.
Program-test logging integration
sdk-libs/program-test/src/logging/mod.rs, sdk-libs/program-test/src/program_test/rpc.rs
Captures pre/post account states (AccountStates), diffs them into AccountStateSnapshots, threads pre/post states into enhanced logging APIs and populates EnhancedTransactionLog.account_states for formatter consumption.

Sequence Diagram(s)

sequenceDiagram
    actor ProgramTest
    participant PreCtx as Pre-Execution<br/>LiteSVM Context
    participant Executor as Transaction<br/>Executor
    participant PostCtx as Post-Execution<br/>LiteSVM Context
    participant Logger as Enhanced<br/>Logger
    participant Formatter as Transaction<br/>Formatter

    ProgramTest->>PreCtx: Snapshot account states
    ProgramTest->>Executor: Execute transaction
    Executor->>Executor: Modify accounts (lamports, data)
    Executor->>PostCtx: Capture final state
    ProgramTest->>Logger: log_transaction_enhanced(pre, post)
    Logger->>Logger: Build AccountStateSnapshot map (deltas per account)
    Logger->>Logger: Populate EnhancedTransactionLog.account_states
    Logger->>Formatter: Format instructions with account_states
    Formatter->>Formatter: Render 7-column outer account table
    Formatter-->>ProgramTest: Enhanced transaction view
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: program test loggig #1927 — Related: earlier enhanced logging subsystem additions that this change builds upon (logging/decoder/formatter integration).

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • SwenSchaeferjohann

Poem

✨ Resolvers wake and names align,
Snapshots catch each lamport sign,
Pre to post the diffs are shown,
Logs glow bright with state now known,
Decoder and tests sing in kind.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main focus of the changeset: enhancing instruction decoder account parsing and naming for the light token program, which is reflected across multiple files with resolver functions and dynamic account name resolution.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch jorrit/fix-instruction-decoder-account-parsing

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk-libs/instruction-decoder/src/programs/light_system.rs (1)

873-879: Address naming inconsistency between instruction decoder and SDK implementations.

The instruction-decoder now uses log_program in the account_names array, but the broader SDK and program implementations still extensively reference noop_program (106 occurrences across SDK libs, tests, and programs). This creates a naming mismatch where the decoded account name (log_program) differs from the actual account field names (noop_program) used throughout the system.

Either propagate the log_program naming consistently across sdk-libs and related instructions, or revert the decoder to use noop_program for alignment with the rest of the codebase. Documentation (e.g., programs/compressed-token/program/docs/) also describes this account as "Noop program for event emission," which aligns with the noop_program naming convention.

🤖 Fix all issues with AI agents
In `@sdk-libs/instruction-decoder-derive/src/builder.rs`:
- Around line 273-294: The generated code passes an empty string as the
DecodedField name (in the fields_code block for both the custom formatter path
and the Debug path) which reduces readability; update both branches that call
light_instruction_decoder::DecodedField::new("", ...) to supply a meaningful
name (e.g., "params" or derive a name from the params identifier/type available
in the macro input), using the same identifier used elsewhere (params) or a
value pulled from variant_args/variant metadata so the field label matches
attribute_impl.rs changes; ensure both the branch where `#formatter_path`(&params,
accounts) is used and the branch that uses format!("{:#?}", params) pass the
chosen non-empty name.

In `@sdk-libs/instruction-decoder/src/formatter.rs`:
- Around line 671-776: The duplicated account-name resolution in formatter.rs
(used in the outer_rows loop and the account_rows loop when building tables)
should be extracted into a helper method (e.g., resolve_account_name) on the
same impl so both branches call it; implement a method like fn
resolve_account_name(&self, instruction: &EnhancedInstructionLog, idx: usize,
pubkey: &Pubkey) -> String that performs the decoded_instruction lookup, filters
out empty names, and falls back to self.get_account_name(pubkey), then replace
the inline resolution logic in both places (the loops that build OuterAccountRow
and AccountRow) with calls to this helper.
- Around line 697-714: The subtraction casting lamports_after and
lamports_before to i64 (in the block that computes change for
states.get(&account.pubkey)) can overflow; change the computation to use a
wider/safe integer or saturating/checked arithmetic (e.g., promote to i128
before subtracting or use checked_sub/saturating_sub) and then convert/clamp the
result to the signed type expected by format_signed_with_thousands_separator,
ensuring you preserve the existing formatting logic (the change_str branches and
the use of format_signed_with_thousands_separator remain the same).

In `@sdk-libs/instruction-decoder/src/programs/ctoken.rs`:
- Around line 208-521: The resolve_transfer2_account_names function is large;
extract the packed-role construction into a helper to simplify it. Create a new
helper fn build_transfer2_packed_roles(data:
&CompressedTokenInstructionDataTransfer2) -> HashMap<u8, String> that contains
the packed_roles HashMap and all counters (owner_count, mint_count,
delegate_count, in_merkle_count, in_queue_count, compress_mint_count,
compress_source_count, compress_auth_count) and the logic that fills
packed_roles from data.output_queue, data.in_token_data, data.out_token_data,
and data.compressions; return the populated HashMap. In
resolve_transfer2_account_names replace the inline packed-role setup with a call
to build_transfer2_packed_roles(data) and remove the moved local variables,
keeping the subsequent loop that iterates packed_idx and uses packed_roles,
known_pubkeys, names, and idx unchanged. Ensure the helper is cfg-gated the same
way and preserves types and semantics.
- Around line 667-697: The calculate_mint_action_packed_accounts_start function
currently returns fixed offsets and misses accounts that precede
LightSystemAccounts; update it to compute the start dynamically by mirroring
resolve_mint_action_account_names: begin with 1 for light_system_program, add 1
if data.create_mint is true for mint_signer, add 1 for authority, then add 1
each for optional compressible_config, cmint, and rent_sponsor (presentness
determined from data), then add 6 for the LightSystemAccounts and finally +1 if
data.cpi_context.is_some() (unless cpi_context_write_mode is true, in which case
keep returning 3); use these fields (create_mint, compressible_config, cmint,
rent_sponsor, cpi_context) and the function
calculate_mint_action_packed_accounts_start and
resolve_mint_action_account_names to locate and implement the corrected
computation.

In `@sdk-libs/instruction-decoder/src/programs/light_system.rs`:
- Around line 668-742: The two functions
resolve_invoke_cpi_account_info_account_names and
resolve_invoke_cpi_readonly_account_names duplicate the same account-name
resolution logic; extract a shared helper (e.g.,
resolve_light_system_account_names_internal) that accepts
cpi_context_write_mode: bool and accounts: &[AccountMeta] and returns
Vec<String>, move the common indexing/naming logic there (including the
fixed-name sequence and the 3-account CPI-write path) and have both resolver
functions compute their cpi_context_write_mode from their respective
data.cpi_context fields and call the helper, removing the duplicated add_name
closure and repeated code.
- Around line 549-623: The function resolve_invoke_cpi_readonly_account_names
contains dead scaffolding: remove the unused HashMap known_pubkeys and all
references to it; update the add_name closure signature to drop the known: &mut
HashMap parameter and remove the known.insert(...) call inside the closure, and
update all calls to add_name to stop passing &mut known_pubkeys so the function
only computes and returns names as before; also remove the unused
std::collections::HashMap import.

In `@sdk-libs/program-test/src/logging/mod.rs`:
- Around line 249-257: The current mapping of pre_account/post_account to
(lamports, data_len) loses whether the account existed; update the snapshot
representation used in logging (e.g., replace the tuple mapping with
Option<AccountStateSnapshot> or extend AccountStateSnapshot with an exists:
bool) so pre_account.as_ref() and post_account.as_ref() produce Either
Some(AccountStateSnapshot { exists: true, lamports, data_len }) or None for
non‑existent accounts; adjust the code paths that read
lamports_before/data_len_before and lamports_after/data_len_after to handle the
Option<AccountStateSnapshot> (or the new exists field) when formatting logs so
"account doesn't exist" is distinct from "0 lamports, 0 data".

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
sdk-libs/instruction-decoder/src/formatter.rs (1)

200-231: Clarify pre/post state in the outer account table.

Line 711-713 formats *_before values, but the headers in Line 224-227 read as generic “Data Len/Lamports,” which can look like current state (e.g., newly created accounts appear as zero). Either display the *_after values or make the headers explicit. A minimal label fix:

✏️ Suggested label clarification
-    #[tabled(rename = "Data Len")]
+    #[tabled(rename = "Data Len (pre)")]
...
-    #[tabled(rename = "Lamports")]
+    #[tabled(rename = "Lamports (pre)")]

Also applies to: 671-739

🤖 Fix all issues with AI agents
In `@sdk-libs/program-test/src/logging/mod.rs`:
- Around line 237-242: The Rust doc comment for EnhancedTransactionLog
references a non-existent helper; update the doc so it points to the real helper
function name. Locate the comment block around EnhancedTransactionLog and
replace the incorrect function reference with the correct helper name
capture_pre_account_states in the sentence that currently reads "Use `...`
before and after sending the transaction." to ensure the doc links to the actual
snapshot helper.
♻️ Duplicate comments (1)
sdk-libs/program-test/src/logging/mod.rs (1)

243-283: Account existence ambiguity remains in snapshots.
Line 266 defaults missing accounts to (0, 0), which is indistinguishable from a real zero‑lamport account. This was already flagged in a prior review (suggested: Option<AccountStateSnapshot> or an exists flag).

Also applies to: 352-352

Comment on lines 237 to +242
/// Create EnhancedTransactionLog from LiteSVM transaction result
///
/// If pre_states and post_states are provided, captures account state snapshots
/// for all accounts in the transaction.
///
/// Use `capture_pre_account_states` before and after sending the transaction.
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix doc reference to the snapshot helper.
Line 242 references a function name that doesn’t exist, which can mislead readers.

📝 Suggested fix
-/// Use `capture_pre_account_states` before and after sending the transaction.
+/// Use `capture_account_states` before and after sending the transaction.
🤖 Prompt for AI Agents
In `@sdk-libs/program-test/src/logging/mod.rs` around lines 237 - 242, The Rust
doc comment for EnhancedTransactionLog references a non-existent helper; update
the doc so it points to the real helper function name. Locate the comment block
around EnhancedTransactionLog and replace the incorrect function reference with
the correct helper name capture_pre_account_states in the sentence that
currently reads "Use `...` before and after sending the transaction." to ensure
the doc links to the actual snapshot helper.

Copy link
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
sdk-libs/program-test/src/program_test/rpc.rs (1)

562-615: Consider applying consistent logging patterns across transaction methods.

_create_and_send_transaction_with_event uses basic maybe_print_logs(), while _send_transaction_with_batched_event captures pre/post account states and uses enhanced logging. Since both methods are test utilities that execute transactions and maintain context snapshots, applying the same logging approach would improve debugging consistency.

If you decide to add enhanced logging here, follow the pattern in _send_transaction_with_batched_event: capture account states via crate::logging::capture_account_states() before and after execution, then call crate::logging::log_transaction_enhanced() with the captured states. Alternatively, if this method is intentionally kept lightweight for performance reasons, a brief comment explaining the design choice would help future maintainers understand the trade-off.

sdk-libs/instruction-decoder/src/formatter.rs (1)

1172-1202: Remove SDK lib interdependency or relocate shared types.

The get_account_name function depends on light_sdk_types::constants (line 1174), but the architectural guideline is clear: SDK libs must depend only on program-libs, light-prover-client, and external crates. Since light-sdk-types is itself an SDK lib, this violates that constraint.

Per the protocol architecture, shared type definitions and constants should reside in program-libs (see light-token-interface pattern) so SDKs and clients can import them without pulling in SDK-specific dependencies. Consider either:

  1. Moving the protocol constants to a program-libs location, or
  2. Refactoring to access constants through a re-exported program-libs dependency instead of directly from light-sdk-types
🤖 Fix all issues with AI agents
In `@sdk-libs/instruction-decoder/src/formatter.rs`:
- Around line 1238-1247: Add a unit test covering the i64::MIN edge case to
document that format_signed_with_thousands_separator (which relies on
unsigned_abs()) handles the minimum i64 correctly: create a new test function
(e.g., test_format_signed_edge_cases) that asserts
format_signed_with_thousands_separator(i64::MIN) == "-9,223,372,036,854,775,808"
so the behavior is explicitly verified.
- Around line 676-685: Extract the duplicated AccountAccess determination into a
single helper and replace both inline blocks with calls to it: add a method like
AccountAccess::from_flags(is_signer: bool, is_writable: bool) (or a free fn
account_access_from_flags) that returns the correct variant (SignerWritable,
Signer, Writable, Readonly), then update the loops over instruction.accounts
(the block that currently builds `access` in formatter.rs and the similar block
at the other location) to call this helper instead of duplicating the if/else
logic.

In `@sdk-libs/program-test/src/logging/mod.rs`:
- Around line 237-243: The doc comment on from_transaction_result incorrectly
references capture_pre_account_states; update the comment to reference the
actual helper name capture_account_states (or any other correct exported
function name) and ensure phrasing matches the API (e.g., "Use
`capture_account_states` before and after sending the transaction."). Locate the
comment above the pub fn from_transaction_result and replace the incorrect
symbol so docs and examples match the real function name.
♻️ Duplicate comments (1)
sdk-libs/instruction-decoder/src/formatter.rs (1)

687-694: Consider extracting duplicate account resolution logic.

The account name resolution pattern appears twice (lines 687-694 for outer rows and 756-761 for inner rows). A helper method would centralize this logic:

fn resolve_account_name(
    &self,
    instruction: &EnhancedInstructionLog,
    idx: usize,
    pubkey: &Pubkey,
) -> String {
    instruction
        .decoded_instruction
        .as_ref()
        .and_then(|decoded| decoded.account_names.get(idx).cloned())
        .filter(|name| !name.is_empty())
        .unwrap_or_else(|| self.get_account_name(pubkey))
}

Also applies to: 756-761

Comment on lines +676 to +685
for (idx, account) in instruction.accounts.iter().enumerate() {
let access = if account.is_signer && account.is_writable {
AccountAccess::SignerWritable
} else if account.is_signer {
AccountAccess::Signer
} else if account.is_writable {
AccountAccess::Writable
} else {
AccountAccess::Readonly
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: Duplicated access type determination.

The AccountAccess determination logic (lines 676-685 and 744-752) is duplicated. While simple, you could extract this to a helper or use a method on AccountAccess like from_flags(is_signer, is_writable) if this pattern appears elsewhere in the codebase.

🤖 Prompt for AI Agents
In `@sdk-libs/instruction-decoder/src/formatter.rs` around lines 676 - 685,
Extract the duplicated AccountAccess determination into a single helper and
replace both inline blocks with calls to it: add a method like
AccountAccess::from_flags(is_signer: bool, is_writable: bool) (or a free fn
account_access_from_flags) that returns the correct variant (SignerWritable,
Signer, Writable, Readonly), then update the loops over instruction.accounts
(the block that currently builds `access` in formatter.rs and the similar block
at the other location) to call this helper instead of duplicating the if/else
logic.

Comment on lines +1238 to +1247
#[test]
fn test_format_signed_with_thousands_separator() {
assert_eq!(format_signed_with_thousands_separator(0), "0");
assert_eq!(format_signed_with_thousands_separator(1234), "1,234");
assert_eq!(format_signed_with_thousands_separator(-1234), "-1,234");
assert_eq!(
format_signed_with_thousands_separator(-1000000),
"-1,000,000"
);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider adding edge case tests for signed formatter.

The current tests are good, but you might want to add coverage for i64::MIN to ensure unsigned_abs() handles it correctly (it does, but a test documents this guarantee):

💡 Additional test case
#[test]
fn test_format_signed_edge_cases() {
    // i64::MIN is handled correctly by unsigned_abs()
    assert_eq!(
        format_signed_with_thousands_separator(i64::MIN),
        "-9,223,372,036,854,775,808"
    );
}
🤖 Prompt for AI Agents
In `@sdk-libs/instruction-decoder/src/formatter.rs` around lines 1238 - 1247, Add
a unit test covering the i64::MIN edge case to document that
format_signed_with_thousands_separator (which relies on unsigned_abs()) handles
the minimum i64 correctly: create a new test function (e.g.,
test_format_signed_edge_cases) that asserts
format_signed_with_thousands_separator(i64::MIN) == "-9,223,372,036,854,775,808"
so the behavior is explicitly verified.

@ananas-block ananas-block merged commit fc6071c into main Jan 22, 2026
21 checks passed
@ananas-block ananas-block deleted the jorrit/fix-instruction-decoder-account-parsing branch January 22, 2026 16:38
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