Skip to content

fix: swen csdk#2055

Closed
ananas-block wants to merge 28 commits into
mainfrom
jorrit/fix-swen-csdk
Closed

fix: swen csdk#2055
ananas-block wants to merge 28 commits into
mainfrom
jorrit/fix-swen-csdk

Conversation

@ananas-block
Copy link
Copy Markdown
Contributor

@ananas-block ananas-block commented Nov 14, 2025

Summary by CodeRabbit

Release Notes

  • New Features

    • Added compressed token transfer operations between SPL and compressed tokens
    • Introduced compressible accounts framework for account compression workflows
    • Added address lookup table loading for enhanced RPC functionality
    • New token pool and mint derivation utilities
    • Enhanced test infrastructure with compression simulation capabilities
  • Refactor

    • Reorganized token and proof module structure for improved organization
    • Updated import paths across SDK libraries for consistency
  • Chores

    • Updated dependencies and workspace configurations
    • Added feature gates for optional functionality

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 14, 2025

Walkthrough

This pull request substantially restructures the compressed token and compressible account infrastructure. It migrates token types to a new light_compressed_token_sdk::compat module, introduces new compressible account configuration and compression utilities, refactors the compressed token SDK with renamed functions and new transfer modules, and establishes a new compressible-client library.

Changes

Cohort / File(s) Summary
Token Type Migrations
forester/tests/*, sdk-libs/client/src/indexer/types.rs, sdk-libs/program-test/src/indexer/*, sdk-libs/client/src/rpc/mod.rs
Updated import paths for TokenDataWithMerkleContext and related types from light_sdk::token to light_compressed_token_sdk::compat; adjusted trait implementations (Into, TryFrom) to use the new compat module.
Compressed Token SDK Core Refactoring
sdk-libs/compressed-token-sdk/Cargo.toml, sdk-libs/compressed-token-sdk/src/account2.rs, sdk-libs/compressed-token-sdk/src/lib.rs, sdk-libs/compressed-token-sdk/src/error.rs
Removed public transfer instruction factories from account2.rs; added light-compressible dependency; introduced new error variants (SplBridgeConfigRequired, IncompleteSplBridgeConfig, etc.); expanded public module exports and re-exports for pack, ctoken, and utils.
Compressed Token SDK – New Modules
sdk-libs/compressed-token-sdk/src/pack.rs, sdk-libs/compressed-token-sdk/src/ctoken.rs, sdk-libs/compressed-token-sdk/src/transfer_ctoken.rs, sdk-libs/compressed-token-sdk/src/transfer_interface.rs, sdk-libs/compressed-token-sdk/tests/pack_test.rs
Added pack.rs with Pack/Unpack traits and compat submodule for Solana Pubkey-based token data; added ctoken.rs with program ID and PDA derivation helpers; introduced transfer_ctoken.rs and transfer_interface.rs for unified SPL↔CTOKEN transfer operations with signed CPI variants.
Compressed Token SDK – Function Renames
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs, sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs, sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs, sdk-libs/compressed-token-sdk/src/instructions/mod.rs
Renamed derive_compressed_mint_from_spl_mintderive_cmint_from_spl_mint; renamed create_compressible_token_accountcreate_compressible_token_account_instruction; added create_ctoken_account_signed; reorganized module exports to include new transfer modules.
Compressed Token SDK – Instruction Builders
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs, sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs, sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs
Added signed CPI wrappers: compress_and_close_ctoken_accounts_signed, create_associated_ctoken_account, create_associated_ctoken_account_idempotent; enhanced decompress_full with owner-based signer tracking; updated signer resolution logic.
Compressed Token SDK – Utilities
sdk-libs/compressed-token-sdk/src/utils.rs
Added is_ctoken_account function, CLOSE_TOKEN_ACCOUNT_DISCRIMINATOR constant, PackedCompressedTokenDataWithContext struct, account_meta_from_account_info function, and AccountInfoToCompress struct.
SDK – Compressible Account Infrastructure
sdk-libs/sdk/src/compressible/mod.rs, sdk-libs/sdk/src/compressible/close.rs, sdk-libs/sdk/src/compressible/compression_info.rs, sdk-libs/sdk/src/compressible/config.rs, sdk-libs/sdk/src/compressible/compress_account.rs, sdk-libs/sdk/src/compressible/compress_account_on_init.rs, sdk-libs/sdk/src/compressible/decompress_idempotent.rs
Introduced new compressible module with traits (Pack, Unpack, HasCompressionInfo, CompressAs, Space), CompressionInfo struct for tracking account state, CompressibleConfig for program configuration, and feature-gated (v2) account preparation functions (prepare_account_for_compression, prepare_compressed_account_on_init, prepare_account_for_decompression_idempotent).
SDK – Proof & Type Changes
sdk-libs/sdk/src/lib.rs, sdk-libs/sdk/src/proof.rs, sdk-libs/sdk/src/token.rs, sdk-libs/sdk/src/account.rs, sdk-libs/sdk/src/address.rs
Replaced token module with proof module; added borsh_compat submodule with CompressedProof and ValidityProof types; removed token data types (AccountState, TokenData, TokenDataWithMerkleContext); added size() and remove_data() methods to LightAccountInner; added derive_compressed_address to address::v2.
SDK – Feature & Dependencies
sdk-libs/sdk/Cargo.toml
Added anchor-discriminator feature; added dependencies: solana-clock, solana-sysvar, solana-system-interface, solana-loader-v3-interface, bincode, light-ctoken-types.
New Compressible Client Library
sdk-libs/compressible-client/Cargo.toml, sdk-libs/compressible-client/src/get_compressible_account.rs, sdk-libs/compressible-client/src/lib.rs
New crate for compressible account handling; exposes CompressibleAccountError, MerkleContext, AccountInfoInterface types; provides async functions for fetching unified account interfaces and conditional deserialization (Anchor vs. non-Anchor); includes instruction builders for compression configuration.
Program Test Infrastructure
sdk-libs/program-test/Cargo.toml, sdk-libs/program-test/src/compressible.rs, sdk-libs/program-test/src/program_test/compressible_setup.rs, sdk-libs/program-test/src/program_test/config.rs, sdk-libs/program-test/src/program_test/light_program_test.rs, sdk-libs/program-test/src/program_test/mod.rs, sdk-libs/program-test/src/program_test/test_rpc.rs, sdk-libs/program-test/src/utils/*
Added compressible account test utilities (create_mock_program_data, setup_mock_program_data, initialize_compression_config, update_compression_config); introduced auto-compression infrastructure with auto_compress_programs field and register_auto_compression method; added simulation utility simulate_cu.
Token Client Updates
sdk-libs/token-client/src/actions/*, sdk-libs/token-client/src/instructions/*, sdk-libs/token-client/src/lib.rs
Renamed transfer functions: ctoken_transfertransfer_ctoken, create_ctoken_transfer_instructioncreate_transfer_ctoken_instruction; updated to use new transfer instruction paths from compressed-token-sdk; exposed ctoken module re-export.
Program Library Configuration
program-libs/batched-merkle-tree/Cargo.toml, program-libs/compressed-account/Cargo.toml, program-libs/ctoken-types/Cargo.toml
Added keccak support across features; integrated light-program-profiler with profile-program and profile-heap features; removed #[profile] attribute from ZOutputCompressedAccountWithPackedContextMut::set.
Compressible Program Utilities
program-libs/compressible/src/config.rs
Added public PDA derivation methods: derive_compression_authority_pda, derive_rent_sponsor_pda, ctoken_v1_compression_authority_pda, ctoken_v1_rent_sponsor_pda.
Miscellaneous
sdk-libs/client/Cargo.toml, sdk-libs/client/src/constants.rs, sdk-libs/client/src/rpc/lut.rs, sdk-libs/compressed-token-types/src/lib.rs, sdk-libs/compressed-token-types/src/token_data.rs, programs/compressed-token/program/src/shared/token_input.rs, forester/Cargo.toml
Added workspace dependencies (solana-signer, solana-message, light-compressed-token-sdk); added LIGHT_PROTOCOL_LOOKUP_TABLE_ADDRESS constant; added load_lookup_table function for address lookup table fetching; removed token_data module from compressed-token-types public API; removed public token data types; minor comment cleanup.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant TokenClient
    participant CompressedTokenSDK
    participant CompressibleConfig
    participant Program

    Client->>TokenClient: transfer_ctoken(from, to, amount)
    TokenClient->>CompressedTokenSDK: create_transfer_ctoken_instruction()
    CompressedTokenSDK->>CompressedTokenSDK: construct instruction<br/>(discriminator + amount)
    CompressedTokenSDK-->>TokenClient: Instruction
    TokenClient->>TokenClient: sign transaction
    TokenClient->>Program: submit transaction
    Program->>Program: validate & execute
    Program-->>Client: success/error
Loading
sequenceDiagram
    participant Test as Test Framework
    participant Setup as CompressibleSetup
    participant CompressibleConfig as Config Instruction
    participant Program as Compressible Program
    participant RPC

    Test->>Setup: initialize_compression_config()
    Setup->>CompressibleConfig: CompressibleInstruction::initialize_compression_config()
    CompressibleConfig->>CompressibleConfig: build instruction with discriminator
    CompressibleConfig-->>Setup: Instruction
    Setup->>Setup: select signers (payer, authority)
    Setup->>RPC: send signed transaction
    RPC->>Program: execute instruction
    Program->>Program: validate & store config
    Program-->>RPC: success/error
    RPC-->>Test: Signature
Loading
sequenceDiagram
    participant App
    participant CompressibleClient as CompressibleClient
    participant RPC as RPC/Indexer
    participant OnChain as On-Chain Account

    App->>CompressibleClient: get_account_info_interface(address)
    CompressibleClient->>RPC: get_account(address)
    alt Account is compressed
        RPC->>CompressibleClient: compressed account from indexer
        CompressibleClient->>CompressibleClient: derive merkle context
        CompressibleClient->>CompressibleClient: reconstruct Account struct
        CompressibleClient-->>App: AccountInfoInterface {<br/>  is_compressed: true,<br/>  merkle_context: Some(...)<br/>}
    else Account is on-chain
        RPC->>CompressibleClient: Account
        CompressibleClient-->>App: AccountInfoInterface {<br/>  is_compressed: false,<br/>  merkle_context: None<br/>}
    end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Specific areas requiring careful attention:

  • Trait implementations in sdk-libs/sdk/src/compressible/compression_info.rs: Multiple new traits (Pack, Unpack, HasCompressionInfo, CompressAs, Space) with complex generic bounds and conditional feature-gated implementations. Verify bounds are correct and trait coherence holds.

  • Token type migration and compat submodule (sdk-libs/compressed-token-sdk/src/pack.rs): New compat module introduces TokenData and AccountState that mirror removed types from sdk-libs/sdk/src/token.rs. Ensure bi-directional conversions are correct and no serialization inconsistencies arise.

  • Public API surface changes in compressed-token-sdk: Multiple function renames, module reorganizations, and new transfer instruction builders. Verify all public re-exports are complete and that old names are fully deprecated.

  • Feature-gated v2 code paths (compress_account.rs, compress_account_on_init.rs, decompress_idempotent.rs): These functions are conditionally compiled. Verify feature flags don't leak into non-v2 builds and that all error paths are consistent.

  • Compressible client library (sdk-libs/compressible-client/): New crate with async functions and trait bounds. Verify error handling paths, especially around RPC/Indexer errors and deserialization edge cases.

  • Program profiler integration: Changes to profile-program and profile-heap features across multiple Cargo.toml files. Ensure no circular dependencies and that feature combinations are valid.

  • Test utility auto-compression (program-test/src/program_test/test_rpc.rs): New logic in warp_slot_forward that iterates over registered programs. Verify this doesn't break existing tests or introduce unexpected batching side effects.

Possibly related PRs

  • PR #1954: Directly related—both change token type paths from light_sdk::token to light_compressed_token_sdk::compat and involve compressed account type migrations.
  • PR #1993: Related—overlapping changes to token data types and light_compressed_token_sdk module reorganization.
  • PR #2000: Related—both modify create-associated-token-account flows and ATA instruction variants in the compressed-token SDK.

Suggested labels

ai-review, sdk, compressed-token-sdk, compressible-accounts, breaking-change, refactor

Suggested reviewers

  • SwenSchaeferjohann
  • sergeytimoshin

Poem

🔐 Tokens dance through compat's door,
Compression blooms where types were before.
PDAs derived, accounts now flow,
From light to shadow—watch the SDK grow! 🌳✨


Principal Review Notes

Architecture & Design:

This is a well-structured consolidation of the compressed account and token infrastructure. The migration of token types to light_compressed_token_sdk::compat is a smart boundary-drawing decision—it keeps the old SDK-layer abstractions separate from the new compressed-token semantics.

Key Strengths:

  1. Trait-based abstraction in compression_info.rs is clean (Pack, Unpack, HasCompressionInfo), though generic bounds are dense—ensure these don't create compile time pain.
  2. Unified account interface (compressible-client) provides a smooth experience for handling both compressed and on-chain accounts without branching logic in user code.
  3. Feature-gated v2 paths are properly isolated, reducing risk to existing non-v2 consumers.

Watch Points:

  1. Serialization round-trips: The token data migrations involve both Borsh and Anchor serialization. Test these conversions thoroughly, especially the compat bidirectional implementations.
  2. Feature interaction complexity: The profiler feature integration across three crate manifests increases surface area for misconfiguration. Validate in CI with all feature combinations.
  3. Error propagation in async code: The compressible-client async functions map multiple error types. Ensure these don't mask underlying issues—add tracing where appropriate.

Pre-merge checks and finishing touches

❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'fix: swen csdk' is vague and cryptic—it uses non-descriptive abbreviations that don't clearly convey what is being fixed. Clarify the title to describe the actual primary change. For example, 'refactor: migrate token types to compressed-token-sdk' or 'fix: unify token data types across SDK' would better explain the changeset's intent.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 70.00%.
✨ 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-swen-csdk

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

wip

add borsh_compat compressed_proof

add new_address_owner to instructiondata trait

add derive_compressed_address

remaining new_address_owner impl

add csdk-anchor-test program

lint add address_owner trait impl

add sdk libs - wip

add transfer_interface, transfer_ctoken, transfer_spl_to_ctoken, transfer_ctoken_to_spl, signed, instructions

rename consistently transfer_x

rename file transfer_decompressed to transfer_ctoken

add todos

add create_ctoken_account_signed, rename create_compressible_token_account to create_compressible_token_account_instruction

add create_associated_ctoken_account

add inline comment to copyLocalProgramBinaries.sh

remove new_address_owner

add pack and unpack for tokendata

first pass, compressible helpers for light-sdk

wip

compiles

lint

compressAccountsIdempotent csdk works, adjust test asserts to account is_none

ctoken add signer flags for decompress_full_ctoken_accounts_with_indices

wip

stash: removing ctoken from compression

all tests working

add auto-comp, clean up tests

rm dependency on patch

fmt

lint

lint

refactor

rm

wip

clean

fmt

clean
dedupe derivation methods

clean

fmt

revert copyLocalProgramBinaries.sh diff

add csdk_anchor_test binary to ci

fix indexer

fix doctest

fix cli ci build target

fix cli build

clean

address nits

fix cli cache

fix cache

clean

fix csdk anchor test program build

add pkg.json csdk

rebuild

fix syntax

fix nx

rm panics

fix ci

fix build sdk-anchor-test

try fix

bmt keccak

spawn_prover fix

fix lint

fix clock sysvar

add test feature to account-compression

revert profiler

refactor csdk-anchor-test program lib.rs

split tests

fmt

revert cli script
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: 42

Caution

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

⚠️ Outside diff range comments (13)
sdk-libs/token-client/Cargo.toml (1)

8-18: Consider whether light-compressible should be gated by a feature

Adding light-compressible directly is fine if the client always needs compressible primitives, but if usage is limited (e.g., only for specific flows), consider a feature flag to avoid pulling it into consumers that never touch those paths.

program-libs/compressible/src/config.rs (1)

184-193: PDA helpers match existing seed layout; consider a minor allocation tweak

The new PDA helpers:

  • Reuse get_compression_authority_seeds / get_rent_sponsor_seeds
  • Call Pubkey::find_program_address with the same seed order already used in new()

So they should derive exactly the same PDAs as the constructor path, and the v1 convenience methods correctly hard‑code the same program IDs you use elsewhere.

If you want to shave a bit of overhead in hot paths, you could avoid the intermediate Vec<u8> allocations in derive_*_pda and construct the seed slices directly from a version_bytes array, mirroring the new() implementation. Functionally it’s fine as‑is; this would just be a micro‑optimization.

Also applies to: 262-285

forester/Cargo.toml (1)

73-83: Redundant dev-dependency on light-compressed-token-sdk

You already depend on light-compressed-token-sdk in [dependencies], so adding it again in [dev-dependencies] with the same workspace spec is redundant. Unless tests need a different feature set than the main binary, you can drop the dev-dependency entry and let tests use the normal dependency.

forester/tests/e2e_test.rs (1)

35-39: Address formatting issue flagged by linter

The pipeline failure indicates a formatting inconsistency around the TokenDataVersion import. Please run the project's formatter to resolve the import ordering issue.

sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)

204-210: Consider adding documentation to clarify when to use each derivation function.

Two address derivation paths now exist:

  • derive_compressed_mint_address: seed → SPL mint → compressed mint (lines 193-202)
  • derive_cmint_from_spl_mint: SPL mint → compressed mint (lines 204-210)

The new function is useful when you already have the SPL mint pubkey, but the naming abbreviation "cmint" is less clear than the full "compressed_mint".

Consider adding doc comments to clarify usage:

+/// Derives the compressed mint address directly from an SPL mint pubkey.
+/// Use this when you already have the SPL mint pubkey.
+/// For seed-based derivation, use `derive_compressed_mint_address`.
 pub fn derive_cmint_from_spl_mint(mint: &Pubkey, address_tree_pubkey: &Pubkey) -> [u8; 32] {
sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (1)

37-48: Fix hardcoded SPL_TOKEN_PROGRAM_ID - breaks Token-2022 compatibility.

Line 46 hardcodes SPL_TOKEN_PROGRAM_ID with a TODO comment, but the correct token program ID should be extracted from the source token account. This breaks compatibility with Token-2022 and other token program variants.

The token account data already includes the program owner. Apply this fix:

-    let mint = pod_account.mint;
+    let mint = pod_account.mint;
+    let token_program_id = token_account_info.owner;

     let (token_pool_pda, bump) = find_token_pool_pda_with_index(&mint, 0);

     let ix = create_transfer_spl_to_ctoken_instruction(
         source_spl_token_account,
         to,
         amount,
         authority.pubkey(),
         mint,
         payer.pubkey(),
         token_pool_pda,
         bump,
-        Pubkey::new_from_array(SPL_TOKEN_PROGRAM_ID), // TODO: make dynamic
+        token_program_id,
     )
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)

8-38: Fix instruction layout: remove dummy account, align with SDK helper, and correct discriminator/authority writable flag

The review identifies three real issues confirmed by comparing implementations:

  1. Dummy Pubkey::default() account will cause transaction failure
    Token-client adds AccountMeta::new_readonly(Pubkey::default(), false) as a fourth account. This pubkey does not exist on Solana, so the transaction will fail with AccountNotFound before the program executes.

  2. Authority should be read-only, not writable
    Token-client: AccountMeta::new(authority, true) (writable + signer)
    SDK: AccountMeta::new_readonly(authority, true) (read-only + signer)
    Authority only needs to authorize the transfer, not modify state. The mismatch breaks ABI compatibility.

  3. Data layout diverges from canonical SDK helper
    Token-client emits: [3u8, amount.to_le_bytes()]
    SDK emits: [3u8, 3u8, amount.to_le_bytes()] (two discriminators)
    The on-chain program expects the SDK layout; token-client's instruction will be parsed incorrectly.

Remove the local helper in sdk-libs/token-client/src/actions/ctoken_transfer.rs (lines 40–75) and replace the call at line 29 with the canonical SDK helper from compressed-token-sdk. This ensures client and program stay in lockstep and eliminates the risk of silent ABI drift.

sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)

40-90: ---

create_ctoken_account_signed: account ordering/count mismatch will cause runtime panic; seed handling is fragile

Verification of Solana CPI semantics confirms two serious issues:

  1. AccountMeta vs AccountInfo count/ordering mismatch (Lines 76–83 vs 160–168) — CRITICAL
    create_compressible_token_account_instruction builds an Instruction with six AccountMetas:

    1. token_account (signer)
    2. mint
    3. payer (signer)
    4. compressible_config
    5. system_program (readonly)
    6. rent_sponsor

    But create_ctoken_account_signed invokes with only five AccountInfos in a different order:

    solana_cpi::invoke_signed(
        &ix,
        &[payer, token_account, mint_account, ctoken_rent_sponsor, ctoken_config_account],
        &[signer_seeds],
    )

    Solana's runtime enforces that the AccountInfo slice must align in order and length with Instruction.accounts. The runtime will panic with "Missing or not enough accounts passed to invoke" if the count doesn't match. With the current ordering and count mismatch, the callee will receive accounts in the wrong positions, leading to certain runtime failure.

    Reorder to match the Instruction and add system_program if required:

    solana_cpi::invoke_signed(
        &ix,
        &[
            token_account,
            mint_account,
            payer,
            ctoken_config_account,
            system_program,          // 5. system_program
            ctoken_rent_sponsor,     // 6. rent_sponsor
        ],
        &[signer_seeds],
    )
  2. Signer seeds / bump handling is fragile (Lines 134–138, 149–153)

    • let bump = signer_seeds[signer_seeds.len() - 1][0]; will panic if signer_seeds is empty and assumes the last seed is exactly one byte.
    • No validation of signer_seeds length or the shape of the final seed element.

    Add defensive checks and document the expected layout (e.g., final seed must be &[bump]), or return a TokenSdkError instead of panicking.

sdk-libs/client/src/indexer/types.rs (1)

741-813: Factor out shared logic between TokenAccount and TokenAccountV2 conversions

The two TryFrom<&photon_api::models::TokenAccount> and TryFrom<&photon_api::models::TokenAccountV2> impls are effectively identical except for the input type. This duplication makes it easy for the implementations to drift if one is updated (e.g., handling new AccountState variants or TLV changes) and the other is not.

Consider extracting a small helper (e.g., a generic over the photon model or a private function that takes a trait/struct with the needed fields) so both conversions share exactly the same path.

sdk-libs/compressed-token-sdk/src/account2.rs (4)

21-51: Enforce homogeneous mint/owner invariants and use a dedicated error for empty inputs

CTokenAccount2::new and CTokenAccount2::new_delegated both rely on the first element’s mint, owner, and version, and the comments state that all mint indices and owners “must be the same”. However, the code never checks this; mixed inputs will silently produce an inconsistent output, which can later generate invalid transfer instructions that are hard to diagnose.

You’re also using TokenSdkError::InsufficientBalance as a placeholder error when token_data is empty, which conflates “no inputs provided” with “not enough funds”.

I’d strongly recommend:

  • Adding explicit runtime checks that all token_data entries share the same mint and owner (and perhaps version), returning a clear error if not.
  • Introducing a more specific error variant for the “no inputs” case (e.g., InvalidInputs or EmptyInputs) and using that instead of InsufficientBalance.

This makes the API’s invariants explicit and prevents subtle instruction construction bugs.

Also applies to: 57-87


139-165: Clarify and consistently use the delegate_is_set/method_used flags

delegate_is_set and method_used are written in several methods but never read inside this module, and their semantics are currently inconsistent:

  • new_delegated creates an output with has_delegate = true but sets delegate_is_set: false.
  • approve returns a new CTokenAccount2 with delegate_is_set: true.
  • method_used is set to true on the mutably-borrowed self but reset to false on the returned Self, so it does not reliably indicate “this instance has already been used”.

If these flags are intended to enforce a “single-use builder” pattern or to distinguish delegated vs. non-delegated flows, they should be:

  • Set consistently (new_delegated probably wants delegate_is_set: true).
  • Either enforced inside this type (e.g., panic/return error if a second method is called) or removed if the state is unused.

Right now they’re at risk of becoming misleading dead state that future code might misinterpret.


169-346: Compression helpers guard double-use but still rely on caller discipline; consider stronger invariants and overflow safety

The compression/decompression methods (compress_ctoken, compress_spl, decompress_ctoken, decompress_spl, compress_full, compress_and_close) correctly prevent multiple compression operations per CTokenAccount2 instance via the self.compression.is_some() check, which is good.

There are a few remaining sharp edges worth tightening:

  • All compression methods do self.output.amount += amount (or add the full SPL balance) without checked arithmetic. In extreme cases (many inputs or very large amounts) this could wrap the u64 amount silently. Using checked_add (and returning an error on overflow) would make this safer.
  • The comments explicitly say some methods “must not be used in combination” with others (e.g., transfer vs. compress*, compress* vs. decompress*), but this is only documented, not enforced. A state-machine-style API (consuming self or asserting !self.method_used) would make misuse impossible rather than just discouraged in comments.

If you expect these helpers to be used by many call sites, baking these invariants into the type will prevent subtle value and instruction mismatches down the line.


362-376: Defensive indexing in mint/owner helper methods

mint and owner index directly into the account_infos slice using self.mint/self.owner (via Deref to MultiTokenTransferOutputData):

pub fn mint(&self, account_infos: &[AccountInfo]) -> Pubkey {
    *account_infos[self.mint as usize].key
}
pub fn owner(&self, account_infos: &[AccountInfo]) -> Pubkey {
    *account_infos[self.owner as usize].key
}

If the provided account_infos slice is too short or mis-ordered, these will panic. Elsewhere (e.g., in Unpack impls) you already treat out-of-bounds indices as ProgramError::InvalidAccountData, which is a nicer failure mode.

If these helpers are used in on-chain or production code (not just tests), consider switching to .get(..).ok_or(..)? and returning a Result<Pubkey, TokenSdkError> instead, to propagate a structured error rather than panicking.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f30d22b and b0c20f0.

⛔ Files ignored due to path filters (56)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/cli-v1.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • cli/src/commands/token-balance/index.ts is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/account-compression-test/Cargo.toml is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/create.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/shared.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/mint/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/v1.rs is excluded by none and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • program-tests/system-cpi-test/Cargo.toml is excluded by none and included by none
  • program-tests/system-cpi-test/tests/test.rs is excluded by none and included by none
  • program-tests/utils/src/assert_mint_to_compressed.rs is excluded by none and included by none
  • program-tests/utils/src/assert_token_tx.rs is excluded by none and included by none
  • program-tests/utils/src/assert_transfer2.rs is excluded by none and included by none
  • program-tests/utils/src/conversions.rs is excluded by none and included by none
  • program-tests/utils/src/e2e_test_env.rs is excluded by none and included by none
  • program-tests/utils/src/spl.rs is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/client-test/tests/light_client.rs is excluded by none and included by none
  • sdk-tests/client-test/tests/light_program_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/constants.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/mod.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/user_record_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_four_transfer2.rs is excluded by none and included by none
📒 Files selected for processing (73)
  • forester/Cargo.toml (1 hunks)
  • forester/tests/e2e_test.rs (1 hunks)
  • forester/tests/legacy/batched_state_async_indexer_test.rs (1 hunks)
  • program-libs/batched-merkle-tree/Cargo.toml (3 hunks)
  • program-libs/compressed-account/Cargo.toml (1 hunks)
  • program-libs/compressed-account/src/instruction_data/zero_copy_set.rs (0 hunks)
  • program-libs/compressible/src/config.rs (1 hunks)
  • program-libs/ctoken-types/Cargo.toml (1 hunks)
  • programs/compressed-token/program/src/shared/token_input.rs (1 hunks)
  • sdk-libs/client/Cargo.toml (3 hunks)
  • sdk-libs/client/src/constants.rs (1 hunks)
  • sdk-libs/client/src/indexer/types.rs (2 hunks)
  • sdk-libs/client/src/rpc/lut.rs (1 hunks)
  • sdk-libs/client/src/rpc/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/Cargo.toml (1 hunks)
  • sdk-libs/compressed-token-sdk/src/account2.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/error.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (4 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/mod.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs (0 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/pack.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/token_pool.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/utils.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/tests/pack_test.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/lib.rs (0 hunks)
  • sdk-libs/compressed-token-types/src/token_data.rs (0 hunks)
  • sdk-libs/compressible-client/Cargo.toml (1 hunks)
  • sdk-libs/compressible-client/src/get_compressible_account.rs (1 hunks)
  • sdk-libs/compressible-client/src/lib.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (3 hunks)
  • sdk-libs/program-test/src/compressible.rs (1 hunks)
  • sdk-libs/program-test/src/indexer/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/indexer/test_indexer.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/mod.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
  • sdk-libs/program-test/src/utils/mod.rs (1 hunks)
  • sdk-libs/program-test/src/utils/simulation.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (3 hunks)
  • sdk-libs/sdk/src/account.rs (1 hunks)
  • sdk-libs/sdk/src/address.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/close.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (1 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
  • sdk-libs/sdk/src/proof.rs (1 hunks)
  • sdk-libs/sdk/src/token.rs (0 hunks)
  • sdk-libs/token-client/Cargo.toml (1 hunks)
  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1 hunks)
  • sdk-libs/token-client/src/actions/ctoken_transfer.rs (6 hunks)
  • sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3 hunks)
  • sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (4 hunks)
  • sdk-libs/token-client/src/instructions/create_spl_mint.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_action.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2 hunks)
  • sdk-libs/token-client/src/lib.rs (1 hunks)
💤 Files with no reviewable changes (5)
  • program-libs/compressed-account/src/instruction_data/zero_copy_set.rs
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs
  • sdk-libs/compressed-token-types/src/lib.rs
  • sdk-libs/compressed-token-types/src/token_data.rs
  • sdk-libs/sdk/src/token.rs
🧰 Additional context used
🧬 Code graph analysis (38)
sdk-libs/program-test/src/program_test/test_rpc.rs (1)
sdk-libs/program-test/src/compressible.rs (1)
  • auto_compress_program_pdas (171-239)
sdk-libs/program-test/src/utils/mod.rs (1)
sdk-libs/program-test/src/utils/simulation.rs (1)
  • simulate_cu (12-36)
sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (1)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • packed_accounts (348-351)
sdk-libs/sdk/src/compressible/compress_account.rs (2)
sdk-libs/sdk/src/account.rs (5)
  • account (583-585)
  • account (647-649)
  • address (298-300)
  • new_empty (380-424)
  • new_empty (768-813)
sdk-libs/sdk/src/address.rs (2)
  • derive_address (89-99)
  • derive_address (169-179)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (2)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/sdk/src/cpi/invoke.rs (2)
  • invoke (13-13)
  • invoke (39-84)
sdk-libs/sdk/src/address.rs (1)
program-libs/compressed-account/src/address.rs (1)
  • derive_address (14-26)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
sdk-libs/program-test/src/program_test/config.rs (1)
  • new (60-69)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (5)
sdk-libs/sdk/src/cpi/invoke.rs (2)
  • invoke (13-13)
  • invoke (39-84)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • transfer_ctoken_signed (54-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs (1)
  • create_transfer2_instruction (71-141)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • is_ctoken_account (22-37)
sdk-libs/compressed-token-sdk/src/account2.rs (6)
  • mint (362-364)
  • new (23-51)
  • compress_spl (193-220)
  • decompress_ctoken (224-247)
  • compress_ctoken (169-190)
  • decompress_spl (250-279)
program-libs/compressible/src/config.rs (1)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/client/src/rpc/mod.rs (1)
sdk-libs/client/src/rpc/lut.rs (1)
  • load_lookup_table (20-37)
sdk-libs/token-client/src/instructions/mint_action.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/mint_action/instruction.rs (1)
  • create_mint_action (444-446)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (2)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (6)
  • create_compressed_mint (186-190)
  • create_compressed_mint_cpi (41-106)
  • create_compressed_mint_cpi_write (123-183)
  • derive_cmint_from_spl_mint (204-210)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • derive_cmint_from_spl_mint (204-210)
sdk-libs/compressed-token-sdk/src/token_pool.rs (2)
  • derive_token_pool (29-36)
  • find_token_pool_pda_with_index (13-21)
sdk-libs/sdk/src/proof.rs (1)
program-libs/compressed-account/src/instruction_data/compressed_proof.rs (6)
  • default (33-39)
  • new (89-91)
  • from (101-103)
  • from (107-109)
  • from (112-114)
  • from (118-120)
sdk-libs/program-test/src/compressible.rs (2)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (48-50)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (284-390)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)
  • create_compressible_token_account_instruction (40-90)
sdk-libs/compressed-token-sdk/src/utils.rs (2)
sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (1)
  • pod_from_bytes (30-30)
sdk-libs/compressed-token-sdk/src/error.rs (3)
  • from (75-77)
  • from (81-83)
  • from (87-119)
sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1)
  • create_transfer_spl_to_ctoken_instruction (23-87)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
sdk-libs/sdk/src/account.rs (6)
  • account (583-585)
  • account (647-649)
  • new_close (426-435)
  • new_close (820-828)
  • size (142-142)
  • size (306-311)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (35-35)
  • new_decompressed (74-79)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (2)
program-libs/compressed-account/src/instruction_data/traits.rs (2)
  • bump (58-58)
  • seed (67-67)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/compressible-client/src/get_compressible_account.rs (2)
sdk-libs/sdk/src/address.rs (2)
  • derive_address (89-99)
  • derive_address (169-179)
sdk-libs/client/src/indexer/types.rs (1)
  • from (564-581)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (2)
  • create_transfer_ctoken_instruction (52-76)
  • transfer_ctoken (20-38)
sdk-libs/sdk/src/lib.rs (5)
program-libs/compressed-account/src/instruction_data/traits.rs (1)
  • proof (56-56)
program-libs/compressed-account/src/instruction_data/with_readonly.rs (1)
  • proof (470-472)
program-libs/compressed-account/src/instruction_data/with_account_info.rs (1)
  • proof (570-572)
program-libs/compressed-account/src/instruction_data/zero_copy.rs (2)
  • proof (455-457)
  • proof (603-605)
sdk-libs/sdk/src/compressible/config.rs (3)
  • process_initialize_compression_config_account_info (150-240)
  • process_initialize_compression_config_checked (411-446)
  • process_update_compression_config (256-315)
sdk-libs/program-test/src/program_test/compressible_setup.rs (1)
sdk-libs/compressible-client/src/lib.rs (2)
  • initialize_compression_config (75-124)
  • update_compression_config (127-163)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • create_transfer_ctoken_instruction (18-39)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (3)
program-libs/compressed-account/src/instruction_data/traits.rs (6)
  • data (26-33)
  • data (86-86)
  • data (107-107)
  • address (83-83)
  • address (104-104)
  • address_merkle_tree_account_index (69-69)
sdk-libs/sdk/src/account.rs (4)
  • account (583-585)
  • account (647-649)
  • address (298-300)
  • new_init (250-272)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (35-35)
  • new_decompressed (74-79)
sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (2)
  • create_transfer_ctoken_to_spl_instruction (91-160)
  • transfer_ctoken_to_spl (252-290)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/token-client/src/instructions/create_spl_mint.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/compressible-client/src/lib.rs (3)
program-libs/compressed-account/src/instruction_data/traits.rs (4)
  • discriminator (20-20)
  • data (26-33)
  • data (86-86)
  • data (107-107)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (48-50)
sdk-libs/program-test/src/compressible.rs (1)
  • new (49-69)
sdk-libs/client/src/indexer/types.rs (1)
sdk-libs/compressed-token-sdk/src/pack.rs (4)
  • from (72-81)
  • from (148-162)
  • from (166-175)
  • try_from (87-93)
sdk-libs/compressed-token-sdk/src/instructions/mod.rs (4)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
  • transfer_ctoken (20-38)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • transfer_ctoken_signed (54-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (2)
  • transfer_interface (354-437)
  • transfer_interface_signed (441-539)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3)
  • create_compressible_token_account_instruction (40-90)
  • create_ctoken_account_signed (122-171)
  • create_token_account (92-118)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • get_token_account_balance (11-20)
sdk-libs/sdk/src/compressible/mod.rs (6)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-40)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • compression_info (35-35)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
  • prepare_account_for_compression (30-115)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1)
  • prepare_compressed_account_on_init (35-102)
sdk-libs/sdk/src/compressible/config.rs (3)
  • process_initialize_compression_config_account_info (150-240)
  • process_initialize_compression_config_checked (411-446)
  • process_update_compression_config (256-315)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • into_compressed_meta_with_address (23-42)
  • prepare_account_for_decompression_idempotent (80-141)
sdk-libs/compressed-token-sdk/src/pack.rs (1)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/compressed-token-sdk/src/ctoken.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (3)
  • create_compressed_mint (186-190)
  • derive_cmint_from_spl_mint (204-210)
  • find_spl_mint_address (212-217)
program-libs/compressible/src/config.rs (3)
  • ctoken_v1_config_pda (248-250)
  • ctoken_v1_rent_sponsor_pda (283-285)
  • ctoken_v1_compression_authority_pda (275-281)
sdk-libs/compressed-token-sdk/src/lib.rs (3)
sdk-libs/compressed-token-sdk/src/pack.rs (5)
  • pack (17-17)
  • pack (30-43)
  • pack (181-194)
  • pack (266-271)
  • pack (297-302)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • pack (15-15)
sdk-libs/compressed-token-sdk/src/utils.rs (2)
  • account_meta_from_account_info (48-54)
  • is_ctoken_account (22-37)
🪛 GitHub Actions: lint
forester/tests/e2e_test.rs

[warning] 35-35: Diff in e2e_test.rs:35 detected. The code shows a swap of TokenDataVersion import order. This appears to be a formatting/consistency issue.

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Forester e2e test
  • GitHub Check: Test batched-merkle-tree-simulate
  • GitHub Check: Test program-libs-fast

"solana-msg",
"light-zero-copy/solana",
"light-hasher/solana",
"light-hasher/keccak",
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Clarify the feature enablement pattern—redundancy between direct features and feature gates.

The keccak support is being enabled in two conflicting ways:

  1. Direct dependency features (lines 49, 57, 64): Always enable keccak
  2. Feature gates (lines 20, 25, 36): Enable keccak only when solana/pinocchio features are selected

If keccak should always be available, the feature gates become redundant. If it should be conditional, the direct feature flags are incorrect. The current pattern is ambiguous and may lead to unexpected behavior or confusion during maintenance.

Additionally, pinocchio is missing light-hasher/keccak (line 31 has no keccak entry), while solana includes it (line 20). This asymmetry suggests either pinocchio should also declare keccak support, or there's a reason pinocchio intentionally excludes it.

Please clarify:

  1. Should keccak be unconditionally enabled (remove feature gates, keep direct features)?
  2. Should keccak be conditionally enabled (remove direct features, keep feature gates)?
  3. Should pinocchio match solana's feature coverage by adding "light-hasher/keccak" on line 31?

Also applies to: 25-25, 36-36, 49-49, 57-57, 64-64


🏁 Script executed:

#!/bin/bash
# Examine the full Cargo.toml file to verify feature flag structure
cat program-libs/batched-merkle-tree/Cargo.toml

Length of output: 2321


🏁 Script executed:

#!/bin/bash
# Check if there are any docs or comments explaining feature flag patterns
find program-libs/batched-merkle-tree -type f \( -name "*.md" -o -name "CLAUDE.md" \) -exec cat {} \;

Length of output: 74899


Remove redundant keccak feature gates; the unconditional model is already in place.

The review comment correctly identified redundancy. Your Cargo.toml unconditionally enables keccak for both light-hasher (line 18: features = ["poseidon", "keccak"]) and light-compressed-account (line 24: features = ["std", "keccak"]). The feature gate entries on lines 20, 25, and 36 are redundant—they're requesting features that are already always enabled.

The asymmetry between solana (includes "light-hasher/keccak" on line 20) and pinocchio (omits it on line 31) doesn't cause a functional problem since keccak is unconditional anyway, but it adds unnecessary confusion during maintenance.

To clarify intent: Remove the redundant feature gates ("light-hasher/keccak" from lines 20 and 36; "light-compressed-account/keccak" from lines 25 and 36), since keccak is already unconditionally enabled in [dependencies]. The code already implements the unconditional model—these gates are just noise.

Lines affected: 20, 25, 36

🤖 Prompt for AI Agents
In program-libs/batched-merkle-tree/Cargo.toml around lines 20, 25 and 36,
remove the redundant feature gate entries referencing keccak—specifically delete
"light-hasher/keccak" on lines 20 and 36 and "light-compressed-account/keccak"
on line 25—because keccak is already unconditionally enabled in [dependencies];
simply remove those three feature strings so the features section no longer
contains redundant keccak entries.

solana-signature = { workspace = true }
solana-commitment-config = { workspace = true }
solana-account = { workspace = true }
solana-signer = { workspace = true }
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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

Verify that these new dependencies are used in the codebase.

The solana-signer and light-compressed-token-sdk dependencies are added but not imported in any of the files under review. While the AI summary mentions a broader refactoring involving light_compressed_token_sdk::compat, these dependencies should be verified to ensure they're actually required by this crate.

Also applies to: 48-48


🏁 Script executed:

#!/bin/bash
# Verify solana-signer usage
echo "=== Checking solana-signer usage ==="
rg -n --type rust "use.*solana_signer" sdk-libs/client/src/

# Verify light-compressed-token-sdk usage  
echo "=== Checking light-compressed-token-sdk usage ==="
rg -n --type rust "use.*light_compressed_token" sdk-libs/client/src/

Length of output: 192



Wait, I realize I need the repo cloned for that. Let me fix it:



</thinking>

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if solana-signer is used indirectly or in re-exports
echo "=== Checking all Rust files for solana_signer (broader search) ==="
rg -nP "solana_signer" sdk-libs/client/src/ 2>/dev/null || echo "No matches"

# Check Cargo.toml to see exact dependency declarations
echo ""
echo "=== Viewing sdk-libs/client/Cargo.toml around line 30 and 48 ==="
sed -n '25,55p' sdk-libs/client/Cargo.toml

# Check if solana-signer appears in feature flags
echo ""
echo "=== Checking for solana-signer in optional features ==="
rg -nP "solana-signer|solana_signer" sdk-libs/client/Cargo.toml

Length of output: 1433


🏁 Script executed:

#!/bin/bash
# Check if solana-signer is used indirectly or in re-exports
echo "=== Checking all Rust files for solana_signer (broader search) ==="
rg -nP "solana_signer" sdk-libs/client/src/ 2>/dev/null || echo "No matches"

# Check Cargo.toml to see exact dependency declarations
echo ""
echo "=== Viewing sdk-libs/client/Cargo.toml around line 30 and 48 ==="
sed -n '25,55p' sdk-libs/client/Cargo.toml

# Check if solana-signer appears in feature flags
echo ""
echo "=== Checking for solana-signer in optional features ==="
rg -nP "solana-signer|solana_signer" sdk-libs/client/Cargo.toml

Length of output: 1433


🏁 Script executed:

#!/bin/bash
# View the Cargo.toml file around lines 30 and 48
echo "=== sdk-libs/client/Cargo.toml (lines 25-55) ==="
sed -n '25,55p' sdk-libs/client/Cargo.toml

echo ""
echo "=== Checking for solana-signer in entire Cargo.toml ==="
rg -n "solana-signer" sdk-libs/client/Cargo.toml

echo ""
echo "=== Checking for solana-signer in feature definitions ==="
rg -n -A5 "\[features\]" sdk-libs/client/Cargo.toml | head -20

Length of output: 1515


Remove the unused solana-signer dependency from line 30.

Verification confirms that solana-signer is not used anywhere in the codebase—no direct or indirect imports found. However, light-compressed-token-sdk is correctly used: it's imported in sdk-libs/client/src/indexer/types.rs:9 for AccountState and TokenData types, so that dependency should remain.

Remove solana-signer = { workspace = true } from line 30 of sdk-libs/client/Cargo.toml.

🤖 Prompt for AI Agents
In sdk-libs/client/Cargo.toml around line 30, remove the unused dependency entry
"solana-signer = { workspace = true }" (delete that line), save the file, and
run cargo build to ensure the manifest remains valid; keep the existing
light-compressed-token-sdk dependency unchanged.

Comment on lines +1 to +3
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
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 | 🔵 Trivial

Consider narrowing the public re-exports.

Re-exporting error, instruction, and program from solana_address_lookup_table_interface expands the public API surface. Unless these are explicitly needed by consumers of this module, consider re-exporting only AddressLookupTable which is used internally.

 pub use solana_address_lookup_table_interface::{
-    error, instruction, program, state::AddressLookupTable,
+    state::AddressLookupTable,
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
pub use solana_address_lookup_table_interface::{
state::AddressLookupTable,
};
🤖 Prompt for AI Agents
In sdk-libs/client/src/rpc/lut.rs around lines 1 to 3, the file currently
re-exports error, instruction, and program from
solana_address_lookup_table_interface which unnecessarily widens the public API;
modify the exports to only publicly re-export state::AddressLookupTable (e.g.
pub use solana_address_lookup_table_interface::state::AddressLookupTable;) and
remove the public re-exports of error, instruction, and program (or make them
pub(crate) if internal consumers in the crate need them) so the module exposes
only the required type.

Comment on lines +25 to +27
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
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 | 🔵 Trivial

Enhance error message with lookup table address.

The error message for deserialization failure would be more useful for debugging if it included the lookup table address that failed.

-    let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
-        RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
-    })?;
+    let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
+        RpcError::CustomError(format!(
+            "Failed to deserialize AddressLookupTable at {lookup_table_address}: {e:?}"
+        ))
+    })?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!(
"Failed to deserialize AddressLookupTable at {lookup_table_address}: {e:?}"
))
})?;
🤖 Prompt for AI Agents
In sdk-libs/client/src/rpc/lut.rs around lines 25 to 27, the deserialization
error message for AddressLookupTable lacks the lookup-table address; update the
map_err closure to include the lookup table identifier (the Pubkey or string
variable you used to fetch raw_account, e.g., lookup_table_pubkey or
lookup_table_address) in the formatted message so it becomes something like
"Failed to deserialize AddressLookupTable for {lookup_table_address}: {e:?}",
ensuring the address variable is in scope and included in the error string.

Comment on lines +5 to +18
pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");

pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy");

/// Returns the program ID for the Compressed Token Program
pub fn id() -> Pubkey {
CTOKEN_PROGRAM_ID
}

/// Return the cpi authority pda of the Compressed Token Program.
pub fn cpi_authority() -> Pubkey {
CTOKEN_CPI_AUTHORITY
}

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 | 🔵 Trivial

Centralizing CTOKEN program IDs is good; verify consistency with types crate

Defining CTOKEN_PROGRAM_ID and CTOKEN_CPI_AUTHORITY here and providing id() / cpi_authority() accessors is a solid way to avoid sprinkling literals across the codebase.

Given other modules still reference light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID, please ensure those values match this CTOKEN_PROGRAM_ID; otherwise PDAs derived here (and in CompressibleConfig::ctoken_v1_*) could diverge from those constructed via the types crate.

If they are already identical, consider re-exporting or aliasing rather than re-encoding the literal in multiple places to prevent drift.

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/ctoken.rs around lines 5 to 18, the file
re-defines CTOKEN_PROGRAM_ID and CTOKEN_CPI_AUTHORITY as hard-coded Pubkey
literals which may diverge from light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID;
verify that the values match the corresponding constants in the types crate and
if they do, replace these duplicated literals with a direct re-export or alias
(e.g., pub use light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID as
CTOKEN_PROGRAM_ID) or initialize these constants from the types crate instead of
re-encoding the literal; if they differ, update the constants to the canonical
values in the types crate and run cargo check/tests to ensure PDAs derived
elsewhere remain consistent.

Comment on lines +1 to +42
#![allow(clippy::all)] // TODO: Remove.

use light_compressed_account::address::derive_address;
use light_sdk_types::instruction::account_meta::{
CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress,
};
use solana_account_info::AccountInfo;
use solana_cpi::invoke_signed;
use solana_msg::msg;
use solana_pubkey::Pubkey;
use solana_system_interface::instruction as system_instruction;
use solana_sysvar::{rent::Rent, Sysvar};

use crate::{
account::sha::LightAccount, compressible::compression_info::HasCompressionInfo,
cpi::v2::CpiAccounts, error::LightSdkError, AnchorDeserialize, AnchorSerialize,
LightDiscriminator,
};

/// Convert a `CompressedAccountMetaNoLamportsNoAddress` to a
/// `CompressedAccountMeta` by deriving the compressed address from the solana
/// account's pubkey.
pub fn into_compressed_meta_with_address<'info>(
compressed_meta_no_lamports_no_address: &CompressedAccountMetaNoLamportsNoAddress,
solana_account: &AccountInfo<'info>,
address_space: Pubkey,
program_id: &Pubkey,
) -> CompressedAccountMeta {
let derived_c_pda = derive_address(
&solana_account.key.to_bytes(),
&address_space.to_bytes(),
&program_id.to_bytes(),
);

let meta_with_address = CompressedAccountMeta {
tree_info: compressed_meta_no_lamports_no_address.tree_info,
address: derived_c_pda,
output_state_tree_index: compressed_meta_no_lamports_no_address.output_state_tree_index,
};

meta_with_address
}
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 | 🔵 Trivial

Address-derivation helper looks fine; clippy allow should be temporary

  • into_compressed_meta_with_address correctly wraps derive_address and reuses the existing tree_info / output_state_tree_index. As long as the (account_pubkey, address_space, program_id) argument order matches the rest of your compressed address scheme, this is a straightforward and useful helper.
  • The crate‑wide #![allow(clippy::all)] is fine as a temporary measure while the module is in flux, but do plan to either remove it or narrow it to specific lints once things settle. Keeping clippy enabled here will pay off quickly given the amount of generic & CPI logic in this file.
🤖 Prompt for AI Agents
sdk-libs/sdk/src/compressible/decompress_idempotent.rs lines 1-42: remove or
narrow the crate-wide `#![allow(clippy::all)]` directive — replace it by
removing it entirely or by allowing only the specific clippy lints you need
(e.g., `#![allow(clippy::some_lint, clippy::other_lint)]`) so clippy runs for
the rest of the crate; update Cargo/CI or run `cargo clippy` locally to identify
which specific lints to allow and then commit the narrower allow or no allow at
all.

Comment on lines +76 to +141
/// Helper function to decompress a compressed account into a PDA
/// idempotently with seeds.
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_idempotent<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_payer: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
signer_seeds: &[&[u8]],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ HasCompressionInfo
+ 'info,
{
if !solana_account.data_is_empty() {
msg!("Account already initialized, skipping");
return Ok(None);
}
let rent = Rent::get().map_err(|err| {
msg!("Failed to get rent: {:?}", err);
LightSdkError::Borsh
})?;

let light_account = LightAccount::<T>::new_close(program_id, &compressed_meta, data)?;

let space = T::size(&light_account.account);
let rent_minimum_balance = rent.minimum_balance(space);

invoke_create_account_with_heap(
rent_payer,
solana_account,
rent_minimum_balance,
space as u64,
&cpi_accounts.self_program_id(),
signer_seeds,
cpi_accounts.system_program()?,
)?;

let mut decompressed_pda = light_account.account.clone();
*decompressed_pda.compression_info_mut_opt() =
Some(super::compression_info::CompressionInfo::new_decompressed()?);

let mut account_data = solana_account.try_borrow_mut_data()?;
let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR);
decompressed_pda
.serialize(&mut &mut account_data[discriminator_len..])
.map_err(|err| {
msg!("Failed to serialize decompressed PDA: {:?}", err);
LightSdkError::Borsh
})?;

Ok(Some(light_account.to_account_info()?))
}
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Account size vs discriminator: possible under-allocation in prepare_account_for_decompression_idempotent

The overall flow in prepare_account_for_decompression_idempotent is solid:

  • Idempotency via data_is_empty check.
  • Rent‑exempt balance from Rent::minimum_balance.
  • PDA creation through invoke_create_account_with_heap.
  • Initializing CompressionInfo to Decompressed.
  • Writing T::LIGHT_DISCRIMINATOR followed by serialized decompressed_pda into the account data buffer.

The fragile part is how space is computed:

let light_account = LightAccount::<T>::new_close(program_id, &compressed_meta, data)?;
let space = T::size(&light_account.account);
let rent_minimum_balance = rent.minimum_balance(space);
invoke_create_account_with_heap(
    rent_payer,
    solana_account,
    rent_minimum_balance,
    space as u64,
    &cpi_accounts.self_program_id(),
    signer_seeds,
    cpi_accounts.system_program()?,
)?;
let mut account_data = solana_account.try_borrow_mut_data()?;
let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR);
decompressed_pda
    .serialize(&mut &mut account_data[discriminator_len..])
    .map_err(|err| { /* ... */ })?;

Here we allocate space bytes for the account and then treat the buffer as:

  • first discriminator_len bytes for the discriminator, and
  • the remainder for Borsh‑serialized decompressed_pda.

This is only correct if your Size implementation for T returns discriminator_len + serialized_len(T). If Size is defined as “just the serialized struct size” (which is a common pattern), then:

  • account_data.len() == space == serialized_len(T)
  • account_data[discriminator_len..] has length serialized_len(T) - discriminator_len
  • Borsh serialization will fail at runtime (or, in the worst case, write past the intended layout).

To make this robust regardless of how Size is implemented, I’d recommend explicitly accounting for the discriminator in the allocated space:

-    let space = T::size(&light_account.account);
+    let payload_size = T::size(&light_account.account);
+    let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
+    let space = discriminator_len + payload_size;
@@
-    let rent_minimum_balance = rent.minimum_balance(space);
+    let rent_minimum_balance = rent.minimum_balance(space);
@@
-        rent_minimum_balance,
-        space as u64,
+        rent_minimum_balance,
+        space as u64,
@@
-    let mut account_data = solana_account.try_borrow_mut_data()?;
-    let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
+    let mut account_data = solana_account.try_borrow_mut_data()?;

This keeps the serialized layout ([disc][data]) consistent with the allocated space and avoids subtle dependence on how Size is currently implemented.

Separately, note that you pass:

  • program_id into LightAccount::<T>::new_close, but
  • cpi_accounts.self_program_id() as the owner when creating the account.

Those should effectively represent the same program for this to be coherent; if you ever allow them to differ, it’s worth documenting that invariant or collapsing to one source of truth.


I'll verify this review comment by examining how the Size trait is implemented and used, checking the discriminator handling, and confirming whether the space allocation is indeed problematic.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find Size trait definition and its usage pattern
rg -n "trait Size" --type rust -A 10

Length of output: 788


🏁 Script executed:

#!/bin/bash
# Find LIGHT_DISCRIMINATOR definition and usage
rg -n "LIGHT_DISCRIMINATOR" --type rust -B 2 -A 2 | head -50

Length of output: 3312


🏁 Script executed:

#!/bin/bash
# Check LightDiscriminator trait definition
rg -n "trait LightDiscriminator" --type rust -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Look for similar space calculation patterns in the file
rg -n "let space" sdk-libs/sdk/src/compressible/ --type rust -B 2 -A 5

Length of output: 754


🏁 Script executed:

#!/bin/bash
# Find test files related to decompress_idempotent
fd -e rs "test" sdk-libs/sdk/src/compressible/ | grep -i decompress

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for Size implementations to understand the contract
rg -n "impl.*Size.*for" --type rust -A 3 | head -100

Length of output: 256


Need to understand the trait bounds better and find the actual LIGHT_DISCRIMINATOR definition:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find where LIGHT_DISCRIMINATOR is defined as a const
rg -n "const LIGHT_DISCRIMINATOR|LIGHT_DISCRIMINATOR\s*:" --type rust -B 2 -A 2

Length of output: 3468


🏁 Script executed:

#!/bin/bash
# Search for trait definitions that include LIGHT_DISCRIMINATOR
rg -n "LIGHT_DISCRIMINATOR" --type rust | grep -E "(trait|pub trait)" -A 5

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Find the trait where LIGHT_DISCRIMINATOR comes from (check trait bounds)
rg -n "T::LIGHT_DISCRIMINATOR" --type rust -B 5 -A 2

Length of output: 2748


🏁 Script executed:

#!/bin/bash
# Look at the full function signature and bounds to identify all traits
cat sdk-libs/sdk/src/compressible/decompress_idempotent.rs | grep -A 20 "pub fn prepare_account_for_decompression_idempotent"

Length of output: 626


🏁 Script executed:

#!/bin/bash
# Search for other decompress functions to see if they handle discriminator differently
rg -n "decompress" sdk-libs/sdk/src/compressible/ --type rust -B 1 -A 1 | head -60

Length of output: 3068


🏁 Script executed:

#!/bin/bash
# Find HasCompressionInfo trait definition
rg -n "trait HasCompressionInfo|pub trait HasCompressionInfo" --type rust -A 10

Length of output: 962


🏁 Script executed:

#!/bin/bash
# Search for how accounts are typically serialized with discriminators in this codebase
rg -n "serialize.*discriminator|discriminator.*serialize" --type rust -B 2 -A 2 | head -40

Length of output: 2684


Need to find concrete Size implementations and compare with similar functions:


---

🏁 Script executed:

```shell
#!/bin/bash
# Find concrete Size trait implementations
rg -n "impl Size for|fn size\(" sdk-libs/sdk/src/ --type rust -B 2 -A 5 | head -80

Length of output: 893


🏁 Script executed:

#!/bin/bash
# Look at compress_account_on_init for comparison (similar operation)
cat sdk-libs/sdk/src/compressible/compress_account_on_init.rs | grep -A 40 "let space"

Length of output: 0


🏁 Script executed:

#!/bin/bash
# Search for any comments or TODOs about space/discriminator size
rg -n "TODO|FIXME|XXX|discriminator.*size|size.*discriminator" sdk-libs/sdk/src/compressible/ --type rust

Length of output: 293


🏁 Script executed:

#!/bin/bash
# Check if LightAccount tracks discriminator in size calculations
rg -n "impl.*LightAccount|fn size" sdk-libs/sdk/src/account.rs --type rust -B 2 -A 8 | head -100

Length of output: 723


🏁 Script executed:

#!/bin/bash
# Find the file referenced in decompress_idempotent for other patterns
cat sdk-libs/sdk/src/compressible/decompress_idempotent.rs | head -75

Length of output: 2336


Account space calculation must explicitly account for discriminator length in prepare_account_for_decompression_idempotent

The issue is confirmed. The Size trait has no documentation specifying whether it includes the 8-byte discriminator. Your code writes [discriminator][serialized_data] to the account buffer but allocates only T::size() bytes. If Size implementations return only the serialized struct size (a common pattern), the account will be under-allocated by 8 bytes, causing serialization to fail or overflow.

The fix you suggested is correct and necessary:

-    let space = T::size(&light_account.account);
+    let payload_size = T::size(&light_account.account);
+    let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
+    let space = discriminator_len + payload_size;

This makes the layout invariant explicit: allocated space = discriminator + payload, eliminating fragile dependence on an undocumented trait contract. It aligns with the codebase pattern seen in compressible-client where discriminator and serialized data are treated as separate concerns.

Regarding program_id vs. cpi_accounts.self_program_id(): those should match for coherence, and it's worth documenting that invariant if they're ever allowed to differ.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/decompress_idempotent.rs around lines 76 to
141, the code computes account space using T::size() but then writes an 8-byte
discriminator plus the serialized payload, so the allocation must include the
discriminator; update the space calculation to include
T::LIGHT_DISCRIMINATOR.len() (use let discriminator_len =
T::LIGHT_DISCRIMINATOR.len(); let space = discriminator_len +
T::size(&light_account.account);), recompute rent_minimum_balance from that new
space, and pass the updated space (as u64) into invoke_create_account_with_heap
so the account is correctly sized before serializing into account_data.

Comment on lines +1 to +28
pub mod close;
pub mod compression_info;
pub mod config;

#[cfg(feature = "v2")]
pub mod compress_account;
#[cfg(feature = "v2")]
pub mod compress_account_on_init;
#[cfg(feature = "v2")]
pub mod decompress_idempotent;
#[cfg(feature = "v2")]
pub use close::close;
#[cfg(feature = "v2")]
pub use compress_account::prepare_account_for_compression;
#[cfg(feature = "v2")]
pub use compress_account_on_init::prepare_compressed_account_on_init;
pub use compression_info::{
CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack,
};
pub use config::{
process_initialize_compression_config_account_info,
process_initialize_compression_config_checked, process_update_compression_config,
CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE,
};
#[cfg(feature = "v2")]
pub use decompress_idempotent::{
into_compressed_meta_with_address, prepare_account_for_decompression_idempotent,
};
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 | 🔵 Trivial

Consider aligning close module availability with its re-export gating

Right now close is always compiled (pub mod close;), but the convenience re-export pub use close::close; is only available under cfg(feature = "v2"). That means consumers without the v2 feature can import compressible::close::close but not compressible::close, which is a bit surprising.

Either:

  • gate the module itself behind v2, or
  • expose pub use close::close; unconditionally (if the helper is meant to be generally usable),

so the module and its top-level re-export follow the same feature story.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/mod.rs around lines 1 to 28, the close module
is always compiled but its top-level re-export (pub use close::close;) is gated
by cfg(feature = "v2"), causing mismatched availability; fix by making the
module and its re-export consistent: either add #[cfg(feature = "v2")] above pub
mod close; so the module is only compiled when v2 is enabled, or remove the
cfg(feature = "v2") from the pub use close::close; line so the helper is
exported unconditionally—apply whichever matches intended public API and keep
module and re-export gating identical.

Comment thread sdk-libs/sdk/src/lib.rs
Comment on lines +150 to 151
pub mod proof;
/// Transfer compressed sol between compressed accounts.
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 | 🔵 Trivial

Broader root re-exports are convenient; be aware of API surface commitments

Exposing proof (with borsh_compat) and re-exporting the full set of compressible::* symbols (CompressibleConfig, CompressionInfo, Pack/Unpack, seeds/constants, etc.) at the crate root will make these features much easier for downstream users to discover and use.

The trade-offs to keep in mind:

  • This effectively “blesses” these types and functions as part of the stable light_sdk public API; future refactors in compressible will need to preserve or carefully deprecate these names.
  • The generic names Pack and Unpack at the root may overlap conceptually with other Pack traits (e.g., Solana’s program_pack::Pack). It’s not wrong, but users may appreciate clear docs/examples to avoid confusion.

If that stability commitment is intentional, the layout here looks good and aligns with how other modules in this crate are exposed.

Also applies to: 155-169

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/lib.rs around lines 150-151 (also apply the same changes for
lines 155-169), re-export the proof module's borsh_compat entry and the key
symbols from the compressible module at the crate root so downstream users can
discover them easily; specifically add crate-root pub re-exports for proof
(including its borsh_compat type) and for compressible::{CompressibleConfig,
CompressionInfo, Pack, Unpack, relevant seeds/constants} and update the
crate-level docs/comments to call out these stable exports so maintainers know
these names are part of the public API.

Comment thread sdk-libs/sdk/src/proof.rs
Comment on lines +5 to +87
#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
pub struct CompressedProof {
pub a: [u8; 32],
pub b: [u8; 64],
pub c: [u8; 32],
}

impl Default for CompressedProof {
fn default() -> Self {
Self {
a: [0; 32],
b: [0; 64],
c: [0; 32],
}
}
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
/// Borsh-compatible ValidityProof. Use this in your anchor program unless
/// you have zero-copy instruction data.
pub struct ValidityProof(pub Option<CompressedProof>);

impl ValidityProof {
pub fn new(proof: Option<CompressedProof>) -> Self {
Self(proof)
}
}

impl From<light_compressed_account::instruction_data::compressed_proof::CompressedProof>
for CompressedProof
{
fn from(
proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof,
) -> Self {
Self {
a: proof.a,
b: proof.b,
c: proof.c,
}
}
}

impl From<CompressedProof>
for light_compressed_account::instruction_data::compressed_proof::CompressedProof
{
fn from(proof: CompressedProof) -> Self {
Self {
a: proof.a,
b: proof.b,
c: proof.c,
}
}
}

impl From<light_compressed_account::instruction_data::compressed_proof::ValidityProof>
for ValidityProof
{
fn from(
proof: light_compressed_account::instruction_data::compressed_proof::ValidityProof,
) -> Self {
Self(proof.0.map(|p| p.into()))
}
}

impl From<ValidityProof>
for light_compressed_account::instruction_data::compressed_proof::ValidityProof
{
fn from(proof: ValidityProof) -> Self {
Self(proof.0.map(|p| p.into()))
}
}

impl From<CompressedProof> for ValidityProof {
fn from(proof: CompressedProof) -> Self {
Self(Some(proof))
}
}

impl From<Option<CompressedProof>> for ValidityProof {
fn from(proof: Option<CompressedProof>) -> Self {
Self(proof)
}
}
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 | 🔵 Trivial

Compressed proof compatibility looks correct; consider full parity with upstream conversions

The Borsh-compatible CompressedProof/ValidityProof layout and the From conversions line up with the program-libs/compressed-account::instruction_data::compressed_proof types, so the serialization boundary looks sound. If you want strict API parity (and fewer temporary clones when working with references), consider also adding the From<&CompressedProof> / From<&Option<CompressedProof>> variants that exist on the upstream ValidityProof so callers can convert from references without first copying into owned values.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/proof.rs around lines 5 to 87, add the reference-based
conversion helpers to match upstream parity: implement From<&CompressedProof>
for
light_compressed_account::instruction_data::compressed_proof::CompressedProof
(copy the byte arrays from the referenced value), and implement
From<&Option<CompressedProof>> for ValidityProof (map Some(&p) to Some(p.into())
and None to None) so callers can convert from references without creating owned
temporaries; ensure implementations simply copy the fixed-size arrays and mirror
the existing owned From logic.

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

Caution

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

⚠️ Outside diff range comments (9)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)

204-210: New derive_cmint_from_spl_mint helper looks correct; consider clarifying usage

The helper cleanly parallels derive_compressed_mint_address, but takes an SPL mint directly and forwards it into derive_address. That’s a nice separation between “from mint seed” vs “from SPL mint” flows.

To avoid misuse, consider a short doc comment spelling out when callers should use derive_compressed_mint_address vs derive_cmint_from_spl_mint (e.g., “use this when you already know the SPL mint PDA”).

sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)

222-226: Clarify rent_sponsor_is_signer semantics

Here rent_sponsor_is_signer actually controls whether the authority vs the owner meta is marked as signer:

if rent_sponsor_is_signer {
    packed_account_metas[idx.authority_index as usize].is_signer = true;
} else {
    packed_account_metas[idx.owner_index as usize].is_signer = true;
}

Given how CompressAndCloseIndices is built in pack_for_compress_and_close (authority is the compression authority when using the extension, otherwise the owner), this logic makes sense, but the parameter name is misleading and makes future misuse likely.

I’d recommend:

  • Renaming the parameter to something like compression_authority_is_signer or use_extension_authority, and
  • Updating its documentation accordingly so call sites don’t accidentally flip the semantics.
forester/Cargo.toml (1)

73-83: Duplicate dependency declaration for light-compressed-token-sdk

You list light-compressed-token-sdk = { workspace = true } under both [dependencies] (Line 33) and [dev-dependencies] (Line 82). Cargo will de‑duplicate this, so it’s functionally harmless, but it’s redundant and may confuse readers.

Unless you plan different feature sets for dev vs main, you can drop the [dev-dependencies] entry and keep only the one under [dependencies].

sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (1)

37-47: Instruction creation updated correctly, but TODO needs attention.

The function now uses the new instruction path. However, the hardcoded SPL_TOKEN_PROGRAM_ID at Line 46 limits support to the standard SPL Token program.

Verify whether SPL Token 2022 support is required. If so, consider:

  1. Adding a spl_token_program parameter to the function
  2. Detecting the program from the token account data
  3. Defaulting to standard SPL Token with an override option

Would you like me to generate a refactored version that supports both programs?

sdk-libs/token-client/src/instructions/mint_to_compressed.rs (1)

58-75: Avoid deriving the token‑pool PDA twice

Functionally this is correct, but you currently derive the token‑pool PDA twice when spl_mint_initialized is true:

  • Line 60: find_token_pool_pda_with_index(&spl_mint_pda, 0) for decompressed_mint_config.
  • Line 72: derive_token_pool(&spl_mint_pda, 0), which itself calls find_token_pool_pda_with_index.

You can tighten this up by deriving once and reusing it for both the config and the token_pool struct, e.g.:

let token_pool = if compressed_mint.metadata.spl_mint_initialized {
    let tp = derive_token_pool(&spl_mint_pda, 0);
    let decompressed_mint_config = Some(DecompressedMintConfig {
        mint_pda: spl_mint_pda,
        token_pool_pda: tp.pubkey,
        token_program: spl_token_2022::ID,
    });
    Some((tp, decompressed_mint_config))
} else {
    None
};

(or similar restructuring that avoids an extra PDA derivation call).

sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)

40-75: Unify the ctoken transfer instruction builders across client and on-chain SDKs before production deployment.

Your review comment is precisely accurate. The verification confirms three material divergences:

  1. Data encoding (lines 68–70 vs 32–34): The token-client builder encodes [3u8, amount_le], while the SDK builder encodes [3u8, 3u8, amount_le]. The SDK itself carries a TODO: "check why we have 2 discriminators"—explicitly flagging uncertainty about the second discriminator's purpose. The token-client omits it entirely.

  2. Account metadata structure: Token-client includes four accounts (lines 60–65), with authority as writable (AccountMeta::new). The SDK includes three accounts (lines 24–27), with authority as readonly (AccountMeta::new_readonly). Additionally, token-client adds a Pubkey::default() readonly account that the SDK does not have.

  3. Authority mutability: This is the most concerning divergence. Marking authority as writable in token-client but readonly in the SDK means these code paths will behave differently if the program accepts or processes authority mutations. This could silently fail or cause protocol drift.

Both codebases acknowledge the issue: the token-client explicitly states // TODO: consume the variant from compressed-token-sdk instead, and the SDK has its own TODO questioning the dual discriminators. This is known technical debt.

Recommendation: Export and use a single, unified builder from compressed-token-sdk for both CPI and client paths. If you must maintain separate implementations for now, add an integration test that byte-compares the exact instruction data and account metas produced by both paths to prevent silent divergence.

sdk-libs/client/src/indexer/types.rs (1)

815-864: Compat conversions are correct; you can tighten them up a bit

The new Into/TryFrom impls against light_compressed_token_sdk::compat::TokenDataWithMerkleContext look semantically correct and line up with the existing CompressedAccount <-> CompressedAccountWithMerkleContext conversions.

Two small cleanups to consider:

  • In the Into<Vec<...>> impl, token_account comes from into_iter(), so token_account.account.clone() is unnecessary; you can move it directly:
-                    compressed_account: CompressedAccountWithMerkleContext::from(
-                        token_account.account.clone(),
-                    ),
+                    compressed_account:
+                        CompressedAccountWithMerkleContext::from(token_account.account),
  • To reduce verbosity, you can use light_compressed_token_sdk::compat::TokenDataWithMerkleContext; at the top and then write the impls against TokenDataWithMerkleContext and Vec<TokenDataWithMerkleContext> instead of fully‑qualified paths everywhere.

These are purely ergonomic improvements; behavior stays the same.

sdk-libs/compressed-token-sdk/src/lib.rs (1)

11-22: Public use statements required for crate-wide visibility in derive macros

The review comment is accurate. Lines 11–15 use plain use instead of pub use, which means AnchorDeserialize and AnchorSerialize exist only within lib.rs module scope.

However, actual code in the codebase references these through the crate root:

  • sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs uses #[derive(..., crate::AnchorSerialize, crate::AnchorDeserialize)]
  • sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs uses #[derive(..., crate::AnchorSerialize, crate::AnchorDeserialize)]

Derive macro paths must resolve from the crate root namespace. With plain use, these names won't resolve—this will cause a compile error. The suggested diff to add pub is the correct fix.

-// Conditional anchor re-exports
-#[cfg(feature = "anchor")]
-use anchor_lang::{AnchorDeserialize, AnchorSerialize};
-#[cfg(not(feature = "anchor"))]
-use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
+// Conditional anchor re-exports for use across the crate
+#[cfg(feature = "anchor")]
+pub use anchor_lang::{AnchorDeserialize, AnchorSerialize};
+#[cfg(not(feature = "anchor"))]
+pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)

40-90: System program meta is a dummy Pubkey; account ordering and missing system_program AccountInfo will cause runtime failures

The review identified a real critical issue. The instruction has 6 account metas, but the implementation has multiple problems:

  1. Line 81: System program meta uses Pubkey::default() instead of the actual system program ID (11111…111).

  2. Lines 160–168: The create_ctoken_account_signed function calls invoke_signed with only 5 AccountInfos, but the instruction specifies 6 metas. Worse, the account order passed doesn't match the meta order:

    • Metas: [token_account, mint, payer, compressible_config, system_program, rent_sponsor]
    • Passed: [payer, token_account, mint_account, ctoken_rent_sponsor, ctoken_config_account]

    The system program is missing entirely, and the order is scrambled.

This will fail at runtime when the Solana runtime validates that each AccountInfo corresponds to the correct AccountMeta. The TODO at line 159 confirms the developer was uncertain about this.

Fix:

  • Replace Pubkey::default() with the actual system program ID on line 81
  • Pass AccountInfos to invoke_signed in the same order as the metas: &[token_account, mint_account, payer, ctoken_config_account, system_program, ctoken_rent_sponsor]
  • Add system_program as a parameter to create_ctoken_account_signed and pass it to invoke_signed

If the system program truly isn't needed by the on-chain processor, remove it entirely from both the metas and the call signature instead of keeping a placeholder that will confuse future maintainers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f30d22b and b0c20f0.

⛔ Files ignored due to path filters (56)
  • .github/actions/setup-and-build/action.yml is excluded by none and included by none
  • .github/workflows/cli-v1.yml is excluded by none and included by none
  • .github/workflows/sdk-tests.yml is excluded by none and included by none
  • Cargo.lock is excluded by !**/*.lock and included by none
  • Cargo.toml is excluded by none and included by none
  • cli/src/commands/token-balance/index.ts is excluded by none and included by none
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml and included by none
  • program-tests/account-compression-test/Cargo.toml is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/compress_and_close.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/create.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/ctoken/shared.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/mint/functional.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/transfer2/spl_ctoken.rs is excluded by none and included by none
  • program-tests/compressed-token-test/tests/v1.rs is excluded by none and included by none
  • program-tests/registry-test/tests/tests.rs is excluded by none and included by none
  • program-tests/system-cpi-test/Cargo.toml is excluded by none and included by none
  • program-tests/system-cpi-test/tests/test.rs is excluded by none and included by none
  • program-tests/utils/src/assert_mint_to_compressed.rs is excluded by none and included by none
  • program-tests/utils/src/assert_token_tx.rs is excluded by none and included by none
  • program-tests/utils/src/assert_transfer2.rs is excluded by none and included by none
  • program-tests/utils/src/conversions.rs is excluded by none and included by none
  • program-tests/utils/src/e2e_test_env.rs is excluded by none and included by none
  • program-tests/utils/src/spl.rs is excluded by none and included by none
  • sdk-tests/client-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/client-test/tests/light_client.rs is excluded by none and included by none
  • sdk-tests/client-test/tests/light_program_test.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Anchor.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Cargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/Xargo.toml is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/package.json is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/constants.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/errors.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instruction_accounts.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/mod.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_game_session.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/instructions/update_record.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/lib.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/seeds.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/src/state.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/game_session_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/helpers.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/idempotency_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/multi_account_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/placeholder_tests.rs is excluded by none and included by none
  • sdk-tests/csdk-anchor-test/tests/user_record_tests.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rs is excluded by none and included by none
  • sdk-tests/sdk-token-test/src/process_four_transfer2.rs is excluded by none and included by none
📒 Files selected for processing (73)
  • forester/Cargo.toml (1 hunks)
  • forester/tests/e2e_test.rs (1 hunks)
  • forester/tests/legacy/batched_state_async_indexer_test.rs (1 hunks)
  • program-libs/batched-merkle-tree/Cargo.toml (3 hunks)
  • program-libs/compressed-account/Cargo.toml (1 hunks)
  • program-libs/compressed-account/src/instruction_data/zero_copy_set.rs (0 hunks)
  • program-libs/compressible/src/config.rs (1 hunks)
  • program-libs/ctoken-types/Cargo.toml (1 hunks)
  • programs/compressed-token/program/src/shared/token_input.rs (1 hunks)
  • sdk-libs/client/Cargo.toml (3 hunks)
  • sdk-libs/client/src/constants.rs (1 hunks)
  • sdk-libs/client/src/indexer/types.rs (2 hunks)
  • sdk-libs/client/src/rpc/lut.rs (1 hunks)
  • sdk-libs/client/src/rpc/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/Cargo.toml (1 hunks)
  • sdk-libs/compressed-token-sdk/src/account2.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/error.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (4 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/mod.rs (3 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs (0 hunks)
  • sdk-libs/compressed-token-sdk/src/lib.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/pack.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/src/token_pool.rs (2 hunks)
  • sdk-libs/compressed-token-sdk/src/utils.rs (1 hunks)
  • sdk-libs/compressed-token-sdk/tests/pack_test.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/instruction/update_compressed_mint.rs (1 hunks)
  • sdk-libs/compressed-token-types/src/lib.rs (0 hunks)
  • sdk-libs/compressed-token-types/src/token_data.rs (0 hunks)
  • sdk-libs/compressible-client/Cargo.toml (1 hunks)
  • sdk-libs/compressible-client/src/get_compressible_account.rs (1 hunks)
  • sdk-libs/compressible-client/src/lib.rs (1 hunks)
  • sdk-libs/program-test/Cargo.toml (3 hunks)
  • sdk-libs/program-test/src/compressible.rs (1 hunks)
  • sdk-libs/program-test/src/indexer/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/indexer/test_indexer.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/compressible_setup.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/config.rs (2 hunks)
  • sdk-libs/program-test/src/program_test/extensions.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/light_program_test.rs (4 hunks)
  • sdk-libs/program-test/src/program_test/mod.rs (1 hunks)
  • sdk-libs/program-test/src/program_test/test_rpc.rs (1 hunks)
  • sdk-libs/program-test/src/utils/mod.rs (1 hunks)
  • sdk-libs/program-test/src/utils/simulation.rs (1 hunks)
  • sdk-libs/sdk/Cargo.toml (3 hunks)
  • sdk-libs/sdk/src/account.rs (1 hunks)
  • sdk-libs/sdk/src/address.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/close.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/compression_info.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/config.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/decompress_idempotent.rs (1 hunks)
  • sdk-libs/sdk/src/compressible/mod.rs (1 hunks)
  • sdk-libs/sdk/src/lib.rs (1 hunks)
  • sdk-libs/sdk/src/proof.rs (1 hunks)
  • sdk-libs/sdk/src/token.rs (0 hunks)
  • sdk-libs/token-client/Cargo.toml (1 hunks)
  • sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1 hunks)
  • sdk-libs/token-client/src/actions/ctoken_transfer.rs (6 hunks)
  • sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3 hunks)
  • sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (4 hunks)
  • sdk-libs/token-client/src/instructions/create_spl_mint.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_action.rs (1 hunks)
  • sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2 hunks)
  • sdk-libs/token-client/src/lib.rs (1 hunks)
💤 Files with no reviewable changes (5)
  • sdk-libs/compressed-token-sdk/src/instructions/update_compressed_mint/account_metas.rs
  • sdk-libs/sdk/src/token.rs
  • program-libs/compressed-account/src/instruction_data/zero_copy_set.rs
  • sdk-libs/compressed-token-types/src/lib.rs
  • sdk-libs/compressed-token-types/src/token_data.rs
🧰 Additional context used
🧬 Code graph analysis (37)
sdk-libs/client/src/rpc/mod.rs (1)
sdk-libs/client/src/rpc/lut.rs (1)
  • load_lookup_table (20-37)
sdk-libs/token-client/src/instructions/create_spl_mint.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/token-client/src/instructions/mint_action.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (2)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • derive_token_pool (29-36)
sdk-libs/program-test/src/utils/mod.rs (1)
sdk-libs/program-test/src/utils/simulation.rs (1)
  • simulate_cu (12-36)
sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rs (1)
sdk-libs/sdk/src/instruction/pack_accounts.rs (1)
  • packed_accounts (348-351)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/program-test/src/program_test/light_program_test.rs (1)
sdk-libs/program-test/src/program_test/config.rs (1)
  • new (60-69)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (3)
program-libs/compressed-account/src/instruction_data/traits.rs (6)
  • data (26-33)
  • data (86-86)
  • data (107-107)
  • address (83-83)
  • address (104-104)
  • address_merkle_tree_account_index (69-69)
sdk-libs/sdk/src/account.rs (4)
  • account (583-585)
  • account (647-649)
  • address (298-300)
  • new_init (250-272)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (35-35)
  • new_decompressed (74-79)
sdk-libs/program-test/src/program_test/test_rpc.rs (1)
sdk-libs/program-test/src/compressible.rs (1)
  • auto_compress_program_pdas (171-239)
sdk-libs/sdk/src/compressible/compress_account.rs (2)
sdk-libs/sdk/src/account.rs (5)
  • account (583-585)
  • account (647-649)
  • address (298-300)
  • new_empty (380-424)
  • new_empty (768-813)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (35-35)
  • last_written_slot (90-92)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
sdk-libs/sdk/src/cpi/invoke.rs (2)
  • invoke (13-13)
  • invoke (39-84)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (2)
  • create_transfer_ctoken_instruction (52-76)
  • transfer_ctoken (20-38)
sdk-libs/sdk/src/address.rs (1)
program-libs/compressed-account/src/address.rs (1)
  • derive_address (14-26)
sdk-libs/program-test/src/program_test/compressible_setup.rs (1)
sdk-libs/compressible-client/src/lib.rs (2)
  • initialize_compression_config (75-124)
  • update_compression_config (127-163)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • get_token_account_balance (11-20)
sdk-libs/token-client/src/actions/create_compressible_token_account.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (1)
  • create_compressible_token_account_instruction (40-90)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (4)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • transfer_ctoken_signed (54-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer2/instruction.rs (1)
  • create_transfer2_instruction (71-141)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
  • is_ctoken_account (22-37)
sdk-libs/compressed-token-sdk/src/account2.rs (6)
  • mint (362-364)
  • new (23-51)
  • compress_spl (193-220)
  • decompress_ctoken (224-247)
  • compress_ctoken (169-190)
  • decompress_spl (250-279)
sdk-libs/program-test/src/compressible.rs (2)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (48-50)
sdk-libs/compressible-client/src/lib.rs (1)
  • compress_accounts_idempotent (284-390)
sdk-libs/token-client/src/actions/transfer2/spl_to_ctoken.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (1)
  • create_transfer_spl_to_ctoken_instruction (23-87)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/compressed-token-sdk/src/ctoken.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (3)
  • create_compressed_mint (186-190)
  • derive_cmint_from_spl_mint (204-210)
  • find_spl_mint_address (212-217)
program-libs/compressible/src/config.rs (3)
  • ctoken_v1_config_pda (248-250)
  • ctoken_v1_rent_sponsor_pda (283-285)
  • ctoken_v1_compression_authority_pda (275-281)
sdk-libs/sdk/src/compressible/mod.rs (6)
sdk-libs/sdk/src/compressible/close.rs (1)
  • close (6-40)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • compression_info (35-35)
sdk-libs/sdk/src/compressible/compress_account.rs (1)
  • prepare_account_for_compression (30-115)
sdk-libs/sdk/src/compressible/compress_account_on_init.rs (1)
  • prepare_compressed_account_on_init (35-102)
sdk-libs/sdk/src/compressible/config.rs (3)
  • process_initialize_compression_config_account_info (150-240)
  • process_initialize_compression_config_checked (411-446)
  • process_update_compression_config (256-315)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
  • into_compressed_meta_with_address (23-42)
  • prepare_account_for_decompression_idempotent (80-141)
sdk-libs/compressed-token-sdk/src/instructions/create_associated_token_account.rs (2)
sdk-libs/compressed-token-sdk/src/account2.rs (1)
  • mint (362-364)
sdk-libs/sdk/src/cpi/invoke.rs (2)
  • invoke (13-13)
  • invoke (39-84)
sdk-libs/sdk/src/proof.rs (1)
program-libs/compressed-account/src/instruction_data/compressed_proof.rs (6)
  • default (33-39)
  • new (89-91)
  • from (101-103)
  • from (107-109)
  • from (112-114)
  • from (118-120)
sdk-libs/compressed-token-sdk/src/lib.rs (3)
sdk-libs/compressed-token-sdk/src/pack.rs (5)
  • pack (17-17)
  • pack (30-43)
  • pack (181-194)
  • pack (266-271)
  • pack (297-302)
sdk-libs/sdk/src/compressible/compression_info.rs (1)
  • pack (15-15)
sdk-libs/compressed-token-sdk/src/utils.rs (2)
  • account_meta_from_account_info (48-54)
  • is_ctoken_account (22-37)
sdk-libs/sdk/src/lib.rs (5)
program-libs/compressed-account/src/instruction_data/traits.rs (1)
  • proof (56-56)
program-libs/compressed-account/src/instruction_data/with_readonly.rs (1)
  • proof (470-472)
program-libs/compressed-account/src/instruction_data/with_account_info.rs (1)
  • proof (570-572)
program-libs/compressed-account/src/instruction_data/zero_copy.rs (2)
  • proof (455-457)
  • proof (603-605)
sdk-libs/sdk/src/compressible/config.rs (2)
  • process_initialize_compression_config_account_info (150-240)
  • process_update_compression_config (256-315)
sdk-libs/compressible-client/src/get_compressible_account.rs (3)
sdk-libs/sdk/src/address.rs (2)
  • derive_address (89-99)
  • derive_address (169-179)
sdk-libs/client/src/indexer/types.rs (1)
  • from (564-581)
program-libs/compressible/src/config.rs (1)
  • try_deserialize (98-112)
sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs (1)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/compressed-token-sdk/src/utils.rs (1)
sdk-libs/compressed-token-sdk/src/error.rs (3)
  • from (75-77)
  • from (81-83)
  • from (87-119)
sdk-libs/compressed-token-sdk/src/instructions/mod.rs (4)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
  • transfer_ctoken (20-38)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • transfer_ctoken_signed (54-68)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (2)
  • transfer_interface (354-437)
  • transfer_interface_signed (441-539)
sdk-libs/compressed-token-sdk/src/instructions/create_token_account/instruction.rs (3)
  • create_compressible_token_account_instruction (40-90)
  • create_ctoken_account_signed (122-171)
  • create_token_account (92-118)
sdk-libs/sdk/src/compressible/decompress_idempotent.rs (2)
sdk-libs/sdk/src/account.rs (6)
  • account (583-585)
  • account (647-649)
  • new_close (426-435)
  • new_close (820-828)
  • size (142-142)
  • size (306-311)
sdk-libs/sdk/src/compressible/compression_info.rs (2)
  • compression_info (35-35)
  • new_decompressed (74-79)
sdk-libs/token-client/src/actions/transfer2/ctoken_to_spl.rs (3)
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs (2)
  • create_transfer_ctoken_to_spl_instruction (91-160)
  • transfer_ctoken_to_spl (252-290)
sdk-libs/compressed-token-sdk/src/token_pool.rs (1)
  • find_token_pool_pda_with_index (13-21)
program-libs/compressed-account/src/pubkey.rs (1)
  • new_from_array (79-81)
sdk-libs/token-client/src/instructions/mint_to_compressed.rs (2)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (1)
  • derive_cmint_from_spl_mint (204-210)
sdk-libs/compressed-token-sdk/src/token_pool.rs (2)
  • derive_token_pool (29-36)
  • find_token_pool_pda_with_index (13-21)
sdk-libs/compressible-client/src/lib.rs (3)
sdk-libs/program-test/src/program_test/compressible_setup.rs (2)
  • initialize_compression_config (75-111)
  • update_compression_config (128-151)
program-libs/compressed-account/src/instruction_data/traits.rs (4)
  • discriminator (20-20)
  • data (26-33)
  • data (86-86)
  • data (107-107)
sdk-libs/sdk/src/compressible/config.rs (1)
  • derive_pda (48-50)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/mod.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/create_compressed_mint/instruction.rs (6)
  • create_compressed_mint (186-190)
  • create_compressed_mint_cpi (41-106)
  • create_compressed_mint_cpi_write (123-183)
  • derive_cmint_from_spl_mint (204-210)
  • derive_compressed_mint_address (193-202)
  • find_spl_mint_address (212-217)
sdk-libs/client/src/indexer/types.rs (1)
sdk-libs/compressed-token-sdk/src/pack.rs (4)
  • from (72-81)
  • from (148-162)
  • from (166-175)
  • try_from (87-93)
program-libs/compressible/src/config.rs (1)
sdk-libs/compressible-client/src/lib.rs (1)
  • seeds (310-310)
sdk-libs/token-client/src/actions/ctoken_transfer.rs (1)
sdk-libs/compressed-token-sdk/src/instructions/transfer_ctoken.rs (2)
  • transfer_ctoken (42-51)
  • create_transfer_ctoken_instruction (18-39)
🪛 GitHub Actions: lint
forester/tests/e2e_test.rs

[warning] 35-35: Diff in forester/tests/e2e_test.rs: a line import was changed ("-use light_ctoken_types::state::TokenDataVersion;" vs "+use light_ctoken_types::state::TokenDataVersion;"). Possible lint/formatting discrepancy.

⏰ Context from checks skipped due to timeout of 900000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Test batched-merkle-tree-simulate

use light_hasher::Poseidon;
use light_program_test::accounts::test_accounts::TestAccounts;
use light_prover_client::prover::spawn_prover;
use light_sdk::token::TokenDataWithMerkleContext;
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.

⚠️ Potential issue | 🟡 Minor

LGTM - Consistent import path migration.

The import path change aligns with the broader migration to light_compressed_token_sdk::compat. However, note the pipeline failure warning about Line 35.

The pipeline reports a lint/formatting discrepancy on Line 35 (related to light_ctoken_types::state::TokenDataVersion). Although Line 35 isn't modified in this diff, please verify the formatting is correct and run the project's formatter to resolve any inconsistencies.

🤖 Prompt for AI Agents
In forester/tests/e2e_test.rs around lines 35 to 39, the CI reported a
lint/formatting discrepancy on Line 35 (the import of
light_ctoken_types::state::TokenDataVersion) even though the recent change was
at Line 39; open the file, inspect Line 35 for trailing whitespace, incorrect
punctuation, or import ordering, correct the formatting to match project style,
then run the Rust formatter (cargo fmt or rustfmt) and re-run the tests to
ensure the formatting error is resolved.

Comment on lines +1 to +37
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
use solana_message::AddressLookupTableAccount;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;

use crate::rpc::errors::RpcError;

/// Gets a lookup table account state from the network.
///
/// # Arguments
///
/// * `client` - The RPC client to use to get the lookup table account state.
/// * `lookup_table_address` - The address of the lookup table account to get.
///
/// # Returns
///
/// * `AddressLookupTableAccount` - The lookup table account state.
pub fn load_lookup_table(
client: &RpcClient,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, RpcError> {
let raw_account = client.get_account(lookup_table_address)?;
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
let address_lookup_table_account = AddressLookupTableAccount {
key: lookup_table_address.to_bytes().into(),
addresses: address_lookup_table
.addresses
.iter()
.map(|p| p.to_bytes().into())
.collect(),
};
Ok(address_lookup_table_account)
}
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 | 🔵 Trivial

Validate the LUT account owner before deserializing

load_lookup_table is a useful helper, but it currently trusts that get_account returns an address‑lookup‑table account and immediately tries to deserialize. If a caller passes the wrong pubkey, you only see a deserialization error.

To make failures clearer and avoid attempting to parse arbitrary data as an LUT, consider checking the owner against the LUT program id before deserializing, e.g.:

 pub fn load_lookup_table(
     client: &RpcClient,
     lookup_table_address: &Pubkey,
 ) -> Result<AddressLookupTableAccount, RpcError> {
-    let raw_account = client.get_account(lookup_table_address)?;
+    let raw_account = client.get_account(lookup_table_address)?;
+
+    if raw_account.owner != program::id() {
+        return Err(RpcError::CustomError(format!(
+            "Account {lookup_table_address} is not owned by the address-lookup-table program"
+        )));
+    }
+
     let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
         RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
     })?;

This keeps error messages more actionable and avoids silently treating arbitrary accounts as LUTs.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
use solana_message::AddressLookupTableAccount;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;
use crate::rpc::errors::RpcError;
/// Gets a lookup table account state from the network.
///
/// # Arguments
///
/// * `client` - The RPC client to use to get the lookup table account state.
/// * `lookup_table_address` - The address of the lookup table account to get.
///
/// # Returns
///
/// * `AddressLookupTableAccount` - The lookup table account state.
pub fn load_lookup_table(
client: &RpcClient,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, RpcError> {
let raw_account = client.get_account(lookup_table_address)?;
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
let address_lookup_table_account = AddressLookupTableAccount {
key: lookup_table_address.to_bytes().into(),
addresses: address_lookup_table
.addresses
.iter()
.map(|p| p.to_bytes().into())
.collect(),
};
Ok(address_lookup_table_account)
}
pub use solana_address_lookup_table_interface::{
error, instruction, program, state::AddressLookupTable,
};
use solana_message::AddressLookupTableAccount;
use solana_pubkey::Pubkey;
use solana_rpc_client::rpc_client::RpcClient;
use crate::rpc::errors::RpcError;
/// Gets a lookup table account state from the network.
///
/// # Arguments
///
/// * `client` - The RPC client to use to get the lookup table account state.
/// * `lookup_table_address` - The address of the lookup table account to get.
///
/// # Returns
///
/// * `AddressLookupTableAccount` - The lookup table account state.
pub fn load_lookup_table(
client: &RpcClient,
lookup_table_address: &Pubkey,
) -> Result<AddressLookupTableAccount, RpcError> {
let raw_account = client.get_account(lookup_table_address)?;
if raw_account.owner != program::id() {
return Err(RpcError::CustomError(format!(
"Account {lookup_table_address} is not owned by the address-lookup-table program"
)));
}
let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| {
RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}"))
})?;
let address_lookup_table_account = AddressLookupTableAccount {
key: lookup_table_address.to_bytes().into(),
addresses: address_lookup_table
.addresses
.iter()
.map(|p| p.to_bytes().into())
.collect(),
};
Ok(address_lookup_table_account)
}
🤖 Prompt for AI Agents
In sdk-libs/client/src/rpc/lut.rs around lines 1 to 37, the function currently
deserializes the account data without verifying the account owner; before
calling AddressLookupTable::deserialize, check that raw_account.owner equals the
LUT program id (program::id()) and return a clear RpcError::CustomError if it
does not (e.g., "account is not an address lookup table: owner mismatch"), so
you fail fast with an actionable error instead of attempting to parse arbitrary
account data.

},
utils::get_token_account_balance,
};
use crate::{error::TokenSdkError, utils::get_token_account_balance};
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 | 🔵 Trivial

compress_full helper is coherent with existing compression flows; clarify caller responsibilities

The new compress_full method:

  • Reuses the existing compression.is_some() guard and method_used pattern.
  • Derives amount from get_token_account_balance(token_account_info) and adds it to self.output.amount.
  • Builds a Compression with mode Compress and zeroed pool fields, which is consistent with how non‑pool compress_ctoken cases are represented.

Functionally this is fine, but it relies on the caller to pass a SPL token account whose mint matches self.output.mint and whose index (source_or_recipient_index) is wired correctly in the packed accounts. Consider documenting that precondition so misuse is caught at the call‑site rather than only via program‑level validation.

Also applies to: 281-313

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/account2.rs around lines 10 and also
applying to lines 281-313, the new compress_full helper assumes callers pass a
SPL token account whose mint equals self.output.mint and whose
source_or_recipient_index is correctly wired in the packed accounts; add
explicit documentation and a short runtime assertion to fail early: update the
function docs (///) to state these preconditions and add a check that
token_account_info.mint == self.output.mint (and that source_or_recipient_index
points to token_account_info when possible) returning a clear TokenSdkError if
they don’t match so misuse is detected at call-site instead of only in program
validation.

Comment on lines +1 to +50
use light_compressed_token_types::POOL_SEED;
use light_compressible::config::CompressibleConfig;
use solana_pubkey::{pubkey, Pubkey};

pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");

pub const CTOKEN_CPI_AUTHORITY: Pubkey = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy");

/// Returns the program ID for the Compressed Token Program
pub fn id() -> Pubkey {
CTOKEN_PROGRAM_ID
}

/// Return the cpi authority pda of the Compressed Token Program.
pub fn cpi_authority() -> Pubkey {
CTOKEN_CPI_AUTHORITY
}

pub fn get_token_pool_address_and_bump(mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[POOL_SEED, mint.as_ref()], &CTOKEN_PROGRAM_ID)
}

/// Returns the associated ctoken address for a given owner and mint.
pub fn get_associated_ctoken_address(owner: &Pubkey, mint: &Pubkey) -> Pubkey {
get_associated_ctoken_address_and_bump(owner, mint).0
}

/// Returns the associated ctoken address and bump for a given owner and mint.
pub fn get_associated_ctoken_address_and_bump(owner: &Pubkey, mint: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[&owner.to_bytes(), &id().to_bytes(), &mint.to_bytes()],
&id(),
)
}

pub use crate::instructions::create_compressed_mint::{
derive_cmint_from_spl_mint, find_spl_mint_address,
};

pub fn config_pda() -> Pubkey {
CompressibleConfig::ctoken_v1_config_pda()
}

pub fn rent_sponsor_pda() -> Pubkey {
CompressibleConfig::ctoken_v1_rent_sponsor_pda()
}

pub fn compression_authority_pda() -> Pubkey {
CompressibleConfig::ctoken_v1_compression_authority_pda()
}
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 | 🔵 Trivial

Public CTOKEN helpers look sound; consider deriving the Pubkey from the shared type-level ID

The PDA helpers and re-exports here are consistent with standard Solana patterns:

  • get_token_pool_address_and_bump uses [POOL_SEED, mint] with the CTOKEN program id.
  • get_associated_ctoken_address{,_and_bump} follow the ATA-style seed layout of [owner, program_id, mint].
  • Config / authority PDAs are correctly delegated to CompressibleConfig::*ctoken_v1_*_pda() so you don’t re-encode that logic here.

The only structural risk is that CTOKEN_PROGRAM_ID is hard-coded via pubkey!("cTokenm…") while other parts of the codebase already treat light_ctoken_types::COMPRESSED_TOKEN_PROGRAM_ID as the source of truth. If those ever diverge, you’ll get extremely subtle bugs (address mismatch, cross-crate incompatibility).

I’d strongly recommend wiring the constant through the shared type-level ID instead of duplicating the literal, e.g.:

-use light_compressed_token_types::POOL_SEED;
+use light_compressed_token_types::{POOL_SEED, COMPRESSED_TOKEN_PROGRAM_ID};
@@
-pub const CTOKEN_PROGRAM_ID: Pubkey = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m");
+pub const CTOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array(COMPRESSED_TOKEN_PROGRAM_ID);

That keeps all CTOKEN-related crates on a single canonical definition and makes future upgrades safer.

Comment on lines +411 to +457
pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>(
token_accounts_to_compress: &[AccountInfoToCompress<'info>],
fee_payer: AccountInfo<'info>,
output_queue: AccountInfo<'info>,
compressed_token_rent_sponsor: AccountInfo<'info>,
compressed_token_cpi_authority: AccountInfo<'info>,
cpi_authority: AccountInfo<'info>,
post_system: &[AccountInfo<'info>],
remaining_accounts: &[AccountInfo<'info>],
with_compression_authority: bool,
) -> Result<(), TokenSdkError> {
let mut packed_accounts = Vec::with_capacity(post_system.len() + 4);
packed_accounts.extend_from_slice(post_system);
packed_accounts.push(cpi_authority);
packed_accounts.push(compressed_token_rent_sponsor.clone());

let ctoken_infos: Vec<&AccountInfo<'info>> = token_accounts_to_compress
.iter()
.map(|t| t.account_info.as_ref())
.collect();

let instruction = compress_and_close_ctoken_accounts(
*fee_payer.key,
with_compression_authority,
output_queue,
&ctoken_infos,
&packed_accounts,
)?;
// infos
let total_capacity = packed_accounts.len() + remaining_accounts.len() + 1;
let mut account_infos: Vec<AccountInfo<'info>> = Vec::with_capacity(total_capacity);
account_infos.extend_from_slice(&packed_accounts);
account_infos.push(compressed_token_cpi_authority);
account_infos.extend_from_slice(remaining_accounts);

let token_seeds_refs: Vec<Vec<&[u8]>> = token_accounts_to_compress
.iter()
.map(|t| t.signer_seeds.iter().map(|v| v.as_slice()).collect())
.collect();
let mut all_signer_seeds: Vec<&[&[u8]]> = Vec::with_capacity(token_seeds_refs.len());
for seeds in &token_seeds_refs {
all_signer_seeds.push(seeds.as_slice());
}

invoke_signed(&instruction, &account_infos, &all_signer_seeds)
.map_err(|e| TokenSdkError::CpiError(e.to_string()))?;
Ok(())
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Verify account coverage and ordering in compress_and_close_ctoken_accounts_signed

This helper is nice ergonomically, but a few details are easy to get wrong and could cause runtime InvalidAccountData or CPI failures:

  1. Where do the ctoken/mint/owner accounts live?

    compress_and_close_ctoken_accounts uses:

    let find_index = |pubkey: &Pubkey| -> Option<u8> {
        packed_accounts
            .iter()
            .position(|account| account.key == pubkey)
            .map(|idx| (idx + 1) as u8)
    };

    and find_account_indices expects to find:

    • source ctoken account,
    • mint,
    • owner,
    • authority,
    • rent_sponsor,
    • destination,

    all in packed_accounts.

    In this wrapper, packed_accounts is built as:

    packed_accounts.extend_from_slice(post_system);
    packed_accounts.push(cpi_authority);
    packed_accounts.push(compressed_token_rent_sponsor.clone());

    If post_system doesn’t already contain the ctoken accounts, mints, and owners, find_account_indices inside compress_and_close_ctoken_accounts will fail with TokenSdkError::InvalidAccountData.

    At minimum, the function’s docs should clearly state that post_system must include all accounts that compress_and_close_ctoken_accounts expects to find (ctoken source, mint, owner, authority, rent_sponsor, destination), not just “post system” ones. Even better would be to derive the needed entries from token_accounts_to_compress (and their associated mint/owner infos) so the API is harder to misuse.

  2. Account ordering for CPI

    The CPI call does:

    let mut account_infos = Vec::with_capacity(...);
    account_infos.extend_from_slice(&packed_accounts);
    account_infos.push(compressed_token_cpi_authority);
    account_infos.extend_from_slice(remaining_accounts);
    
    invoke_signed(&instruction, &account_infos, &all_signer_seeds)?;

    create_transfer2_instruction constructs its Instruction.accounts using Transfer2AccountsMetaConfig plus the packed_account_metas. For invoke_signed to succeed, the account_infos order must exactly match the Instruction.accounts order.

    Right now that coupling is implicit:

    • fee_payer’s AccountInfo is not pushed explicitly here, so it must be present in either post_system or remaining_accounts in the position that Transfer2AccountsMetaConfig expects.
    • The same holds for the ctoken accounts, mints, owners, and any system/Light accounts required by transfer2.

    I’d strongly recommend:

    • Documenting the required ordering constraints for post_system and remaining_accounts relative to what Transfer2AccountsMetaConfig produces, and/or
    • Refactoring this helper to build account_infos in the exact same way the non‑CPI path builds its AccountMeta vector (e.g., by sharing a helper that returns both metas and infos), so the mapping can’t drift.
  3. Minor nits

    • Lifetime 'b on the function is unused and can be dropped.
    • Vec::with_capacity(post_system.len() + 4) for packed_accounts currently only uses +2; either add the missing accounts explicitly or tighten the capacity for clarity.

Given how subtle these account-ordering and coverage requirements are, I’d add targeted tests that exercise compress_and_close_ctoken_accounts_signed end‑to‑end (both with and without with_compression_authority) to catch regressions.


🏁 Script executed:

#!/bin/bash
# Inspect definition and typical usage of AccountInfoToCompress and
# compress_and_close_ctoken_accounts_signed to ensure post_system/remaining_accounts
# actually contain all accounts that find_account_indices expects.

rg -n "struct\s+AccountInfoToCompress" -S
rg -n "compress_and_close_ctoken_accounts_signed" -S
rg -n "compress_and_close_ctoken_accounts\(" -S

Length of output: 352


I'll verify the concerns raised in the review by inspecting the relevant function definitions and call sites.

Now let me inspect the code more thoroughly to verify the remaining concerns:

Let me now inspect the actual code to verify the specific concerns:


🏁 Script executed:

#!/bin/bash
# Deep dive into AccountInfoToCompress and the related functions

# Get AccountInfoToCompress definition
echo "=== AccountInfoToCompress Definition ===" 
rg -A 10 "pub struct AccountInfoToCompress" sdk-libs/compressed-token-sdk/src/

# Get compress_and_close_ctoken_accounts function
echo -e "\n=== compress_and_close_ctoken_accounts Function ===" 
rg -A 30 "pub fn compress_and_close_ctoken_accounts\(" sdk-libs/compressed-token-sdk/src/ -t rust

# Search for find_account_indices usage
echo -e "\n=== find_account_indices Function ===" 
rg -B 5 -A 15 "find_account_indices" sdk-libs/compressed-token-sdk/src/ -t rust

Length of output: 4649


Let me search more broadly to understand the full picture:


🏁 Script executed:

#!/bin/bash
# Search for compress_and_close_ctoken_accounts and its definition
echo "=== Looking for compress_and_close_ctoken_accounts definition ===" 
rg "fn compress_and_close_ctoken_accounts" sdk-libs/compressed-token-sdk/src/ -B 2 -A 50

# Check for call sites of compress_and_close_ctoken_accounts_signed
echo -e "\n=== Call sites of compress_and_close_ctoken_accounts_signed ===" 
rg "compress_and_close_ctoken_accounts_signed" sdk-libs/compressed-token-sdk/src/ -B 3 -A 3

# Check if lifetime 'b is used anywhere
echo -e "\n=== Check function signature and lifetime 'b usage ===" 
rg -A 15 "pub fn compress_and_close_ctoken_accounts_signed<'b, 'info>" sdk-libs/compressed-token-sdk/src/

# Check capacity calculation
echo -e "\n=== Check packed_accounts capacity ===" 
rg -B 5 -A 10 "Vec::with_capacity.*post_system" sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs

Length of output: 21292


Now let me search for any documentation in the repository itself:

Verify account coverage in compress_and_close_ctoken_accounts_signed, but reconsider the CPI account ordering concern.

The wrapper function's approach to building packed_accounts creates a legitimate risk: if post_system doesn't include all accounts that find_account_indices expects to locate (source ctoken, mint, owner, authority, rent_sponsor, destination), the CPI will fail with InvalidAccountData. The function should either:

  1. Document explicitly that post_system must contain all required accounts (mints, owners, destination, authority, rent_sponsor) in addition to post-system accounts, or
  2. Build these dependencies from token_accounts_to_compress and its associated metadata so the API is harder to misuse.

However, the concern about account ordering is based on a misunderstanding: the account infos are passed to the callee in the order defined by the instruction, not the account infos. The Instruction struct contains the correct account order; account_infos is a lookup pool. So the current account construction logic is correct—as long as all required accounts are present (the coverage concern above).

For the minor issues:

  • The lifetime parameter 'b is genuinely unused; remove it from the signature.
  • Vec::with_capacity(post_system.len() + 4) reserves room for 4 extra items but only uses 2 (cpi_authority and rent_sponsor); either add the missing accounts or adjust to + 2.

Add targeted tests for this function end-to-end to catch account-coverage regressions.

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rs around
lines 411 to 457, remove the unused lifetime 'b' from the function signature,
fix the capacity reservation and ensure required accounts are always present:
replace Vec::with_capacity(post_system.len() + 4) with
with_capacity(post_system.len() + 2) (or compute exact count), and append the
missing expected accounts (for each token_accounts_to_compress add the source
ctoken account info plus its mint, owner, authority and rent_sponsor/destination
as provided by the AccountInfoToCompress metadata) into packed_accounts instead
of relying on callers to include them in post_system; keep the
instruction/account info ordering as-is, then update tests to include an
end-to-end case that omits those accounts from post_system to verify the wrapper
builds the full account pool and avoids InvalidAccountData.

Comment on lines +59 to +101
#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)]
pub struct CompressionInfo {
pub last_written_slot: u64,
pub state: CompressionState,
}

#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize, PartialEq)]
pub enum CompressionState {
#[default]
Uninitialized,
Decompressed,
Compressed,
}

impl CompressionInfo {
pub fn new_decompressed() -> Result<Self, crate::ProgramError> {
Ok(Self {
last_written_slot: Clock::get()?.slot,
state: CompressionState::Decompressed,
})
}

pub fn bump_last_written_slot(&mut self) -> Result<(), crate::ProgramError> {
self.last_written_slot = Clock::get()?.slot;
Ok(())
}

pub fn set_last_written_slot(&mut self, slot: u64) {
self.last_written_slot = slot;
}

pub fn last_written_slot(&self) -> u64 {
self.last_written_slot
}

pub fn set_compressed(&mut self) {
self.state = CompressionState::Compressed;
}

pub fn is_compressed(&self) -> bool {
self.state == CompressionState::Compressed
}
}
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 | 🔵 Trivial

CompressionInfo API is sound; ensure call‑sites bump slot and state together

CompressionInfo cleanly separates last_written_slot and CompressionState, with helpers to construct a decompressed instance and bump the slot based on Clock::get(). The semantics look good.

One thing to watch operationally: callers need to remember to call both bump_last_written_slot() and set_compressed() when transitioning to a compressed state. If you find multiple call‑sites doing this dance, a small helper like set_compressed_now() that both bumps the slot and sets Compressed could reduce drift.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/compression_info.rs around lines 59 to 101, add
a convenience method that atomically bumps the last_written_slot from
Clock::get() and marks the state Compressed so callers no longer need to call
bump_last_written_slot() and set_compressed() separately; implement pub fn
set_compressed_now(&mut self) -> Result<(), crate::ProgramError> {
self.last_written_slot = Clock::get()?.slot; self.state =
CompressionState::Compressed; Ok(()) } (or similarly named) and update any
call-sites that perform the two-step sequence to use this single helper.

Comment on lines +1 to +18
#![allow(clippy::all)] // TODO: Remove.

use light_compressed_account::address::derive_address;
use light_sdk_types::instruction::account_meta::{
CompressedAccountMeta, CompressedAccountMetaNoLamportsNoAddress,
};
use solana_account_info::AccountInfo;
use solana_cpi::invoke_signed;
use solana_msg::msg;
use solana_pubkey::Pubkey;
use solana_system_interface::instruction as system_instruction;
use solana_sysvar::{rent::Rent, Sysvar};

use crate::{
account::sha::LightAccount, compressible::compression_info::HasCompressionInfo,
cpi::v2::CpiAccounts, error::LightSdkError, AnchorDeserialize, AnchorSerialize,
LightDiscriminator,
};
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 | 🔵 Trivial

Avoid blanket clippy::all suppression

Disabling all Clippy lints at the file level hides potentially useful diagnostics, especially in new code that’s still evolving.

Once things settle, it would be better to:

  • Remove the #![allow(clippy::all)], or
  • Narrow it to specific lints you’ve intentionally decided to ignore.

This keeps future refactors safer and surfaces subtle issues early.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/decompress_idempotent.rs lines 1-18, the
file-wide attribute #![allow(clippy::all)] blanket-suppresses all Clippy lints;
remove that line (or replace it with targeted allows) and then run cargo clippy
to see remaining warnings; fix the reported issues where feasible, and for any
intentional exceptions, add per-lint allow attributes with short comments
explaining why that lint is suppressed.

Comment on lines +1 to +28
pub mod close;
pub mod compression_info;
pub mod config;

#[cfg(feature = "v2")]
pub mod compress_account;
#[cfg(feature = "v2")]
pub mod compress_account_on_init;
#[cfg(feature = "v2")]
pub mod decompress_idempotent;
#[cfg(feature = "v2")]
pub use close::close;
#[cfg(feature = "v2")]
pub use compress_account::prepare_account_for_compression;
#[cfg(feature = "v2")]
pub use compress_account_on_init::prepare_compressed_account_on_init;
pub use compression_info::{
CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack,
};
pub use config::{
process_initialize_compression_config_account_info,
process_initialize_compression_config_checked, process_update_compression_config,
CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE,
};
#[cfg(feature = "v2")]
pub use decompress_idempotent::{
into_compressed_meta_with_address, prepare_account_for_decompression_idempotent,
};
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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

close::close is only re-exported under feature = "v2", which is likely not intended

The module declarations themselves are unconditional:

pub mod close;
pub mod compression_info;
pub mod config;

But the re-export of the close helper is gated:

#[cfg(feature = "v2")]
pub use close::close;

Given that close.rs doesn’t appear to rely on v2-only functionality and the high-level summary mentions close as part of the general compressible surface, this gating is surprising:

  • With feature = "v2" disabled, users can access compressible::close::close, but not compressible::close at the root.
  • With feature = "v2" enabled, they get the nicer compressible::close re-export.

If close is meant to be universally available (and the v2 feature is only for the extra compression/decompression helpers), I’d drop the cfg on the re-export:

-#[cfg(feature = "v2")]
-pub use close::close;
+pub use close::close;

This keeps the public API predictable across feature flags and avoids subtle compile-time differences in name resolution.


I'll verify the review comment by inspecting the implementation details and feature gate scope.


🏁 Script executed:

#!/bin/bash
# Inspect the close.rs implementation
echo "=== close.rs implementation ==="
find . -name "close.rs" -path "*/compressible/*" -exec cat {} \;

Length of output: 1163


🏁 Script executed:

#!/bin/bash
# Check compress_account.rs to understand v2 scope
echo "=== compress_account.rs (v2-gated module) ==="
find . -name "compress_account.rs" -path "*/compressible/*" -exec cat {} \;

Length of output: 4198


🏁 Script executed:

#!/bin/bash
# Search for usages of close in the codebase
echo "=== Usages of compressible::close ==="
rg -n "compressible::close|use.*close::" --type rust -B2 -A2 -g "!node_modules"

Length of output: 4448


🏁 Script executed:

#!/bin/bash
# Check if close is feature-gated elsewhere or in tests
echo "=== close mentions in tests and examples ==="
rg -n "close\(" --type rust -B3 -A1 -g "*/tests/*" -g "*/examples/*" | head -50

Length of output: 44


Remove the #[cfg(feature = "v2")] gate on the close::close re-export

Your detection is spot-on. The close() function in close.rs is entirely feature-independent—it uses only standard Solana account manipulation utilities and your crate's error types. There's no trace of v2-specific logic anywhere in that module.

The inconsistency is real: the module declaration is unconditional (pub mod close;), but its re-export is gated behind v2. This creates an awkward API asymmetry where users without v2 can still access the function via compressible::close::close but lose the convenience of the shorter path compressible::close. Worse, it silently changes the API shape across feature flags—a subtle breaking change lurking in feature combinations.

Since close() is purely a utility function with no v2 dependencies, the re-export should be unconditional:

-#[cfg(feature = "v2")]
-pub use close::close;
+pub use close::close;

This keeps the public API surface predictable and lets v2 feature flags affect only the actual v2-specific compression/decompression helpers.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub mod close;
pub mod compression_info;
pub mod config;
#[cfg(feature = "v2")]
pub mod compress_account;
#[cfg(feature = "v2")]
pub mod compress_account_on_init;
#[cfg(feature = "v2")]
pub mod decompress_idempotent;
#[cfg(feature = "v2")]
pub use close::close;
#[cfg(feature = "v2")]
pub use compress_account::prepare_account_for_compression;
#[cfg(feature = "v2")]
pub use compress_account_on_init::prepare_compressed_account_on_init;
pub use compression_info::{
CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack,
};
pub use config::{
process_initialize_compression_config_account_info,
process_initialize_compression_config_checked, process_update_compression_config,
CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE,
};
#[cfg(feature = "v2")]
pub use decompress_idempotent::{
into_compressed_meta_with_address, prepare_account_for_decompression_idempotent,
};
pub mod close;
pub mod compression_info;
pub mod config;
#[cfg(feature = "v2")]
pub mod compress_account;
#[cfg(feature = "v2")]
pub mod compress_account_on_init;
#[cfg(feature = "v2")]
pub mod decompress_idempotent;
pub use close::close;
#[cfg(feature = "v2")]
pub use compress_account::prepare_account_for_compression;
#[cfg(feature = "v2")]
pub use compress_account_on_init::prepare_compressed_account_on_init;
pub use compression_info::{
CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack, Space, Unpack,
};
pub use config::{
process_initialize_compression_config_account_info,
process_initialize_compression_config_checked, process_update_compression_config,
CompressibleConfig, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE,
};
#[cfg(feature = "v2")]
pub use decompress_idempotent::{
into_compressed_meta_with_address, prepare_account_for_decompression_idempotent,
};
🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/mod.rs around lines 1 to 28, the re-export of
close::close is incorrectly gated with #[cfg(feature = "v2")] while the close
module itself is always compiled; remove the #[cfg(feature = "v2")] from the pub
use close::close; line so the close() function is re-exported unconditionally
(keep the pub mod close; line as-is) to restore a consistent public API surface
across feature flags.

Comment thread sdk-libs/sdk/src/lib.rs
Comment on lines +150 to +169
pub mod proof;
/// Transfer compressed sol between compressed accounts.
pub mod transfer;
pub mod utils;

pub use proof::borsh_compat;
pub mod compressible;
#[cfg(feature = "merkle-tree")]
pub mod merkle_tree;

#[cfg(feature = "anchor")]
use anchor_lang::{AnchorDeserialize, AnchorSerialize};
#[cfg(not(feature = "anchor"))]
use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize};
pub use compressible::{
process_initialize_compression_config_account_info,
process_initialize_compression_config_checked, process_update_compression_config, CompressAs,
CompressedInitSpace, CompressibleConfig, CompressionInfo, HasCompressionInfo, Pack, Space,
Unpack, COMPRESSIBLE_CONFIG_SEED, MAX_ADDRESS_TREES_PER_SPACE,
};
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 | 🔵 Trivial

New proof/compressible exports look coherent; consider documenting Pack/Unpack naming

The addition of proof and compressible modules and the top‑level re‑exports for CompressibleConfig, CompressionInfo, Pack, Unpack, etc., makes the SDK’s public surface much easier to consume from downstream programs.

One thing to keep in mind: exporting generic trait names like Pack/Unpack at the crate root can easily collide in user code with similarly named traits from other crates (e.g., compressed‑token SDK). It’s not wrong, but it’s worth explicitly documenting in the crate‑level docs that these come from light_sdk::compressible so downstream users know how to disambiguate if they pull in multiple Pack traits.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/lib.rs around lines 150 to 169, add a short crate-level doc
note near the new re-exports that explicitly states Pack and Unpack (and any
other generic trait names) are re-exported from light_sdk::compressible and may
conflict with similarly named traits from other crates; instruct downstream
users to disambiguate by either fully qualifying the trait (compressible::Pack)
or aliasing on import (use light_sdk::compressible::Pack as LightPack), and
include a brief example of the two disambiguation options in the doc comment so
consumers know how to resolve collisions.

Comment thread sdk-libs/sdk/src/proof.rs
Comment on lines +1 to +88
// TODO: try removing in separate PR
pub mod borsh_compat {
use crate::{AnchorDeserialize, AnchorSerialize};

#[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
pub struct CompressedProof {
pub a: [u8; 32],
pub b: [u8; 64],
pub c: [u8; 32],
}

impl Default for CompressedProof {
fn default() -> Self {
Self {
a: [0; 32],
b: [0; 64],
c: [0; 32],
}
}
}

#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)]
/// Borsh-compatible ValidityProof. Use this in your anchor program unless
/// you have zero-copy instruction data.
pub struct ValidityProof(pub Option<CompressedProof>);

impl ValidityProof {
pub fn new(proof: Option<CompressedProof>) -> Self {
Self(proof)
}
}

impl From<light_compressed_account::instruction_data::compressed_proof::CompressedProof>
for CompressedProof
{
fn from(
proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof,
) -> Self {
Self {
a: proof.a,
b: proof.b,
c: proof.c,
}
}
}

impl From<CompressedProof>
for light_compressed_account::instruction_data::compressed_proof::CompressedProof
{
fn from(proof: CompressedProof) -> Self {
Self {
a: proof.a,
b: proof.b,
c: proof.c,
}
}
}

impl From<light_compressed_account::instruction_data::compressed_proof::ValidityProof>
for ValidityProof
{
fn from(
proof: light_compressed_account::instruction_data::compressed_proof::ValidityProof,
) -> Self {
Self(proof.0.map(|p| p.into()))
}
}

impl From<ValidityProof>
for light_compressed_account::instruction_data::compressed_proof::ValidityProof
{
fn from(proof: ValidityProof) -> Self {
Self(proof.0.map(|p| p.into()))
}
}

impl From<CompressedProof> for ValidityProof {
fn from(proof: CompressedProof) -> Self {
Self(Some(proof))
}
}

impl From<Option<CompressedProof>> for ValidityProof {
fn from(proof: Option<CompressedProof>) -> Self {
Self(proof)
}
}
}
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 | 🔵 Trivial

Compat types and conversions look correct; add guardrails against drift with the upstream definitions

The borsh_compat::CompressedProof / ValidityProof shapes and From conversions mirror the upstream light_compressed_account::instruction_data::compressed_proof types as expected. Field-by-field copies are correct, and the ValidityProof::new / From<CompressedProof> / From<Option<CompressedProof>> constructors preserve the same semantics as in the program crate.

The main long‑term risk here is silent ABI drift if the upstream compressed proof types change (different field sizes or ordering). Because this module redefines the structs rather than re-exporting them, any such change could desynchronize client and program serialization without a compiler error.

Two practical ways to harden this:

  • Add unit tests that round‑trip between the upstream and local types via From and Borsh/Anchor serialization to assert that byte layouts remain identical.
  • If possible, add compile‑time size/layout checks (e.g., assert_eq!(std::mem::size_of::<CompressedProof>(), 32 + 64 + 32) in a #[test] or const context) so unexpected upstream changes become immediately visible.

The current implementation is functionally fine; this is about making future changes safer around a critical proof type.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/proof.rs around lines 1 to 88, the local borsh_compat
CompressedProof/ValidityProof mirror upstream types but lack guardrails against
silent ABI drift; add tests and compile-time size/layout checks: add unit tests
that serialize/deserialize (Borsh/Anchor) and round-trip via the From
conversions between the local types and
light_compressed_account::instruction_data::compressed_proof types asserting
byte-equality, and add static assertions (or std::mem::size_of! checks in a
#[test] or a const_assert via the static_assertions crate) asserting
size_of::<CompressedProof>() == 32+64+32 and size_of::<ValidityProof>() matches
the expected layout so any upstream field/size changes fail fast.

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.

Review continued from previous batch...

Comment on lines +21 to +87
#[allow(clippy::too_many_arguments)]
#[profile]
pub fn create_transfer_spl_to_ctoken_instruction(
source_spl_token_account: Pubkey,
to: Pubkey,
amount: u64,
authority: Pubkey,
mint: Pubkey,
payer: Pubkey,
token_pool_pda: Pubkey,
token_pool_pda_bump: u8,
spl_token_program: Pubkey,
) -> Result<Instruction, TokenSdkError> {
let packed_accounts = vec![
// Mint (index 0)
AccountMeta::new_readonly(mint, false),
// Destination token account (index 1)
AccountMeta::new(to, false),
// Authority for compression (index 2) - signer
AccountMeta::new_readonly(authority, true),
// Source SPL token account (index 3) - writable
AccountMeta::new(source_spl_token_account, false),
// Token pool PDA (index 4) - writable
AccountMeta::new(token_pool_pda, false),
// SPL Token program (index 5) - needed for CPI
AccountMeta::new_readonly(spl_token_program, false),
];

let wrap_spl_to_ctoken_account = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::compress_spl(
amount,
0, // mint
3, // source or recpient
2, // authority
4, // pool_account_index:
0, // pool_index
token_pool_pda_bump,
)),
delegate_is_set: false,
method_used: true,
};

let ctoken_account = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::decompress_ctoken(amount, 0, 1)),
delegate_is_set: false,
method_used: true,
};

let inputs = Transfer2Inputs {
validity_proof: ValidityProof::new(None),
transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(
payer,
packed_accounts,
),
in_lamports: None,
out_lamports: None,
token_accounts: vec![wrap_spl_to_ctoken_account, ctoken_account],
output_queue: 0, // Decompressed accounts only, no output queue needed
};

create_transfer2_instruction(inputs)
}
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 | 🔵 Trivial

SPL→ctoken Transfer2 wiring looks consistent but is index‑brittle

The SPL→ctoken constructor wires:

  • packed_accounts indices 0–5 exactly as documented in comments.
  • Compression::compress_spl with indices (mint=0, source=3, authority=2, pool=4, pool_index=0).
  • Compression::decompress_ctoken with (mint=0, source=1) for the destination ctoken.

That shape is internally consistent with the account2 helpers and Transfer2 abstractions. The main risk here is maintenance: a small change to packed_accounts ordering could silently break the hard‑coded indices.

To make this more robust, consider hoisting the indices into small const definitions so they are shared between the AccountMeta construction and the Compression::* calls.

🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs around
lines 21 to 87, the code uses hard‑coded numeric account indices when building
packed_accounts and passing them into Compression::compress_spl /
Compression::decompress_ctoken, which is brittle; define small consts (e.g.
const IDX_MINT: usize = 0; const IDX_DEST: usize = 1; const IDX_AUTH: usize = 2;
const IDX_SRC: usize = 3; const IDX_POOL: usize = 4; const IDX_SPL_PROGRAM:
usize = 5;) and then use those constants when creating
AccountMeta::new/_readonly (or when documenting each entry) and in the
Compression::compress_spl / decompress_ctoken calls so the index values are
single-sourced and easier to maintain.

Comment on lines +89 to +160
#[allow(clippy::too_many_arguments)]
#[profile]
pub fn create_transfer_ctoken_to_spl_instruction(
source_ctoken_account: Pubkey,
destination_spl_token_account: Pubkey,
amount: u64,
authority: Pubkey,
mint: Pubkey,
payer: Pubkey,
token_pool_pda: Pubkey,
token_pool_pda_bump: u8,
spl_token_program: Pubkey,
) -> Result<Instruction, TokenSdkError> {
let packed_accounts = vec![
// Mint (index 0)
AccountMeta::new_readonly(mint, false),
// Source ctoken account (index 1) - writable
AccountMeta::new(source_ctoken_account, false),
// Destination SPL token account (index 2) - writable
AccountMeta::new(destination_spl_token_account, false),
// Authority (index 3) - signer
AccountMeta::new_readonly(authority, true),
// Token pool PDA (index 4) - writable
AccountMeta::new(token_pool_pda, false),
// SPL Token program (index 5) - needed for CPI
AccountMeta::new_readonly(spl_token_program, false),
];

// First operation: compress from ctoken account to pool using compress_spl
let compress_to_pool = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::compress_ctoken(
amount, 0, // mint index
1, // source ctoken account index
3, // authority index
)),
delegate_is_set: false,
method_used: true,
};

// Second operation: decompress from pool to SPL token account using decompress_spl
let decompress_to_spl = CTokenAccount2 {
inputs: vec![],
output: MultiTokenTransferOutputData::default(),
compression: Some(Compression::decompress_spl(
amount,
0, // mint index
2, // destination SPL token account index
4, // pool_account_index
0, // pool_index (TODO: make dynamic)
token_pool_pda_bump,
)),
delegate_is_set: false,
method_used: true,
};

let inputs = Transfer2Inputs {
validity_proof: ValidityProof::new(None),
transfer_config: Transfer2Config::default().filter_zero_amount_outputs(),
meta_config: Transfer2AccountsMetaConfig::new_decompressed_accounts_only(
payer,
packed_accounts,
),
in_lamports: None,
out_lamports: None,
token_accounts: vec![compress_to_pool, decompress_to_spl],
output_queue: 0, // Decompressed accounts only, no output queue needed
};

create_transfer2_instruction(inputs)
}
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.

⚠️ Potential issue | 🟡 Minor

ctoken→SPL constructor is coherent; fix misleading comment and consider index constants

The ctoken→SPL constructor uses:

  • compress_ctoken(amount, 0, 1, 3) for (mint=0, source_ctoken=1, authority=3).
  • decompress_spl(amount, 0, 2, 4, 0, token_pool_pda_bump) for (mint=0, dest_spl=2, pool=4, pool_index=0).

This matches the indices established in packed_accounts and mirrors the SPL→ctoken flow.

The comment at Line 117, however, says “compress from ctoken account to pool using compress_spl” while the actual code correctly uses compress_ctoken. That’s confusing for future readers.

I’d recommend updating the comment and, as above, pulling the magic indices into local constants to reduce the chance of drift between the AccountMeta vector and the Compression calls.

-    // First operation: compress from ctoken account to pool using compress_spl
+    // First operation: compress from ctoken account to pool using `compress_ctoken`
@@
-    // Second operation: decompress from pool to SPL token account using decompress_spl
+    // Second operation: decompress from pool to SPL token account using `decompress_spl`
🤖 Prompt for AI Agents
In sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs around
lines 89 to 160 the inline comment incorrectly says "compress from ctoken
account to pool using compress_spl" while the code uses compress_ctoken, and the
numeric account indices (0,1,2,3,4) are used as magic numbers which can drift;
update the misleading comment to reference compress_ctoken and replace the magic
indices with small local consts (e.g., MINT_IDX=0, SRC_CTOKEN_IDX=1,
DEST_SPL_IDX=2, AUTHORITY_IDX=3, POOL_IDX=4, POOL_INDEX=0) then use those
constants when building packed_accounts and when calling
Compression::compress_ctoken and Compression::decompress_spl so the mapping is
explicit and less error-prone.

Comment on lines +162 to +247
/// Transfer SPL tokens to compressed tokens
#[allow(clippy::too_many_arguments)]
pub fn transfer_spl_to_ctoken<'info>(
payer: AccountInfo<'info>,
authority: AccountInfo<'info>,
source_spl_token_account: AccountInfo<'info>,
destination_ctoken_account: AccountInfo<'info>,
mint: AccountInfo<'info>,
spl_token_program: AccountInfo<'info>,
compressed_token_pool_pda: AccountInfo<'info>,
compressed_token_pool_pda_bump: u8,
compressed_token_program_authority: AccountInfo<'info>,
amount: u64,
) -> Result<(), ProgramError> {
let instruction = create_transfer_spl_to_ctoken_instruction(
*source_spl_token_account.key,
*destination_ctoken_account.key,
amount,
*authority.key,
*mint.key,
*payer.key,
*compressed_token_pool_pda.key,
compressed_token_pool_pda_bump,
*spl_token_program.key,
)
.map_err(|_| ProgramError::InvalidInstructionData)?;

// let mut account_infos = remaining_accounts.to_vec();
let account_infos = vec![
payer,
compressed_token_program_authority,
mint, // Index 0: Mint
destination_ctoken_account, // Index 1: Destination owner
authority, // Index 2: Authority (signer)
source_spl_token_account, // Index 3: Source SPL token account
compressed_token_pool_pda, // Index 4: Token pool PDA
spl_token_program, // Index 5: SPL Token program
];

invoke(&instruction, &account_infos)?;
Ok(())
}

// TODO: must test this.
/// Transfer SPL tokens to compressed tokens via CPI signer
#[allow(clippy::too_many_arguments)]
pub fn transfer_spl_to_ctoken_signed<'info>(
payer: AccountInfo<'info>,
authority: AccountInfo<'info>,
source_spl_token_account: AccountInfo<'info>,
destination_ctoken_account: AccountInfo<'info>,
mint: AccountInfo<'info>,
spl_token_program: AccountInfo<'info>,
compressed_token_pool_pda: AccountInfo<'info>,
compressed_token_pool_pda_bump: u8,
compressed_token_program_authority: AccountInfo<'info>,
amount: u64,
signer_seeds: &[&[&[u8]]],
) -> Result<(), ProgramError> {
let instruction = create_transfer_spl_to_ctoken_instruction(
*source_spl_token_account.key,
*destination_ctoken_account.key,
amount,
*authority.key,
*mint.key,
*payer.key,
*compressed_token_pool_pda.key,
compressed_token_pool_pda_bump,
*spl_token_program.key,
)
.map_err(|_| ProgramError::InvalidInstructionData)?;

let account_infos = vec![
payer,
compressed_token_program_authority,
mint, // Index 0: Mint
destination_ctoken_account, // Index 1: Destination owner
authority, // Index 2: Authority (signer)
source_spl_token_account, // Index 3: Source SPL token account
compressed_token_pool_pda, // Index 4: Token pool PDA
spl_token_program, // Index 5: SPL Token program
];

invoke_signed(&instruction, &account_infos, signer_seeds)?;
Ok(())
}
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 | 🔵 Trivial

CPI helpers for SPL→ctoken are wired correctly; ensure meta ordering stays in sync

Both transfer_spl_to_ctoken and transfer_spl_to_ctoken_signed:

  • Reuse create_transfer_spl_to_ctoken_instruction (good separation of concern).
  • Assemble account_infos in the expected order: payer, compressed_token_program_authority, followed by the same accounts used when building packed_accounts (mint, dest ctoken, authority, source SPL, pool, SPL program).
  • Use invoke/invoke_signed appropriately.

The correctness here hinges on the account_infos order continuing to match the metas produced by Transfer2AccountsMetaConfig::new_decompressed_accounts_only. If that meta builder ever changes, these wrappers can silently break.

It’s worth adding a brief doc comment or test asserting the expected account order to catch future refactors.

🤖 Prompt for AI Agents
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs lines
162-247: the CPI wrappers rely on a specific account order matching
Transfer2AccountsMetaConfig::new_decompressed_accounts_only and could break
silently if that builder changes; add a short doc comment above both
transfer_spl_to_ctoken and transfer_spl_to_ctoken_signed stating the exact
required account order (payer, compressed_token_program_authority, mint,
destination_ctoken_account, authority, source_spl_token_account,
compressed_token_pool_pda, spl_token_program) and add a unit/integration test
that constructs the metas via
Transfer2AccountsMetaConfig::new_decompressed_accounts_only and asserts the
resulting meta order/keys equals the order used in these wrappers so CI will
catch future refactors that change meta ordering.

Comment on lines +335 to +437
/// Unified transfer interface for ctoken<->ctoken and ctoken<->spl transfers
///
/// # Arguments
/// * `source_account` - Source token account (can be ctoken or SPL)
/// * `destination_account` - Destination token account (can be ctoken or SPL)
/// * `authority` - Authority for the transfer (must be signer)
/// * `amount` - Amount to transfer
/// * `payer` - Payer for the transaction
/// * `compressed_token_program_authority` - Compressed token program authority
/// * `mint` - Optional mint account (required for SPL<->ctoken transfers)
/// * `spl_token_program` - Optional SPL token program (required for SPL<->ctoken transfers)
/// * `compressed_token_pool_pda` - Optional token pool PDA (required for SPL<->ctoken transfers)
/// * `compressed_token_pool_pda_bump` - Optional bump seed for token pool PDA
///
/// # Errors
/// * `SplBridgeConfigRequired` - If transferring to/from SPL without required accounts
/// * `UseRegularSplTransfer` - If both source and destination are SPL accounts
/// * `CannotDetermineAccountType` - If account type cannot be determined
#[allow(clippy::too_many_arguments)]
pub fn transfer_interface<'info>(
source_account: &AccountInfo<'info>,
destination_account: &AccountInfo<'info>,
authority: &AccountInfo<'info>,
amount: u64,
payer: &AccountInfo<'info>,
compressed_token_program_authority: &AccountInfo<'info>,
mint: Option<&AccountInfo<'info>>,
spl_token_program: Option<&AccountInfo<'info>>,
compressed_token_pool_pda: Option<&AccountInfo<'info>>,
compressed_token_pool_pda_bump: Option<u8>,
) -> Result<(), ProgramError> {
let source_is_ctoken =
is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?;
let dest_is_ctoken =
is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?;

match (source_is_ctoken, dest_is_ctoken) {
(true, true) => transfer_ctoken(source_account, destination_account, authority, amount),

(true, false) => {
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_ctoken_to_spl(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
)
}

(false, true) => {
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_spl_to_ctoken(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
)
}

// spl -> spl: Not supported
(false, false) => Err(ProgramError::Custom(
TokenSdkError::UseRegularSplTransfer.into(),
)),
}
}
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.

⚠️ Potential issue | 🟠 Major

is_ctoken_account error mapping conflicts with documented CannotDetermineAccountType

The transfer_interface docs state that CannotDetermineAccountType is returned “if account type cannot be determined”, but the implementation does:

let source_is_ctoken =
    is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?;
let dest_is_ctoken =
    is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?;

This collapses all TokenSdkError variants from is_ctoken_account into InvalidAccountData, so callers never see CannotDetermineAccountType (or any other finer‑grained TokenSdkError).

If you want the documented behavior and consistent custom error codes on the program boundary, consider propagating the original error:

-    let source_is_ctoken =
-        is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?;
-    let dest_is_ctoken =
-        is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?;
+    let source_is_ctoken =
+        is_ctoken_account(source_account).map_err(|e| ProgramError::Custom(e.into()))?;
+    let dest_is_ctoken =
+        is_ctoken_account(destination_account).map_err(|e| ProgramError::Custom(e.into()))?;

This way, TokenSdkError::CannotDetermineAccountType and other variants are preserved as documented.

🤖 Prompt for AI Agents
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs lines
335-437: the current mapping of is_ctoken_account errors collapses all
TokenSdkError variants into ProgramError::InvalidAccountData, contradicting the
docs that say CannotDetermineAccountType (and other TokenSdkError variants)
should be returned; change the map_err calls to convert the returned
TokenSdkError into a ProgramError::Custom by mapping the original error into
ProgramError::Custom(err.into()) (do this for both source and destination
checks) so the original TokenSdkError variants are preserved and propagated to
the program boundary.

Comment on lines +439 to +539
/// Unified transfer interface with CPI
#[allow(clippy::too_many_arguments)]
pub fn transfer_interface_signed<'info>(
source_account: &AccountInfo<'info>,
destination_account: &AccountInfo<'info>,
authority: &AccountInfo<'info>,
amount: u64,
payer: &AccountInfo<'info>,
compressed_token_program_authority: &AccountInfo<'info>,
mint: Option<&AccountInfo<'info>>,
spl_token_program: Option<&AccountInfo<'info>>,
compressed_token_pool_pda: Option<&AccountInfo<'info>>,
compressed_token_pool_pda_bump: Option<u8>,
signer_seeds: &[&[&[u8]]],
) -> Result<(), ProgramError> {
// Determine account types
let source_is_ctoken =
is_ctoken_account(source_account).map_err(|_| ProgramError::InvalidAccountData)?;
let dest_is_ctoken =
is_ctoken_account(destination_account).map_err(|_| ProgramError::InvalidAccountData)?;

match (source_is_ctoken, dest_is_ctoken) {
// ctoken -> ctoken: Direct transfer (bridge accounts not needed)
(true, true) => transfer_ctoken_signed(
source_account,
destination_account,
authority,
amount,
signer_seeds,
),

// ctoken -> spl: Requires bridge accounts
(true, false) => {
// Validate all required accounts are provided
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_ctoken_to_spl_signed(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
signer_seeds,
)
}

// spl -> ctoken: Requires bridge accounts
(false, true) => {
// Validate all required accounts are provided
let (mint_acct, spl_program, pool_pda, bump) = match (
mint,
spl_token_program,
compressed_token_pool_pda,
compressed_token_pool_pda_bump,
) {
(Some(m), Some(p), Some(pd), Some(b)) => (m, p, pd, b),
_ => {
return Err(ProgramError::Custom(
TokenSdkError::IncompleteSplBridgeConfig.into(),
))
}
};

transfer_spl_to_ctoken_signed(
payer.clone(),
authority.clone(),
source_account.clone(),
destination_account.clone(),
mint_acct.clone(),
spl_program.clone(),
pool_pda.clone(),
bump,
compressed_token_program_authority.clone(),
amount,
signer_seeds,
)
}

// spl -> spl: Not supported
(false, false) => Err(ProgramError::Custom(
TokenSdkError::UseRegularSplTransfer.into(),
)),
}
}
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.

⚠️ Potential issue | 🟡 Minor

Signed unified interface mirrors the unsigned path; same error‑mapping consideration applies

transfer_interface_signed cleanly mirrors transfer_interface:

  • Same (ctoken, spl) arm structure.
  • Delegates to the _signed variants in the SPL bridge and ctoken transfer helpers.
  • Correctly passes through signer_seeds.

As with the unsigned version, you’re currently mapping is_ctoken_account errors to ProgramError::InvalidAccountData. If you adopt the error propagation change suggested above, it should be applied here as well for consistency.

🤖 Prompt for AI Agents
sdk-libs/compressed-token-sdk/src/instructions/transfer_interface.rs around
lines 439 to 539: both is_ctoken_account calls currently swallow the original
error by mapping any failure to ProgramError::InvalidAccountData; update those
two lines to propagate the original TokenSdkError into the function's Result
instead of always returning InvalidAccountData — do this by using the ? operator
(or map_err(Into::into)) so the TokenSdkError is converted into a ProgramError
and returned, keeping error propagation consistent with the unsigned variant.

Comment on lines +38 to +45
impl CompressibleConfig {
pub const LEN: usize = 1 + 4 + 32 + 32 + 1 + 4 + (32 * MAX_ADDRESS_TREES_PER_SPACE) + 1; // 107 bytes max

/// Calculate the exact size needed for a CompressibleConfig with the given
/// number of address spaces
pub fn size_for_address_space(num_address_trees: usize) -> usize {
1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1
}
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 | 🔵 Trivial

Align length checks with MAX_ADDRESS_TREES_PER_SPACE for maintainability

validate and size_for_address_space hard‑code 1 for address_space.len() rather than using MAX_ADDRESS_TREES_PER_SPACE. That’s correct today but makes future expansion (e.g. allowing more trees per space) easier to miss.

You could avoid drift by tying all length checks to the constant:

-        if self.address_space.len() != 1 {
+        if self.address_space.len() != MAX_ADDRESS_TREES_PER_SPACE {
@@
-    pub fn size_for_address_space(num_address_trees: usize) -> usize {
-        1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1
-    }
+    pub fn size_for_address_space(num_address_trees: usize) -> usize {
+        1 + 4 + 32 + 32 + 1 + 4 + (32 * num_address_trees) + 1
+    }

…and mirroring the same pattern in the initializer where you currently compare to 1.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/config.rs around lines 38 to 45, the code
hard-codes the byte count for address_space length as `1` in LEN and size
calculations; update those to use the constant MAX_ADDRESS_TREES_PER_SPACE
instead of the literal `1`, and mirror the same change in validate and the
initializer where you currently compare to `1` so all length checks and size
math reference the single MAX_ADDRESS_TREES_PER_SPACE constant for
maintainability.

Comment on lines +150 to +240
pub fn process_initialize_compression_config_account_info<'info>(
config_account: &AccountInfo<'info>,
update_authority: &AccountInfo<'info>,
rent_sponsor: &Pubkey,
address_space: Vec<Pubkey>,
compression_delay: u32,
config_bump: u8,
payer: &AccountInfo<'info>,
system_program: &AccountInfo<'info>,
program_id: &Pubkey,
) -> Result<(), crate::ProgramError> {
// CHECK: only 1 address_space
if config_bump != 0 {
msg!("Config bump must be 0 for now, found: {}", config_bump);
return Err(LightSdkError::ConstraintViolation.into());
}

// CHECK: not already initialized
if config_account.data_len() > 0 {
msg!("Config account already initialized");
return Err(LightSdkError::ConstraintViolation.into());
}

// CHECK: only 1 address_space
if address_space.len() != 1 {
msg!(
"Address space must contain exactly 1 pubkey, found: {}",
address_space.len()
);
return Err(LightSdkError::ConstraintViolation.into());
}

// CHECK: unique pubkeys in address_space
validate_address_space_no_duplicates(&address_space)?;

// CHECK: signer
if !update_authority.is_signer {
msg!("Update authority must be signer for initial config creation");
return Err(LightSdkError::ConstraintViolation.into());
}

// CHECK: pda derivation
let (derived_pda, bump) = CompressibleConfig::derive_pda(program_id, config_bump);
if derived_pda != *config_account.key {
msg!("Invalid config PDA");
return Err(LightSdkError::ConstraintViolation.into());
}

let rent = Rent::get().map_err(LightSdkError::from)?;
let account_size = CompressibleConfig::size_for_address_space(address_space.len());
let rent_lamports = rent.minimum_balance(account_size);

let seeds = &[COMPRESSIBLE_CONFIG_SEED, &[config_bump], &[bump]];
let create_account_ix = system_instruction::create_account(
payer.key,
config_account.key,
rent_lamports,
account_size as u64,
program_id,
);

invoke_signed(
&create_account_ix,
&[
payer.clone(),
config_account.clone(),
system_program.clone(),
],
&[seeds],
)
.map_err(LightSdkError::from)?;

let config = CompressibleConfig {
version: 1,
compression_delay,
update_authority: *update_authority.key,
rent_sponsor: *rent_sponsor,
config_bump,
address_space,
bump,
};

let mut data = config_account
.try_borrow_mut_data()
.map_err(LightSdkError::from)?;
config
.serialize(&mut &mut data[..])
.map_err(|_| LightSdkError::Borsh)?;

Ok(())
}
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 | 🔵 Trivial

Config initialization flow is sound; consider minor validation tightenings

The initialization path correctly enforces:

  • config_bump == 0 (matching validate)
  • uninitialized config_account (by data length)
  • address space cardinality + duplicates
  • signer requirement on update_authority
  • PDA derivation consistency
  • rent‑exempt allocation using size_for_address_space

Two small optional tightenings:

  1. Use MAX_ADDRESS_TREES_PER_SPACE instead of a literal 1 for the address space length check, as suggested earlier.
  2. Optionally assert the pre‑init owner is the system program to fail a bit earlier/more explicitly if a wrong account is passed:
+    // CHECK: config account must be system-owned pre-initialization
+    if config_account.owner != &solana_system_interface::ID {
+        msg!("Config account must be system-owned before initialization");
+        return Err(LightSdkError::ConstraintViolation.into());
+    }

(if that matches how you typically bootstrap PDAs in this codebase).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/config.rs around lines 150 to 240, replace the
hard-coded address_space length check that expects 1 with a check against the
canonical MAX_ADDRESS_TREES_PER_SPACE constant, and add an early owner
validation to ensure config_account.owner == &system_program::id() (returning
ConstraintViolation if not) before calling system_instruction::create_account;
keep existing duplicate-check, signer, PDA and rent logic unchanged.

Comment on lines +448 to +476
/// Validates that address_space contains no duplicate pubkeys
fn validate_address_space_no_duplicates(address_space: &[Pubkey]) -> Result<(), LightSdkError> {
let mut seen = HashSet::new();
for pubkey in address_space {
if !seen.insert(pubkey) {
msg!("Duplicate pubkey found in address_space: {}", pubkey);
return Err(LightSdkError::ConstraintViolation);
}
}
Ok(())
}

/// Validates that new_address_space only adds to existing address_space (no removals)
fn validate_address_space_only_adds(
existing_address_space: &[Pubkey],
new_address_space: &[Pubkey],
) -> Result<(), LightSdkError> {
// Check that all existing pubkeys are still present in new address space
for existing_pubkey in existing_address_space {
if !new_address_space.contains(existing_pubkey) {
msg!(
"Cannot remove existing pubkey from address_space: {}",
existing_pubkey
);
return Err(LightSdkError::ConstraintViolation);
}
}
Ok(())
}
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 | 🔵 Trivial

Address space validators are correct; could be made more scalable if needed

The duplicate check and “only adds” validator are logically correct and keep address_space evolution monotonic. Complexity is O(n²) with contains on a slice, but given MAX_ADDRESS_TREES_PER_SPACE is 1 (and likely small even when raised), this is fine.

If you ever raise the limit significantly, consider switching new_address_space.contains to a HashSet look‑up to keep this O(n).

🤖 Prompt for AI Agents
In sdk-libs/sdk/src/compressible/config.rs around lines 448 to 476, the
validators are correct but validate_address_space_only_adds does an O(n²) check
by calling new_address_space.contains for each existing pubkey; to make it
scalable, build a HashSet from new_address_space once (e.g., let new_set:
HashSet<_> = new_address_space.iter().cloned().collect();) and use
new_set.contains(existing_pubkey) inside the loop, returning the same
ConstraintViolation error when missing; this keeps behavior identical but
reduces complexity to O(n).

Comment on lines +76 to +141
/// Helper function to decompress a compressed account into a PDA
/// idempotently with seeds.
#[inline(never)]
#[cfg(feature = "v2")]
pub fn prepare_account_for_decompression_idempotent<'a, 'info, T>(
program_id: &Pubkey,
data: T,
compressed_meta: CompressedAccountMeta,
solana_account: &AccountInfo<'info>,
rent_payer: &AccountInfo<'info>,
cpi_accounts: &CpiAccounts<'a, 'info>,
signer_seeds: &[&[u8]],
) -> Result<
Option<light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo>,
LightSdkError,
>
where
T: Clone
+ crate::account::Size
+ LightDiscriminator
+ Default
+ AnchorSerialize
+ AnchorDeserialize
+ HasCompressionInfo
+ 'info,
{
if !solana_account.data_is_empty() {
msg!("Account already initialized, skipping");
return Ok(None);
}
let rent = Rent::get().map_err(|err| {
msg!("Failed to get rent: {:?}", err);
LightSdkError::Borsh
})?;

let light_account = LightAccount::<T>::new_close(program_id, &compressed_meta, data)?;

let space = T::size(&light_account.account);
let rent_minimum_balance = rent.minimum_balance(space);

invoke_create_account_with_heap(
rent_payer,
solana_account,
rent_minimum_balance,
space as u64,
&cpi_accounts.self_program_id(),
signer_seeds,
cpi_accounts.system_program()?,
)?;

let mut decompressed_pda = light_account.account.clone();
*decompressed_pda.compression_info_mut_opt() =
Some(super::compression_info::CompressionInfo::new_decompressed()?);

let mut account_data = solana_account.try_borrow_mut_data()?;
let discriminator_len = T::LIGHT_DISCRIMINATOR.len();
account_data[..discriminator_len].copy_from_slice(&T::LIGHT_DISCRIMINATOR);
decompressed_pda
.serialize(&mut &mut account_data[discriminator_len..])
.map_err(|err| {
msg!("Failed to serialize decompressed PDA: {:?}", err);
LightSdkError::Borsh
})?;

Ok(Some(light_account.to_account_info()?))
}
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 | 🔵 Trivial

Check owner consistency between program_id and cpi_accounts.self_program_id()

prepare_account_for_decompression_idempotent correctly:

  • Makes the operation idempotent by early‑returning when solana_account.data_is_empty() is false.
  • Computes rent and size via Rent::get and T::size(&light_account.account).
  • Uses LightAccount::<T>::new_close(program_id, &compressed_meta, data)? to construct the decompressed representation.
  • Initializes compression info to Decompressed and writes the discriminator + serialized payload into the account data.

One subtle point to double‑check:

  • The LightAccount is built with program_id as the owner.
  • The on‑chain account is created with &cpi_accounts.self_program_id() as the owner:
invoke_create_account_with_heap(
    rent_payer,
    solana_account,
    rent_minimum_balance,
    space as u64,
    &cpi_accounts.self_program_id(),
    signer_seeds,
    cpi_accounts.system_program()?,
)?;

If cpi_accounts.self_program_id() is ever allowed to differ from the program_id passed into this function, you will end up with a mismatch between the on‑chain account owner and the owner encoded in LightAccount<T>. That can lead to confusing behavior when validating ownership or when other helpers rely on the program_id parameter.

If they are guaranteed to be identical by construction, it might be clearer (and safer) to pass program_id directly here, or at least assert equality before calling invoke_create_account_with_heap:

+    debug_assert_eq!(
+        cpi_accounts.self_program_id(),
+        *program_id,
+        "prepare_account_for_decompression_idempotent: program_id mismatch"
+    );
@@
-        &cpi_accounts.self_program_id(),
+        program_id,

Also, mapping Rent::get failures to LightSdkError::Borsh is slightly misleading semantically; consider a more specific variant if you ever extend the error enum.

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