fix: swen csdk#2055
Conversation
WalkthroughThis pull request substantially restructures the compressed token and compressible account infrastructure. It migrates token types to a new Changes
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
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
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
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Specific areas requiring careful attention:
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Principal Review NotesArchitecture & Design: This is a well-structured consolidation of the compressed account and token infrastructure. The migration of token types to Key Strengths:
Watch Points:
Pre-merge checks and finishing touches❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
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
61afcd1 to
b0c20f0
Compare
There was a problem hiding this comment.
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 whetherlight-compressibleshould be gated by a featureAdding
light-compressibledirectly 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 tweakThe new PDA helpers:
- Reuse
get_compression_authority_seeds/get_rent_sponsor_seeds- Call
Pubkey::find_program_addresswith the same seed order already used innew()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 inderive_*_pdaand construct the seed slices directly from aversion_bytesarray, mirroring thenew()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 onlight-compressed-token-sdkYou already depend on
light-compressed-token-sdkin[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 linterThe pipeline failure indicates a formatting inconsistency around the
TokenDataVersionimport. 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_IDwith 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 flagThe review identifies three real issues confirmed by comparing implementations:
Dummy
Pubkey::default()account will cause transaction failure
Token-client addsAccountMeta::new_readonly(Pubkey::default(), false)as a fourth account. This pubkey does not exist on Solana, so the transaction will fail withAccountNotFoundbefore the program executes.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.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 fromcompressed-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:
AccountMeta vs AccountInfo count/ordering mismatch (Lines 76–83 vs 160–168) — CRITICAL
create_compressible_token_account_instructionbuilds anInstructionwith sixAccountMetas:
- token_account (signer)
- mint
- payer (signer)
- compressible_config
- system_program (readonly)
- rent_sponsor
But
create_ctoken_account_signedinvokes with only fiveAccountInfos 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
AccountInfoslice must align in order and length withInstruction.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
Instructionand addsystem_programif 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], )Signer seeds / bump handling is fragile (Lines 134–138, 149–153)
let bump = signer_seeds[signer_seeds.len() - 1][0];will panic ifsigner_seedsis empty and assumes the last seed is exactly one byte.- No validation of
signer_seedslength 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 aTokenSdkErrorinstead of panicking.sdk-libs/client/src/indexer/types.rs (1)
741-813: Factor out shared logic betweenTokenAccountandTokenAccountV2conversionsThe two
TryFrom<&photon_api::models::TokenAccount>andTryFrom<&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 newAccountStatevariants 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::newandCTokenAccount2::new_delegatedboth rely on the first element’smint,owner, andversion, 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 inconsistentoutput, which can later generate invalid transfer instructions that are hard to diagnose.You’re also using
TokenSdkError::InsufficientBalanceas a placeholder error whentoken_datais empty, which conflates “no inputs provided” with “not enough funds”.I’d strongly recommend:
- Adding explicit runtime checks that all
token_dataentries share the samemintandowner(and perhapsversion), returning a clear error if not.- Introducing a more specific error variant for the “no inputs” case (e.g.,
InvalidInputsorEmptyInputs) and using that instead ofInsufficientBalance.This makes the API’s invariants explicit and prevents subtle instruction construction bugs.
Also applies to: 57-87
139-165: Clarify and consistently use thedelegate_is_set/method_usedflags
delegate_is_setandmethod_usedare written in several methods but never read inside this module, and their semantics are currently inconsistent:
new_delegatedcreates an output withhas_delegate = truebut setsdelegate_is_set: false.approvereturns a newCTokenAccount2withdelegate_is_set: true.method_usedis set totrueon the mutably-borrowedselfbut reset tofalseon the returnedSelf, 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_delegatedprobably wantsdelegate_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 safetyThe compression/decompression methods (
compress_ctoken,compress_spl,decompress_ctoken,decompress_spl,compress_full,compress_and_close) correctly prevent multiple compression operations perCTokenAccount2instance via theself.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 theu64amount silently. Usingchecked_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.,
transfervs.compress*,compress*vs.decompress*), but this is only documented, not enforced. A state-machine-style API (consumingselfor 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 inmint/ownerhelper methods
mintandownerindex directly into theaccount_infosslice usingself.mint/self.owner(viaDereftoMultiTokenTransferOutputData):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_infosslice is too short or mis-ordered, these will panic. Elsewhere (e.g., inUnpackimpls) you already treat out-of-bounds indices asProgramError::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 aResult<Pubkey, TokenSdkError>instead, to propagate a structured error rather than panicking.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (56)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/cli-v1.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonecli/src/commands/token-balance/index.tsis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/account-compression-test/Cargo.tomlis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/compress_and_close.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/create.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/shared.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/mint/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/transfer2/spl_ctoken.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/v1.rsis excluded by none and included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by noneprogram-tests/system-cpi-test/Cargo.tomlis excluded by none and included by noneprogram-tests/system-cpi-test/tests/test.rsis excluded by none and included by noneprogram-tests/utils/src/assert_mint_to_compressed.rsis excluded by none and included by noneprogram-tests/utils/src/assert_token_tx.rsis excluded by none and included by noneprogram-tests/utils/src/assert_transfer2.rsis excluded by none and included by noneprogram-tests/utils/src/conversions.rsis excluded by none and included by noneprogram-tests/utils/src/e2e_test_env.rsis excluded by none and included by noneprogram-tests/utils/src/spl.rsis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/client-test/tests/light_client.rsis excluded by none and included by nonesdk-tests/client-test/tests/light_program_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/constants.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/mod.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/user_record_tests.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_four_transfer2.rsis 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", |
There was a problem hiding this comment.
🧩 Analysis chain
Clarify the feature enablement pattern—redundancy between direct features and feature gates.
The keccak support is being enabled in two conflicting ways:
- Direct dependency features (lines 49, 57, 64): Always enable keccak
- 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:
- Should keccak be unconditionally enabled (remove feature gates, keep direct features)?
- Should keccak be conditionally enabled (remove direct features, keep feature gates)?
- 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.tomlLength 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 } |
There was a problem hiding this comment.
🧩 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.tomlLength 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 -20Length 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.
| pub use solana_address_lookup_table_interface::{ | ||
| error, instruction, program, state::AddressLookupTable, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| 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.
| let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| { | ||
| RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}")) | ||
| })?; |
There was a problem hiding this comment.
🧹 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.
| 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.
| 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 | ||
| } | ||
|
|
There was a problem hiding this comment.
🧹 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.
| #![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 | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Address-derivation helper looks fine; clippy allow should be temporary
into_compressed_meta_with_addresscorrectly wrapsderive_addressand reuses the existingtree_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.
| /// 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()?)) | ||
| } |
There was a problem hiding this comment.
🧩 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_emptycheck. - Rent‑exempt balance from
Rent::minimum_balance. - PDA creation through
invoke_create_account_with_heap. - Initializing
CompressionInfotoDecompressed. - Writing
T::LIGHT_DISCRIMINATORfollowed by serializeddecompressed_pdainto 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_lenbytes 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 lengthserialized_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_idintoLightAccount::<T>::new_close, butcpi_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 -50Length of output: 3312
🏁 Script executed:
#!/bin/bash
# Check LightDiscriminator trait definition
rg -n "trait LightDiscriminator" --type rust -A 5Length 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 5Length 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 decompressLength 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 -100Length 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 5Length 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 2Length 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 -60Length of output: 3068
🏁 Script executed:
#!/bin/bash
# Find HasCompressionInfo trait definition
rg -n "trait HasCompressionInfo|pub trait HasCompressionInfo" --type rust -A 10Length 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 -40Length 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 rustLength 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 -100Length 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 -75Length 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| pub mod proof; | ||
| /// Transfer compressed sol between compressed accounts. |
There was a problem hiding this comment.
🧹 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_sdkpublic API; future refactors incompressiblewill need to preserve or carefully deprecate these names. - The generic names
PackandUnpackat the root may overlap conceptually with otherPacktraits (e.g., Solana’sprogram_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.
| #[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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
There was a problem hiding this comment.
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: Newderive_cmint_from_spl_minthelper looks correct; consider clarifying usageThe helper cleanly parallels
derive_compressed_mint_address, but takes an SPL mint directly and forwards it intoderive_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_addressvsderive_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: Clarifyrent_sponsor_is_signersemanticsHere
rent_sponsor_is_signeractually 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
CompressAndCloseIndicesis built inpack_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_signeroruse_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 forlight-compressed-token-sdkYou 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_IDat Line 46 limits support to the standard SPL Token program.Verify whether SPL Token 2022 support is required. If so, consider:
- Adding a
spl_token_programparameter to the function- Detecting the program from the token account data
- 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 twiceFunctionally this is correct, but you currently derive the token‑pool PDA twice when
spl_mint_initializedis true:
- Line 60:
find_token_pool_pda_with_index(&spl_mint_pda, 0)fordecompressed_mint_config.- Line 72:
derive_token_pool(&spl_mint_pda, 0), which itself callsfind_token_pool_pda_with_index.You can tighten this up by deriving once and reusing it for both the config and the
token_poolstruct, 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:
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.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 aPubkey::default()readonly account that the SDK does not have.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-sdkfor 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 bitThe new
Into/TryFromimpls againstlight_compressed_token_sdk::compat::TokenDataWithMerkleContextlook semantically correct and line up with the existingCompressedAccount <-> CompressedAccountWithMerkleContextconversions.Two small cleanups to consider:
- In the
Into<Vec<...>>impl,token_accountcomes frominto_iter(), sotoken_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 againstTokenDataWithMerkleContextandVec<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: Publicusestatements required for crate-wide visibility in derive macrosThe review comment is accurate. Lines 11–15 use plain
useinstead ofpub use, which meansAnchorDeserializeandAnchorSerializeexist only withinlib.rsmodule scope.However, actual code in the codebase references these through the crate root:
sdk-libs/compressed-token-sdk/src/instructions/decompress_full.rsuses#[derive(..., crate::AnchorSerialize, crate::AnchorDeserialize)]sdk-libs/compressed-token-sdk/src/instructions/compress_and_close.rsuses#[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 addpubis 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 failuresThe review identified a real critical issue. The instruction has 6 account metas, but the implementation has multiple problems:
Line 81: System program meta uses
Pubkey::default()instead of the actual system program ID (11111…111).Lines 160–168: The
create_ctoken_account_signedfunction callsinvoke_signedwith 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_signedin the same order as the metas:&[token_account, mint_account, payer, ctoken_config_account, system_program, ctoken_rent_sponsor]- Add
system_programas a parameter tocreate_ctoken_account_signedand pass it toinvoke_signedIf 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
⛔ Files ignored due to path filters (56)
.github/actions/setup-and-build/action.ymlis excluded by none and included by none.github/workflows/cli-v1.ymlis excluded by none and included by none.github/workflows/sdk-tests.ymlis excluded by none and included by noneCargo.lockis excluded by!**/*.lockand included by noneCargo.tomlis excluded by none and included by nonecli/src/commands/token-balance/index.tsis excluded by none and included by nonepnpm-lock.yamlis excluded by!**/pnpm-lock.yamland included by noneprogram-tests/account-compression-test/Cargo.tomlis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/compress_and_close.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/create.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/ctoken/shared.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/mint/functional.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/transfer2/spl_ctoken.rsis excluded by none and included by noneprogram-tests/compressed-token-test/tests/v1.rsis excluded by none and included by noneprogram-tests/registry-test/tests/tests.rsis excluded by none and included by noneprogram-tests/system-cpi-test/Cargo.tomlis excluded by none and included by noneprogram-tests/system-cpi-test/tests/test.rsis excluded by none and included by noneprogram-tests/utils/src/assert_mint_to_compressed.rsis excluded by none and included by noneprogram-tests/utils/src/assert_token_tx.rsis excluded by none and included by noneprogram-tests/utils/src/assert_transfer2.rsis excluded by none and included by noneprogram-tests/utils/src/conversions.rsis excluded by none and included by noneprogram-tests/utils/src/e2e_test_env.rsis excluded by none and included by noneprogram-tests/utils/src/spl.rsis excluded by none and included by nonesdk-tests/client-test/Cargo.tomlis excluded by none and included by nonesdk-tests/client-test/tests/light_client.rsis excluded by none and included by nonesdk-tests/client-test/tests/light_program_test.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/Anchor.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/Cargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/Xargo.tomlis excluded by none and included by nonesdk-tests/csdk-anchor-test/package.jsonis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/constants.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/errors.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instruction_accounts.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/compress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_placeholder_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/create_user_record_and_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/decompress_accounts_idempotent.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/initialize_compression_config.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/mod.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_compression_config.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_game_session.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/instructions/update_record.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/lib.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/seeds.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/src/state.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/game_session_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/helpers.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/idempotency_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/multi_account_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/placeholder_tests.rsis excluded by none and included by nonesdk-tests/csdk-anchor-test/tests/user_record_tests.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_create_ctoken_with_compress_to_pubkey.rsis excluded by none and included by nonesdk-tests/sdk-token-test/src/process_four_transfer2.rsis 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; |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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}; |
There was a problem hiding this comment.
🧹 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 andmethod_usedpattern. - Derives
amountfromget_token_account_balance(token_account_info)and adds it toself.output.amount. - Builds a
Compressionwith modeCompressand zeroed pool fields, which is consistent with how non‑poolcompress_ctokencases 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.
| 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() | ||
| } |
There was a problem hiding this comment.
🧹 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_bumpuses[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.
| 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(()) |
There was a problem hiding this comment.
🧩 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:
-
Where do the ctoken/mint/owner accounts live?
compress_and_close_ctoken_accountsuses:let find_index = |pubkey: &Pubkey| -> Option<u8> { packed_accounts .iter() .position(|account| account.key == pubkey) .map(|idx| (idx + 1) as u8) };
and
find_account_indicesexpects to find:- source ctoken account,
- mint,
- owner,
- authority,
- rent_sponsor,
- destination,
all in
packed_accounts.In this wrapper,
packed_accountsis built as:packed_accounts.extend_from_slice(post_system); packed_accounts.push(cpi_authority); packed_accounts.push(compressed_token_rent_sponsor.clone());
If
post_systemdoesn’t already contain the ctoken accounts, mints, and owners,find_account_indicesinsidecompress_and_close_ctoken_accountswill fail withTokenSdkError::InvalidAccountData.At minimum, the function’s docs should clearly state that
post_systemmust include all accounts thatcompress_and_close_ctoken_accountsexpects to find (ctoken source, mint, owner, authority, rent_sponsor, destination), not just “post system” ones. Even better would be to derive the needed entries fromtoken_accounts_to_compress(and their associated mint/owner infos) so the API is harder to misuse. -
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_instructionconstructs itsInstruction.accountsusingTransfer2AccountsMetaConfigplus thepacked_account_metas. Forinvoke_signedto succeed, theaccount_infosorder must exactly match theInstruction.accountsorder.Right now that coupling is implicit:
fee_payer’sAccountInfois not pushed explicitly here, so it must be present in eitherpost_systemorremaining_accountsin the position thatTransfer2AccountsMetaConfigexpects.- 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_systemandremaining_accountsrelative to whatTransfer2AccountsMetaConfigproduces, and/or - Refactoring this helper to build
account_infosin the exact same way the non‑CPI path builds itsAccountMetavector (e.g., by sharing a helper that returns both metas and infos), so the mapping can’t drift.
-
Minor nits
- Lifetime
'bon the function is unused and can be dropped. Vec::with_capacity(post_system.len() + 4)forpacked_accountscurrently only uses+2; either add the missing accounts explicitly or tighten the capacity for clarity.
- Lifetime
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\(" -SLength 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 rustLength 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.rsLength 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:
- Document explicitly that
post_systemmust contain all required accounts (mints, owners, destination, authority, rent_sponsor) in addition to post-system accounts, or - Build these dependencies from
token_accounts_to_compressand 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
'bis 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.
| #[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 | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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.
| #![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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧩 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 accesscompressible::close::close, but notcompressible::closeat the root. - With
feature = "v2"enabled, they get the nicercompressible::closere-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 -50Length 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.
| 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
🧹 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.
| // 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) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🧹 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
Fromand 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.
| #[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) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
SPL→ctoken Transfer2 wiring looks consistent but is index‑brittle
The SPL→ctoken constructor wires:
packed_accountsindices 0–5 exactly as documented in comments.Compression::compress_splwith indices(mint=0, source=3, authority=2, pool=4, pool_index=0).Compression::decompress_ctokenwith(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.
| #[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) | ||
| } |
There was a problem hiding this comment.
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.
| /// 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(()) | ||
| } |
There was a problem hiding this comment.
🧹 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_infosin the expected order:payer,compressed_token_program_authority, followed by the same accounts used when buildingpacked_accounts(mint, dest ctoken, authority, source SPL, pool, SPL program). - Use
invoke/invoke_signedappropriately.
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.
| /// 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(), | ||
| )), | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| /// 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(), | ||
| )), | ||
| } | ||
| } |
There was a problem hiding this comment.
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
_signedvariants 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🧹 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.
| 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(()) | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Config initialization flow is sound; consider minor validation tightenings
The initialization path correctly enforces:
config_bump == 0(matchingvalidate)- 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:
- Use
MAX_ADDRESS_TREES_PER_SPACEinstead of a literal1for the address space length check, as suggested earlier. - 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.
| /// 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(()) | ||
| } |
There was a problem hiding this comment.
🧹 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).
| /// 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()?)) | ||
| } |
There was a problem hiding this comment.
🧹 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::getandT::size(&light_account.account). - Uses
LightAccount::<T>::new_close(program_id, &compressed_meta, data)?to construct the decompressed representation. - Initializes compression info to
Decompressedand writes the discriminator + serialized payload into the account data.
One subtle point to double‑check:
- The
LightAccountis built withprogram_idas 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.
Summary by CodeRabbit
Release Notes
New Features
Refactor
Chores