Skip to content

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

Merged
SwenSchaeferjohann merged 2 commits into
mainfrom
swen/pub-beta-cov
Feb 18, 2026
Merged

fix: add delegate to packed accounts in decompress, chunk proofs#2284
SwenSchaeferjohann merged 2 commits into
mainfrom
swen/pub-beta-cov

Conversation

@SwenSchaeferjohann

@SwenSchaeferjohannSwenSchaeferjohann commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

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.

constbatches=awaitcreateTransferInterfaceInstructions(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.

    // Oldconstixs: TransactionInstruction[]=awaitcreateLoadAtaInstructions(rpc,ata,owner,mint,);// Newconstbatches: TransactionInstruction[][]=awaitcreateLoadAtaInstructions(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.

Summary by CodeRabbit

  • Documentation
    • Removed c-token payment workflows documentation guide, which included setup instructions, transaction flows, balance tracking, and API usage examples.

@coderabbitai

coderabbitaiBot commented Feb 13, 2026

Copy link
Copy Markdown
Contributor

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

The documentation file ctoken_for_payments.md was removed entirely, deleting the c-token payments guide, examples, and related integration guidance (setup, send/receive flows, instruction-level patterns, batching, unwrap, balance/history, and quick reference).

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

Possibly related PRs

Suggested labels

ai-review

Suggested reviewers

  • sergeytimoshin
  • ananas-block

Poem

A page of flows and examples took flight,
Its lines folded into quiet night.
The repo clears a little space,
For docs to change and find their place. ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 1
❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Title check⚠️ WarningThe 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 nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage✅ PassedNo 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

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.

@SwenSchaeferjohannSwenSchaeferjohann changed the title fix(js): add delegate to packed accounts in decompress, chunk proofsfix: add delegate to packed accounts in decompress, chunk proofsFeb 13, 2026

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment threadctoken_for_payments.md Outdated

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment threadctoken_for_payments.md Outdated
@ananas-block

Copy link
Copy Markdown
Contributor

V1 proof chunk sizes: docs are missing size 3.

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

exportconstVALID_V1_PROOF_SIZES=[8,4,3,2,1]asconst;

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

ananas-block commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

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

ExportFileLine
getAssociatedCTokenAddressAndBumpv3/derivation.ts46
getAssociatedCTokenAddressv3/derivation.ts57
parseCTokenHotv3/get-account-interface.ts159
parseCTokenColdv3/get-account-interface.ts179
createAssociatedCTokenAccountv3/actions/create-associated-ctoken.ts33
createAssociatedCTokenAccountIdempotentv3/actions/create-associated-ctoken.ts80
createAssociatedCTokenAccountInstructionv3/instructions/create-associated-ctoken.ts143
createAssociatedCTokenAccountIdempotentInstructionv3/instructions/create-associated-ctoken.ts215
mintTo as mintToCToken (export alias)index.ts98
createDecompressCtokenv3/layout/layout-transfer2.ts481
createCompressCtokenv3/layout/layout-transfer2.ts509
createMintToCompressedInstructionv3/instructions/mint-to-compressed.ts118
mintToCompressedv3/actions/mint-to-compressed.ts34
deriveCMintAddressv3/derivation.ts12

Exported Types/Interfaces

ExportFileLine
CTokenConfigv3/instructions/create-ata-interface.ts22
CreateAssociatedCTokenAccountParamsv3/instructions/create-associated-ctoken.ts51
CreateAssociatedCTokenAccountInstructionParamsv3/instructions/create-associated-ctoken.ts123
MintToCTokenActionLayoutv3/layout/layout-mint-action.ts40
MintToCTokenActionv3/layout/layout-mint-action.ts189
CompressAndCloseCMintActionLayoutv3/layout/layout-mint-action.ts68
CompressAndCloseCMintActionv3/layout/layout-mint-action.ts217
MintToCompressedAction / MintToCompressedActionLayoutv3/layout/layout-mint-action.ts31, 180

Exported Enums/Constants

ExportFileLine
TokenAccountSourceType.CTokenHot = 'ctoken-hot'v3/get-account-interface.ts32
TokenAccountSourceType.CTokenCold = 'ctoken-cold'v3/get-account-interface.ts33
COMPRESSIBLE_CTOKEN_ACCOUNT_SIZEconstants.ts89
COMPRESSIBLE_CTOKEN_RENT_PER_EPOCHconstants.ts103

Exported TokenPool Backward-Compat Wrappers

ExportFileLine
TokenPoolInfo typeutils/get-token-pool-infos.ts54
toTokenPoolInfo()utils/get-token-pool-infos.ts102
deriveTokenPoolInfo()utils/get-token-pool-infos.ts367
checkTokenPoolInfo()utils/get-token-pool-infos.ts379
getTokenPoolInfos()utils/get-token-pool-infos.ts389
selectTokenPoolInfo()utils/get-token-pool-infos.ts401
selectTokenPoolInfosForDecompression()utils/get-token-pool-infos.ts410
TokenPoolActivity type aliasutils/get-token-pool-infos.ts362
CreateTokenPoolParams type aliasprogram.ts395
AddTokenPoolParams type aliasprogram.ts419
deriveTokenPoolPda()program.ts688
findTokenPoolIndexAndBump()program.ts720
deriveTokenPoolPdaWithIndex()program.ts760
createTokenPool()program.ts875
addTokenPool()program.ts893
CREATE_TOKEN_POOL_DISCRIMINATORconstants.ts33
ADD_TOKEN_POOL_DISCRIMINATORconstants.ts55
isSingleTokenPoolInfotypes.ts96

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
Copy Markdown
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:

FieldJS parser reads atActual SPL offset
delegate option1 byte at 724 bytes at 72-75
delegate pubkey73-10476-107
statebyte 105byte 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.

@ananas-block

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

ananas-block commented Feb 16, 2026

Copy link
Copy Markdown
Contributor

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

Proposed fix:

constunfrozenBalance=(accountInterface._sources??[]).filter(s=>!s.parsed.isFrozen).reduce((sum,s)=>sum+s.amount,BigInt(0));constunwrapAmount=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
Copy Markdown
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
Copy Markdown
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)){throwe;}// No SPL interface and no SPL/T22 balance -- safe to ignore.}

…on-aware proof chunking
wip
fixes
fixes
fix ci
test fixes
fix and lint
upd
fix ci
rev
fix
order
exporting
fix regr
wip
fixes
bump versions
decompress mint at create
fix ci
bump versions
rm md
js(compressed-token): MAX_TOP_UP constant and optional maxTopUp override
- 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>
unskip tests
changelog.md
@SwenSchaeferjohann
SwenSchaeferjohann merged commit 6624cb4 into mainFeb 18, 2026
15 of 17 checks passed
Sign up for freeto 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

@SwenSchaeferjohann@ananas-block@tilo-14