From eb7942826de674c78581813ed436589442863218 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 24 Mar 2026 14:11:21 +0000 Subject: [PATCH 1/3] wrap options --- js/compressed-token/CHANGELOG.md | 3 +- js/compressed-token/src/index.ts | 2 - .../src/v3/actions/approve-interface.ts | 10 +- .../v3/actions/get-or-create-ata-interface.ts | 3 +- .../src/v3/actions/load-ata.ts | 3 +- .../src/v3/actions/transfer-interface.ts | 23 ++--- .../src/v3/instructions/approve-interface.ts | 8 +- .../src/v3/instructions/load-ata.ts | 2 +- .../src/v3/instructions/transfer-interface.ts | 15 ++- js/compressed-token/src/v3/unified/index.ts | 34 ++----- .../tests/e2e/load-ata-combined.test.ts | 3 +- .../tests/e2e/load-ata-freeze.test.ts | 93 +++++-------------- .../tests/e2e/transfer-interface.test.ts | 14 +-- .../unit/approve-revoke-regressions.test.ts | 12 +-- js/stateless.js/src/utils/instruction.ts | 3 +- scripts/format.sh | 5 +- 16 files changed, 73 insertions(+), 160 deletions(-) diff --git a/js/compressed-token/CHANGELOG.md b/js/compressed-token/CHANGELOG.md index e6ea6125b9..b310a6c99b 100644 --- a/js/compressed-token/CHANGELOG.md +++ b/js/compressed-token/CHANGELOG.md @@ -5,13 +5,14 @@ - **Delegate approval and revocation** for SPL Token, Token-2022, and light-token, aligned with existing interface helpers: - **Actions:** `approveInterface`, `revokeInterface`. - **Instruction builders:** `createApproveInterfaceInstructions`, `createRevokeInterfaceInstructions` — each inner array is one transaction’s instructions (same batching style as other interface instruction builders). - - **Program-level helpers:** `createLightTokenApproveInstruction`, `createLightTokenRevokeInstruction` + - **Program-level helpers:** `createLightTokenApproveInstruction`, `createLightTokenRevokeInstruction` - **Shared options:** approve/revoke accept optional `InterfaceOptions` (same type as `transferInterface`), including `splInterfaceInfos` when you need to supply SPL interface pool accounts explicitly. ### Changed - **`approveInterface` / `revokeInterface`:** optional `options?: InterfaceOptions` and `decimals?: number` after `wrap`. For SPL or Token-2022 with `wrap: false`, the SDK skips an extra mint fetch used only for decimals on that path (you can still pass `decimals` when your flow requires it). - **`@lightprotocol/compressed-token/unified`:** approve/revoke APIs accept the same optional `options` and `decimals`; unified entrypoints keep their existing default wrapping behavior (`wrap: true`). +- **Interface API normalization:** `programId` is now flat on transfer interface helpers/instruction builders (SPL-style), while `wrap` is consistently nested under `InterfaceOptions` across transfer/approve/revoke/load interface methods and their unified/root wrappers. ### Fixed diff --git a/js/compressed-token/src/index.ts b/js/compressed-token/src/index.ts index d7844443c9..76819bf767 100644 --- a/js/compressed-token/src/index.ts +++ b/js/compressed-token/src/index.ts @@ -182,7 +182,6 @@ export async function createLoadAtaInstructions( mintInterface.mint.decimals, payer, options, - false, ); } @@ -216,7 +215,6 @@ export async function loadAta( payer, confirmOptions, interfaceOptions, - false, decimals, ); } diff --git a/js/compressed-token/src/v3/actions/approve-interface.ts b/js/compressed-token/src/v3/actions/approve-interface.ts index 83b02452cb..11d5099ae2 100644 --- a/js/compressed-token/src/v3/actions/approve-interface.ts +++ b/js/compressed-token/src/v3/actions/approve-interface.ts @@ -42,7 +42,7 @@ import { TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID } from '@solana/spl-token'; * @param owner Owner of the token account (signer) * @param confirmOptions Optional confirm options * @param programId Token program ID (default: LIGHT_TOKEN_PROGRAM_ID) - * @param wrap When true and mint is SPL/T22, wrap into light-token then approve + * @param options Optional interface options (`wrap` is nested here) * @returns Transaction signature */ export async function approveInterface( @@ -55,7 +55,6 @@ export async function approveInterface( owner: Signer, confirmOptions?: ConfirmOptions, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, - wrap = false, options?: InterfaceOptions, decimals?: number, ): Promise { @@ -76,6 +75,7 @@ export async function approveInterface( const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID); + const wrap = options?.wrap ?? false; const resolvedDecimals = decimals ?? (isSplOrT22 && !wrap @@ -91,7 +91,6 @@ export async function approveInterface( owner.publicKey, resolvedDecimals, programId, - wrap, options, ); @@ -127,7 +126,7 @@ export async function approveInterface( * @param owner Owner of the token account (signer) * @param confirmOptions Optional confirm options * @param programId Token program ID (default: LIGHT_TOKEN_PROGRAM_ID) - * @param wrap When true and mint is SPL/T22, wrap into light-token then revoke + * @param options Optional interface options (`wrap` is nested here) * @returns Transaction signature */ export async function revokeInterface( @@ -138,7 +137,6 @@ export async function revokeInterface( owner: Signer, confirmOptions?: ConfirmOptions, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, - wrap = false, options?: InterfaceOptions, decimals?: number, ): Promise { @@ -159,6 +157,7 @@ export async function revokeInterface( const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID); + const wrap = options?.wrap ?? false; const resolvedDecimals = decimals ?? (isSplOrT22 && !wrap @@ -172,7 +171,6 @@ export async function revokeInterface( owner.publicKey, resolvedDecimals, programId, - wrap, options, ); 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 a40e49d93b..ee69f975c4 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 @@ -293,8 +293,7 @@ async function getOrCreateLightTokenAta( mint, payer, confirmOptions, - undefined, - wrap, + wrap ? { wrap: true } : undefined, ); // Re-fetch the updated account state diff --git a/js/compressed-token/src/v3/actions/load-ata.ts b/js/compressed-token/src/v3/actions/load-ata.ts index 836229fad7..091ef7e323 100644 --- a/js/compressed-token/src/v3/actions/load-ata.ts +++ b/js/compressed-token/src/v3/actions/load-ata.ts @@ -35,7 +35,6 @@ export async function loadAta( payer?: Signer, confirmOptions?: ConfirmOptions, interfaceOptions?: InterfaceOptions, - wrap = false, decimals?: number, ): Promise { assertBetaEnabled(); @@ -44,6 +43,7 @@ export async function loadAta( const resolvedDecimals = decimals ?? (await getMintInterface(rpc, mint)).mint.decimals; + const wrap = interfaceOptions?.wrap ?? false; const batches = await createLoadAtaInstructions( rpc, ata, @@ -52,7 +52,6 @@ export async function loadAta( resolvedDecimals, payer.publicKey, interfaceOptions, - wrap, ); if (batches.length === 0) { diff --git a/js/compressed-token/src/v3/actions/transfer-interface.ts b/js/compressed-token/src/v3/actions/transfer-interface.ts index 5a723abc92..dca1828db5 100644 --- a/js/compressed-token/src/v3/actions/transfer-interface.ts +++ b/js/compressed-token/src/v3/actions/transfer-interface.ts @@ -25,6 +25,7 @@ import { assertTransactionSizeWithinLimit } from '../utils/estimate-tx-size'; export interface InterfaceOptions { splInterfaceInfos?: SplInterfaceInfo[]; + wrap?: boolean; /** * ATA owner (authority owner) used to derive the ATA when the signer is a * delegate. For owner-signed flows, omit this field. @@ -43,7 +44,6 @@ export async function transferToAccountInterface( programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, - wrap = false, decimals?: number, ): Promise { assertBetaEnabled(); @@ -75,9 +75,8 @@ export async function transferToAccountInterface( resolvedDecimals, { ...options, - wrap, - programId, }, + programId, ); const additionalSigners = dedupeSigner(payer, [owner]); @@ -105,7 +104,6 @@ export async function transferInterface( programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, - wrap = false, decimals?: number, ): Promise { assertBetaEnabled(); @@ -135,9 +133,8 @@ export async function transferInterface( resolvedDecimals, { ...options, - wrap, - programId, }, + programId, ); const additionalSigners = dedupeSigner(payer, [owner]); @@ -154,12 +151,6 @@ export async function transferInterface( return sendAndConfirmTx(rpc, tx, confirmOptions); } -export interface TransferOptions extends InterfaceOptions { - wrap?: boolean; - programId?: PublicKey; -} -export type TransferToAccountOptions = TransferOptions; - export { sliceLast } from './slice-last'; export async function createTransferInterfaceInstructions( @@ -170,12 +161,9 @@ export async function createTransferInterfaceInstructions( sender: PublicKey, recipient: PublicKey, decimals: number, - options?: TransferOptions, + options?: InterfaceOptions, + programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, ): Promise { - // Convenience path intentionally derives ATA from a wallet recipient. - // PDA/off-curve recipients should use transferToAccountInterface with an - // explicitly derived destination token account. - const programId = options?.programId ?? LIGHT_TOKEN_PROGRAM_ID; const destination = getAssociatedTokenAddressInterface( mint, recipient, @@ -191,6 +179,7 @@ export async function createTransferInterfaceInstructions( destination, decimals, options, + programId, ); const ensureRecipientAtaIx = diff --git a/js/compressed-token/src/v3/instructions/approve-interface.ts b/js/compressed-token/src/v3/instructions/approve-interface.ts index e4e6d83c18..3d740e9020 100644 --- a/js/compressed-token/src/v3/instructions/approve-interface.ts +++ b/js/compressed-token/src/v3/instructions/approve-interface.ts @@ -64,7 +64,7 @@ function calculateRevokeCU(loadBatch: InternalLoadBatch | null): number { * @param owner Owner public key * @param decimals Token decimals * @param programId Token program ID (default: LIGHT_TOKEN_PROGRAM_ID) - * @param wrap When true and mint is SPL/T22, wrap into light-token then approve + * @param options Optional interface options (`wrap` is nested here) * @returns Instruction batches */ export async function createApproveInterfaceInstructions( @@ -77,7 +77,6 @@ export async function createApproveInterfaceInstructions( owner: PublicKey, decimals: number, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, - wrap = false, options?: InterfaceOptions, ): Promise { assertBetaEnabled(); @@ -87,6 +86,7 @@ export async function createApproveInterfaceInstructions( const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID); + const wrap = options?.wrap ?? false; const accountInterface = await _getAtaInterface( rpc, @@ -209,7 +209,7 @@ export async function createApproveInterfaceInstructions( * @param owner Owner public key * @param decimals Token decimals * @param programId Token program ID (default: LIGHT_TOKEN_PROGRAM_ID) - * @param wrap When true and mint is SPL/T22, wrap into light-token then revoke + * @param options Optional interface options (`wrap` is nested here) * @returns Instruction batches */ export async function createRevokeInterfaceInstructions( @@ -220,7 +220,6 @@ export async function createRevokeInterfaceInstructions( owner: PublicKey, decimals: number, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, - wrap = false, options?: InterfaceOptions, ): Promise { assertBetaEnabled(); @@ -228,6 +227,7 @@ export async function createRevokeInterfaceInstructions( const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || programId.equals(TOKEN_2022_PROGRAM_ID); + const wrap = options?.wrap ?? false; const accountInterface = await _getAtaInterface( rpc, diff --git a/js/compressed-token/src/v3/instructions/load-ata.ts b/js/compressed-token/src/v3/instructions/load-ata.ts index 644a14a542..217fad1d00 100644 --- a/js/compressed-token/src/v3/instructions/load-ata.ts +++ b/js/compressed-token/src/v3/instructions/load-ata.ts @@ -161,10 +161,10 @@ export async function createLoadAtaInstructions( decimals: number, payer?: PublicKey, interfaceOptions?: InterfaceOptions, - wrap = false, ): Promise { assertBetaEnabled(); payer ??= owner; + const wrap = interfaceOptions?.wrap ?? false; const effectiveOwner = interfaceOptions?.owner ?? owner; diff --git a/js/compressed-token/src/v3/instructions/transfer-interface.ts b/js/compressed-token/src/v3/instructions/transfer-interface.ts index 1628c8e519..b6add88528 100644 --- a/js/compressed-token/src/v3/instructions/transfer-interface.ts +++ b/js/compressed-token/src/v3/instructions/transfer-interface.ts @@ -31,7 +31,7 @@ import { filterInterfaceForAuthority, } from '../get-account-interface'; import { assertTransactionSizeWithinLimit } from '../utils/estimate-tx-size'; -import type { TransferOptions } from '../actions/transfer-interface'; +import type { InterfaceOptions } from '../actions/transfer-interface'; import { calculateCombinedCU } from './calculate-combined-cu'; const LIGHT_TOKEN_TRANSFER_DISCRIMINATOR = 3; @@ -153,7 +153,8 @@ export async function createTransferToAccountInterfaceInstructions( sender: PublicKey, destination: PublicKey, decimals: number, - options?: TransferOptions, + options?: InterfaceOptions, + programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, ): Promise { assertBetaEnabled(); @@ -163,12 +164,8 @@ export async function createTransferToAccountInterfaceInstructions( throw new Error('Transfer amount must be greater than zero.'); } - const { - wrap = false, - programId = LIGHT_TOKEN_PROGRAM_ID, - owner: optionsOwner, - ...interfaceOptions - } = options ?? {}; + const wrap = options?.wrap ?? false; + const optionsOwner = options?.owner; const effectiveOwner = optionsOwner ?? sender; @@ -229,7 +226,7 @@ export async function createTransferToAccountInterfaceInstructions( rpc, payer, senderInterface, - interfaceOptions, + options, wrap, senderAta, amountBigInt, diff --git a/js/compressed-token/src/v3/unified/index.ts b/js/compressed-token/src/v3/unified/index.ts index c3221af208..290a058d04 100644 --- a/js/compressed-token/src/v3/unified/index.ts +++ b/js/compressed-token/src/v3/unified/index.ts @@ -36,7 +36,6 @@ import { createTransferInterfaceInstructions as _createTransferInterfaceInstructions, createTransferToAccountInterfaceInstructions as _createTransferToAccountInterfaceInstructions, } from '../actions/transfer-interface'; -import type { TransferOptions as _TransferOptions } from '../actions/transfer-interface'; import { approveInterface as _approveInterface, revokeInterface as _revokeInterface, @@ -140,8 +139,7 @@ export async function createLoadAtaInstructions( mint, mintInterface.mint.decimals, payer, - options, - true, + { ...options, wrap: true }, ); } @@ -180,8 +178,7 @@ export async function loadAta( mint, payer, confirmOptions, - interfaceOptions, - true, + { ...interfaceOptions, wrap: true }, decimals, ); @@ -253,8 +250,7 @@ export async function transferInterface( amount, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID confirmOptions, - options, - true, // wrap=true for unified + { ...options, wrap: true }, // unified always wraps decimals, ); } @@ -286,8 +282,7 @@ export async function transferToAccountInterface( amount, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID confirmOptions, - options, - true, // wrap=true for unified + { ...options, wrap: true }, // unified always wraps decimals, ); } @@ -362,7 +357,7 @@ export async function createTransferInterfaceInstructions( amount: number | bigint | BN, sender: PublicKey, recipient: PublicKey, - options?: Omit<_TransferOptions, 'wrap'>, + options?: InterfaceOptions, ): Promise { const mintInterface = await getMintInterface(rpc, mint); return _createTransferInterfaceInstructions( @@ -391,7 +386,7 @@ export async function createTransferToAccountInterfaceInstructions( amount: number | bigint | BN, sender: PublicKey, destination: PublicKey, - options?: Omit<_TransferOptions, 'wrap'>, + options?: InterfaceOptions, ): Promise { const mintInterface = await getMintInterface(rpc, mint); return _createTransferToAccountInterfaceInstructions( @@ -497,11 +492,6 @@ export async function unwrap( ); } -export type { - _TransferOptions as TransferOptions, - _TransferOptions as TransferToAccountOptions, -}; - /** * Approve a delegate for an associated token account. * @@ -544,8 +534,7 @@ export async function approveInterface( owner, confirmOptions, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID - true, // wrap=true for unified - options, + { ...options, wrap: true }, // unified always wraps decimals, ); } @@ -583,8 +572,7 @@ export async function createApproveInterfaceInstructions( owner, resolvedDecimals, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID - true, // wrap=true for unified - options, + { ...options, wrap: true }, // unified always wraps ); } @@ -623,8 +611,7 @@ export async function revokeInterface( owner, confirmOptions, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID - true, // wrap=true for unified - options, + { ...options, wrap: true }, // unified always wraps decimals, ); } @@ -656,8 +643,7 @@ export async function createRevokeInterfaceInstructions( owner, resolvedDecimals, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID - true, // wrap=true for unified - options, + { ...options, wrap: true }, // unified always wraps ); } diff --git a/js/compressed-token/tests/e2e/load-ata-combined.test.ts b/js/compressed-token/tests/e2e/load-ata-combined.test.ts index 250214758b..09ef1cf721 100644 --- a/js/compressed-token/tests/e2e/load-ata-combined.test.ts +++ b/js/compressed-token/tests/e2e/load-ata-combined.test.ts @@ -223,8 +223,7 @@ describe('loadAta - Export Path Verification', () => { mint, undefined, undefined, - undefined, - true, // wrap=true + { wrap: true }, ); expect(signature).not.toBeNull(); diff --git a/js/compressed-token/tests/e2e/load-ata-freeze.test.ts b/js/compressed-token/tests/e2e/load-ata-freeze.test.ts index e5223501b1..235db60de4 100644 --- a/js/compressed-token/tests/e2e/load-ata-freeze.test.ts +++ b/js/compressed-token/tests/e2e/load-ata-freeze.test.ts @@ -514,16 +514,9 @@ describe('loadAta unified (wrap=true) - frozen SPL source', () => { owner.publicKey, ); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); const splAfter = await getAccount(rpc, splAta); @@ -574,8 +567,7 @@ describe('loadAta unified (wrap=true) - frozen SPL source', () => { mint, TEST_TOKEN_DECIMALS, payer.publicKey, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|load is not allowed/); }, 90_000); @@ -634,22 +626,14 @@ describe('loadAta unified (wrap=true) - frozen SPL source', () => { mint, TEST_TOKEN_DECIMALS, payer.publicKey, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|load is not allowed/); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); const splAfter = await getAccount(rpc, splAta); @@ -751,16 +735,9 @@ describe('loadAta unified (wrap=true) - frozen T22 source', () => { owner.publicKey, ); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - t22Mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, t22Mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); const t22After = await getAccount( @@ -842,22 +819,14 @@ describe('loadAta unified (wrap=true) - frozen T22 source', () => { t22Mint, TEST_TOKEN_DECIMALS, payer.publicKey, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|load is not allowed/); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - t22Mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, t22Mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); const t22After = await getAccount( @@ -975,8 +944,7 @@ describe('loadAta unified (wrap=true) - combined freeze scenarios', () => { mint, TEST_TOKEN_DECIMALS, payer.publicKey, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|load is not allowed/); @@ -1033,16 +1001,9 @@ describe('loadAta unified (wrap=true) - combined freeze scenarios', () => { owner.publicKey, ); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); expect((await getAccount(rpc, splAta)).amount).toBe(BigInt(400)); @@ -1115,22 +1076,14 @@ describe('loadAta unified (wrap=true) - combined freeze scenarios', () => { mint, TEST_TOKEN_DECIMALS, payer.publicKey, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|load is not allowed/); await expect( - loadAta( - rpc, - lightTokenAta, - owner, - mint, - payer, - undefined, - undefined, - true, - ), + loadAta(rpc, lightTokenAta, owner, mint, payer, undefined, { + wrap: true, + }), ).rejects.toThrow(/Account is frozen|load is not allowed/); }, 90_000); diff --git a/js/compressed-token/tests/e2e/transfer-interface.test.ts b/js/compressed-token/tests/e2e/transfer-interface.test.ts index 9327da9da3..518201d7a8 100644 --- a/js/compressed-token/tests/e2e/transfer-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-interface.test.ts @@ -1288,13 +1288,12 @@ describe('transfer-interface', () => { payer, senderSplAta, mint, - recipientAta, + recipient.publicKey, sender, BigInt(500), TOKEN_PROGRAM_ID, undefined, - { splInterfaceInfos: tokenPoolInfos }, - true, // wrap=true + { splInterfaceInfos: tokenPoolInfos, wrap: true }, ), ).rejects.toThrow(/For wrap=true, ata must be the light-token ATA/); }, 120_000); @@ -1483,7 +1482,6 @@ describe('transfer-interface', () => { TOKEN_PROGRAM_ID, undefined, { splInterfaceInfos: tokenPoolInfos }, - false, ); expect(signature).toBeDefined(); @@ -1544,12 +1542,12 @@ describe('transfer-interface', () => { mint, BigInt(1000), sender.publicKey, - recipientSplAta, + recipient.publicKey, TEST_TOKEN_DECIMALS, { - programId: TOKEN_PROGRAM_ID, splInterfaceInfos: tokenPoolInfos, }, + TOKEN_PROGRAM_ID, ); // Should have at least one batch with the transfer @@ -1624,7 +1622,6 @@ describe('transfer-interface', () => { TOKEN_PROGRAM_ID, undefined, undefined, - false, ); expect(signature).toBeDefined(); @@ -1690,9 +1687,9 @@ describe('transfer-interface', () => { recipient.publicKey, TEST_TOKEN_DECIMALS, { - programId: TOKEN_2022_PROGRAM_ID, splInterfaceInfos: t22TokenPoolInfos, }, + TOKEN_2022_PROGRAM_ID, ); expect(batches.length).toBeGreaterThan(0); @@ -1751,7 +1748,6 @@ describe('transfer-interface', () => { TOKEN_2022_PROGRAM_ID, undefined, { splInterfaceInfos: t22TokenPoolInfos }, - false, ); expect(signature).toBeDefined(); diff --git a/js/compressed-token/tests/unit/approve-revoke-regressions.test.ts b/js/compressed-token/tests/unit/approve-revoke-regressions.test.ts index 7b736d1673..4808df31c8 100644 --- a/js/compressed-token/tests/unit/approve-revoke-regressions.test.ts +++ b/js/compressed-token/tests/unit/approve-revoke-regressions.test.ts @@ -75,8 +75,7 @@ describe('approve/revoke regressions', () => { owner, 6, undefined, - true, - undefined, + { wrap: true }, ); expect(createRevokeInterfaceInstructionsMock).toHaveBeenCalledWith( rpc, @@ -86,8 +85,7 @@ describe('approve/revoke regressions', () => { owner, 6, undefined, - true, - undefined, + { wrap: true }, ); }); @@ -127,8 +125,7 @@ describe('approve/revoke regressions', () => { owner, 9, undefined, - true, - undefined, + { wrap: true }, ); expect(createRevokeInterfaceInstructionsMock).toHaveBeenCalledWith( rpc, @@ -138,8 +135,7 @@ describe('approve/revoke regressions', () => { owner, 9, undefined, - true, - undefined, + { wrap: true }, ); }); }); diff --git a/js/stateless.js/src/utils/instruction.ts b/js/stateless.js/src/utils/instruction.ts index 02b1249e81..9293945e34 100644 --- a/js/stateless.js/src/utils/instruction.ts +++ b/js/stateless.js/src/utils/instruction.ts @@ -2,8 +2,7 @@ import { AccountMeta, PublicKey, SystemProgram } from '@solana/web3.js'; import { defaultStaticAccountsStruct, featureFlags } from '../constants'; import { LightSystemProgram } from '../programs'; -const toStrictBool = (value: unknown): boolean => - value === true || value === 1; +const toStrictBool = (value: unknown): boolean => value === true || value === 1; export class PackedAccounts { private preAccounts: AccountMeta[] = []; diff --git a/scripts/format.sh b/scripts/format.sh index d7a62aa480..0eb7d645ca 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -8,7 +8,10 @@ cd js/compressed-token && pnpm format && cd ../.. # Rust formatting cargo +nightly fmt --all -cargo clippy \ +# Reduce memory pressure on macOS CI/dev machines. Override by exporting +# CARGO_BUILD_JOBS before running this script. +CLIPPY_JOBS="${CARGO_BUILD_JOBS:-1}" +CARGO_BUILD_JOBS="$CLIPPY_JOBS" cargo clippy \ --workspace \ --no-deps \ --all-features \ From 5cf09af3540b6a0e8b6efbbb1f4263fcd271a4eb Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 24 Mar 2026 14:56:33 +0000 Subject: [PATCH 2/3] wip --- js/compressed-token/CHANGELOG.md | 10 ++ js/compressed-token/package.json | 36 ++-- .../src/v3/actions/approve-interface.ts | 6 +- .../actions/create-associated-light-token.ts | 6 +- .../src/v3/actions/create-ata-interface.ts | 6 +- .../src/v3/actions/create-mint-interface.ts | 4 +- .../src/v3/actions/decompress-mint.ts | 4 +- .../v3/actions/get-or-create-ata-interface.ts | 4 +- .../src/v3/actions/load-ata.ts | 4 +- .../src/v3/actions/mint-to-compressed.ts | 4 +- .../src/v3/actions/mint-to-interface.ts | 4 +- js/compressed-token/src/v3/actions/mint-to.ts | 4 +- .../src/v3/actions/transfer-interface.ts | 46 +++-- js/compressed-token/src/v3/actions/unwrap.ts | 4 +- .../src/v3/actions/update-metadata.ts | 8 +- .../src/v3/actions/update-mint.ts | 6 +- js/compressed-token/src/v3/actions/wrap.ts | 4 +- .../src/v3/get-account-interface.ts | 6 +- .../src/v3/get-mint-interface.ts | 4 +- .../src/v3/instructions/approve-interface.ts | 6 +- .../src/v3/instructions/load-ata.ts | 18 +- .../src/v3/instructions/transfer-interface.ts | 34 ++-- .../src/v3/instructions/unwrap.ts | 4 +- js/compressed-token/src/v3/unified/index.ts | 19 +- .../tests/e2e/multi-cold-inputs.test.ts | 3 + .../tests/e2e/payment-flows.test.ts | 3 + .../e2e/transfer-delegated-failures.test.ts | 6 +- .../e2e/transfer-delegated-interface.test.ts | 4 +- .../e2e/transfer-delegated-spl-t22.test.ts | 12 +- .../tests/e2e/transfer-interface.test.ts | 33 ++-- .../tests/e2e/v3-interface-migration.test.ts | 1 + .../unit/get-account-interface-errors.test.ts | 163 +++++++++--------- .../unit/get-mint-interface-errors.test.ts | 86 +++++---- .../tests/unit/unified-guards.test.ts | 4 +- js/stateless.js/package.json | 4 +- js/stateless.js/src/constants.ts | 64 ++----- js/stateless.js/src/rpc.ts | 12 +- js/stateless.js/tests/unit/version.test.ts | 26 +-- 38 files changed, 332 insertions(+), 340 deletions(-) diff --git a/js/compressed-token/CHANGELOG.md b/js/compressed-token/CHANGELOG.md index b310a6c99b..4e73702577 100644 --- a/js/compressed-token/CHANGELOG.md +++ b/js/compressed-token/CHANGELOG.md @@ -14,6 +14,16 @@ - **`@lightprotocol/compressed-token/unified`:** approve/revoke APIs accept the same optional `options` and `decimals`; unified entrypoints keep their existing default wrapping behavior (`wrap: true`). - **Interface API normalization:** `programId` is now flat on transfer interface helpers/instruction builders (SPL-style), while `wrap` is consistently nested under `InterfaceOptions` across transfer/approve/revoke/load interface methods and their unified/root wrappers. +### Breaking Changes + +- **Transfer interface owner/authority split:** `transferInterface`, `transferToAccountInterface`, and the unified wrappers now take the token-account owner pubkey and the signing authority separately. + - **Action:** `transferInterface(rpc, payer, source, mint, recipient, owner, authority, amount, ...)` + - **Action:** `transferToAccountInterface(rpc, payer, source, mint, destination, owner, authority, amount, ...)` + - Owner-signed flows now pass `owner.publicKey, owner`; delegated flows pass `ownerPublicKey, delegateSigner`. +- **`InterfaceOptions.owner` removed:** transfer interface helpers no longer accept the account owner inside `InterfaceOptions`. + - Instruction builders keep flat `owner` as the canonical account owner. + - Delegated instruction planning must use `options.delegatePubkey`. + ### Fixed - **Browser bundles:** Terser no longer rewrites booleans to integers in minified output, keeping `AccountMeta` flags compatible with `@solana/web3.js` and runtime expectations (same change as `stateless.js`; see [#2347](https://github.com/Lightprotocol/light-protocol/pull/2347)). diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 378ff64425..61d3a3be90 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -92,8 +92,8 @@ "test": "vitest run tests/unit && if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V1\" ]; then pnpm test:e2e:legacy:all; else pnpm test:e2e:lighttoken:all; fi", "test-ci": "pnpm test:v1 && pnpm test:v2", "test:v1": "pnpm build:v1 && LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V1 pnpm test:e2e:legacy:all", - "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true pnpm test:e2e:lighttoken:all", - "test:v2:lighttoken": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true pnpm test:e2e:lighttoken:all", + "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:lighttoken:all", + "test:v2:lighttoken": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:lighttoken:all", "test-all": "vitest run", "test:unit:all": "EXCLUDE_E2E=true vitest run", "test:unit:all:v1": "LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit --reporter=verbose", @@ -107,7 +107,7 @@ "test:e2e:create-associated-light-token": "pnpm test-validator && vitest run tests/e2e/create-associated-light-token.test.ts --reporter=verbose", "test:e2e:mint-to-light-token": "pnpm test-validator && vitest run tests/e2e/mint-to-light-token.test.ts --reporter=verbose", "test:e2e:mint-to-compressed": "pnpm test-validator && vitest run tests/e2e/mint-to-compressed.test.ts --reporter=verbose", - "test:e2e:mint-to-interface": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/mint-to-interface.test.ts --reporter=verbose", + "test:e2e:mint-to-interface": "pnpm test-validator && vitest run tests/e2e/mint-to-interface.test.ts --reporter=verbose", "test:e2e:mint-workflow": "pnpm test-validator && vitest run tests/e2e/mint-workflow.test.ts --reporter=verbose", "test:e2e:update-mint": "pnpm test-validator && vitest run tests/e2e/update-mint.test.ts --reporter=verbose", "test:e2e:update-metadata": "pnpm test-validator && vitest run tests/e2e/update-metadata.test.ts --reporter=verbose", @@ -130,21 +130,21 @@ "test:e2e:rpc-multi-trees": "pnpm test-validator && vitest run tests/e2e/rpc-multi-trees.test.ts --reporter=verbose", "test:e2e:multi-pool": "pnpm test-validator && vitest run tests/e2e/multi-pool.test.ts --reporter=verbose", "test:e2e:legacy:all": "pnpm test-validator && vitest run tests/e2e/create-mint.test.ts && vitest run tests/e2e/mint-to.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/delegate.test.ts && vitest run tests/e2e/transfer-delegated.test.ts && vitest run tests/e2e/multi-pool.test.ts && vitest run tests/e2e/decompress-delegated.test.ts && vitest run tests/e2e/merge-token-accounts.test.ts && pnpm test-validator-skip-prover && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/compress-spl-token-account.test.ts && vitest run tests/e2e/decompress.test.ts && vitest run tests/e2e/create-token-pool.test.ts && vitest run tests/e2e/approve-and-mint-to.test.ts && vitest run tests/e2e/rpc-token-interop.test.ts && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/select-accounts.test.ts", - "test:e2e:wrap": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/wrap.test.ts --reporter=verbose", - "test:e2e:interface:all": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-mint-interface.test.ts tests/e2e/create-ata-interface.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/transfer-interface.test.ts tests/e2e/wrap.test.ts tests/e2e/unwrap.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1", - "test:e2e:get-mint-interface": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-mint-interface.test.ts --reporter=verbose", - "test:e2e:get-or-create-ata-interface": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-or-create-ata-interface.test.ts --reporter=verbose", - "test:e2e:get-account-interface": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-account-interface.test.ts --reporter=verbose", - "test:e2e:load-ata-standard": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-standard.test.ts --reporter=verbose", - "test:e2e:load-ata-unified": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-unified.test.ts --reporter=verbose", - "test:e2e:load-ata-combined": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-combined.test.ts --reporter=verbose", - "test:e2e:load-ata-spl-t22": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-spl-t22.test.ts --reporter=verbose", - "test:e2e:multi-cold-inputs-batching": "pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"instruction-level|hash uniqueness|ensureRecipientAta\" --reporter=verbose && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"parallel multi-tx\" --reporter=verbose", - "test:e2e:load-ata:all": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-standard.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-unified.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-combined.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-spl-t22.test.ts --bail=1", - "test:e2e:freeze-thaw-light-token": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/freeze-thaw-light-token.test.ts --reporter=verbose", - "test:e2e:load-ata-freeze": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-freeze.test.ts --reporter=verbose", - "test:e2e:review-fixes": "pnpm build:v2 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-associated-light-token.test.ts tests/e2e/mint-to-light-token.test.ts tests/e2e/freeze-thaw-light-token.test.ts tests/e2e/load-ata-freeze.test.ts tests/e2e/decompress2.test.ts --reporter=verbose --bail=1", - "test:e2e:lighttoken:all": "pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-compressed-mint.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-associated-light-token.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/mint-to-light-token.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/mint-to-compressed.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/mint-to-interface.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/mint-workflow.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/update-mint.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/update-metadata.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/compressible-load.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/wrap.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-mint-interface.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-account-interface.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-mint-interface.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/create-ata-interface.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/transfer-interface.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/unwrap.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/decompress2.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/freeze-thaw-light-token.test.ts --bail=1 && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/payment-flows.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/v1-v2-migration.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-standard.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-unified.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-combined.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-freeze.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/load-ata-spl-t22.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"instruction-level|hash uniqueness|ensureRecipientAta\" --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"parallel multi-tx\" --bail=1", + "test:e2e:wrap": "pnpm test-validator && vitest run tests/e2e/wrap.test.ts --reporter=verbose", + "test:e2e:interface:all": "pnpm test-validator && vitest run tests/e2e/create-mint-interface.test.ts tests/e2e/create-ata-interface.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/transfer-interface.test.ts tests/e2e/wrap.test.ts tests/e2e/unwrap.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1", + "test:e2e:get-mint-interface": "pnpm test-validator && vitest run tests/e2e/get-mint-interface.test.ts --reporter=verbose", + "test:e2e:get-or-create-ata-interface": "pnpm test-validator && vitest run tests/e2e/get-or-create-ata-interface.test.ts --reporter=verbose", + "test:e2e:get-account-interface": "pnpm test-validator && vitest run tests/e2e/get-account-interface.test.ts --reporter=verbose", + "test:e2e:load-ata-standard": "pnpm test-validator && vitest run tests/e2e/load-ata-standard.test.ts --reporter=verbose", + "test:e2e:load-ata-unified": "pnpm test-validator && vitest run tests/e2e/load-ata-unified.test.ts --reporter=verbose", + "test:e2e:load-ata-combined": "pnpm test-validator && vitest run tests/e2e/load-ata-combined.test.ts --reporter=verbose", + "test:e2e:load-ata-spl-t22": "pnpm test-validator && vitest run tests/e2e/load-ata-spl-t22.test.ts --reporter=verbose", + "test:e2e:multi-cold-inputs-batching": "pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"instruction-level|hash uniqueness|ensureRecipientAta\" --reporter=verbose && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"parallel multi-tx\" --reporter=verbose", + "test:e2e:load-ata:all": "pnpm test-validator && vitest run tests/e2e/load-ata-standard.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-unified.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-combined.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-spl-t22.test.ts --bail=1", + "test:e2e:freeze-thaw-light-token": "pnpm test-validator && vitest run tests/e2e/freeze-thaw-light-token.test.ts --reporter=verbose", + "test:e2e:load-ata-freeze": "pnpm test-validator && vitest run tests/e2e/load-ata-freeze.test.ts --reporter=verbose", + "test:e2e:review-fixes": "pnpm build:v2 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/create-associated-light-token.test.ts tests/e2e/mint-to-light-token.test.ts tests/e2e/freeze-thaw-light-token.test.ts tests/e2e/load-ata-freeze.test.ts tests/e2e/decompress2.test.ts --reporter=verbose --bail=1", + "test:e2e:lighttoken:all": "pnpm test-validator && vitest run tests/e2e/create-compressed-mint.test.ts --bail=1 && vitest run tests/e2e/create-associated-light-token.test.ts --bail=1 && vitest run tests/e2e/mint-to-light-token.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/mint-to-compressed.test.ts --bail=1 && vitest run tests/e2e/mint-to-interface.test.ts --bail=1 && vitest run tests/e2e/mint-workflow.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/update-mint.test.ts --bail=1 && vitest run tests/e2e/update-metadata.test.ts --bail=1 && vitest run tests/e2e/compressible-load.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/wrap.test.ts --bail=1 && vitest run tests/e2e/get-mint-interface.test.ts --bail=1 && vitest run tests/e2e/get-account-interface.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/create-mint-interface.test.ts --bail=1 && vitest run tests/e2e/create-ata-interface.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1 && vitest run tests/e2e/transfer-interface.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/unwrap.test.ts --bail=1 && vitest run tests/e2e/decompress2.test.ts --bail=1 && vitest run tests/e2e/freeze-thaw-light-token.test.ts --bail=1 && vitest run tests/e2e/payment-flows.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/v1-v2-migration.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-standard.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-unified.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-combined.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-freeze.test.ts --bail=1 && pnpm test-validator && vitest run tests/e2e/load-ata-spl-t22.test.ts --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"instruction-level|hash uniqueness|ensureRecipientAta\" --bail=1 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/multi-cold-inputs-batching.test.ts -t \"parallel multi-tx\" --bail=1", "test:e2e:all": "pnpm test:e2e:legacy:all && pnpm test:e2e:lighttoken:all", "pull-idl": "../../scripts/push-compressed-token-idl.sh", "build": "if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V1\" ]; then LIGHT_PROTOCOL_VERSION=V1 pnpm build:bundle; else LIGHT_PROTOCOL_VERSION=V2 pnpm build:bundle; fi", diff --git a/js/compressed-token/src/v3/actions/approve-interface.ts b/js/compressed-token/src/v3/actions/approve-interface.ts index 11d5099ae2..f4c3cc93a3 100644 --- a/js/compressed-token/src/v3/actions/approve-interface.ts +++ b/js/compressed-token/src/v3/actions/approve-interface.ts @@ -9,7 +9,7 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, - assertBetaEnabled, + assertV2Enabled, LIGHT_TOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; @@ -58,7 +58,7 @@ export async function approveInterface( options?: InterfaceOptions, decimals?: number, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const expectedAta = getAssociatedTokenAddressInterface( mint, @@ -140,7 +140,7 @@ export async function revokeInterface( options?: InterfaceOptions, decimals?: number, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const expectedAta = getAssociatedTokenAddressInterface( mint, diff --git a/js/compressed-token/src/v3/actions/create-associated-light-token.ts b/js/compressed-token/src/v3/actions/create-associated-light-token.ts index bfaf6c1473..1afe2f66a9 100644 --- a/js/compressed-token/src/v3/actions/create-associated-light-token.ts +++ b/js/compressed-token/src/v3/actions/create-associated-light-token.ts @@ -8,7 +8,7 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createAssociatedLightTokenAccountInstruction, @@ -41,7 +41,7 @@ export async function createAssociatedLightTokenAccount( rentPayerPda?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const ix = createAssociatedLightTokenAccountInstruction( payer.publicKey, @@ -89,7 +89,7 @@ export async function createAssociatedLightTokenAccountIdempotent( rentPayerPda?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const ix = createAssociatedLightTokenAccountIdempotentInstruction( payer.publicKey, 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 369ddae208..0dc490246f 100644 --- a/js/compressed-token/src/v3/actions/create-ata-interface.ts +++ b/js/compressed-token/src/v3/actions/create-ata-interface.ts @@ -11,7 +11,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, buildAndSignTx, sendAndConfirmTx, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createAssociatedTokenAccountInterfaceInstruction, @@ -52,7 +52,7 @@ export async function createAtaInterface( associatedTokenProgramId?: PublicKey, lightTokenConfig?: LightTokenConfig, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const effectiveAtaProgramId = associatedTokenProgramId ?? getAtaProgramId(programId); @@ -128,7 +128,7 @@ export async function createAtaInterfaceIdempotent( associatedTokenProgramId?: PublicKey, lightTokenConfig?: LightTokenConfig, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const effectiveAtaProgramId = associatedTokenProgramId ?? getAtaProgramId(programId); 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 a31b04909d..9e795d503b 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -18,7 +18,7 @@ import { DerivationMode, LIGHT_TOKEN_PROGRAM_ID, getDefaultAddressTreeInfo, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { @@ -60,7 +60,7 @@ export async function createMintInterface( outputStateTreeInfo?: TreeInfo, addressTreeInfo?: AddressTreeInfo, ): Promise<{ mint: PublicKey; transactionSignature: TransactionSignature }> { - assertBetaEnabled(); + assertV2Enabled(); // Dispatch to SPL/Token-2022 mint creation if ( diff --git a/js/compressed-token/src/v3/actions/decompress-mint.ts b/js/compressed-token/src/v3/actions/decompress-mint.ts index 7be7b4e915..771592cbcf 100644 --- a/js/compressed-token/src/v3/actions/decompress-mint.ts +++ b/js/compressed-token/src/v3/actions/decompress-mint.ts @@ -12,7 +12,7 @@ import { DerivationMode, bn, LIGHT_TOKEN_PROGRAM_ID, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createDecompressMintInstruction } from '../instructions/decompress-mint'; import { getMintInterface } from '../get-mint-interface'; @@ -54,7 +54,7 @@ export async function decompressMint( params?: DecompressMintParams, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // Use payer as authority if not provided (decompressMint is permissionless) const effectiveAuthority = authority ?? payer; 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 ee69f975c4..08681ded2c 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 @@ -3,7 +3,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, buildAndSignTx, sendAndConfirmTx, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { getAssociatedTokenAddressSync, @@ -71,7 +71,7 @@ export async function getOrCreateAtaInterface( programId = LIGHT_TOKEN_PROGRAM_ID, associatedTokenProgramId = getAtaProgramId(programId), ): Promise { - assertBetaEnabled(); + assertV2Enabled(); return _getOrCreateAtaInterface( rpc, diff --git a/js/compressed-token/src/v3/actions/load-ata.ts b/js/compressed-token/src/v3/actions/load-ata.ts index 091ef7e323..d7862ac5cf 100644 --- a/js/compressed-token/src/v3/actions/load-ata.ts +++ b/js/compressed-token/src/v3/actions/load-ata.ts @@ -3,7 +3,7 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { PublicKey, @@ -37,7 +37,7 @@ export async function loadAta( interfaceOptions?: InterfaceOptions, decimals?: number, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); payer ??= owner; diff --git a/js/compressed-token/src/v3/actions/mint-to-compressed.ts b/js/compressed-token/src/v3/actions/mint-to-compressed.ts index bda1cf2e32..a90ef88dc5 100644 --- a/js/compressed-token/src/v3/actions/mint-to-compressed.ts +++ b/js/compressed-token/src/v3/actions/mint-to-compressed.ts @@ -14,7 +14,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, selectStateTreeInfo, TreeInfo, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createMintToCompressedInstruction } from '../instructions/mint-to-compressed'; import { getMintInterface } from '../get-mint-interface'; @@ -47,7 +47,7 @@ export async function mintToCompressed( maxTopUp?: number, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInfo = await getMintInterface( rpc, 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 7f04823661..5511ddc2c8 100644 --- a/js/compressed-token/src/v3/actions/mint-to-interface.ts +++ b/js/compressed-token/src/v3/actions/mint-to-interface.ts @@ -9,7 +9,7 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createMintToInterfaceInstruction } from '../instructions/mint-to-interface'; import { getMintInterface } from '../get-mint-interface'; @@ -46,7 +46,7 @@ export async function mintToInterface( confirmOptions?: ConfirmOptions, programId?: PublicKey, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // Fetch mint interface (auto-detects program type if not provided) const mintInterface = await getMintInterface( diff --git a/js/compressed-token/src/v3/actions/mint-to.ts b/js/compressed-token/src/v3/actions/mint-to.ts index c51e16c6cc..abbc4003a0 100644 --- a/js/compressed-token/src/v3/actions/mint-to.ts +++ b/js/compressed-token/src/v3/actions/mint-to.ts @@ -9,7 +9,7 @@ import { Rpc, buildAndSignTx, sendAndConfirmTx, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { MAX_TOP_UP } from '../../constants'; import { createMintToInstruction } from '../instructions/mint-to'; @@ -40,7 +40,7 @@ export async function mintTo( maxTopUp?: number, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // Use payer as fee payer for top-ups if authority is different from payer const feePayer = authority.publicKey.equals(payer.publicKey) diff --git a/js/compressed-token/src/v3/actions/transfer-interface.ts b/js/compressed-token/src/v3/actions/transfer-interface.ts index dca1828db5..f223eecdd7 100644 --- a/js/compressed-token/src/v3/actions/transfer-interface.ts +++ b/js/compressed-token/src/v3/actions/transfer-interface.ts @@ -11,7 +11,7 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, - assertBetaEnabled, + assertV2Enabled, LIGHT_TOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; @@ -27,10 +27,9 @@ export interface InterfaceOptions { splInterfaceInfos?: SplInterfaceInfo[]; wrap?: boolean; /** - * ATA owner (authority owner) used to derive the ATA when the signer is a - * delegate. For owner-signed flows, omit this field. + * Delegate authority pubkey. For owner-signed flows, omit this field. */ - owner?: PublicKey; + delegatePubkey?: PublicKey; } export async function transferToAccountInterface( @@ -39,19 +38,22 @@ export async function transferToAccountInterface( source: PublicKey, mint: PublicKey, destination: PublicKey, - owner: Signer, + owner: PublicKey, + authority: Signer, amount: number | bigint | BN, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, decimals?: number, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); + const delegatePubkey = authority.publicKey.equals(owner) + ? undefined + : authority.publicKey; - const effectiveOwner = options?.owner ?? owner.publicKey; const expectedSource = getAssociatedTokenAddressInterface( mint, - effectiveOwner, + owner, false, programId, ); @@ -70,16 +72,17 @@ export async function transferToAccountInterface( payer.publicKey, mint, amountBigInt, - owner.publicKey, + owner, destination, resolvedDecimals, { ...options, + delegatePubkey: options?.delegatePubkey ?? delegatePubkey, }, programId, ); - const additionalSigners = dedupeSigner(payer, [owner]); + const additionalSigners = dedupeSigner(payer, [authority]); const { rest: loads, last: transferIxs } = sliceLast(batches); await Promise.all( loads.map(async ixs => { @@ -99,19 +102,22 @@ export async function transferInterface( source: PublicKey, mint: PublicKey, recipient: PublicKey, - owner: Signer, + owner: PublicKey, + authority: Signer, amount: number | bigint | BN, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, decimals?: number, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); + const delegatePubkey = authority.publicKey.equals(owner) + ? undefined + : authority.publicKey; - const effectiveOwner = options?.owner ?? owner.publicKey; const expectedSource = getAssociatedTokenAddressInterface( mint, - effectiveOwner, + owner, false, programId, ); @@ -128,16 +134,17 @@ export async function transferInterface( payer.publicKey, mint, amount, - owner.publicKey, + owner, recipient, resolvedDecimals, { ...options, + delegatePubkey: options?.delegatePubkey ?? delegatePubkey, }, programId, ); - const additionalSigners = dedupeSigner(payer, [owner]); + const additionalSigners = dedupeSigner(payer, [authority]); const { rest: loads, last: transferIxs } = sliceLast(batches); await Promise.all( loads.map(async ixs => { @@ -158,7 +165,7 @@ export async function createTransferInterfaceInstructions( payer: PublicKey, mint: PublicKey, amount: number | bigint | BN, - sender: PublicKey, + owner: PublicKey, recipient: PublicKey, decimals: number, options?: InterfaceOptions, @@ -175,7 +182,7 @@ export async function createTransferInterfaceInstructions( payer, mint, amount, - sender, + owner, destination, decimals, options, @@ -207,7 +214,8 @@ export async function createTransferInterfaceInstructions( ensureRecipientAtaIx, ...finalBatch.slice(insertionIdx), ]; - const numSigners = payer.equals(sender) ? 1 : 2; + const authority = options?.delegatePubkey ?? owner; + const numSigners = payer.equals(authority) ? 1 : 2; assertTransactionSizeWithinLimit( patchedFinalBatch, numSigners, diff --git a/js/compressed-token/src/v3/actions/unwrap.ts b/js/compressed-token/src/v3/actions/unwrap.ts index c7785fe887..4852c8ef25 100644 --- a/js/compressed-token/src/v3/actions/unwrap.ts +++ b/js/compressed-token/src/v3/actions/unwrap.ts @@ -10,7 +10,7 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { sliceLast } from './transfer-interface'; import BN from 'bn.js'; @@ -33,7 +33,7 @@ export async function unwrap( decimals?: number, wrap = false, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const resolvedDecimals = decimals ?? (await getMintInterface(rpc, mint)).mint.decimals; diff --git a/js/compressed-token/src/v3/actions/update-metadata.ts b/js/compressed-token/src/v3/actions/update-metadata.ts index 6ca4f5321d..8e9e63c29f 100644 --- a/js/compressed-token/src/v3/actions/update-metadata.ts +++ b/js/compressed-token/src/v3/actions/update-metadata.ts @@ -12,7 +12,7 @@ import { DerivationMode, bn, LIGHT_TOKEN_PROGRAM_ID, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createUpdateMetadataFieldInstruction, @@ -72,7 +72,7 @@ export async function updateMetadataField( extensionIndex: number = 0, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInterface = await getMintInterface( rpc, @@ -149,7 +149,7 @@ export async function updateMetadataAuthority( extensionIndex: number = 0, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInterface = await getMintInterface( rpc, @@ -226,7 +226,7 @@ export async function removeMetadataKey( extensionIndex: number = 0, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInterface = await getMintInterface( rpc, diff --git a/js/compressed-token/src/v3/actions/update-mint.ts b/js/compressed-token/src/v3/actions/update-mint.ts index db0e5962f5..b52f4bbab6 100644 --- a/js/compressed-token/src/v3/actions/update-mint.ts +++ b/js/compressed-token/src/v3/actions/update-mint.ts @@ -12,7 +12,7 @@ import { DerivationMode, bn, LIGHT_TOKEN_PROGRAM_ID, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { createUpdateMintAuthorityInstruction, @@ -40,7 +40,7 @@ export async function updateMintAuthority( newMintAuthority: PublicKey | null, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInterface = await getMintInterface( rpc, @@ -116,7 +116,7 @@ export async function updateFreezeAuthority( newFreezeAuthority: PublicKey | null, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const mintInterface = await getMintInterface( rpc, diff --git a/js/compressed-token/src/v3/actions/wrap.ts b/js/compressed-token/src/v3/actions/wrap.ts index 3b74b83ff5..aa98dafc1c 100644 --- a/js/compressed-token/src/v3/actions/wrap.ts +++ b/js/compressed-token/src/v3/actions/wrap.ts @@ -10,7 +10,7 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { getMint } from '@solana/spl-token'; import { createWrapInstruction } from '../instructions/wrap'; @@ -64,7 +64,7 @@ export async function wrap( maxTopUp?: number, confirmOptions?: ConfirmOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // Get SPL interface info if not provided let resolvedSplInterfaceInfo = splInterfaceInfo; diff --git a/js/compressed-token/src/v3/get-account-interface.ts b/js/compressed-token/src/v3/get-account-interface.ts index 7d0beea546..705d08c877 100644 --- a/js/compressed-token/src/v3/get-account-interface.ts +++ b/js/compressed-token/src/v3/get-account-interface.ts @@ -14,7 +14,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, MerkleContext, CompressedAccountWithMerkleContext, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { Buffer } from 'buffer'; import BN from 'bn.js'; @@ -364,7 +364,7 @@ export async function getAccountInterface( commitment?: Commitment, programId?: PublicKey, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); return _getAccountInterface(rpc, address, commitment, programId, undefined); } @@ -392,7 +392,7 @@ export async function getAtaInterface( wrap = false, allowOwnerOffCurve = false, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // Invariant: ata MUST match a valid derivation from mint+owner. // Hot path: if programId provided, only validate against that program. diff --git a/js/compressed-token/src/v3/get-mint-interface.ts b/js/compressed-token/src/v3/get-mint-interface.ts index 37870b3ea4..7b3b31a7ac 100644 --- a/js/compressed-token/src/v3/get-mint-interface.ts +++ b/js/compressed-token/src/v3/get-mint-interface.ts @@ -7,7 +7,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, getDefaultAddressTreeInfo, MerkleContext, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { Mint, @@ -61,7 +61,7 @@ export async function getMintInterface( commitment?: Commitment, programId?: PublicKey, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); // try all three programs in parallel if (!programId) { diff --git a/js/compressed-token/src/v3/instructions/approve-interface.ts b/js/compressed-token/src/v3/instructions/approve-interface.ts index 3d740e9020..96cb65a154 100644 --- a/js/compressed-token/src/v3/instructions/approve-interface.ts +++ b/js/compressed-token/src/v3/instructions/approve-interface.ts @@ -5,7 +5,7 @@ import { } from '@solana/web3.js'; import { Rpc, - assertBetaEnabled, + assertV2Enabled, LIGHT_TOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; import { @@ -79,7 +79,7 @@ export async function createApproveInterfaceInstructions( programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, options?: InterfaceOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const amountBigInt = BigInt(amount.toString()); @@ -222,7 +222,7 @@ export async function createRevokeInterfaceInstructions( programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, options?: InterfaceOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || diff --git a/js/compressed-token/src/v3/instructions/load-ata.ts b/js/compressed-token/src/v3/instructions/load-ata.ts index 217fad1d00..f58e1bdda2 100644 --- a/js/compressed-token/src/v3/instructions/load-ata.ts +++ b/js/compressed-token/src/v3/instructions/load-ata.ts @@ -3,7 +3,7 @@ import { LIGHT_TOKEN_PROGRAM_ID, ParsedTokenAccount, bn, - assertBetaEnabled, + assertV2Enabled, } from '@lightprotocol/stateless.js'; import { assertV2Only } from '../assert-v2-only'; import { @@ -162,11 +162,12 @@ export async function createLoadAtaInstructions( payer?: PublicKey, interfaceOptions?: InterfaceOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); payer ??= owner; const wrap = interfaceOptions?.wrap ?? false; - const effectiveOwner = interfaceOptions?.owner ?? owner; + const effectiveOwner = owner; + const authorityPubkey = interfaceOptions?.delegatePubkey ?? owner; let accountInterface: AccountInterface; try { @@ -186,14 +187,17 @@ export async function createLoadAtaInstructions( throw e; } - const isDelegate = !effectiveOwner.equals(owner); + const isDelegate = !effectiveOwner.equals(authorityPubkey); if (isDelegate) { - if (!isAuthorityForInterface(accountInterface, owner)) { + if (!isAuthorityForInterface(accountInterface, authorityPubkey)) { throw new Error( 'Signer is not the owner or a delegate of the account.', ); } - accountInterface = filterInterfaceForAuthority(accountInterface, owner); + accountInterface = filterInterfaceForAuthority( + accountInterface, + authorityPubkey, + ); } const internalBatches = await _buildLoadBatches( @@ -204,7 +208,7 @@ export async function createLoadAtaInstructions( wrap, ata, undefined, - owner, + authorityPubkey, decimals, ); diff --git a/js/compressed-token/src/v3/instructions/transfer-interface.ts b/js/compressed-token/src/v3/instructions/transfer-interface.ts index b6add88528..6ff6d444fb 100644 --- a/js/compressed-token/src/v3/instructions/transfer-interface.ts +++ b/js/compressed-token/src/v3/instructions/transfer-interface.ts @@ -6,7 +6,7 @@ import { } from '@solana/web3.js'; import { Rpc, - assertBetaEnabled, + assertV2Enabled, LIGHT_TOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; import { @@ -150,13 +150,13 @@ export async function createTransferToAccountInterfaceInstructions( payer: PublicKey, mint: PublicKey, amount: number | bigint | BN, - sender: PublicKey, + owner: PublicKey, destination: PublicKey, decimals: number, options?: InterfaceOptions, programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const amountBigInt = BigInt(amount.toString()); @@ -165,9 +165,9 @@ export async function createTransferToAccountInterfaceInstructions( } const wrap = options?.wrap ?? false; - const optionsOwner = options?.owner; - - const effectiveOwner = optionsOwner ?? sender; + const delegatePubkey = options?.delegatePubkey; + const effectiveOwner = owner; + const authorityPubkey = delegatePubkey ?? effectiveOwner; const isSplOrT22 = programId.equals(TOKEN_PROGRAM_ID) || @@ -200,20 +200,26 @@ export async function createTransferToAccountInterfaceInstructions( checkNotFrozen(senderInterface, 'transfer'); - const isDelegate = !effectiveOwner.equals(sender); + const isDelegate = !effectiveOwner.equals(authorityPubkey); if (isDelegate) { - if (!isAuthorityForInterface(senderInterface, sender)) { + if (!isAuthorityForInterface(senderInterface, authorityPubkey)) { throw new Error( 'Signer is not the owner or a delegate of the sender account.', ); } - const spendable = spendableAmountForAuthority(senderInterface, sender); + const spendable = spendableAmountForAuthority( + senderInterface, + authorityPubkey, + ); if (amountBigInt > spendable) { throw new Error( `Insufficient delegated balance. Required: ${amountBigInt}, Available (delegate): ${spendable}`, ); } - senderInterface = filterInterfaceForAuthority(senderInterface, sender); + senderInterface = filterInterfaceForAuthority( + senderInterface, + authorityPubkey, + ); } else { if (senderInterface.parsed.amount < amountBigInt) { throw new Error( @@ -230,7 +236,7 @@ export async function createTransferToAccountInterfaceInstructions( wrap, senderAta, amountBigInt, - sender, + authorityPubkey, decimals, ); @@ -240,7 +246,7 @@ export async function createTransferToAccountInterfaceInstructions( senderAta, mint, destination, - sender, + authorityPubkey, amountBigInt, decimals, [], @@ -251,14 +257,14 @@ export async function createTransferToAccountInterfaceInstructions( senderAta, destination, mint, - sender, + authorityPubkey, amountBigInt, decimals, payer, ); } - const numSigners = payer.equals(sender) ? 1 : 2; + const numSigners = payer.equals(authorityPubkey) ? 1 : 2; if (internalBatches.length === 0) { const cu = calculateTransferCU(null); diff --git a/js/compressed-token/src/v3/instructions/unwrap.ts b/js/compressed-token/src/v3/instructions/unwrap.ts index 1f63fb6390..63d8387874 100644 --- a/js/compressed-token/src/v3/instructions/unwrap.ts +++ b/js/compressed-token/src/v3/instructions/unwrap.ts @@ -6,7 +6,7 @@ import { } from '@solana/web3.js'; import { Rpc, - assertBetaEnabled, + assertV2Enabled, LIGHT_TOKEN_PROGRAM_ID, } from '@lightprotocol/stateless.js'; import { TokenAccountNotFoundError } from '@solana/spl-token'; @@ -175,7 +175,7 @@ export async function createUnwrapInstructions( interfaceOptions?: InterfaceOptions, wrap = false, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); payer ??= owner; diff --git a/js/compressed-token/src/v3/unified/index.ts b/js/compressed-token/src/v3/unified/index.ts index 290a058d04..d636e43f41 100644 --- a/js/compressed-token/src/v3/unified/index.ts +++ b/js/compressed-token/src/v3/unified/index.ts @@ -222,7 +222,8 @@ export async function loadAta( * @param source Source light-token associated token account address * @param mint Mint address * @param recipient Destination owner wallet address - * @param owner Source owner (signer) + * @param owner Source account owner pubkey + * @param authority Signing authority (owner or approved delegate) * @param amount Amount to transfer * @param confirmOptions Optional confirm options * @param options Optional interface options @@ -234,7 +235,8 @@ export async function transferInterface( source: PublicKey, mint: PublicKey, recipient: PublicKey, - owner: Signer, + owner: PublicKey, + authority: Signer, amount: number | bigint | BN, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, @@ -247,6 +249,7 @@ export async function transferInterface( mint, recipient, owner, + authority, amount, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID confirmOptions, @@ -266,7 +269,8 @@ export async function transferToAccountInterface( source: PublicKey, mint: PublicKey, destination: PublicKey, - owner: Signer, + owner: PublicKey, + authority: Signer, amount: number | bigint | BN, confirmOptions?: ConfirmOptions, options?: InterfaceOptions, @@ -279,6 +283,7 @@ export async function transferToAccountInterface( mint, destination, owner, + authority, amount, undefined, // programId: use default LIGHT_TOKEN_PROGRAM_ID confirmOptions, @@ -355,7 +360,7 @@ export async function createTransferInterfaceInstructions( payer: PublicKey, mint: PublicKey, amount: number | bigint | BN, - sender: PublicKey, + owner: PublicKey, recipient: PublicKey, options?: InterfaceOptions, ): Promise { @@ -365,7 +370,7 @@ export async function createTransferInterfaceInstructions( payer, mint, amount, - sender, + owner, recipient, mintInterface.mint.decimals, { @@ -384,7 +389,7 @@ export async function createTransferToAccountInterfaceInstructions( payer: PublicKey, mint: PublicKey, amount: number | bigint | BN, - sender: PublicKey, + owner: PublicKey, destination: PublicKey, options?: InterfaceOptions, ): Promise { @@ -394,7 +399,7 @@ export async function createTransferToAccountInterfaceInstructions( payer, mint, amount, - sender, + owner, destination, mintInterface.mint.decimals, { diff --git a/js/compressed-token/tests/e2e/multi-cold-inputs.test.ts b/js/compressed-token/tests/e2e/multi-cold-inputs.test.ts index c95b621e1b..2d7139fe50 100644 --- a/js/compressed-token/tests/e2e/multi-cold-inputs.test.ts +++ b/js/compressed-token/tests/e2e/multi-cold-inputs.test.ts @@ -646,6 +646,7 @@ describe('Multi-Cold-Inputs', () => { sourceAta, mint, recipientAta, + owner.publicKey, owner, totalAmount, ); @@ -699,6 +700,7 @@ describe('Multi-Cold-Inputs', () => { sourceAta, mint, recipientAta, + owner.publicKey, owner, totalAmount, ); @@ -751,6 +753,7 @@ describe('Multi-Cold-Inputs', () => { sourceAta, mint, recipientAta, + owner.publicKey, owner, totalAmount, ); diff --git a/js/compressed-token/tests/e2e/payment-flows.test.ts b/js/compressed-token/tests/e2e/payment-flows.test.ts index 61ff46606b..d175779eb2 100644 --- a/js/compressed-token/tests/e2e/payment-flows.test.ts +++ b/js/compressed-token/tests/e2e/payment-flows.test.ts @@ -116,6 +116,7 @@ describe('Payment Flows', () => { sourceAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, amount, undefined, @@ -166,6 +167,7 @@ describe('Payment Flows', () => { sourceAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(2000), undefined, @@ -241,6 +243,7 @@ describe('Payment Flows', () => { sourceAta, mint, destAta, + sender.publicKey, sender, BigInt(500), ); diff --git a/js/compressed-token/tests/e2e/transfer-delegated-failures.test.ts b/js/compressed-token/tests/e2e/transfer-delegated-failures.test.ts index c320b02ef3..3983754d87 100644 --- a/js/compressed-token/tests/e2e/transfer-delegated-failures.test.ts +++ b/js/compressed-token/tests/e2e/transfer-delegated-failures.test.ts @@ -89,11 +89,11 @@ describe('transferDelegatedInterface - failure cases', () => { ownerAta, mint, recipient.publicKey, + owner.publicKey, delegate, 600_000_000, // > 500M allowance undefined, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 30_000); @@ -106,11 +106,11 @@ describe('transferDelegatedInterface - failure cases', () => { ownerAta, mint, recipient.publicKey, + owner.publicKey, stranger, // not approved 100_000_000, undefined, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 30_000); @@ -126,11 +126,11 @@ describe('transferDelegatedInterface - failure cases', () => { ownerAta, mint, recipient.publicKey, + owner.publicKey, delegate, 100_000_000, undefined, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 60_000); diff --git a/js/compressed-token/tests/e2e/transfer-delegated-interface.test.ts b/js/compressed-token/tests/e2e/transfer-delegated-interface.test.ts index dedda7d98a..56a206601b 100644 --- a/js/compressed-token/tests/e2e/transfer-delegated-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-delegated-interface.test.ts @@ -106,11 +106,9 @@ describe('transferDelegatedInterface - e2e', () => { ownerAta, mint, recipient.publicKey, + owner.publicKey, delegate, 200_000_000, - undefined, - undefined, - { owner: owner.publicKey }, ); expect(sig).toBeTruthy(); diff --git a/js/compressed-token/tests/e2e/transfer-delegated-spl-t22.test.ts b/js/compressed-token/tests/e2e/transfer-delegated-spl-t22.test.ts index fedf46beb2..018973b1d0 100644 --- a/js/compressed-token/tests/e2e/transfer-delegated-spl-t22.test.ts +++ b/js/compressed-token/tests/e2e/transfer-delegated-spl-t22.test.ts @@ -127,11 +127,12 @@ describe('delegated transfer - SPL mint', () => { ownerAta, splMint, recipient.publicKey, + owner.publicKey, delegate, 200_000_000n, TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey, splInterfaceInfos: [] }, + { splInterfaceInfos: [] }, ); expect(sig).toBeTruthy(); @@ -195,11 +196,11 @@ describe('delegated transfer - SPL mint', () => { ownerAta, splMint, recipient.publicKey, + owner.publicKey, delegate, 200_000_000n, // > 100M allowance TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 60_000); @@ -212,11 +213,11 @@ describe('delegated transfer - SPL mint', () => { ownerAta, splMint, recipient.publicKey, + owner.publicKey, stranger, 50_000_000n, TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 60_000); @@ -239,11 +240,11 @@ describe('delegated transfer - SPL mint', () => { ownerAta, splMint, recipient.publicKey, + owner.publicKey, delegate, 50_000_000n, TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 60_000); @@ -335,11 +336,12 @@ describe('delegated transfer - Token-2022 mint', () => { ownerAta, t22Mint, recipient.publicKey, + owner.publicKey, delegate, 200_000_000n, TOKEN_2022_PROGRAM_ID, undefined, - { owner: owner.publicKey, splInterfaceInfos: [] }, + { splInterfaceInfos: [] }, ); expect(sig).toBeTruthy(); diff --git a/js/compressed-token/tests/e2e/transfer-interface.test.ts b/js/compressed-token/tests/e2e/transfer-interface.test.ts index 518201d7a8..089f68b3e3 100644 --- a/js/compressed-token/tests/e2e/transfer-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-interface.test.ts @@ -250,6 +250,7 @@ describe('transfer-interface', () => { senderSplAta, splMintWithFreeze, recipientSplAta, + sender.publicKey, sender, BigInt(100), TOKEN_PROGRAM_ID, @@ -335,12 +336,12 @@ describe('transfer-interface', () => { senderAta, freezableMint, recipientAta, + sender.publicKey, sender, BigInt(100), LIGHT_TOKEN_PROGRAM_ID, undefined, - undefined, - true, + { wrap: true }, ), ).rejects.toThrow(/Account is frozen|transfer is not allowed/); }); @@ -404,11 +405,11 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta, + owner.publicKey, delegate, BigInt(500), LIGHT_TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey }, ); expect(signature).toBeDefined(); const recipientInfo = await rpc.getAccountInfo(recipientAta); @@ -450,10 +451,10 @@ describe('transfer-interface', () => { payer.publicKey, mint, BigInt(500), - delegate.publicKey, + owner.publicKey, recipientAta, TEST_TOKEN_DECIMALS, - { owner: owner.publicKey }, + { delegatePubkey: delegate.publicKey }, ); expect(batches.length).toBeGreaterThan(0); @@ -506,11 +507,11 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta, + owner.publicKey, delegate, BigInt(700), LIGHT_TOKEN_PROGRAM_ID, undefined, - { owner: owner.publicKey }, ), ).rejects.toThrow(); }, 120_000); @@ -543,10 +544,10 @@ describe('transfer-interface', () => { payer.publicKey, mint, BigInt(100), - other, + owner.publicKey, recipientAta, TEST_TOKEN_DECIMALS, - { owner: owner.publicKey }, + { delegatePubkey: other }, ), ).rejects.toThrow(/Signer is not the owner or a delegate/); }); @@ -578,10 +579,10 @@ describe('transfer-interface', () => { payer.publicKey, mint, BigInt(500), - delegate.publicKey, + owner.publicKey, recipientAta, TEST_TOKEN_DECIMALS, - { owner: owner.publicKey }, + { delegatePubkey: delegate.publicKey }, ), ).rejects.toThrow(/Insufficient delegated balance/); }); @@ -768,6 +769,7 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(1000), ); @@ -820,6 +822,7 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(2000), undefined, @@ -859,6 +862,7 @@ describe('transfer-interface', () => { wrongSource, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(100), ), @@ -899,6 +903,7 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(99999), undefined, @@ -979,6 +984,7 @@ describe('transfer-interface', () => { sourceAta, mint, recipientAta, + sender.publicKey, sender, BigInt(500), ); @@ -1024,6 +1030,7 @@ describe('transfer-interface', () => { senderAta, mint, recipientAta.parsed.address, + sender.publicKey, sender, BigInt(1), ); @@ -1073,6 +1080,7 @@ describe('transfer-interface', () => { ownerAta, mint, ownerAtaDest, + owner.publicKey, owner, BigInt(100), ); @@ -1230,6 +1238,7 @@ describe('transfer-interface', () => { senderAta, mint, recipient.publicKey, + sender.publicKey, sender, BigInt(250), ); @@ -1289,6 +1298,7 @@ describe('transfer-interface', () => { senderSplAta, mint, recipient.publicKey, + sender.publicKey, sender, BigInt(500), TOKEN_PROGRAM_ID, @@ -1477,6 +1487,7 @@ describe('transfer-interface', () => { senderSplAta, mint, recipientSplAta, + sender.publicKey, sender, BigInt(2000), TOKEN_PROGRAM_ID, @@ -1617,6 +1628,7 @@ describe('transfer-interface', () => { senderSplAta, mint, recipientSplAta, + sender.publicKey, sender, BigInt(1500), TOKEN_PROGRAM_ID, @@ -1743,6 +1755,7 @@ describe('transfer-interface', () => { senderT22Ata, t22Mint, recipient.publicKey, + sender.publicKey, sender, BigInt(900), TOKEN_2022_PROGRAM_ID, diff --git a/js/compressed-token/tests/e2e/v3-interface-migration.test.ts b/js/compressed-token/tests/e2e/v3-interface-migration.test.ts index 3c248530e5..b732b16041 100644 --- a/js/compressed-token/tests/e2e/v3-interface-migration.test.ts +++ b/js/compressed-token/tests/e2e/v3-interface-migration.test.ts @@ -370,6 +370,7 @@ describe('v3-interface-v1-rejection', () => { sourceAta, mint, recipientAta, + owner.publicKey, owner, BigInt(500), ), diff --git a/js/compressed-token/tests/unit/get-account-interface-errors.test.ts b/js/compressed-token/tests/unit/get-account-interface-errors.test.ts index 4635330bfe..22bc5403a3 100644 --- a/js/compressed-token/tests/unit/get-account-interface-errors.test.ts +++ b/js/compressed-token/tests/unit/get-account-interface-errors.test.ts @@ -64,94 +64,91 @@ const makeCompressedAccount = () => proveByIndex: false, }) as any; -describe.skipIf(!featureFlags.isV2() || !featureFlags.isBeta())( - 'get-account-interface errors', - () => { - it('throws TokenInvalidAccountOwnerError for light-token mode owner mismatch', async () => { - const rpc = { - getAccountInfo: async () => - makeOnchainAccount(PublicKey.unique(), Buffer.alloc(10)), - getCompressedTokenAccountsByOwner: async () => ({ items: [] }), - } as any; +describe.skipIf(!featureFlags.isV2())('get-account-interface errors', () => { + it('throws TokenInvalidAccountOwnerError for light-token mode owner mismatch', async () => { + const rpc = { + getAccountInfo: async () => + makeOnchainAccount(PublicKey.unique(), Buffer.alloc(10)), + getCompressedTokenAccountsByOwner: async () => ({ items: [] }), + } as any; - await expect( - getAccountInterface( - rpc, - PublicKey.unique(), - 'confirmed', - LIGHT_TOKEN_PROGRAM_ID, - ), - ).rejects.toBeInstanceOf(TokenInvalidAccountOwnerError); - }); - - it('propagates SPL parse failures instead of downgrading to not-found', async () => { - const rpc = { - getAccountInfo: async () => - makeOnchainAccount(TOKEN_PROGRAM_ID, Buffer.alloc(1)), - } as any; + await expect( + getAccountInterface( + rpc, + PublicKey.unique(), + 'confirmed', + LIGHT_TOKEN_PROGRAM_ID, + ), + ).rejects.toBeInstanceOf(TokenInvalidAccountOwnerError); + }); - await expect( - getAccountInterface( - rpc, - PublicKey.unique(), - 'confirmed', - TOKEN_PROGRAM_ID, - ), - ).rejects.toThrow('Failed to fetch token account data from RPC'); - }); + it('propagates SPL parse failures instead of downgrading to not-found', async () => { + const rpc = { + getAccountInfo: async () => + makeOnchainAccount(TOKEN_PROGRAM_ID, Buffer.alloc(1)), + } as any; - it('throws on RPC failures even when a compressed source exists', async () => { - const rpc = { - getAccountInfo: async () => { - throw new Error('solana down'); - }, - getCompressedTokenAccountsByOwner: async () => ({ - items: [{ compressedAccount: makeCompressedAccount() }], - }), - } as any; + await expect( + getAccountInterface( + rpc, + PublicKey.unique(), + 'confirmed', + TOKEN_PROGRAM_ID, + ), + ).rejects.toThrow('Failed to fetch token account data from RPC'); + }); - await expect( - getAccountInterface( - rpc, - PublicKey.unique(), - 'confirmed', - LIGHT_TOKEN_PROGRAM_ID, - ), - ).rejects.toThrow( - 'Failed to fetch token account data from RPC: solana down', - ); - }); + it('throws on RPC failures even when a compressed source exists', async () => { + const rpc = { + getAccountInfo: async () => { + throw new Error('solana down'); + }, + getCompressedTokenAccountsByOwner: async () => ({ + items: [{ compressedAccount: makeCompressedAccount() }], + }), + } as any; - it('getAtaInterface propagates compressed RPC fetch failures', async () => { - const owner = PublicKey.unique(); - const mint = PublicKey.unique(); - const ata = getAssociatedTokenAddressSync( - mint, - owner, - false, + await expect( + getAccountInterface( + rpc, + PublicKey.unique(), + 'confirmed', LIGHT_TOKEN_PROGRAM_ID, - getAtaProgramId(LIGHT_TOKEN_PROGRAM_ID), - ); + ), + ).rejects.toThrow( + 'Failed to fetch token account data from RPC: solana down', + ); + }); + + it('getAtaInterface propagates compressed RPC fetch failures', async () => { + const owner = PublicKey.unique(); + const mint = PublicKey.unique(); + const ata = getAssociatedTokenAddressSync( + mint, + owner, + false, + LIGHT_TOKEN_PROGRAM_ID, + getAtaProgramId(LIGHT_TOKEN_PROGRAM_ID), + ); - const rpc = { - getAccountInfo: async () => null, - getCompressedTokenAccountsByOwner: async () => { - throw new Error('compression timeout'); - }, - } as any; + const rpc = { + getAccountInfo: async () => null, + getCompressedTokenAccountsByOwner: async () => { + throw new Error('compression timeout'); + }, + } as any; - await expect( - getAtaInterface( - rpc, - ata, - owner, - mint, - 'confirmed', - LIGHT_TOKEN_PROGRAM_ID, - ), - ).rejects.toThrow( - 'Failed to fetch token account data from RPC: compression timeout', - ); - }); - }, -); + await expect( + getAtaInterface( + rpc, + ata, + owner, + mint, + 'confirmed', + LIGHT_TOKEN_PROGRAM_ID, + ), + ).rejects.toThrow( + 'Failed to fetch token account data from RPC: compression timeout', + ); + }); +}); diff --git a/js/compressed-token/tests/unit/get-mint-interface-errors.test.ts b/js/compressed-token/tests/unit/get-mint-interface-errors.test.ts index 65c6bd1fb3..41b5132fd7 100644 --- a/js/compressed-token/tests/unit/get-mint-interface-errors.test.ts +++ b/js/compressed-token/tests/unit/get-mint-interface-errors.test.ts @@ -31,54 +31,46 @@ const makeCompressedMintSentinelAccount = () => lamports: { toNumber: () => 0 }, }) as any; -describe.skipIf(!featureFlags.isV2() || !featureFlags.isBeta())( - 'get-mint-interface errors', - () => { - it('throws TokenInvalidAccountOwnerError when decompressed on-chain mint owner mismatches', async () => { - const mint = PublicKey.unique(); - const rpc = { - getCompressedAccount: async () => - makeCompressedMintSentinelAccount(), - getAccountInfo: async () => ({ - executable: false, - owner: PublicKey.unique(), - lamports: 1, - data: Buffer.alloc(100), - rentEpoch: 0, - }), - } as any; +describe.skipIf(!featureFlags.isV2())('get-mint-interface errors', () => { + it('throws TokenInvalidAccountOwnerError when decompressed on-chain mint owner mismatches', async () => { + const mint = PublicKey.unique(); + const rpc = { + getCompressedAccount: async () => + makeCompressedMintSentinelAccount(), + getAccountInfo: async () => ({ + executable: false, + owner: PublicKey.unique(), + lamports: 1, + data: Buffer.alloc(100), + rentEpoch: 0, + }), + } as any; - await expect( - getMintInterface( - rpc, - mint, - 'confirmed', - LIGHT_TOKEN_PROGRAM_ID, - ), - ).rejects.toBeInstanceOf(TokenInvalidAccountOwnerError); - }); + await expect( + getMintInterface(rpc, mint, 'confirmed', LIGHT_TOKEN_PROGRAM_ID), + ).rejects.toBeInstanceOf(TokenInvalidAccountOwnerError); + }); - it('forwards commitment when fetching decompressed on-chain mint', async () => { - const mint = PublicKey.unique(); - const commitment: Commitment = 'processed'; - let seenCommitment: Commitment | undefined; + it('forwards commitment when fetching decompressed on-chain mint', async () => { + const mint = PublicKey.unique(); + const commitment: Commitment = 'processed'; + let seenCommitment: Commitment | undefined; - const rpc = { - getCompressedAccount: async () => - makeCompressedMintSentinelAccount(), - getAccountInfo: async ( - _address: PublicKey, - incomingCommitment?: Commitment, - ) => { - seenCommitment = incomingCommitment; - return null; - }, - } as any; + const rpc = { + getCompressedAccount: async () => + makeCompressedMintSentinelAccount(), + getAccountInfo: async ( + _address: PublicKey, + incomingCommitment?: Commitment, + ) => { + seenCommitment = incomingCommitment; + return null; + }, + } as any; - await expect( - getMintInterface(rpc, mint, commitment, LIGHT_TOKEN_PROGRAM_ID), - ).rejects.toBeInstanceOf(TokenAccountNotFoundError); - expect(seenCommitment).toBe(commitment); - }); - }, -); + await expect( + getMintInterface(rpc, mint, commitment, LIGHT_TOKEN_PROGRAM_ID), + ).rejects.toBeInstanceOf(TokenAccountNotFoundError); + expect(seenCommitment).toBe(commitment); + }); +}); diff --git a/js/compressed-token/tests/unit/unified-guards.test.ts b/js/compressed-token/tests/unit/unified-guards.test.ts index d7405aaefe..1dc98880c0 100644 --- a/js/compressed-token/tests/unit/unified-guards.test.ts +++ b/js/compressed-token/tests/unit/unified-guards.test.ts @@ -59,8 +59,8 @@ describe('unified guards', () => { ).not.toThrow(); }); - // Skip unless V2+beta - createLoadAtaInstructions is a V2-only interface method requiring beta - it.skipIf(!featureFlags.isV2() || !featureFlags.isBeta())( + // Skip unless V2 - createLoadAtaInstructions is a V2-only interface method + it.skipIf(!featureFlags.isV2())( 'throws when unified createLoadAtaInstructions receives non light-token ATA', async () => { const rpc = {} as Rpc; diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 635ba27119..64ad45e54f 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -88,7 +88,7 @@ "test": "pnpm test:unit:all && pnpm test:e2e:all", "test-ci": "vitest run tests/unit && pnpm test:e2e:all", "test:v1": "pnpm build:v1 && LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V1 pnpm test:e2e:all", - "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true pnpm test:e2e:all", + "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:all", "test-all": "vitest run", "test:unit:all": "vitest run tests/unit --reporter=verbose", "test:unit:all:v1": "LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit --reporter=verbose", @@ -103,7 +103,7 @@ "test:e2e:rpc-interop": "pnpm test-validator && vitest run tests/e2e/rpc-interop.test.ts --reporter=verbose --bail=1", "test:e2e:rpc-multi-trees": "pnpm test-validator && vitest run tests/e2e/rpc-multi-trees.test.ts --reporter=verbose --bail=1", "test:e2e:browser": "pnpm playwright test", - "test:e2e:all": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/rpc-interop.test.ts && if [ \"$LIGHT_PROTOCOL_VERSION\" != \"V1\" ]; then LIGHT_PROTOCOL_VERSION=V2 LIGHT_PROTOCOL_BETA=true vitest run tests/e2e/interface-methods.test.ts; fi && pnpm test-validator-skip-prover && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/safe-conversion.test.ts", + "test:e2e:all": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts && vitest run tests/e2e/compress.test.ts && vitest run tests/e2e/transfer.test.ts && vitest run tests/e2e/rpc-interop.test.ts && if [ \"$LIGHT_PROTOCOL_VERSION\" != \"V1\" ]; then LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e/interface-methods.test.ts; fi && pnpm test-validator-skip-prover && vitest run tests/e2e/rpc-multi-trees.test.ts && vitest run tests/e2e/layout.test.ts && vitest run tests/e2e/safe-conversion.test.ts", "test:index": "vitest run tests/e2e/program.test.ts", "test:e2e:layout": "vitest run tests/e2e/layout.test.ts --reporter=verbose", "test:e2e:safe-conversion": "vitest run tests/e2e/safe-conversion.test.ts --reporter=verbose", diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index 648dee70a4..04f56fcd79 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -8,12 +8,6 @@ export enum VERSION { V2 = 'V2', } -/** - * @internal - * Feature flags. Only use if you know what you are doing. - */ -let _betaEnabled = true; // Default to enabled for beta releases - export const featureFlags = { version: ((): VERSION => { // Check if we're in a build environment (replaced by rollup) @@ -34,65 +28,31 @@ export const featureFlags = { isV2: () => featureFlags.version.replace(/['"]/g, '').toUpperCase() === 'V2', /** - * Beta flag for interface methods (not yet deployed on mainnet). - * Checks programmatic override first, then env var LIGHT_PROTOCOL_BETA. + * @deprecated Interface methods are production-ready; beta gating is ignored. */ - isBeta: (): boolean => { - if (_betaEnabled) { - return true; - } - if ( - typeof process !== 'undefined' && - process.env?.LIGHT_PROTOCOL_BETA - ) { - const val = process.env.LIGHT_PROTOCOL_BETA.toLowerCase(); - return val === 'true' || val === '1'; - } - return false; - }, + isBeta: (): boolean => true, /** - * Enable beta features programmatically. - * Call this once at app initialization to unlock interface methods. - * - * @example - * ```typescript - * import { featureFlags } from '@lightprotocol/stateless.js'; - * featureFlags.enableBeta(); - * ``` + * @deprecated Interface methods are production-ready; beta gating is ignored. */ - enableBeta: (): void => { - _betaEnabled = true; - }, + enableBeta: (): void => {}, /** - * Disable beta features programmatically. + * @deprecated Interface methods are production-ready; beta gating is ignored. */ - disableBeta: (): void => { - _betaEnabled = false; - }, + disableBeta: (): void => {}, }; /** - * Error message for beta-gated interface methods. + * Error message for V2-only interface methods. */ -export const BETA_REQUIRED_ERROR = - 'Interface methods require beta feature flag. ' + - 'These features are not yet deployed on mainnet (only localnet/devnet). ' + - 'Set LIGHT_PROTOCOL_BETA=true to enable.'; +export const V2_REQUIRED_ERROR = + 'Interface methods require V2. Set LIGHT_PROTOCOL_VERSION=V2.'; /** - * Assert that beta features are enabled. - * Throws if V2 is not enabled OR if beta is not enabled. - * - * Use this at the entry point of all interface methods. + * Assert that V2 interface methods are enabled. */ -export function assertBetaEnabled(): void { +export function assertV2Enabled(): void { if (!featureFlags.isV2()) { - throw new Error( - 'Interface methods require V2. Set LIGHT_PROTOCOL_VERSION=V2.', - ); - } - if (!featureFlags.isBeta()) { - throw new Error(BETA_REQUIRED_ERROR); + throw new Error(V2_REQUIRED_ERROR); } } diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 218ad5b172..16747512d2 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -91,7 +91,7 @@ import { batchAddressTree, LIGHT_TOKEN_PROGRAM_ID, getDefaultAddressSpace, - assertBetaEnabled, + assertV2Enabled, } from './constants'; import BN from 'bn.js'; import { toCamelCase, toHex } from './utils/conversion'; @@ -2114,7 +2114,7 @@ export class Rpc extends Connection implements CompressionApiInterface { isCold: boolean; loadContext?: MerkleContext; } | null> { - assertBetaEnabled(); + assertV2Enabled(); addressSpace = addressSpace ?? getDefaultAddressSpace(); @@ -2215,7 +2215,7 @@ export class Rpc extends Connection implements CompressionApiInterface { options?: SignaturesForAddressOptions, compressedOptions?: PaginatedOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const [solanaResult, compressedResult] = await Promise.allSettled([ this.getSignaturesForAddress(address, options), @@ -2254,7 +2254,7 @@ export class Rpc extends Connection implements CompressionApiInterface { options?: SignaturesForAddressOptions, compressedOptions?: PaginatedOptions, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const [solanaResult, compressedResult] = await Promise.allSettled([ this.getSignaturesForAddress(owner, options), @@ -2296,7 +2296,7 @@ export class Rpc extends Connection implements CompressionApiInterface { mint: PublicKey, commitment?: Commitment, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const [onChainResult, compressedResult] = await Promise.allSettled([ this.getTokenAccountBalance(address, commitment), @@ -2347,7 +2347,7 @@ export class Rpc extends Connection implements CompressionApiInterface { address: PublicKey, commitment?: Commitment, ): Promise { - assertBetaEnabled(); + assertV2Enabled(); const [onChainResult, compressedResult] = await Promise.allSettled([ this.getBalance(address, commitment), diff --git a/js/stateless.js/tests/unit/version.test.ts b/js/stateless.js/tests/unit/version.test.ts index a0cc7100aa..0ad1c25d4b 100644 --- a/js/stateless.js/tests/unit/version.test.ts +++ b/js/stateless.js/tests/unit/version.test.ts @@ -2,8 +2,8 @@ import { describe, it, expect } from 'vitest'; import { featureFlags, VERSION, - assertBetaEnabled, - BETA_REQUIRED_ERROR, + assertV2Enabled, + V2_REQUIRED_ERROR, } from '../../src/constants'; describe('Version System', () => { @@ -32,22 +32,12 @@ describe('Version System', () => { }); }); -describe('assertBetaEnabled', () => { - it('should throw correct error based on version and beta flag', () => { - const isV2 = featureFlags.isV2(); - const isBeta = featureFlags.isBeta(); - - if (!isV2) { - // V1 mode: should throw V2 required error - expect(() => assertBetaEnabled()).toThrowError( - 'Interface methods require V2. Set LIGHT_PROTOCOL_VERSION=V2.', - ); - } else if (!isBeta) { - // V2 mode without beta: should throw beta required error - expect(() => assertBetaEnabled()).toThrowError(BETA_REQUIRED_ERROR); +describe('assertV2Enabled', () => { + it('should throw only when V2 is disabled', () => { + if (!featureFlags.isV2()) { + expect(() => assertV2Enabled()).toThrowError(V2_REQUIRED_ERROR); } else { - // V2 mode with beta: should not throw - expect(() => assertBetaEnabled()).not.toThrow(); + expect(() => assertV2Enabled()).not.toThrow(); } }); @@ -55,7 +45,7 @@ describe('assertBetaEnabled', () => { // This test ensures the V1 guard is in place and produces the expected error. // If running in V1 mode, this validates the error. If V2, it's a no-op check. if (!featureFlags.isV2()) { - expect(() => assertBetaEnabled()).toThrowError( + expect(() => assertV2Enabled()).toThrowError( /Interface methods require V2/, ); } From 2f52adc5334b84bc1eb8f0d1673c68d845b64b03 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Tue, 24 Mar 2026 15:00:20 +0000 Subject: [PATCH 3/3] upd changelog --- js/compressed-token/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/compressed-token/CHANGELOG.md b/js/compressed-token/CHANGELOG.md index 4e73702577..05fbc5c78e 100644 --- a/js/compressed-token/CHANGELOG.md +++ b/js/compressed-token/CHANGELOG.md @@ -33,7 +33,7 @@ ### Breaking Changes - **`transferInterface` and `createTransferInterfaceInstructions`** now take a recipient wallet address and ensure recipient ATA internally. - - **Action:** `transferInterface(rpc, payer, source, mint, recipient, owner, amount, ...)` — `recipient` is the wallet public key. + - **Action:** `transferInterface(rpc, payer, source, mint, recipient, owner, authority, amount, ...)` — `recipient` is the wallet public key. - **Instruction builder:** `createTransferInterfaceInstructions(rpc, payer, mint, amount, sender, recipient, decimals, options?)` — derives destination ATA and inserts idempotent ATA-create internally. - **Advanced explicit-account path:** use `transferToAccountInterface(...)` and `createTransferToAccountInterfaceInstructions(...)` for destination token-account routing (program-owned/custom accounts), preserving the previous destination-account behavior. - **`decimals` is required** on v3 action-level instruction builders. Fetch with `getMintInterface(rpc, mint).mint.decimals` if not already threaded.