diff --git a/js/compressed-token/src/index.ts b/js/compressed-token/src/index.ts index 4552e61466..4971ee92b8 100644 --- a/js/compressed-token/src/index.ts +++ b/js/compressed-token/src/index.ts @@ -66,6 +66,9 @@ export { createDecompressInterfaceInstruction, createTransferInterfaceInstruction, createCTokenTransferInstruction, + createTransferCheckedInterfaceInstruction, + createCTokenTransferCheckedInstruction, + transferCheckedInterface, // Types TokenMetadataInstructionData, CompressibleConfig, diff --git a/js/compressed-token/src/v3/actions/create-associated-ctoken.ts b/js/compressed-token/src/v3/actions/create-associated-ctoken.ts index 7d7dc7ecd0..12cc9a7fd4 100644 --- a/js/compressed-token/src/v3/actions/create-associated-ctoken.ts +++ b/js/compressed-token/src/v3/actions/create-associated-ctoken.ts @@ -18,7 +18,7 @@ import { import { getAssociatedCTokenAddress } from '../derivation'; /** - * Create an associated c-token account. + * Create an associated light-token account. * * @param rpc RPC connection * @param payer Fee payer diff --git a/js/compressed-token/src/v3/actions/create-ata-interface.ts b/js/compressed-token/src/v3/actions/create-ata-interface.ts index 50618b7280..9db65f1a9b 100644 --- a/js/compressed-token/src/v3/actions/create-ata-interface.ts +++ b/js/compressed-token/src/v3/actions/create-ata-interface.ts @@ -25,7 +25,7 @@ import { getAssociatedTokenAddressInterface } from '../get-associated-token-addr export type { CTokenConfig }; /** - * Create an associated token account for SPL/T22/c-token. Defaults to c-token + * Create an associated token account for SPL/T22/light-token. Defaults to light-token * program. * * @param rpc RPC connection @@ -98,8 +98,8 @@ export async function createAtaInterface( } /** - * Create an associated token account idempotently for SPL/T22/c-token. Defaults - * to c-token program. + * Create an associated token account idempotently for SPL/T22/light-token. Defaults + * to light-token program. * * If the account already exists, the instruction succeeds without error. * @@ -113,7 +113,7 @@ export async function createAtaInterface( * CTOKEN_PROGRAM_ID) * @param associatedTokenProgramId ATA program ID (auto-derived if not * provided) - * @param ctokenConfig Optional c-token-specific configuration + * @param ctokenConfig Optional light-token-specific configuration * * @returns Address of the associated token account */ diff --git a/js/compressed-token/src/v3/actions/create-mint-interface.ts b/js/compressed-token/src/v3/actions/create-mint-interface.ts index 4b9d95b889..c0abb4a2d9 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -31,19 +31,19 @@ import { createMint } from '../../actions/create-mint'; export { TokenMetadataInstructionData }; /** - * Create and initialize a new mint for SPL/T22/c-token. + * Create and initialize a new mint for SPL/T22/light-token. * * @param rpc RPC connection to use * @param payer Fee payer - * @param mintAuthority Account that will control minting (signer for c-token mints) + * @param mintAuthority Account that will control minting (signer for light-token mints) * @param freezeAuthority Account that will control freeze and thaw (optional) * @param decimals Location of the decimal place * @param keypair Mint keypair (defaults to a random keypair) * @param confirmOptions Confirm options * @param programId Token program ID (defaults to CTOKEN_PROGRAM_ID) - * @param tokenMetadata Optional token metadata (c-token mints only) - * @param outputStateTreeInfo Optional output state tree info (c-token mints only) - * @param addressTreeInfo Optional address tree info (c-token mints only) + * @param tokenMetadata Optional token metadata (light-token mints only) + * @param outputStateTreeInfo Optional output state tree info (light-token mints only) + * @param addressTreeInfo Optional address tree info (light-token mints only) * * @returns Object with mint address and transaction signature */ diff --git a/js/compressed-token/src/v3/actions/decompress-interface.ts b/js/compressed-token/src/v3/actions/decompress-interface.ts index d28ee624c3..db013ffb99 100644 --- a/js/compressed-token/src/v3/actions/decompress-interface.ts +++ b/js/compressed-token/src/v3/actions/decompress-interface.ts @@ -55,7 +55,7 @@ export async function decompressInterface( ): Promise { assertBetaEnabled(); - // Determine if this is SPL or c-token destination + // Determine if this is SPL or light-token destination const isSplDestination = splInterfaceInfo !== undefined; // Get compressed token accounts @@ -119,7 +119,7 @@ export async function decompressInterface( splInterfaceInfo.tokenProgram, )); } else { - // c-token destination - use c-token ATA + // light-token destination - use light-token ATA destinationAtaAddress = destinationAta ?? getAssociatedTokenAddressInterface(mint, ataOwner); @@ -143,7 +143,7 @@ export async function decompressInterface( ), ); } else { - // Create c-token ATA + // Create light-token ATA instructions.push( createAssociatedTokenAccountInterfaceIdempotentInstruction( payer.publicKey, diff --git a/js/compressed-token/src/v3/actions/get-or-create-ata-interface.ts b/js/compressed-token/src/v3/actions/get-or-create-ata-interface.ts index 8c3f25479a..2b23d16d50 100644 --- a/js/compressed-token/src/v3/actions/get-or-create-ata-interface.ts +++ b/js/compressed-token/src/v3/actions/get-or-create-ata-interface.ts @@ -131,7 +131,7 @@ export async function _getOrCreateAtaInterface( associatedTokenProgramId, ); - // For c-token, use getAtaInterface which properly aggregates hot+cold balances + // For light-token, use getAtaInterface which properly aggregates hot+cold balances // When wrap=true (unified path), also includes SPL/T22 balances if (programId.equals(CTOKEN_PROGRAM_ID)) { return getOrCreateCTokenAta( @@ -162,7 +162,7 @@ export async function _getOrCreateAtaInterface( } /** - * Get or create c-token ATA with proper cold balance handling. + * Get or create light-token ATA with proper cold balance handling. * * Like SPL's getOrCreateAssociatedTokenAccount, this is a write operation: * 1. Creates hot ATA if it doesn't exist @@ -321,7 +321,7 @@ async function getOrCreateCTokenAta( } /** - * Create c-token ATA idempotently. + * Create light-token ATA idempotently. * @internal */ async function createCTokenAtaIdempotent( diff --git a/js/compressed-token/src/v3/actions/index.ts b/js/compressed-token/src/v3/actions/index.ts index 57c4ff557d..2ce0b5c822 100644 --- a/js/compressed-token/src/v3/actions/index.ts +++ b/js/compressed-token/src/v3/actions/index.ts @@ -12,3 +12,4 @@ export * from './decompress-interface'; export * from './wrap'; export * from './unwrap'; export * from './load-ata'; +export * from './transfer-checked'; diff --git a/js/compressed-token/src/v3/actions/load-ata.ts b/js/compressed-token/src/v3/actions/load-ata.ts index cb3d631c59..b73da06169 100644 --- a/js/compressed-token/src/v3/actions/load-ata.ts +++ b/js/compressed-token/src/v3/actions/load-ata.ts @@ -121,17 +121,17 @@ export { * * Behavior depends on `wrap` parameter: * - wrap=false (standard): Decompress compressed tokens to the target ATA. - * ATA can be SPL (via pool), T22 (via pool), or c-token (direct). - * - wrap=true (unified): Wrap SPL/T22 + decompress all to c-token ATA. - * ATA must be a c-token ATA. + * ATA can be SPL (via pool), T22 (via pool), or light-token (direct). + * - wrap=true (unified): Wrap SPL/T22 + decompress all to light-token ATA. + * ATA must be a light-token ATA. * * @param rpc RPC connection - * @param ata Associated token address (SPL, T22, or c-token) + * @param ata Associated token address (SPL, T22, or light-token) * @param owner Owner public key * @param mint Mint public key * @param payer Fee payer (defaults to owner) * @param options Optional load options - * @param wrap Unified mode: wrap SPL/T22 to c-token (default: false) + * @param wrap Unified mode: wrap SPL/T22 to light-token (default: false) * @returns Array of instructions (empty if nothing to load) */ export async function createLoadAtaInstructions( @@ -149,7 +149,7 @@ export async function createLoadAtaInstructions( // Validation happens inside getAtaInterface via checkAtaAddress helper: // - Always validates ata matches mint+owner derivation - // - For wrap=true, additionally requires c-token ATA + // - For wrap=true, additionally requires light-token ATA try { const ataInterface = await _getAtaInterface( rpc, @@ -185,14 +185,14 @@ export { AtaType } from '../ata-utils'; * * Behavior depends on `wrap` parameter: * - wrap=false (standard): Decompress compressed tokens to the target ATA type - * (SPL ATA via pool, T22 ATA via pool, or c-token ATA direct) - * - wrap=true (unified): Wrap SPL/T22 + decompress all to c-token ATA + * (SPL ATA via pool, T22 ATA via pool, or light-token ATA direct) + * - wrap=true (unified): Wrap SPL/T22 + decompress all to light-token ATA * * @param rpc RPC connection * @param payer Fee payer * @param ata AccountInterface from getAtaInterface (must have _isAta, _owner, _mint) * @param options Optional load options - * @param wrap Unified mode: wrap SPL/T22 to c-token (default: false) + * @param wrap Unified mode: wrap SPL/T22 to light-token (default: false) * @param targetAta Target ATA address (used for type detection in standard mode) * @returns Array of instructions (empty if nothing to load) */ @@ -247,7 +247,7 @@ export async function createLoadAtaInstructionsFromInterface( const validation = checkAtaAddress(targetAta, mint, owner); ataType = validation.type; - // For wrap=true, must be c-token ATA + // For wrap=true, must be light-token ATA if (wrap && ataType !== 'ctoken') { throw new Error( `For wrap=true, targetAta must be c-token ATA. Got ${ataType} ATA.`, @@ -307,9 +307,9 @@ export async function createLoadAtaInstructionsFromInterface( } if (wrap) { - // UNIFIED MODE: Everything goes to c-token ATA + // UNIFIED MODE: Everything goes to light-token ATA - // 1. Create c-token ATA if needed + // 1. Create light-token ATA if needed if (!ctokenHotSource) { instructions.push( createAssociatedTokenAccountInterfaceIdempotentInstruction( @@ -322,7 +322,7 @@ export async function createLoadAtaInstructionsFromInterface( ); } - // 2. Wrap SPL tokens to c-token + // 2. Wrap SPL tokens to light-token if (splBalance > BigInt(0) && splInterfaceInfo) { instructions.push( createWrapInstruction( @@ -338,7 +338,7 @@ export async function createLoadAtaInstructionsFromInterface( ); } - // 3. Wrap T22 tokens to c-token + // 3. Wrap T22 tokens to light-token if (t22Balance > BigInt(0) && splInterfaceInfo) { instructions.push( createWrapInstruction( @@ -354,7 +354,7 @@ export async function createLoadAtaInstructionsFromInterface( ); } - // 4. Decompress compressed tokens to c-token ATA + // 4. Decompress compressed tokens to light-token ATA // Note: v3 interface only supports V2 trees if (coldBalance > BigInt(0) && ctokenColdSource) { const compressedAccounts = @@ -403,7 +403,7 @@ export async function createLoadAtaInstructionsFromInterface( ); if (ataType === 'ctoken') { - // Decompress to c-token ATA (direct) + // Decompress to light-token ATA (direct) if (!ctokenHotSource) { instructions.push( createAssociatedTokenAccountInterfaceIdempotentInstruction( @@ -489,19 +489,19 @@ export async function createLoadAtaInstructionsFromInterface( * * Behavior depends on `wrap` parameter: * - wrap=false (standard): Decompress compressed tokens to the target ATA. - * ATA can be SPL (via pool), T22 (via pool), or c-token (direct). - * - wrap=true (unified): Wrap SPL/T22 + decompress all to c-token ATA. + * ATA can be SPL (via pool), T22 (via pool), or light-token (direct). + * - wrap=true (unified): Wrap SPL/T22 + decompress all to light-token ATA. * * Idempotent: returns null if nothing to load. * * @param rpc RPC connection - * @param ata Associated token address (SPL, T22, or c-token) + * @param ata Associated token address (SPL, T22, or light-token) * @param owner Owner of the tokens (signer) * @param mint Mint public key * @param payer Fee payer (signer, defaults to owner) * @param confirmOptions Optional confirm options * @param interfaceOptions Optional interface options - * @param wrap Unified mode: wrap SPL/T22 to c-token (default: false) + * @param wrap Unified mode: wrap SPL/T22 to light-token (default: false) * @returns Transaction signature, or null if nothing to load */ export async function loadAta( diff --git a/js/compressed-token/src/v3/actions/mint-to-interface.ts b/js/compressed-token/src/v3/actions/mint-to-interface.ts index b16531ee7b..f0380596ec 100644 --- a/js/compressed-token/src/v3/actions/mint-to-interface.ts +++ b/js/compressed-token/src/v3/actions/mint-to-interface.ts @@ -18,14 +18,14 @@ import { getMintInterface } from '../get-mint-interface'; /** * Mint tokens to a decompressed/onchain token account. - * Works with SPL, Token-2022, and compressed token (c-token) mints. + * Works with SPL, Token-2022, and compressed token (light-token) mints. * * This function ONLY mints to decompressed onchain token accounts, never to compressed accounts. * The signature matches the standard SPL mintTo for simplicity and consistency. * * @param rpc - RPC connection to use * @param payer - Transaction fee payer - * @param mint - Mint address (SPL, Token-2022, or compressed mint) + * @param mint - Mint address (SPL, Token-2022, or light mint) * @param destination - Destination token account address (must be an existing onchain token account) * @param authority - Mint authority (can be Signer or PublicKey if multiSigners provided) * @param amount - Amount to mint @@ -56,7 +56,7 @@ export async function mintToInterface( programId, ); - // Fetch validity proof if this is a compressed mint (has merkleContext) + // Fetch validity proof if this is a light mint (has merkleContext) let validityProof; if (mintInterface.merkleContext) { validityProof = await rpc.getValidityProofV2( diff --git a/js/compressed-token/src/v3/actions/transfer-checked.ts b/js/compressed-token/src/v3/actions/transfer-checked.ts new file mode 100644 index 0000000000..23114e96b4 --- /dev/null +++ b/js/compressed-token/src/v3/actions/transfer-checked.ts @@ -0,0 +1,390 @@ +import { + ComputeBudgetProgram, + ConfirmOptions, + PublicKey, + Signer, + TransactionInstruction, + TransactionSignature, +} from '@solana/web3.js'; +import { + Rpc, + buildAndSignTx, + sendAndConfirmTx, + CTOKEN_PROGRAM_ID, + dedupeSigner, + ParsedTokenAccount, + assertBetaEnabled, +} from '@lightprotocol/stateless.js'; +import { assertV2Only } from '../assert-v2-only'; +import { + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddressSync, + getMint, +} from '@solana/spl-token'; +import BN from 'bn.js'; +import { getAtaProgramId } from '../ata-utils'; +import { + createTransferCheckedInterfaceInstruction, + createCTokenTransferCheckedInstruction, +} from '../instructions/transfer-checked'; +import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface'; +import { getAssociatedTokenAddressInterface } from '../get-associated-token-address-interface'; +import { + getSplInterfaceInfos, + SplInterfaceInfo, +} from '../../utils/get-token-pool-infos'; +import { createWrapInstruction } from '../instructions/wrap'; +import { createDecompressInterfaceInstruction } from '../instructions/create-decompress-interface-instruction'; +import { InterfaceOptions } from './transfer-interface'; + +/** + * Calculate compute units needed for the operation + */ +function calculateComputeUnits( + compressedAccounts: ParsedTokenAccount[], + hasValidityProof: boolean, + splWrapCount: number, +): number { + // Base CU for hot light-token transfer + let cu = 5_000; + + // Compressed token decompression + if (compressedAccounts.length > 0) { + if (hasValidityProof) { + cu += 100_000; // Validity proof verification + } + // Per compressed account + for (const acc of compressedAccounts) { + const proveByIndex = acc.compressedAccount.proveByIndex ?? false; + cu += proveByIndex ? 10_000 : 30_000; + } + } + + // SPL/T22 wrap operations + cu += splWrapCount * 5_000; + + // TODO: dynamic + // return cu; + return 200_000; +} + +/** + * Transfer tokens using the light-token interface with decimals validation. + * + * Like transferInterface but validates the amount against the mint's decimals + * on-chain (discriminator 12 instead of 3). + * + * @param rpc RPC connection + * @param payer Fee payer (signer) + * @param source Source light-token ATA address + * @param mint Mint address + * @param destination Destination light-token ATA address (must exist) + * @param owner Source owner (signer) + * @param amount Amount to transfer + * @param decimals Expected decimals of the mint + * @param programId Token program ID (default: CTOKEN_PROGRAM_ID) + * @param confirmOptions Optional confirm options + * @param options Optional interface options + * @param wrap Include SPL/T22 wrapping (default: false) + * @returns Transaction signature + */ +export async function transferCheckedInterface( + rpc: Rpc, + payer: Signer, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: Signer, + amount: number | bigint | BN, + decimals: number, + programId: PublicKey = CTOKEN_PROGRAM_ID, + confirmOptions?: ConfirmOptions, + options?: InterfaceOptions, + wrap = false, +): Promise { + assertBetaEnabled(); + + const amountBigInt = BigInt(amount.toString()); + const { splInterfaceInfos: providedSplInterfaceInfos } = options ?? {}; + + const instructions: TransactionInstruction[] = []; + + // For non-light-token programs, use simple SPL transferChecked (no load) + if (!programId.equals(CTOKEN_PROGRAM_ID)) { + const expectedSource = getAssociatedTokenAddressSync( + mint, + owner.publicKey, + false, + programId, + getAtaProgramId(programId), + ); + if (!source.equals(expectedSource)) { + throw new Error( + `Source mismatch. Expected ${expectedSource.toBase58()}, got ${source.toBase58()}`, + ); + } + + instructions.push( + createTransferCheckedInterfaceInstruction( + source, + mint, + destination, + owner.publicKey, + amountBigInt, + decimals, + [], + programId, + ), + ); + + const { blockhash } = await rpc.getLatestBlockhash(); + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 10_000 }), + ...instructions, + ], + payer, + blockhash, + [owner], + ); + return sendAndConfirmTx(rpc, tx, confirmOptions); + } + + // light-token transfer_checked + const expectedSource = getAssociatedTokenAddressInterface( + mint, + owner.publicKey, + ); + if (!source.equals(expectedSource)) { + throw new Error( + `Source mismatch. Expected ${expectedSource.toBase58()}, got ${source.toBase58()}`, + ); + } + + const ctokenAtaAddress = getAssociatedTokenAddressInterface( + mint, + owner.publicKey, + ); + + // Derive SPL/T22 ATAs only if wrap is true + let splAta: PublicKey | undefined; + let t22Ata: PublicKey | undefined; + + if (wrap) { + splAta = getAssociatedTokenAddressSync( + mint, + owner.publicKey, + false, + TOKEN_PROGRAM_ID, + getAtaProgramId(TOKEN_PROGRAM_ID), + ); + t22Ata = getAssociatedTokenAddressSync( + mint, + owner.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + getAtaProgramId(TOKEN_2022_PROGRAM_ID), + ); + } + + // Fetch sender's accounts in parallel (conditionally include SPL/T22) + const fetchPromises: Promise[] = [ + rpc.getAccountInfo(ctokenAtaAddress), + rpc.getCompressedTokenAccountsByOwner(owner.publicKey, { mint }), + ]; + if (wrap && splAta && t22Ata) { + fetchPromises.push(rpc.getAccountInfo(splAta)); + fetchPromises.push(rpc.getAccountInfo(t22Ata)); + } + + const results = await Promise.all(fetchPromises); + const ctokenAtaInfo = results[0] as Awaited< + ReturnType + >; + const compressedResult = results[1] as Awaited< + ReturnType + >; + const splAtaInfo = wrap + ? (results[2] as Awaited>) + : null; + const t22AtaInfo = wrap + ? (results[3] as Awaited>) + : null; + + const compressedAccounts = compressedResult.items; + + // Parse balances + const hotBalance = + ctokenAtaInfo && ctokenAtaInfo.data.length >= 72 + ? ctokenAtaInfo.data.readBigUInt64LE(64) + : BigInt(0); + const splBalance = + wrap && splAtaInfo && splAtaInfo.data.length >= 72 + ? splAtaInfo.data.readBigUInt64LE(64) + : BigInt(0); + const t22Balance = + wrap && t22AtaInfo && t22AtaInfo.data.length >= 72 + ? t22AtaInfo.data.readBigUInt64LE(64) + : BigInt(0); + const compressedBalance = compressedAccounts.reduce( + (sum, acc) => sum + BigInt(acc.parsed.amount.toString()), + BigInt(0), + ); + + const totalBalance = + hotBalance + splBalance + t22Balance + compressedBalance; + + if (totalBalance < amountBigInt) { + throw new Error( + `Insufficient balance. Required: ${amountBigInt}, Available: ${totalBalance}`, + ); + } + + // Track what we're doing for CU calculation + let splWrapCount = 0; + let hasValidityProof = false; + let compressedToLoad: ParsedTokenAccount[] = []; + + // Create sender's light-token ATA if needed (idempotent) + if (!ctokenAtaInfo) { + instructions.push( + createAssociatedTokenAccountInterfaceIdempotentInstruction( + payer.publicKey, + ctokenAtaAddress, + owner.publicKey, + mint, + CTOKEN_PROGRAM_ID, + ), + ); + } + + // Get SPL interface infos if we need to load + const needsLoad = + splBalance > BigInt(0) || + t22Balance > BigInt(0) || + compressedBalance > BigInt(0); + const splInterfaceInfos = needsLoad + ? (providedSplInterfaceInfos ?? (await getSplInterfaceInfos(rpc, mint))) + : []; + const splInterfaceInfo = splInterfaceInfos.find(info => info.isInitialized); + + // Fetch mint decimals if we need to wrap + let wrapDecimals = 0; + if ( + splInterfaceInfo && + (splBalance > BigInt(0) || t22Balance > BigInt(0)) + ) { + const mintInfo = await getMint( + rpc, + mint, + undefined, + splInterfaceInfo.tokenProgram, + ); + wrapDecimals = mintInfo.decimals; + } + + // Wrap SPL tokens if balance exists (only when wrap=true) + if (wrap && splAta && splBalance > BigInt(0) && splInterfaceInfo) { + instructions.push( + createWrapInstruction( + splAta, + ctokenAtaAddress, + owner.publicKey, + mint, + splBalance, + splInterfaceInfo, + wrapDecimals, + payer.publicKey, + ), + ); + splWrapCount++; + } + + // Wrap T22 tokens if balance exists (only when wrap=true) + if (wrap && t22Ata && t22Balance > BigInt(0) && splInterfaceInfo) { + instructions.push( + createWrapInstruction( + t22Ata, + ctokenAtaAddress, + owner.publicKey, + mint, + t22Balance, + splInterfaceInfo, + wrapDecimals, + payer.publicKey, + ), + ); + splWrapCount++; + } + + // Decompress compressed tokens if they exist + // Note: v3 interface only supports V2 trees + if (compressedBalance > BigInt(0) && compressedAccounts.length > 0) { + assertV2Only(compressedAccounts); + + const proof = await rpc.getValidityProofV0( + compressedAccounts.map(acc => ({ + hash: acc.compressedAccount.hash, + tree: acc.compressedAccount.treeInfo.tree, + queue: acc.compressedAccount.treeInfo.queue, + })), + ); + + hasValidityProof = proof.compressedProof !== null; + compressedToLoad = compressedAccounts; + + instructions.push( + createDecompressInterfaceInstruction( + payer.publicKey, + compressedAccounts, + ctokenAtaAddress, + compressedBalance, + proof, + undefined, + wrapDecimals, + ), + ); + } + + // Determine feePayer: if payer !== owner, pass payer as separate fee payer + const feePayer = !payer.publicKey.equals(owner.publicKey) + ? payer.publicKey + : undefined; + + // Transfer checked (destination must already exist - like SPL Token) + instructions.push( + createCTokenTransferCheckedInstruction( + source, + mint, + destination, + owner.publicKey, + amountBigInt, + decimals, + feePayer, + ), + ); + + // Calculate compute units + const computeUnits = calculateComputeUnits( + compressedToLoad, + hasValidityProof, + splWrapCount, + ); + + // Build and send + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [owner]); + + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnits }), + ...instructions, + ], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, tx, confirmOptions); +} diff --git a/js/compressed-token/src/v3/actions/transfer-interface.ts b/js/compressed-token/src/v3/actions/transfer-interface.ts index 0e9b66612f..7fb413f73a 100644 --- a/js/compressed-token/src/v3/actions/transfer-interface.ts +++ b/js/compressed-token/src/v3/actions/transfer-interface.ts @@ -53,7 +53,7 @@ function calculateComputeUnits( hasValidityProof: boolean, splWrapCount: number, ): number { - // Base CU for hot c-token transfer + // Base CU for hot light-token transfer let cu = 5_000; // Compressed token decompression @@ -77,15 +77,15 @@ function calculateComputeUnits( } /** - * Transfer tokens using the c-token interface. + * Transfer tokens using the light-token interface. * * Matches SPL Token's transferChecked signature order. Destination must exist. * * @param rpc RPC connection * @param payer Fee payer (signer) - * @param source Source c-token ATA address + * @param source Source light-token ATA address * @param mint Mint address - * @param destination Destination c-token ATA address (must exist) + * @param destination Destination light-token ATA address (must exist) * @param owner Source owner (signer) * @param amount Amount to transfer * @param programId Token program ID (default: CTOKEN_PROGRAM_ID) @@ -114,7 +114,7 @@ export async function transferInterface( const instructions: TransactionInstruction[] = []; - // For non-c-token programs, use simple SPL transfer (no load) + // For non-light-token programs, use simple SPL transfer (no load) if (!programId.equals(CTOKEN_PROGRAM_ID)) { const expectedSource = getAssociatedTokenAddressSync( mint, @@ -153,7 +153,7 @@ export async function transferInterface( return sendAndConfirmTx(rpc, tx, confirmOptions); } - // c-token transfer + // light-token transfer const expectedSource = getAssociatedTokenAddressInterface( mint, owner.publicKey, @@ -248,7 +248,7 @@ export async function transferInterface( let hasValidityProof = false; let compressedToLoad: ParsedTokenAccount[] = []; - // Create sender's c-token ATA if needed (idempotent) + // Create sender's light-token ATA if needed (idempotent) if (!ctokenAtaInfo) { instructions.push( createAssociatedTokenAccountInterfaceIdempotentInstruction( diff --git a/js/compressed-token/src/v3/actions/unwrap.ts b/js/compressed-token/src/v3/actions/unwrap.ts index f0bf16ed87..6e52f670da 100644 --- a/js/compressed-token/src/v3/actions/unwrap.ts +++ b/js/compressed-token/src/v3/actions/unwrap.ts @@ -23,12 +23,12 @@ import { getAssociatedTokenAddressInterface } from '../get-associated-token-addr import { loadAta as _loadAta } from './load-ata'; /** - * Unwrap c-tokens to SPL tokens. + * Unwrap light-tokens to SPL tokens. * * @param rpc RPC connection * @param payer Fee payer * @param destination Destination SPL/T22 token account - * @param owner Owner of the c-token (signer) + * @param owner Owner of the light-token (signer) * @param mint Mint address * @param amount Amount to unwrap (defaults to all) * @param splInterfaceInfo SPL interface info @@ -72,17 +72,17 @@ export async function unwrap( ); } - // Load all tokens to c-token hot ATA + // Load all tokens to light-token hot ATA const ctokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); await _loadAta(rpc, ctokenAta, owner, mint, payer, confirmOptions); - // Check c-token hot balance + // Check light-token hot balance const ctokenAccountInfo = await rpc.getAccountInfo(ctokenAta); if (!ctokenAccountInfo) { throw new Error('No c-token ATA found after loading'); } - // Parse c-token account balance + // Parse light-token account balance const data = ctokenAccountInfo.data; const ctokenBalance = data.readBigUInt64LE(64); diff --git a/js/compressed-token/src/v3/actions/wrap.ts b/js/compressed-token/src/v3/actions/wrap.ts index 5efeb6a467..09593a62c3 100644 --- a/js/compressed-token/src/v3/actions/wrap.ts +++ b/js/compressed-token/src/v3/actions/wrap.ts @@ -20,7 +20,7 @@ import { } from '../../utils/get-token-pool-infos'; /** - * Wrap tokens from an SPL/T22 account to a c-token account. + * Wrap tokens from an SPL/T22 account to a light-token account. * * This is an agnostic action that takes explicit account addresses (spl-token style). * Use getAssociatedTokenAddressSync() to derive ATA addresses if needed. @@ -28,7 +28,7 @@ import { * @param rpc RPC connection * @param payer Fee payer * @param source Source SPL/T22 token account (any token account, not just ATA) - * @param destination Destination c-token account + * @param destination Destination light-token account * @param owner Owner/authority of the source account (must sign) * @param mint Mint address * @param amount Amount to wrap @@ -37,7 +37,7 @@ import { * * @example * const splAta = getAssociatedTokenAddressSync(mint, owner.publicKey, false, TOKEN_PROGRAM_ID); - * const ctokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); // defaults to c-token + * const ctokenAta = getAssociatedTokenAddressInterface(mint, owner.publicKey); // defaults to light-token * * await wrap( * rpc, diff --git a/js/compressed-token/src/v3/ata-utils.ts b/js/compressed-token/src/v3/ata-utils.ts index 1ffb3e758a..7d367598b3 100644 --- a/js/compressed-token/src/v3/ata-utils.ts +++ b/js/compressed-token/src/v3/ata-utils.ts @@ -74,7 +74,7 @@ export function checkAtaAddress( let splExpected: PublicKey; let t22Expected: PublicKey; - // c-token + // light-token ctokenExpected = getAssociatedTokenAddressSync( mint, owner, diff --git a/js/compressed-token/src/v3/derivation.ts b/js/compressed-token/src/v3/derivation.ts index 5010a537f6..91f1d29e8c 100644 --- a/js/compressed-token/src/v3/derivation.ts +++ b/js/compressed-token/src/v3/derivation.ts @@ -7,14 +7,14 @@ import { PublicKey } from '@solana/web3.js'; import { Buffer } from 'buffer'; /** - * Returns the compressed mint address as bytes. + * Returns the light mint address as bytes. */ export function deriveCMintAddress( mintSeed: PublicKey, addressTreeInfo: TreeInfo, ) { - // find_mint_address returns [CMint, bump], we want CMint - // In JS, just use the mintSeed directly as the CMint address + // find_mint_address returns [light mint, bump], we want light mint + // In JS, just use the mintSeed directly as the light mint address const address = deriveAddressV2( findMintAddress(mintSeed)[0].toBytes(), addressTreeInfo.tree, @@ -29,7 +29,7 @@ export const COMPRESSED_MINT_SEED: Buffer = Buffer.from([ ]); /** - * Finds the SPL mint PDA for a c-token mint. + * Finds the SPL mint PDA for a light-token mint. * @param mintSeed The mint seed public key. * @returns [PDA, bump] */ @@ -42,7 +42,7 @@ export function findMintAddress(mintSigner: PublicKey): [PublicKey, number] { } /// Same as "getAssociatedTokenAddress" but returns the bump as well. -/// Uses c-token program ID. +/// Uses light-token program ID. export function getAssociatedCTokenAddressAndBump( owner: PublicKey, mint: PublicKey, @@ -53,7 +53,7 @@ export function getAssociatedCTokenAddressAndBump( ); } -/// Same as "getAssociatedTokenAddress" but with c-token program ID. +/// Same as "getAssociatedTokenAddress" but with light-token program ID. export function getAssociatedCTokenAddress(owner: PublicKey, mint: PublicKey) { return PublicKey.findProgramAddressSync( [owner.toBuffer(), CTOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], diff --git a/js/compressed-token/src/v3/get-account-interface.ts b/js/compressed-token/src/v3/get-account-interface.ts index 9cb5b4d5d1..c819323064 100644 --- a/js/compressed-token/src/v3/get-account-interface.ts +++ b/js/compressed-token/src/v3/get-account-interface.ts @@ -200,7 +200,7 @@ export function parseCTokenCold( }; } /** - * Retrieve information about a token account of SPL/T22/c-token. + * Retrieve information about a token account of SPL/T22/light-token. * * @param rpc RPC connection to use * @param address Token account address @@ -247,7 +247,7 @@ export async function getAtaInterface( // Invariant: ata MUST match a valid derivation from mint+owner. // Hot path: if programId provided, only validate against that program. - // For wrap=true, additionally require c-token ATA. + // For wrap=true, additionally require light-token ATA. const validation = checkAtaAddress( ata, mint, @@ -428,7 +428,7 @@ async function _tryFetchCTokenColdByAddress( /** * @internal - * Retrieve information about a token account SPL/T22/c-token. + * Retrieve information about a token account SPL/T22/light-token. */ async function _getAccountInterface( rpc: Rpc, @@ -449,7 +449,7 @@ async function _getAccountInterface( throw new Error('One of address or fetchByOwner is required'); } - // Unified mode (auto-detect: c-token + optional SPL/T22) + // Unified mode (auto-detect: light-token + optional SPL/T22) if (!programId) { return getUnifiedAccountInterface( rpc, @@ -460,7 +460,7 @@ async function _getAccountInterface( ); } - // c-token-only mode + // light-token-only mode if (programId.equals(CTOKEN_PROGRAM_ID)) { return getCTokenAccountInterface( rpc, @@ -494,7 +494,7 @@ async function getUnifiedAccountInterface( fetchByOwner: { owner: PublicKey; mint: PublicKey } | undefined, wrap: boolean, ): Promise { - // Canonical address for unified mode is always the c-token ATA + // Canonical address for unified mode is always the light-token ATA const cTokenAta = address ?? getAssociatedTokenAddressSync( @@ -514,7 +514,7 @@ async function getUnifiedAccountInterface( const fetchTypes: TokenAccountSource['type'][] = []; const fetchAddresses: PublicKey[] = []; - // c-token hot + // light-token hot fetchPromises.push(_tryFetchCTokenHot(rpc, cTokenAta, commitment)); fetchTypes.push(TokenAccountSourceType.CTokenHot); fetchAddresses.push(cTokenAta); @@ -522,7 +522,7 @@ async function getUnifiedAccountInterface( // SPL / Token-2022 (only when wrap is enabled) if (wrap) { // Always derive SPL/T22 addresses from owner+mint, not from the passed - // c-token address. SPL and T22 ATAs are different from c-token ATAs. + // light-token address. SPL and T22 ATAs are different from light-token ATAs. if (!fetchByOwner) { throw new Error( 'fetchByOwner is required for wrap=true to derive SPL/T22 addresses', @@ -552,7 +552,7 @@ async function getUnifiedAccountInterface( fetchAddresses.push(token2022Ata); } - // Fetch ALL cold c-token accounts (not just one) - important for V1/V2 detection + // Fetch ALL cold light-token accounts (not just one) - important for V1/V2 detection const coldAccountsPromise = fetchByOwner ? rpc.getCompressedTokenAccountsByOwner(fetchByOwner.owner, { mint: fetchByOwner.mint, @@ -582,7 +582,7 @@ async function getUnifiedAccountInterface( } } - // Add ALL cold c-token accounts (handles both V1 and V2) + // Add ALL cold light-token accounts (handles both V1 and V2) for (const item of coldResult.items) { const compressedAccount = item.compressedAccount; if ( @@ -608,7 +608,7 @@ async function getUnifiedAccountInterface( throw new TokenAccountNotFoundError(); } - // priority order: c-token hot > c-token cold > SPL/T22 + // priority order: light-token hot > light-token cold > SPL/T22 const priority: TokenAccountSource['type'][] = [ TokenAccountSourceType.CTokenHot, TokenAccountSourceType.CTokenCold, @@ -664,7 +664,7 @@ async function getCTokenAccountInterface( const sources: TokenAccountSource[] = []; - // Collect hot (decompressed) c-token account + // Collect hot (decompressed) light-token account if (onchainAccount && onchainAccount.owner.equals(CTOKEN_PROGRAM_ID)) { const parsed = parseCTokenHot(address, onchainAccount); sources.push({ @@ -676,7 +676,7 @@ async function getCTokenAccountInterface( }); } - // Collect cold (compressed) c-token accounts + // Collect cold (compressed) light-token accounts for (const compressedAccount of compressedAccounts) { if ( compressedAccount && diff --git a/js/compressed-token/src/v3/get-associated-token-address-interface.ts b/js/compressed-token/src/v3/get-associated-token-address-interface.ts index aa1b902f01..993a3e065d 100644 --- a/js/compressed-token/src/v3/get-associated-token-address-interface.ts +++ b/js/compressed-token/src/v3/get-associated-token-address-interface.ts @@ -4,13 +4,13 @@ import { CTOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; import { getAtaProgramId } from './ata-utils'; /** - * Derive the canonical associated token address for any of SPL/T22/c-token. - * Defaults to using c-token as the canonical ATA. + * Derive the canonical associated token address for any of SPL/T22/light-token. + * Defaults to using light-token as the canonical ATA. * * @param mint Mint public key * @param owner Owner public key * @param allowOwnerOffCurve Allow owner to be a PDA. Default false. - * @param programId Token program ID. Default c-token. + * @param programId Token program ID. Default light-token. * * @param associatedTokenProgramId Associated token program ID. Default * auto-detected. diff --git a/js/compressed-token/src/v3/get-mint-interface.ts b/js/compressed-token/src/v3/get-mint-interface.ts index 402ba5ab6f..1a81678f03 100644 --- a/js/compressed-token/src/v3/get-mint-interface.ts +++ b/js/compressed-token/src/v3/get-mint-interface.ts @@ -33,12 +33,12 @@ export interface MintInterface { mintContext?: MintContext; tokenMetadata?: TokenMetadata; extensions?: MintExtension[]; - /** Compression info for c-token mints */ + /** Compression info for light-token mints */ compression?: CompressionInfo; } /** - * Get unified mint info for SPL/T22/c-token mints. + * Get unified mint info for SPL/T22/light-token mints. * * @param rpc RPC connection * @param address The mint address @@ -160,11 +160,11 @@ export async function getMintInterface( } /** - * Unpack mint info from raw account data for SPL/T22/c-token. + * Unpack mint info from raw account data for SPL/T22/light-token. * * @param address The mint pubkey * @param data The raw account data or AccountInfo - * @param programId Token program ID. Default c-token. + * @param programId Token program ID. Default light-token. * @returns Object with mint, optional mintContext and tokenMetadata. */ export function unpackMintInterface( @@ -179,7 +179,7 @@ export function unpackMintInterface( ? Buffer.from(data) : data.data; - // If compressed token program, deserialize as compressed mint + // If light-token program, deserialize as light mint if (programId.equals(CTOKEN_PROGRAM_ID)) { const compressedMintData = deserializeMint(buffer); @@ -226,7 +226,7 @@ export function unpackMintInterface( } /** - * Unpack c-token mint context and metadata from raw account data + * Unpack light-token mint context and metadata from raw account data * * @param data The raw account data * @returns Object with mintContext, tokenMetadata, and extensions diff --git a/js/compressed-token/src/v3/instructions/create-ata-interface.ts b/js/compressed-token/src/v3/instructions/create-ata-interface.ts index 7b79870bc0..939d4ee0df 100644 --- a/js/compressed-token/src/v3/instructions/create-ata-interface.ts +++ b/js/compressed-token/src/v3/instructions/create-ata-interface.ts @@ -13,7 +13,7 @@ import { } from './create-associated-ctoken'; /** - * c-token-specific config for createAssociatedTokenAccountInterfaceInstruction + * light-token-specific config for createAssociatedTokenAccountInterfaceInstruction */ export interface CTokenConfig { compressibleConfig?: CompressibleConfig; @@ -36,7 +36,7 @@ export interface CreateAssociatedTokenAccountInterfaceInstructionParams { /** * Create instruction for creating an associated token account (SPL, Token-2022, - * or c-token). Follows SPL Token API signature with optional c-token config at the + * or light-token). Follows SPL Token API signature with optional light-token config at the * end. * * @param payer Fee payer public key. @@ -45,7 +45,7 @@ export interface CreateAssociatedTokenAccountInterfaceInstructionParams { * @param mint Mint address. * @param programId Token program ID (default: TOKEN_PROGRAM_ID). * @param associatedTokenProgramId Associated token program ID. - * @param ctokenConfig Optional c-token-specific configuration. + * @param ctokenConfig Optional light-token-specific configuration. */ export function createAssociatedTokenAccountInterfaceInstruction( payer: PublicKey, @@ -82,7 +82,7 @@ export function createAssociatedTokenAccountInterfaceInstruction( /** * Create idempotent instruction for creating an associated token account (SPL, - * Token-2022, or c-token). Follows SPL Token API signature with optional c-token + * Token-2022, or light-token). Follows SPL Token API signature with optional light-token * config at the end. * * @param payer Fee payer public key. @@ -91,7 +91,7 @@ export function createAssociatedTokenAccountInterfaceInstruction( * @param mint Mint address. * @param programId Token program ID (default: TOKEN_PROGRAM_ID). * @param associatedTokenProgramId Associated token program ID. - * @param ctokenConfig Optional c-token-specific configuration. + * @param ctokenConfig Optional light-token-specific configuration. */ export function createAssociatedTokenAccountInterfaceIdempotentInstruction( payer: PublicKey, diff --git a/js/compressed-token/src/v3/instructions/create-decompress-interface-instruction.ts b/js/compressed-token/src/v3/instructions/create-decompress-interface-instruction.ts index e50e914634..79375ee0b5 100644 --- a/js/compressed-token/src/v3/instructions/create-decompress-interface-instruction.ts +++ b/js/compressed-token/src/v3/instructions/create-decompress-interface-instruction.ts @@ -94,13 +94,13 @@ function buildInputTokenData( /** * Create decompressInterface instruction using Transfer2. * - * Supports decompressing to both c-token accounts and SPL token accounts: - * - For c-token destinations: No splInterfaceInfo needed + * Supports decompressing to both light-token accounts and SPL token accounts: + * - For light-token destinations: No splInterfaceInfo needed * - For SPL destinations: Provide splInterfaceInfo (token pool info) and decimals * * @param payer Fee payer public key * @param inputCompressedTokenAccounts Input compressed token accounts - * @param toAddress Destination token account address (c-token or SPL ATA) + * @param toAddress Destination token account address (light-token or SPL ATA) * @param amount Amount to decompress * @param validityProof Validity proof (contains compressedProof and rootIndices) * @param splInterfaceInfo Optional: SPL interface info for SPL destinations @@ -124,7 +124,7 @@ export function createDecompressInterfaceInstruction( const owner = inputCompressedTokenAccounts[0].parsed.owner; // Build packed accounts map - // Order: trees/queues first, then mint, owner, c-token account, c-token program + // Order: trees/queues first, then mint, owner, light-token account, light-token program const packedAccountIndices = new Map(); const packedAccounts: PublicKey[] = []; @@ -163,7 +163,7 @@ export function createDecompressInterfaceInstruction( packedAccountIndices.set(owner.toBase58(), ownerIndex); packedAccounts.push(owner); - // Add destination token account (c-token or SPL) + // Add destination token account (light-token or SPL) const destinationIndex = packedAccounts.length; packedAccountIndices.set(toAddress.toBase58(), destinationIndex); packedAccounts.push(toAddress); @@ -235,7 +235,7 @@ export function createDecompressInterfaceInstruction( } // Build decompress compression - // For c-token: pool values are 0 (unused) + // For light-token: pool values are 0 (unused) // For SPL: pool values point to SPL interface PDA const compressions: Compression[] = [ { diff --git a/js/compressed-token/src/v3/instructions/create-load-accounts-params.ts b/js/compressed-token/src/v3/instructions/create-load-accounts-params.ts index 41e187dd7a..814362575b 100644 --- a/js/compressed-token/src/v3/instructions/create-load-accounts-params.ts +++ b/js/compressed-token/src/v3/instructions/create-load-accounts-params.ts @@ -28,7 +28,7 @@ export interface ParsedAccountInfoInterface { /** * Input for createLoadAccountsParams. - * Supports both program PDAs and c-token vaults. + * Supports both program PDAs and light-token vaults. * * The integrating program is responsible for fetching and parsing their accounts. * This helper just packs them for the decompressAccountsIdempotent instruction. @@ -39,7 +39,7 @@ export interface CompressibleAccountInput { /** * Account type key for packing: * - For PDAs: program-specific type name (e.g., "poolState", "observationState") - * - For c-token vaults: "cTokenData" + * - For light-token vaults: "cTokenData" */ accountType: string; /** diff --git a/js/compressed-token/src/v3/instructions/create-mint.ts b/js/compressed-token/src/v3/instructions/create-mint.ts index b91de68252..a6a6e8ab05 100644 --- a/js/compressed-token/src/v3/instructions/create-mint.ts +++ b/js/compressed-token/src/v3/instructions/create-mint.ts @@ -24,7 +24,7 @@ import { import { TokenDataVersion } from '../../constants'; /** - * Token metadata for creating a c-token mint. + * Token metadata for creating a light mint. */ export interface TokenMetadataInstructionData { name: string; @@ -161,7 +161,7 @@ export interface CreateMintInstructionParams { } /** - * Create instruction for initializing a c-token mint. + * Create instruction for initializing a light mint. * * @param mintSigner Mint signer keypair public key. * @param decimals Number of decimals for the mint. diff --git a/js/compressed-token/src/v3/instructions/index.ts b/js/compressed-token/src/v3/instructions/index.ts index c4042bacea..f3f53ef65d 100644 --- a/js/compressed-token/src/v3/instructions/index.ts +++ b/js/compressed-token/src/v3/instructions/index.ts @@ -11,3 +11,4 @@ export * from './create-decompress-interface-instruction'; export * from './create-load-accounts-params'; export * from './wrap'; export * from './unwrap'; +export * from './transfer-checked'; diff --git a/js/compressed-token/src/v3/instructions/mint-to-compressed.ts b/js/compressed-token/src/v3/instructions/mint-to-compressed.ts index 109a1c27cf..11e29034e1 100644 --- a/js/compressed-token/src/v3/instructions/mint-to-compressed.ts +++ b/js/compressed-token/src/v3/instructions/mint-to-compressed.ts @@ -94,14 +94,14 @@ export interface CreateMintToCompressedInstructionParams { } /** - * Create instruction for minting tokens from a c-mint to compressed accounts. - * To mint to onchain token accounts across SPL/T22/c-mints, use + * Create instruction for minting tokens from a light mint to compressed accounts. + * To mint to onchain token accounts across SPL/T22/light mints, use * {@link createMintToInterfaceInstruction} instead. * * @param authority Mint authority public key. * @param payer Fee payer public key. - * @param validityProof Validity proof for the compressed mint. - * @param merkleContext Merkle context of the compressed mint. + * @param validityProof Validity proof for the light mint. + * @param merkleContext Merkle context of the light mint. * @param mintData Mint instruction data. * @param recipients Array of recipients with amounts. * @param outputStateTreeInfo Optional output state tree info. Uses merkle diff --git a/js/compressed-token/src/v3/instructions/mint-to-interface.ts b/js/compressed-token/src/v3/instructions/mint-to-interface.ts index 035047230a..8b68d7d4ab 100644 --- a/js/compressed-token/src/v3/instructions/mint-to-interface.ts +++ b/js/compressed-token/src/v3/instructions/mint-to-interface.ts @@ -27,7 +27,7 @@ export interface CreateMintToInterfaceInstructionParams { * @param authority Mint authority public key. * @param payer Fee payer public key. * @param amount Amount to mint. - * @param validityProof Validity proof (required for compressed mints). + * @param validityProof Validity proof (required for light mints). * @param multiSigners Multi-signature signer public keys. */ export function createMintToInterfaceInstruction( diff --git a/js/compressed-token/src/v3/instructions/mint-to.ts b/js/compressed-token/src/v3/instructions/mint-to.ts index bf41b4bda0..f58d2f431d 100644 --- a/js/compressed-token/src/v3/instructions/mint-to.ts +++ b/js/compressed-token/src/v3/instructions/mint-to.ts @@ -94,8 +94,8 @@ export interface CreateMintToInstructionParams { * * @param authority Mint authority public key. * @param payer Fee payer public key. - * @param validityProof Validity proof for the compressed mint. - * @param merkleContext Merkle context of the compressed mint. + * @param validityProof Validity proof for the light mint. + * @param merkleContext Merkle context of the light mint. * @param mintData Mint instruction data. * @param outputStateTreeInfo Output state tree info. * @param recipientAccount Recipient onchain token account address. diff --git a/js/compressed-token/src/v3/instructions/transfer-checked.ts b/js/compressed-token/src/v3/instructions/transfer-checked.ts new file mode 100644 index 0000000000..8f5940bc62 --- /dev/null +++ b/js/compressed-token/src/v3/instructions/transfer-checked.ts @@ -0,0 +1,145 @@ +import { + PublicKey, + Signer, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { CTOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createTransferCheckedInstruction as createSplTransferCheckedInstruction, +} from '@solana/spl-token'; + +/** + * light-token transfer_checked instruction discriminator + */ +const CTOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12; + +/** + * Create a light-token transfer_checked instruction. + * + * Validates the transfer amount against the mint's decimals on-chain. + * + * @param source Source light-token account + * @param mint Mint account (read-only, used for decimals validation) + * @param destination Destination light-token account + * @param owner Owner of the source account (signer) + * @param amount Amount to transfer + * @param decimals Expected decimals of the mint + * @param feePayer Optional separate fee payer for top-ups + * @returns Transaction instruction for light-token transfer_checked + */ +export function createCTokenTransferCheckedInstruction( + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: number | bigint, + decimals: number, + feePayer?: PublicKey, +): TransactionInstruction { + // Instruction data format: + // byte 0: discriminator (12) + // bytes 1-8: amount (u64 LE) + // byte 9: decimals (u8) + const data = Buffer.alloc(10); + data.writeUInt8(CTOKEN_TRANSFER_CHECKED_DISCRIMINATOR, 0); + data.writeBigUInt64LE(BigInt(amount), 1); + data.writeUInt8(decimals, 9); + + // Authority is writable when no feePayer (owner pays top-ups), + // readonly when feePayer is provided (feePayer pays instead). + const authorityIsWritable = !feePayer; + + const keys = [ + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: destination, isSigner: false, isWritable: true }, + { + pubkey: owner, + isSigner: true, + isWritable: authorityIsWritable, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + if (feePayer) { + keys.push({ + pubkey: feePayer, + isSigner: true, + isWritable: true, + }); + } + + return new TransactionInstruction({ + programId: CTOKEN_PROGRAM_ID, + keys, + data, + }); +} + +/** + * Construct a transfer_checked instruction for SPL/T22/light-token. Defaults to + * light-token program. Validates amount against mint decimals. + * + * @param source Source token account + * @param mint Mint account + * @param destination Destination token account + * @param owner Owner of the source account (signer) + * @param amount Amount to transfer + * @param decimals Expected decimals of the mint + * @param multiSigners Multi-signers (SPL only, not supported for light-token) + * @param programId Token program ID (default: CTOKEN_PROGRAM_ID) + * @returns instruction for transfer_checked + */ +export function createTransferCheckedInterfaceInstruction( + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: PublicKey, + amount: number | bigint, + decimals: number, + multiSigners: (Signer | PublicKey)[] = [], + programId: PublicKey = CTOKEN_PROGRAM_ID, +): TransactionInstruction { + if (programId.equals(CTOKEN_PROGRAM_ID)) { + if (multiSigners.length > 0) { + throw new Error( + 'c-token transfer_checked does not support multi-signers. Use a single owner.', + ); + } + return createCTokenTransferCheckedInstruction( + source, + mint, + destination, + owner, + amount, + decimals, + ); + } + + if ( + programId.equals(TOKEN_PROGRAM_ID) || + programId.equals(TOKEN_2022_PROGRAM_ID) + ) { + return createSplTransferCheckedInstruction( + source, + mint, + destination, + owner, + amount, + decimals, + multiSigners.map(pk => + pk instanceof PublicKey ? pk : pk.publicKey, + ), + programId, + ); + } + + throw new Error(`Unsupported program ID: ${programId.toBase58()}`); +} diff --git a/js/compressed-token/src/v3/instructions/transfer-interface.ts b/js/compressed-token/src/v3/instructions/transfer-interface.ts index 8cefbae9ac..6b84ac4cef 100644 --- a/js/compressed-token/src/v3/instructions/transfer-interface.ts +++ b/js/compressed-token/src/v3/instructions/transfer-interface.ts @@ -7,18 +7,18 @@ import { } from '@solana/spl-token'; /** - * c-token transfer instruction discriminator + * light-token transfer instruction discriminator */ const CTOKEN_TRANSFER_DISCRIMINATOR = 3; /** - * Create a c-token transfer instruction. + * Create a light-token transfer instruction. * - * @param source Source c-token account - * @param destination Destination c-token account + * @param source Source light-token account + * @param destination Destination light-token account * @param owner Owner of the source account (signer, also pays for compressible extension top-ups) * @param amount Amount to transfer - * @returns Transaction instruction for c-token transfer + * @returns Transaction instruction for light-token transfer */ export function createCTokenTransferInstruction( source: PublicKey, @@ -47,14 +47,14 @@ export function createCTokenTransferInstruction( } /** - * Construct a transfer instruction for SPL/T22/c-token. Defaults to c-token - * program. For cross-program transfers (SPL <> c-token), use `wrap`/`unwrap`. + * Construct a transfer instruction for SPL/T22/light-token. Defaults to light-token + * program. For cross-program transfers (SPL <> light-token), use `wrap`/`unwrap`. * * @param source Source token account * @param destination Destination token account * @param owner Owner of the source account (signer) * @param amount Amount to transfer - * @returns instruction for c-token transfer + * @returns instruction for light-token transfer */ export function createTransferInterfaceInstruction( source: PublicKey, diff --git a/js/compressed-token/src/v3/instructions/unwrap.ts b/js/compressed-token/src/v3/instructions/unwrap.ts index 95d0d71ba0..aa36e737b8 100644 --- a/js/compressed-token/src/v3/instructions/unwrap.ts +++ b/js/compressed-token/src/v3/instructions/unwrap.ts @@ -11,10 +11,10 @@ import { } from '../layout/layout-transfer2'; /** - * Create an unwrap instruction that moves tokens from a c-token account to an + * Create an unwrap instruction that moves tokens from a light-token account to an * SPL/T22 account. * - * @param source Source c-token account + * @param source Source light-token account * @param destination Destination SPL/T22 token account * @param owner Owner of the source account (signer) * @param mint Mint address @@ -42,7 +42,7 @@ export function createUnwrapInstruction( const _SPL_TOKEN_PROGRAM_INDEX = 5; const CTOKEN_PROGRAM_INDEX = 6; - // Unwrap flow: compress from c-token, decompress to SPL + // Unwrap flow: compress from light-token, decompress to SPL const compressions: Compression[] = [ createCompressCtoken( amount, diff --git a/js/compressed-token/src/v3/instructions/update-metadata.ts b/js/compressed-token/src/v3/instructions/update-metadata.ts index f3fc520957..bc0896bded 100644 --- a/js/compressed-token/src/v3/instructions/update-metadata.ts +++ b/js/compressed-token/src/v3/instructions/update-metadata.ts @@ -216,7 +216,7 @@ function createUpdateMetadataInstruction( } /** - * Create instruction for updating a compressed mint's metadata field. + * Create instruction for updating a light mint's metadata field. * * Output queue is automatically derived from mintInterface.merkleContext.treeInfo * (preferring nextTreeInfo.queue if available for rollover support). @@ -224,7 +224,7 @@ function createUpdateMetadataInstruction( * @param mintInterface MintInterface from getMintInterface() - must have merkleContext and tokenMetadata * @param authority Metadata update authority public key (must sign) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the light mint * @param fieldType Field to update: 'name', 'symbol', 'uri', or 'custom' * @param value New value for the field * @param customKey Custom key name (required if fieldType is 'custom') @@ -265,7 +265,7 @@ export function createUpdateMetadataFieldInstruction( } /** - * Create instruction for updating a compressed mint's metadata authority. + * Create instruction for updating a light mint's metadata authority. * * Output queue is automatically derived from mintInterface.merkleContext.treeInfo * (preferring nextTreeInfo.queue if available for rollover support). @@ -274,7 +274,7 @@ export function createUpdateMetadataFieldInstruction( * @param currentAuthority Current metadata update authority public key (must sign) * @param newAuthority New metadata update authority public key * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the light mint * @param extensionIndex Extension index (default: 0) */ export function createUpdateMetadataAuthorityInstruction( @@ -301,7 +301,7 @@ export function createUpdateMetadataAuthorityInstruction( } /** - * Create instruction for removing a metadata key from a compressed mint. + * Create instruction for removing a metadata key from a light mint. * * Output queue is automatically derived from mintInterface.merkleContext.treeInfo * (preferring nextTreeInfo.queue if available for rollover support). @@ -309,7 +309,7 @@ export function createUpdateMetadataAuthorityInstruction( * @param mintInterface MintInterface from getMintInterface() - must have merkleContext and tokenMetadata * @param authority Metadata update authority public key (must sign) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the light mint * @param key Metadata key to remove * @param idempotent If true, don't error if key doesn't exist (default: false) * @param extensionIndex Extension index (default: 0) diff --git a/js/compressed-token/src/v3/instructions/update-mint.ts b/js/compressed-token/src/v3/instructions/update-mint.ts index c2a28fdab5..9a040aa36b 100644 --- a/js/compressed-token/src/v3/instructions/update-mint.ts +++ b/js/compressed-token/src/v3/instructions/update-mint.ts @@ -94,13 +94,13 @@ function encodeUpdateMintInstructionData( } /** - * Create instruction for updating a compressed mint's mint authority. + * Create instruction for updating a light mint's mint authority. * * @param mintInterface MintInterface from getMintInterface() - must have merkleContext * @param currentMintAuthority Current mint authority public key (must sign) * @param newMintAuthority New mint authority (or null to revoke) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the light mint */ export function createUpdateMintAuthorityInstruction( mintInterface: MintInterface, @@ -187,7 +187,7 @@ export function createUpdateMintAuthorityInstruction( } /** - * Create instruction for updating a compressed mint's freeze authority. + * Create instruction for updating a light mint's freeze authority. * * Output queue is automatically derived from mintInterface.merkleContext.treeInfo * (preferring nextTreeInfo.queue if available for rollover support). @@ -196,7 +196,7 @@ export function createUpdateMintAuthorityInstruction( * @param currentFreezeAuthority Current freeze authority public key (must sign) * @param newFreezeAuthority New freeze authority (or null to revoke) * @param payer Fee payer public key - * @param validityProof Validity proof for the compressed mint + * @param validityProof Validity proof for the light mint */ export function createUpdateFreezeAuthorityInstruction( mintInterface: MintInterface, diff --git a/js/compressed-token/src/v3/instructions/wrap.ts b/js/compressed-token/src/v3/instructions/wrap.ts index c6271e15d8..c1c163932e 100644 --- a/js/compressed-token/src/v3/instructions/wrap.ts +++ b/js/compressed-token/src/v3/instructions/wrap.ts @@ -12,10 +12,10 @@ import { /** * Create a wrap instruction that moves tokens from an SPL/T22 account to a - * c-token account. + * light-token account. * * @param source Source SPL/T22 token account - * @param destination Destination c-token account + * @param destination Destination light-token account * @param owner Owner of the source account (signer) * @param mint Mint address * @param amount Amount to wrap, diff --git a/js/compressed-token/src/v3/layout/layout-mint.ts b/js/compressed-token/src/v3/layout/layout-mint.ts index 8227d8dcd1..e941c81cfe 100644 --- a/js/compressed-token/src/v3/layout/layout-mint.ts +++ b/js/compressed-token/src/v3/layout/layout-mint.ts @@ -28,12 +28,12 @@ export interface BaseMint { } /** - * Compressed mint context (protocol version, SPL mint reference) + * Light mint context (protocol version, SPL mint reference) */ export interface MintContext { /** Protocol version for upgradability */ version: number; - /** Whether the compressed mint is decompressed to a CMint Solana account */ + /** Whether the light mint is decompressed to a light mint Solana account */ cmintDecompressed: boolean; /** PDA of the associated SPL mint */ splMint: PublicKey; @@ -84,7 +84,7 @@ export const TokenMetadataLayout = borshStruct([ ]); /** - * Complete compressed mint structure (raw format) + * Complete light mint structure (raw format) */ export interface CompressedMint { base: BaseMint; @@ -128,7 +128,7 @@ export const RESERVED_SIZE = 16; /** Account type discriminator size */ export const ACCOUNT_TYPE_SIZE = 1; -/** Account type value for CMint */ +/** Account type value for light mint */ export const ACCOUNT_TYPE_MINT = 1; /** @@ -151,7 +151,7 @@ export interface RentConfig { export const RENT_CONFIG_SIZE = 8; // 2 + 2 + 1 + 1 + 2 /** - * Compression info embedded in CompressedMint + * Compression info embedded in light mint */ export interface CompressionInfo { /** Config account version (0 = uninitialized) */ @@ -314,11 +314,11 @@ function deserializeCompressionInfo( } /** - * Deserialize a compressed mint from buffer + * Deserialize a light mint from buffer * Uses SPL's MintLayout for BaseMint and buffer-layout struct for context * * @param data - The raw account data buffer - * @returns The deserialized CompressedMint + * @returns The deserialized light mint */ export function deserializeMint(data: Buffer | Uint8Array): CompressedMint { const buffer = data instanceof Buffer ? data : Buffer.from(data); @@ -469,10 +469,10 @@ function serializeCompressionInfo(compression: CompressionInfo): Buffer { } /** - * Serialize a CompressedMint to buffer + * Serialize a light mint to buffer * Uses SPL's MintLayout for BaseMint, helper functions for context/metadata * - * @param mint - The CompressedMint to serialize + * @param mint - The light mint to serialize * @returns The serialized buffer */ export function serializeMint(mint: CompressedMint): Buffer { @@ -702,10 +702,10 @@ export interface MintInstructionDataWithMetadata extends MintInstructionData { } /** - * Convert a deserialized CompressedMint to MintInstructionData format + * Convert a deserialized light mint to MintInstructionData format * This extracts and flattens the data structure for instruction encoding * - * @param compressedMint - Deserialized CompressedMint from account data + * @param compressedMint - Deserialized light mint from account data * @returns Flattened MintInstructionData for instruction encoding */ export function toMintInstructionData( @@ -739,10 +739,10 @@ export function toMintInstructionData( } /** - * Convert a deserialized CompressedMint to MintInstructionDataWithMetadata + * Convert a deserialized light mint to MintInstructionDataWithMetadata * Throws if the mint doesn't have metadata extension * - * @param compressedMint - Deserialized CompressedMint from account data + * @param compressedMint - Deserialized light mint from account data * @returns MintInstructionDataWithMetadata for metadata update instructions * @throws Error if metadata extension is not present */ diff --git a/js/compressed-token/src/v3/layout/layout-transfer2.ts b/js/compressed-token/src/v3/layout/layout-transfer2.ts index 293aee4007..6243a50f0a 100644 --- a/js/compressed-token/src/v3/layout/layout-transfer2.ts +++ b/js/compressed-token/src/v3/layout/layout-transfer2.ts @@ -445,7 +445,7 @@ export function encodeTransfer2InstructionData( } /** - * Create a compression struct for wrapping SPL tokens to c-token + * Create a compression struct for wrapping SPL tokens to light-token * (compress from SPL ATA) */ export function createCompressSpl( @@ -472,11 +472,11 @@ export function createCompressSpl( } /** - * Create a compression struct for decompressing to c-token ATA + * Create a compression struct for decompressing to light-token ATA * @param amount - Amount to decompress * @param mintIndex - Index of mint in packed accounts - * @param recipientIndex - Index of recipient c-token account in packed accounts - * @param tokenProgramIndex - Index of c-token program in packed accounts (for CPI) + * @param recipientIndex - Index of recipient light-token account in packed accounts + * @param tokenProgramIndex - Index of light-token program in packed accounts (for CPI) */ export function createDecompressCtoken( amount: bigint, @@ -498,13 +498,13 @@ export function createDecompressCtoken( } /** - * Create a compression struct for compressing c-token (burn from c-token ATA) - * Used in unwrap flow: c-token ATA -> pool -> SPL ATA - * @param amount - Amount to compress (burn from c-token) + * Create a compression struct for compressing light-token (burn from light-token ATA) + * Used in unwrap flow: light-token ATA -> pool -> SPL ATA + * @param amount - Amount to compress (burn from light-token) * @param mintIndex - Index of mint in packed accounts - * @param sourceIndex - Index of source c-token account in packed accounts + * @param sourceIndex - Index of source light-token account in packed accounts * @param authorityIndex - Index of authority/owner in packed accounts (must sign) - * @param tokenProgramIndex - Index of c-token program in packed accounts (for CPI) + * @param tokenProgramIndex - Index of light-token program in packed accounts (for CPI) */ export function createCompressCtoken( amount: bigint, diff --git a/js/compressed-token/src/v3/unified/index.ts b/js/compressed-token/src/v3/unified/index.ts index 88ade99f3a..9c4de15afe 100644 --- a/js/compressed-token/src/v3/unified/index.ts +++ b/js/compressed-token/src/v3/unified/index.ts @@ -1,7 +1,7 @@ /** * Exports for @lightprotocol/compressed-token/unified * - * Import from `/unified` to get a single unified ATA for SPL/T22 and c-token + * Import from `/unified` to get a single unified ATA for SPL/T22 and light-token * mints. */ import { @@ -30,6 +30,7 @@ import { } from '../actions/load-ata'; import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface'; import { transferInterface as _transferInterface } from '../actions/transfer-interface'; +import { transferCheckedInterface as _transferCheckedInterface } from '../actions/transfer-checked'; import { _getOrCreateAtaInterface } from '../actions/get-or-create-ata-interface'; import { getAtaProgramId } from '../ata-utils'; import { InterfaceOptions } from '..'; @@ -57,14 +58,14 @@ export async function getAtaInterface( } /** - * Derive the canonical token ATA for SPL/T22/c-token in the unified path. + * Derive the canonical token ATA for SPL/T22/light-token in the unified path. * * Enforces CTOKEN_PROGRAM_ID. * * @param mint Mint public key * @param owner Owner public key * @param allowOwnerOffCurve Allow owner to be a PDA. Default false. - * @param programId Token program ID. Default c-token. + * @param programId Token program ID. Default light-token. * @param associatedTokenProgramId Associated token program ID. Default * auto-detected. * @returns Associated token address. @@ -92,7 +93,7 @@ export function getAssociatedTokenAddressInterface( } /** - * Create instructions to load ALL token balances into a c-token ATA. + * Create instructions to load ALL token balances into a light-token ATA. * * @param rpc RPC connection * @param ata Associated token address @@ -122,14 +123,14 @@ export async function createLoadAtaInstructions( } /** - * Load all token balances into the c-token ATA. + * Load all token balances into the light-token ATA. * - * Wraps SPL/Token-2022 balances and decompresses compressed c-tokens - * into the on-chain c-token ATA. If no balances exist and the ATA doesn't + * Wraps SPL/Token-2022 balances and decompresses compressed light-tokens + * into the on-chain light-token ATA. If no balances exist and the ATA doesn't * exist, creates an empty ATA (idempotent). * * @param rpc RPC connection - * @param ata Associated token address (c-token) + * @param ata Associated token address (light-token) * @param owner Owner of the tokens (signer) * @param mint Mint public key * @param payer Fee payer (signer, defaults to owner) @@ -195,9 +196,9 @@ export async function loadAta( * * @param rpc RPC connection * @param payer Fee payer (signer) - * @param source Source c-token ATA address + * @param source Source light-token ATA address * @param mint Mint address - * @param destination Destination c-token ATA address (must exist) + * @param destination Destination light-token ATA address (must exist) * @param owner Source owner (signer) * @param amount Amount to transfer * @param programId Token program ID (default: CTOKEN_PROGRAM_ID) @@ -233,18 +234,66 @@ export async function transferInterface( } /** - * Get or create c-token ATA with unified balance detection and auto-loading. + * Transfer tokens using the unified ata interface with decimals validation. + * + * Like transferInterface but validates the amount against the mint's decimals + * on-chain. + * + * @param rpc RPC connection + * @param payer Fee payer (signer) + * @param source Source light-token ATA address + * @param mint Mint address + * @param destination Destination light-token ATA address (must exist) + * @param owner Source owner (signer) + * @param amount Amount to transfer + * @param decimals Expected decimals of the mint + * @param programId Token program ID (default: CTOKEN_PROGRAM_ID) + * @param confirmOptions Optional confirm options + * @param options Optional interface options + * @returns Transaction signature + */ +export async function transferCheckedInterface( + rpc: Rpc, + payer: Signer, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + owner: Signer, + amount: number | bigint | BN, + decimals: number, + programId: PublicKey = CTOKEN_PROGRAM_ID, + confirmOptions?: ConfirmOptions, + options?: InterfaceOptions, +) { + return _transferCheckedInterface( + rpc, + payer, + source, + mint, + destination, + owner, + amount, + decimals, + programId, + confirmOptions, + options, + true, + ); +} + +/** + * Get or create light-token ATA with unified balance detection and auto-loading. * * Enforces CTOKEN_PROGRAM_ID. Aggregates balances from: - * - c-token hot (on-chain) account - * - c-token cold (compressed) accounts + * - light-token hot (on-chain) account + * - light-token cold (compressed) accounts * - SPL token accounts (for unified wrapping) * - Token-2022 accounts (for unified wrapping) * * When owner is a Signer: * - Creates hot ATA if it doesn't exist * - Loads cold (compressed) tokens into hot ATA - * - Wraps SPL/T22 tokens into c-token ATA + * - Wraps SPL/T22 tokens into light-token ATA * - Returns account with all tokens ready to use * * When owner is a PublicKey: @@ -340,6 +389,8 @@ export { createDecompressInterfaceInstruction, createTransferInterfaceInstruction, createCTokenTransferInstruction, + createTransferCheckedInterfaceInstruction, + createCTokenTransferCheckedInstruction, // Types TokenMetadataInstructionData, CompressibleConfig, diff --git a/js/compressed-token/tests/e2e/transfer-checked.test.ts b/js/compressed-token/tests/e2e/transfer-checked.test.ts new file mode 100644 index 0000000000..ce6c01016f --- /dev/null +++ b/js/compressed-token/tests/e2e/transfer-checked.test.ts @@ -0,0 +1,410 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { Keypair, Signer, PublicKey, SystemProgram } from '@solana/web3.js'; +import { + Rpc, + bn, + newAccountWithLamports, + createRpc, + selectStateTreeInfo, + TreeInfo, + CTOKEN_PROGRAM_ID, + VERSION, + featureFlags, +} from '@lightprotocol/stateless.js'; +import { createMint, mintTo } from '../../src/actions'; +import { + getTokenPoolInfos, + selectTokenPoolInfo, + TokenPoolInfo, +} from '../../src/utils/get-token-pool-infos'; +import { getAssociatedTokenAddressInterface } from '../../src/v3/get-associated-token-address-interface'; +import { getOrCreateAtaInterface } from '../../src/v3/actions/get-or-create-ata-interface'; +import { transferCheckedInterface } from '../../src/v3/actions/transfer-checked'; +import { loadAta } from '../../src/v3/actions/load-ata'; +import { + createTransferCheckedInterfaceInstruction, + createCTokenTransferCheckedInstruction, +} from '../../src/v3/instructions/transfer-checked'; + +featureFlags.version = VERSION.V2; + +const TEST_TOKEN_DECIMALS = 9; + +describe('transfer-checked', () => { + let rpc: Rpc; + let payer: Signer; + let mint: PublicKey; + let mintAuthority: Keypair; + let stateTreeInfo: TreeInfo; + let tokenPoolInfos: TokenPoolInfo[]; + + beforeAll(async () => { + rpc = createRpc(); + payer = await newAccountWithLamports(rpc, 10e9); + mintAuthority = Keypair.generate(); + const mintKeypair = Keypair.generate(); + + mint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + ) + ).mint; + + stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + tokenPoolInfos = await getTokenPoolInfos(rpc, mint); + }, 60_000); + + describe('createTransferCheckedInterfaceInstruction', () => { + it('should create c-token transfer_checked instruction with correct accounts', () => { + const source = Keypair.generate().publicKey; + const mintKey = Keypair.generate().publicKey; + const destination = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const amount = BigInt(1000); + + const ix = createTransferCheckedInterfaceInstruction( + source, + mintKey, + destination, + owner, + amount, + 9, + ); + + expect(ix.programId.equals(CTOKEN_PROGRAM_ID)).toBe(true); + expect(ix.keys.length).toBe(5); + expect(ix.keys[0].pubkey.equals(source)).toBe(true); + expect(ix.keys[0].isWritable).toBe(true); + expect(ix.keys[1].pubkey.equals(mintKey)).toBe(true); + expect(ix.keys[1].isWritable).toBe(false); + expect(ix.keys[2].pubkey.equals(destination)).toBe(true); + expect(ix.keys[2].isWritable).toBe(true); + expect(ix.keys[3].pubkey.equals(owner)).toBe(true); + expect(ix.keys[3].isSigner).toBe(true); + expect(ix.keys[4].pubkey.equals(SystemProgram.programId)).toBe( + true, + ); + + // Verify discriminator (12) and 10-byte data + expect(ix.data.length).toBe(10); + expect(ix.data[0]).toBe(12); + }); + + it('should have authority writable when no feePayer (pays for top-ups)', () => { + const source = Keypair.generate().publicKey; + const mintKey = Keypair.generate().publicKey; + const destination = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const amount = BigInt(1000); + + const ix = createCTokenTransferCheckedInstruction( + source, + mintKey, + destination, + owner, + amount, + 9, + ); + + expect(ix.keys.length).toBe(5); + expect(ix.keys[3].pubkey.equals(owner)).toBe(true); + expect(ix.keys[3].isSigner).toBe(true); + expect(ix.keys[3].isWritable).toBe(true); // writable: no feePayer + }); + + it('should have authority readonly with feePayer, and append feePayer key', () => { + const source = Keypair.generate().publicKey; + const mintKey = Keypair.generate().publicKey; + const destination = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const feePayer = Keypair.generate().publicKey; + const amount = BigInt(1000); + + const ix = createCTokenTransferCheckedInstruction( + source, + mintKey, + destination, + owner, + amount, + 9, + feePayer, + ); + + expect(ix.keys.length).toBe(6); + // Authority readonly when feePayer provided + expect(ix.keys[3].pubkey.equals(owner)).toBe(true); + expect(ix.keys[3].isSigner).toBe(true); + expect(ix.keys[3].isWritable).toBe(false); + // feePayer is signer + writable + expect(ix.keys[5].pubkey.equals(feePayer)).toBe(true); + expect(ix.keys[5].isSigner).toBe(true); + expect(ix.keys[5].isWritable).toBe(true); + }); + + it('should reject multi-signers for c-token', () => { + const source = Keypair.generate().publicKey; + const mintKey = Keypair.generate().publicKey; + const destination = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const signer = Keypair.generate(); + + expect(() => + createTransferCheckedInterfaceInstruction( + source, + mintKey, + destination, + owner, + BigInt(100), + 9, + [signer], + CTOKEN_PROGRAM_ID, + ), + ).toThrow('multi-signers'); + }); + }); + + describe('transferCheckedInterface action', () => { + it('should transfer with correct decimals (hot balance)', async () => { + const sender = await newAccountWithLamports(rpc, 1e9); + const recipient = Keypair.generate(); + + // Mint and load sender + await mintTo( + rpc, + payer, + mint, + sender.publicKey, + mintAuthority, + bn(5000), + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), + ); + const senderAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + await loadAta(rpc, senderAta, sender, mint); + + // Create recipient ATA + const recipientAta = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient.publicKey, + ); + + const sourceAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + + // Transfer with correct decimals + const signature = await transferCheckedInterface( + rpc, + payer, + sourceAta, + mint, + recipientAta.parsed.address, + sender, + BigInt(1000), + TEST_TOKEN_DECIMALS, + ); + + expect(signature).toBeDefined(); + + // Verify balances + const senderAtaInfo = await rpc.getAccountInfo(sourceAta); + const senderBalance = senderAtaInfo!.data.readBigUInt64LE(64); + expect(senderBalance).toBe(BigInt(4000)); + + const recipientAtaInfo = await rpc.getAccountInfo( + recipientAta.parsed.address, + ); + const recipientBalance = recipientAtaInfo!.data.readBigUInt64LE(64); + expect(recipientBalance).toBe(BigInt(1000)); + }); + + it('should reject transfer with wrong decimals', async () => { + const sender = await newAccountWithLamports(rpc, 1e9); + const recipient = Keypair.generate(); + + // Mint and load sender + await mintTo( + rpc, + payer, + mint, + sender.publicKey, + mintAuthority, + bn(5000), + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), + ); + const senderAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + await loadAta(rpc, senderAta, sender, mint); + + // Create recipient ATA + const recipientAta = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient.publicKey, + ); + + const sourceAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + + // Transfer with WRONG decimals should fail on-chain + await expect( + transferCheckedInterface( + rpc, + payer, + sourceAta, + mint, + recipientAta.parsed.address, + sender, + BigInt(1000), + 6, // wrong decimals (mint has 9) + ), + ).rejects.toThrow(); + }); + + it('should auto-load cold balance before transfer', async () => { + const sender = await newAccountWithLamports(rpc, 1e9); + const recipient = Keypair.generate(); + + // Mint compressed tokens (cold) - don't load + await mintTo( + rpc, + payer, + mint, + sender.publicKey, + mintAuthority, + bn(3000), + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), + ); + + // Create recipient ATA + const recipientAta = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient.publicKey, + ); + + const sourceAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + + // Transfer should auto-load sender's cold balance + const signature = await transferCheckedInterface( + rpc, + payer, + sourceAta, + mint, + recipientAta.parsed.address, + sender, + BigInt(2000), + TEST_TOKEN_DECIMALS, + CTOKEN_PROGRAM_ID, + undefined, + { splInterfaceInfos: tokenPoolInfos }, + ); + + expect(signature).toBeDefined(); + + // Verify recipient received tokens + const recipientAtaInfo = await rpc.getAccountInfo( + recipientAta.parsed.address, + ); + const recipientBalance = recipientAtaInfo!.data.readBigUInt64LE(64); + expect(recipientBalance).toBe(BigInt(2000)); + + // Sender should have change (loaded all 3000, sent 2000) + const senderAtaInfo = await rpc.getAccountInfo(sourceAta); + const senderBalance = senderAtaInfo!.data.readBigUInt64LE(64); + expect(senderBalance).toBe(BigInt(1000)); + }); + + it('should throw on source mismatch', async () => { + const sender = await newAccountWithLamports(rpc, 1e9); + const recipient = Keypair.generate(); + const wrongSource = Keypair.generate().publicKey; + + const recipientAta = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient.publicKey, + ); + + await expect( + transferCheckedInterface( + rpc, + payer, + wrongSource, + mint, + recipientAta.parsed.address, + sender, + BigInt(100), + TEST_TOKEN_DECIMALS, + ), + ).rejects.toThrow('Source mismatch'); + }); + + it('should throw on insufficient balance', async () => { + const sender = await newAccountWithLamports(rpc, 1e9); + const recipient = Keypair.generate(); + + // Mint small amount + await mintTo( + rpc, + payer, + mint, + sender.publicKey, + mintAuthority, + bn(100), + stateTreeInfo, + selectTokenPoolInfo(tokenPoolInfos), + ); + + const recipientAta = await getOrCreateAtaInterface( + rpc, + payer, + mint, + recipient.publicKey, + ); + + const sourceAta = getAssociatedTokenAddressInterface( + mint, + sender.publicKey, + ); + + await expect( + transferCheckedInterface( + rpc, + payer, + sourceAta, + mint, + recipientAta.parsed.address, + sender, + BigInt(99999), + TEST_TOKEN_DECIMALS, + CTOKEN_PROGRAM_ID, + undefined, + { splInterfaceInfos: tokenPoolInfos }, + ), + ).rejects.toThrow('Insufficient balance'); + }); + }); +});