Skip to content

fix: add delegate to packed accounts in decompress, chunk proofs#2284

Open
SwenSchaeferjohann wants to merge 22 commits intomainfrom
swen/pub-beta-cov
Open

fix: add delegate to packed accounts in decompress, chunk proofs#2284
SwenSchaeferjohann wants to merge 22 commits intomainfrom
swen/pub-beta-cov

Conversation

@SwenSchaeferjohann
Copy link
Contributor

@SwenSchaeferjohann SwenSchaeferjohann commented Feb 13, 2026

list of changes:

Breaking Changes

Renames

  • CTOKEN_PROGRAM_ID: Deprecated. Use LIGHT_TOKEN_PROGRAM_ID (re-exported from @lightprotocol/stateless.js).

  • createCTokenTransferInstruction: Renamed to createLightTokenTransferInstruction. Instruction data layout changed (see below).

  • createTransferInterfaceInstruction (multi-program dispatcher): Deprecated. Use createLightTokenTransferInstruction for Light token transfers, or SPL's createTransferCheckedInstruction for SPL/T22 transfers.

transferInterface (action)

  • destination parameter changed from ATA address to wallet public key. The function now derives the recipient ATA internally and creates it idempotently (no extra RPC fetch). Callers that previously passed a pre-derived ATA address must now pass the recipient's wallet public key instead.

  • programId default changed from CTOKEN_PROGRAM_ID to LIGHT_TOKEN_PROGRAM_ID. Parameter order unchanged: amount, programId?, confirmOptions?, options?, wrap?.

  • Multi-transaction support: For >8 compressed inputs, the action now sends parallel load transactions before the final transfer transaction. Previously, all instructions were packed into a single transaction (which could exceed limits).

createTransferInterfaceInstructions (replaces createTransferInterfaceInstruction)

New function replacing the old monolithic transferInterface internals. Takes recipient as a wallet public key (not ATA). Returns TransactionInstruction[][] where each inner array is one transaction. The last element is always the transfer transaction; all preceding elements are load transactions that can be sent in parallel.

const batches = await createTransferInterfaceInstructions(
    rpc, payer, mint, amount, sender, recipientWallet, options?,
);
const { rest: loads, last: transferTx } = sliceLast(batches);

Options include ensureRecipientAta (default: true) which prepends an idempotent ATA creation instruction to the transfer transaction, and programId which dispatches to SPL transferChecked for TOKEN_PROGRAM_ID/TOKEN_2022_PROGRAM_ID.

createLoadAtaInstructions

  • Return type changed from TransactionInstruction[] (flat) to TransactionInstruction[][] (batched). Each inner array is one transaction. For >8 compressed inputs, multiple transactions are needed because each decompress proof can handle at most 8 inputs.

    // Old
    const ixs: TransactionInstruction[] = await createLoadAtaInstructions(
        rpc,
        ata,
        owner,
        mint,
    );
    
    // New
    const batches: TransactionInstruction[][] = await createLoadAtaInstructions(
        rpc,
        ata,
        owner,
        mint,
    );
    // Each element is one transaction's instructions

createLightTokenTransferInstruction (instruction-level)

  • Instruction data layout changed: Old format was 10 bytes (discriminator + padding + u64 LE at offset 2). New format is 9 bytes (discriminator + u64 LE at offset 1, no padding).

  • Account keys changed: Now always includes system_program (index 3) and fee_payer (index 4) for compressible extension rent top-ups. Old format had 3 required accounts (source, destination, owner) with optional payer. New format has 5 required accounts.

  • owner is now writable (for rent top-ups via compressible extension).

createDecompressInterfaceInstruction

  • New required parameter: decimals: number added after splInterfaceInfo. Required for SPL destination decompression.

  • Delegate handling: Now includes delegate pubkeys from input compressed accounts in the packed accounts list.

Program instruction: createTokenPool → createSplInterface

  • CompressedTokenProgram.createTokenPool: Deprecated. Use CompressedTokenProgram.createSplInterface with the same call signature (feePayer, mint, tokenProgramId?). The high-level action createSplInterface() now calls the new instruction helper; the deprecated action alias createTokenPool still works but points to createSplInterface. CompressedTokenProgram.createMint now uses createSplInterface internally for the third instruction.

Added

  • createTransferInterfaceInstructions: Instruction builder for transfers with multi-transaction batching, frozen account pre-checks, zero-amount rejection, and programId-based dispatch (Light token vs SPL transferChecked).
  • sliceLast helper: Splits instruction batches into { rest, last } for parallel-then-sequential sending.
  • TransferOptions interface: wrap, programId, ensureRecipientAta, extends InterfaceOptions.
  • Version-aware proof chunking: V1 inputs chunked with sizes {8,4,2,1}, V2 with {8,7,6,5,4,3,2,1}. V1 and V2 never mixed in a single proof request.
  • assertUniqueInputHashes: Runtime enforcement that no compressed account hash appears in more than one parallel batch.
  • chunkAccountsByTreeVersion: Exported utility for splitting compressed accounts by tree version into prover-compatible groups.
  • Frozen account handling: _buildLoadBatches skips frozen sources. createTransferInterfaceInstructions throws early if hot account is frozen, reports frozen balance in insufficient-balance errors.
  • loadAta action: Now sends all load batches in parallel (previously sequential single-tx).
  • createUnwrapInstructions: New instruction builder for unwrapping c-tokens to SPL/T22. Returns TransactionInstruction[][] (load batches, if any, then one unwrap batch). Same loop pattern as createLoadAtaInstructions and createTransferInterfaceInstructions. The unwrap action now uses it internally. Use this when you need instruction-level control or to handle multi-batch load + unwrap in one go.
  • LightTokenProgram: Export alias for CompressedTokenProgram for clearer naming in docs and examples.
  • Decompress mint as part of create mint: createMintInterface and the create-mint instruction now decompress the mint in the same transaction. The mint is available on-chain (CMint account created) immediately after creation; a separate decompressMint() call is no longer required before creating ATAs or minting. decompressMint() remains supported and is idempotent: if the mint was already decompressed (e.g. via createMintInterface), it returns successfully without sending a transaction.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 13, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (14)
  • js/compressed-token/src/constants.ts is excluded by none and included by none
  • js/compressed-token/src/v3/actions/decompress-mint.ts is excluded by none and included by none
  • js/compressed-token/src/v3/actions/mint-to-compressed.ts is excluded by none and included by none
  • js/compressed-token/src/v3/actions/mint-to.ts is excluded by none and included by none
  • js/compressed-token/src/v3/actions/unwrap.ts is excluded by none and included by none
  • js/compressed-token/src/v3/actions/wrap.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/create-decompress-interface-instruction.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/create-mint.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/decompress-mint.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/mint-to-compressed.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/unwrap.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/update-metadata.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/update-mint.ts is excluded by none and included by none
  • js/compressed-token/src/v3/instructions/wrap.ts is excluded by none and included by none

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

The documentation file ctoken_for_payments.md was removed entirely; its guide and examples for c-token payment flows, APIs, and usage guidance were deleted.

Changes

Cohort / File(s) Summary
Documentation removed
ctoken_for_payments.md
Complete deletion of the c-token payments guide and all examples (setup, receive/pay flows, instruction-level patterns, batching/transaction assembly, idempotent ATA creation, unwrap, balance/history, and quick reference). No code or public API declarations changed in this diff.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

Poem

A page of flows and examples took flight,
Its lines folded into quiet night.
The repo breathes in empty space,
Ready for a clearer, newer place. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title describes adding delegate to packed accounts in decompress and chunk proofs, but the actual change is removing the ctokens_for_payments.md documentation file entirely. Update the title to accurately reflect the primary change, such as 'docs: remove ctokens_for_payments.md documentation' or clarify what the actual code changes are.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch swen/pub-beta-cov

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@SwenSchaeferjohann SwenSchaeferjohann changed the title fix(js): add delegate to packed accounts in decompress, chunk proofs fix: add delegate to packed accounts in decompress, chunk proofs Feb 13, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
ctoken_for_payments.md (1)

268-311: ⚠️ Potential issue | 🟡 Minor

Fix the misleading "parallel" comment and surface the new decimals requirement.

The example has three issues worth addressing:

  1. Misleading parallel execution: The comment says "in parallel, if any" but the code uses sequential for-loop with await on each iteration. Either execute truly in parallel using Promise.all() or correct the comment to reflect sequential execution.

  2. New decimals parameter is required: The createUnwrapInstruction now requires the mint's decimals (for transfer_checked validation). The example correctly fetches this via getMint, but this change should be called out since it differs from previous API versions.

  3. Missing error handling: In production, both the load batches loop and the unwrap transaction should be wrapped in try-catch blocks with appropriate failure handling (retries, user notifications, etc.).

Example: Fix parallel execution or comment

For truly parallel execution:

-// Send load batches first (in parallel, if any), then unwrap tx
-for (const batch of loadBatches) {
-  await sendAndConfirmTransaction(rpc, new Transaction().add(...batch), [
-    payer,
-    owner,
-  ]);
-}
+// Send load batches in parallel (if any), then unwrap tx
+if (loadBatches.length > 0) {
+  await Promise.all(
+    loadBatches.map(batch =>
+      sendAndConfirmTransaction(rpc, new Transaction().add(...batch), [payer, owner])
+    )
+  );
+}

Or, for sequential (fix comment):

-// Send load batches first (in parallel, if any), then unwrap tx
+// Send load batches first (if any), then unwrap tx
 for (const batch of loadBatches) {
🤖 Fix all issues with AI agents
In `@ctoken_for_payments.md`:
- Around line 144-159: Add a cross-reference to the official
js/compressed-token/docs/payment-integration.md and a brief note next to the
createTransferInterfaceInstructions example: mention the hot/cold sender pattern
and the rule "For a hot sender or <=8 cold inputs, the result is a
single-element array", and instruct readers to run loadBatches (the rest
returned by sliceLast) in parallel and then submit transferBatch sequentially;
reference the symbols createTransferInterfaceInstructions, sliceLast,
loadBatches, and transferBatch and add a link to the payment-integration.md
which contains the full usage example and the table describing contents of each
transaction.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@ctoken_for_payments.md`:
- Around line 312-318: The comment says "Send load batches first (in
parallel...)" but the for-loop over loadBatches runs sequentially; update to
actually send in parallel by mapping loadBatches to an array of
sendAndConfirmTransaction promises using sendAndConfirmTransaction(rpc, new
Transaction().add(...batch), [payer, owner]) and await Promise.all(...) (or,
alternatively, change the comment to remove "in parallel" if sequential behavior
is intended). Ensure you reference loadBatches, sendAndConfirmTransaction, rpc,
Transaction, payer, and owner when making the change so the behavior and intent
remain clear.

@ananas-block
Copy link
Contributor

V1 proof chunk sizes: docs are missing size 3.

Code in js/compressed-token/src/v3/actions/load-ata.ts defines:

export const VALID_V1_PROOF_SIZES = [8, 4, 3, 2, 1] as const;

program-libs/verifier/src/verifying_keys/ includes v1_inclusion_26_3, so size 3 is valid for V1 trees.

Both docs list {8,4,2,1} instead of {8,4,3,2,1}:

  • js/compressed-token/docs/interface.md line 102
  • js/compressed-token/CHANGELOG.md line 80

Comment on lines +135 to +136
rentPayment: 16,
writeTopUp: 766,
Copy link
Contributor

Choose a reason for hiding this comment

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

do we have constants for these default values?

@ananas-block
Copy link
Contributor

ananas-block commented Feb 16, 2026

Legacy "CToken" Naming (js/compressed-token/src/)

The branch renames only 2 public symbols to LightToken:

  • CompressedTokenProgram aliased as LightTokenProgram
  • createCTokenTransferInstruction renamed to createLightTokenTransferInstruction

Below is the full list of public-facing exports that still use legacy CToken/ctoken/TokenPool naming.

Exported Functions

Export File Line
getAssociatedCTokenAddressAndBump v3/derivation.ts 46
getAssociatedCTokenAddress v3/derivation.ts 57
parseCTokenHot v3/get-account-interface.ts 159
parseCTokenCold v3/get-account-interface.ts 179
createAssociatedCTokenAccount v3/actions/create-associated-ctoken.ts 33
createAssociatedCTokenAccountIdempotent v3/actions/create-associated-ctoken.ts 80
createAssociatedCTokenAccountInstruction v3/instructions/create-associated-ctoken.ts 143
createAssociatedCTokenAccountIdempotentInstruction v3/instructions/create-associated-ctoken.ts 215
mintTo as mintToCToken (export alias) index.ts 98
createDecompressCtoken v3/layout/layout-transfer2.ts 481
createCompressCtoken v3/layout/layout-transfer2.ts 509
createMintToCompressedInstruction v3/instructions/mint-to-compressed.ts 118
mintToCompressed v3/actions/mint-to-compressed.ts 34
deriveCMintAddress v3/derivation.ts 12

Exported Types/Interfaces

Export File Line
CTokenConfig v3/instructions/create-ata-interface.ts 22
CreateAssociatedCTokenAccountParams v3/instructions/create-associated-ctoken.ts 51
CreateAssociatedCTokenAccountInstructionParams v3/instructions/create-associated-ctoken.ts 123
MintToCTokenActionLayout v3/layout/layout-mint-action.ts 40
MintToCTokenAction v3/layout/layout-mint-action.ts 189
CompressAndCloseCMintActionLayout v3/layout/layout-mint-action.ts 68
CompressAndCloseCMintAction v3/layout/layout-mint-action.ts 217
MintToCompressedAction / MintToCompressedActionLayout v3/layout/layout-mint-action.ts 31, 180

Exported Enums/Constants

Export File Line
TokenAccountSourceType.CTokenHot = 'ctoken-hot' v3/get-account-interface.ts 32
TokenAccountSourceType.CTokenCold = 'ctoken-cold' v3/get-account-interface.ts 33
COMPRESSIBLE_CTOKEN_ACCOUNT_SIZE constants.ts 89
COMPRESSIBLE_CTOKEN_RENT_PER_EPOCH constants.ts 103

Exported TokenPool Backward-Compat Wrappers

Export File Line
TokenPoolInfo type utils/get-token-pool-infos.ts 54
toTokenPoolInfo() utils/get-token-pool-infos.ts 102
deriveTokenPoolInfo() utils/get-token-pool-infos.ts 367
checkTokenPoolInfo() utils/get-token-pool-infos.ts 379
getTokenPoolInfos() utils/get-token-pool-infos.ts 389
selectTokenPoolInfo() utils/get-token-pool-infos.ts 401
selectTokenPoolInfosForDecompression() utils/get-token-pool-infos.ts 410
TokenPoolActivity type alias utils/get-token-pool-infos.ts 362
CreateTokenPoolParams type alias program.ts 395
AddTokenPoolParams type alias program.ts 419
deriveTokenPoolPda() program.ts 688
findTokenPoolIndexAndBump() program.ts 720
deriveTokenPoolPdaWithIndex() program.ts 760
createTokenPool() program.ts 875
addTokenPool() program.ts 893
CREATE_TOKEN_POOL_DISCRIMINATOR constants.ts 33
ADD_TOKEN_POOL_DISCRIMINATOR constants.ts 55
isSingleTokenPoolInfo types.ts 96

Beyond the public API, there are ~200+ internal occurrences of ctoken/CToken naming in variable names, parameters, internal helper functions, string literals, and comments across the v3/ source files. Key files: get-account-interface.ts (~35), load-ata.ts (~56), create-associated-ctoken.ts (~20), ata-utils.ts (~11), get-or-create-ata-interface.ts (~11).

@ananas-block
Copy link
Contributor

Correctness review: js/compressed-token/src/v3/get-account-interface.ts

1. delegatedAmount hardcoded to 0 for cold accounts

convertTokenDataToAccount() sets delegatedAmount: BigInt(0) for all compressed accounts.

  • Without CompressedOnly extension: When delegate is set, the delegate can act on the full account amount. delegatedAmount should equal amount, not 0.
  • With CompressedOnly extension: The delegated_amount is stored in CompressedOnlyExtension inside the TLV data. The parser never extracts it -- it just passes raw TLV bytes through to tlvData.

2. parseCTokenHot uses wrong binary layout for hot c-token accounts

parseTokenData() uses 1-byte Borsh option tags (correct for compressed TokenData), but hot on-chain c-token accounts use SPL-compatible 4-byte COption tags. This causes a 3-byte misalignment after the delegate field:

Field JS parser reads at Actual SPL offset
delegate option 1 byte at 72 4 bytes at 72-75
delegate pubkey 73-104 76-107
state byte 105 byte 108

When delegate is None (common case), byte 72 happens to be 0 in both formats so delegate = null is accidentally correct. But state reads from byte 105 (zero padding) instead of byte 108, making isInitialized = false for all hot c-token accounts. Hot accounts should use unpackAccountSPL from @solana/spl-token instead, which correctly parses all fields including delegatedAmount at offset 121.

3. Missing fields: closeAuthority, isNative

Both are hardcoded (null and false). For cold accounts this is correct (compressed TokenData doesn't have these fields). For hot accounts this is a consequence of issue 2 -- using unpackAccountSPL would fix these as well.

4. No test coverage for delegatedAmount

No test verifies delegatedAmount is correctly populated for either hot or cold accounts. The e2e tests pass because they primarily verify amount (same offset in both layouts) and don't check isInitialized/isFrozen for hot c-tokens.

- Add MAX_TOP_UP (65535) in constants.ts; use in all instruction builders
- mintTo action: default maxTopUp to MAX_TOP_UP when omitted
- wrap/unwrap: optional maxTopUp on instruction and action
- decompressMint: maxTopUp in DecompressMintParams and DecompressMintInstructionParams
- createDecompressInterfaceInstruction, createMintInstruction,
  createMintToCompressedInstruction, update-mint, update-metadata: optional maxTopUp
- Non-breaking: all new params optional, default no cap

Co-authored-by: Cursor <cursoragent@cursor.com>
@ananas-block
Copy link
Contributor

ctokenHotSource at load-ata.ts:867 not filtered by frozen flag. When a frozen c-token hot account exists with unfrozen cold accounts, decompress targets the frozen ATA and fails on-chain. Fix: add && !s.parsed.isFrozen to the .find() predicate.

@ananas-block
Copy link
Contributor

ananas-block commented Feb 16, 2026

unwrap.ts:119 -- totalBalance includes frozen amounts, causing "unwrap all" to attempt unwrapping more than the loadable unfrozen balance.

Proposed fix:

const unfrozenBalance = (accountInterface._sources ?? [])
    .filter(s => !s.parsed.isFrozen)
    .reduce((sum, s) => sum + s.amount, BigInt(0));
const unwrapAmount = amount != null
    ? BigInt(amount.toString())
    : unfrozenBalance;

Note: using amount != null instead of amount ? also fixes the falsy check at line 124 where explicit amount=0 is treated as "unwrap all".

@ananas-block
Copy link
Contributor

Inconsistent frozen error handling between loadAta and transferInterface

Description:
loadAta returns null when _buildLoadBatches returns [] (all sources frozen or no cold balance). The caller cannot distinguish "nothing to load (already hot)" from "everything is frozen". Meanwhile, transferInterface throws explicit errors: "Cannot transfer: sender token account is frozen" and "Insufficient balance (X frozen, not usable)".

Recommendation:
Consider returning a structured result from loadAta that indicates the reason (e.g., { status: 'already_hot' | 'all_frozen' | 'loaded', txId?: string }), or document that null means "no action taken" and callers should check frozen state separately.

@ananas-block
Copy link
Contributor

effectiveHotAfterSetup over-counts when splInterfaceInfo is missing

Description:
In the wrap=true path at line 1043, effectiveHotAfterSetup = hotBalance + splBalance + t22Balance. This assumes the SPL/T22 balances will be wrapped into the c-token ATA. However, the wrap instructions at lines 946 and 962 are only emitted when splInterfaceInfo is defined. If the SPL interface RPC call fails (caught silently at line 913), splInterfaceInfo is undefined, wrap instructions are not emitted, but the balance calculation still includes SPL/T22 amounts. This causes neededFromCold to be under-calculated.

Recommendation:
Re-throw the error when there are actual SPL/T22 balances that depend on it:

} catch (e) {
    if (splBalance > BigInt(0) || t22Balance > BigInt(0)) {
        throw e;
    }
    // No SPL interface and no SPL/T22 balance -- safe to ignore.
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants