From afa5f81fde4ac079466421b8d7e148f688118555 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 14 Dec 2025 03:05:15 +0400 Subject: [PATCH 01/28] fix prover dl --- cli/src/utils/downloadProverBinary.ts | 7 ++++--- cli/src/utils/processProverServer.ts | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/cli/src/utils/downloadProverBinary.ts b/cli/src/utils/downloadProverBinary.ts index 7f565b389b..bf747e5cc9 100644 --- a/cli/src/utils/downloadProverBinary.ts +++ b/cli/src/utils/downloadProverBinary.ts @@ -4,8 +4,9 @@ import https from "https"; import http from "http"; import { pipeline } from "stream/promises"; -const PROVER_VERSION = "2.0.6"; -const GITHUB_RELEASES_BASE_URL = `https://github.com/Lightprotocol/light-protocol/releases/download/light-prover-v${PROVER_VERSION}`; +const PROVER_RELEASE_TAG = "2.0.6"; +const PROVER_BINARY_VERSION = "2.0.0"; // Version string the binary actually reports +const GITHUB_RELEASES_BASE_URL = `https://github.com/Lightprotocol/light-protocol/releases/download/light-prover-v${PROVER_RELEASE_TAG}`; const MAX_REDIRECTS = 10; interface DownloadOptions { @@ -151,5 +152,5 @@ async function downloadFile( } export function getProverVersion(): string { - return PROVER_VERSION; + return PROVER_BINARY_VERSION; } diff --git a/cli/src/utils/processProverServer.ts b/cli/src/utils/processProverServer.ts index 3e91b30256..9052ad6478 100644 --- a/cli/src/utils/processProverServer.ts +++ b/cli/src/utils/processProverServer.ts @@ -1,4 +1,5 @@ import path from "path"; +import os from "os"; import fs from "fs"; import { execSync } from "child_process"; import { @@ -7,13 +8,15 @@ import { spawnBinary, waitForServers, } from "./process"; -import { LIGHT_PROVER_PROCESS_NAME, BASE_PATH } from "./constants"; +import { LIGHT_PROVER_PROCESS_NAME } from "./constants"; import { downloadProverBinary, getProverVersion as getExpectedProverVersion, } from "./downloadProverBinary"; -const KEYS_DIR = "proving-keys/"; +const LIGHT_CONFIG_DIR = path.join(os.homedir(), ".config", "light"); +const PROVER_BIN_DIR = path.join(LIGHT_CONFIG_DIR, "bin"); +const KEYS_DIR = path.join(LIGHT_CONFIG_DIR, "proving-keys"); export async function killProver() { await killProcess(getProverNameByArch()); @@ -32,11 +35,13 @@ function getInstalledProverVersion(): string | null { } try { - const version = execSync(`"${binaryPath}" version`, { + const output = execSync(`"${binaryPath}" version`, { encoding: "utf-8", timeout: 5000, }).trim(); - return version; + // Extract version number (handles "v2.0.6", "light-prover v2.0.6", "2.0.6", etc.) + const match = output.match(/(\d+\.\d+\.\d+)/); + return match ? match[1] : null; } catch (error) { return null; } @@ -85,10 +90,9 @@ export async function startProver(proverPort: number, redisUrl?: string) { await killProver(); await killProcessByPort(proverPort); - const keysDir = path.join(path.resolve(__dirname, BASE_PATH), KEYS_DIR); const args = ["start"]; - args.push("--keys-dir", keysDir); + args.push("--keys-dir", KEYS_DIR); args.push("--prover-address", `0.0.0.0:${proverPort}`); args.push("--auto-download", "true"); @@ -128,11 +132,8 @@ export function getProverNameByArch(): string { } export function getProverPathByArch(): string { - let binaryName = getProverNameByArch(); - const binDir = path.resolve(__dirname, BASE_PATH); - binaryName = path.join(binDir, binaryName); - - return binaryName; + const binaryName = getProverNameByArch(); + return path.join(PROVER_BIN_DIR, binaryName); } export async function healthCheck( From 47298b68222f173eab08ca9126d1e30f0d567261 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 14 Dec 2025 12:06:55 +0400 Subject: [PATCH 02/28] bump cli alpha.4 --- cli/package.json | 2 +- cli/src/utils/constants.ts | 2 +- scripts/devenv/versions.sh | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 57a1e2518f..7208213a00 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.2", + "version": "0.27.1-alpha.3", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { diff --git a/cli/src/utils/constants.ts b/cli/src/utils/constants.ts index e2f527654d..e3a9137737 100644 --- a/cli/src/utils/constants.ts +++ b/cli/src/utils/constants.ts @@ -24,7 +24,7 @@ export const PHOTON_VERSION = "0.51.2"; // Set these to override Photon requirements with a specific git commit: export const USE_PHOTON_FROM_GIT = true; // If true, will show git install command instead of crates.io. export const PHOTON_GIT_REPO = "https://github.com/lightprotocol/photon.git"; -export const PHOTON_GIT_COMMIT = "711c47b20330c6bb78feb0a2c15e8292fcd0a7b0"; // If empty, will use main branch. +export const PHOTON_GIT_COMMIT = "ac7df6c388db847b7693a7a1cb766a7c9d7809b5"; // If empty, will use main branch. export const LIGHT_PROTOCOL_PROGRAMS_DIR_ENV = "LIGHT_PROTOCOL_PROGRAMS_DIR"; export const BASE_PATH = "../../bin/"; diff --git a/scripts/devenv/versions.sh b/scripts/devenv/versions.sh index 394405759e..e4deec350d 100755 --- a/scripts/devenv/versions.sh +++ b/scripts/devenv/versions.sh @@ -13,7 +13,8 @@ export SOLANA_VERSION="2.2.15" export ANCHOR_VERSION="0.31.1" export JQ_VERSION="1.8.0" export PHOTON_VERSION="0.51.2" -export PHOTON_COMMIT="3dbfb8e6772779fc89c640b5b0823b95d1958efc" +# current main (ci fails): 3dbfb8e6772779fc89c640b5b0823b95d1958efc +export PHOTON_COMMIT="ac7df6c388db847b7693a7a1cb766a7c9d7809b5" export REDIS_VERSION="8.0.1" export ANCHOR_TAG="anchor-v${ANCHOR_VERSION}" From 142920e8272c3b24e63a794a0e839024de702872 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 17 Dec 2025 12:46:13 +0100 Subject: [PATCH 03/28] bump --- cli/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/package.json b/cli/package.json index 7208213a00..8e15aebbd8 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.3", + "version": "0.27.1-alpha.4", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { From 0bb11d4f3c9e46204c9a8a6929454b9fbb4d77aa Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 17 Dec 2025 16:40:40 +0100 Subject: [PATCH 04/28] fix cli startup --- cli/src/utils/initTestEnv.ts | 4 ++++ cli/src/utils/processPhotonIndexer.ts | 4 ++++ cli/src/utils/processProverServer.ts | 2 +- 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cli/src/utils/initTestEnv.ts b/cli/src/utils/initTestEnv.ts index 6492414fa3..d48bb415d7 100644 --- a/cli/src/utils/initTestEnv.ts +++ b/cli/src/utils/initTestEnv.ts @@ -177,11 +177,15 @@ export async function initTestEnv({ const config = getConfig(); config.indexerUrl = `http://127.0.0.1:${indexerPort}`; setConfig(config); + const proverUrlForIndexer = prover + ? `http://127.0.0.1:${proverPort}` + : undefined; await startIndexer( `http://127.0.0.1:${rpcPort}`, indexerPort, checkPhotonVersion, photonDatabaseUrl, + proverUrlForIndexer, ); } diff --git a/cli/src/utils/processPhotonIndexer.ts b/cli/src/utils/processPhotonIndexer.ts index a2407f82f3..1c883a121d 100644 --- a/cli/src/utils/processPhotonIndexer.ts +++ b/cli/src/utils/processPhotonIndexer.ts @@ -41,6 +41,7 @@ export async function startIndexer( indexerPort: number, checkPhotonVersion: boolean = true, photonDatabaseUrl?: string, + proverUrl?: string, ) { await killIndexer(); const resolvedOrNull = which.sync("photon", { nothrow: true }); @@ -61,6 +62,9 @@ export async function startIndexer( if (photonDatabaseUrl) { args.push("--db-url", photonDatabaseUrl); } + if (proverUrl) { + args.push("--prover-url", proverUrl); + } spawnBinary(INDEXER_PROCESS_NAME, args); await waitForServers([{ port: indexerPort, path: "/getIndexerHealth" }]); diff --git a/cli/src/utils/processProverServer.ts b/cli/src/utils/processProverServer.ts index 9052ad6478..c163f07dd6 100644 --- a/cli/src/utils/processProverServer.ts +++ b/cli/src/utils/processProverServer.ts @@ -92,7 +92,7 @@ export async function startProver(proverPort: number, redisUrl?: string) { const args = ["start"]; - args.push("--keys-dir", KEYS_DIR); + args.push("--keys-dir", KEYS_DIR + "/"); args.push("--prover-address", `0.0.0.0:${proverPort}`); args.push("--auto-download", "true"); From f0290bf5c6b040d18115be86092d3bfc4383450a Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 17 Dec 2025 23:01:10 +0100 Subject: [PATCH 05/28] bumps --- cli/package.json | 2 +- js/compressed-token/package.json | 2 +- js/stateless.js/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 8e15aebbd8..8656b9b5d9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.4", + "version": "0.27.1-alpha.5", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index aea4e9cafc..5561648ecb 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.1-alpha.2", + "version": "0.22.1-alpha.3", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 5cfdb89e4f..efb48d77df 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.1-alpha.1", + "version": "0.22.1-alpha.2", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", From 9ee70253e5db471f97e3c4c2e92a2ffa59e7b5d6 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 11:25:33 +0100 Subject: [PATCH 06/28] fix and run unit tests on ci --- js/compressed-token/package.json | 6 +- .../tests/e2e/merge-token-accounts.test.ts | 2 +- .../tests/unit/unified-guards.test.ts | 2 +- js/stateless.js/package.json | 2 +- js/stateless.js/src/constants.ts | 133 ++++++++---------- js/stateless.js/src/rpc.ts | 1 + 6 files changed, 63 insertions(+), 83 deletions(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 5561648ecb..e50304108c 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -89,7 +89,7 @@ "vitest": "^2.1.1" }, "scripts": { - "test": "pnpm test:e2e:legacy:all", + "test": "vitest run tests/unit && pnpm test:e2e:legacy:all", "test-ci": "pnpm test:v1 && pnpm test:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:ctoken:all", "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 pnpm test:e2e:legacy:all", @@ -127,7 +127,7 @@ "test:e2e:rpc-token-interop": "pnpm test-validator && vitest run tests/e2e/rpc-token-interop.test.ts --reporter=verbose", "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 && 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: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 && vitest run tests/e2e/wrap.test.ts --reporter=verbose", "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", @@ -137,7 +137,7 @@ "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: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:ctoken:all": "pnpm test-validator && vitest run tests/e2e/create-compressed-mint.test.ts --bail=1 && vitest run tests/e2e/create-associated-ctoken.test.ts --bail=1 && vitest run tests/e2e/mint-to-ctoken.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 && 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 && 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/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:ctoken:all": "pnpm test-validator && vitest run tests/e2e/create-compressed-mint.test.ts --bail=1 && vitest run tests/e2e/create-associated-ctoken.test.ts --bail=1 && vitest run tests/e2e/mint-to-ctoken.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 && 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 && 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 && vitest run tests/e2e/create-mint-interface.test.ts --bail=1 && vitest run tests/e2e/create-ata-interface.test.ts --bail=1 && vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1 && vitest run tests/e2e/transfer-interface.test.ts --bail=1 && vitest run tests/e2e/unwrap.test.ts --bail=1 && vitest run tests/e2e/decompress2.test.ts --bail=1 && vitest run tests/e2e/payment-flows.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-spl-t22.test.ts --bail=1", "pull-idl": "../../scripts/push-compressed-token-idl.sh", "build": "if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V2\" ]; then LIGHT_PROTOCOL_VERSION=V2 pnpm build:bundle; else LIGHT_PROTOCOL_VERSION=V1 pnpm build:bundle; fi", "build:bundle": "rimraf dist && rollup -c", diff --git a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts index e63a4a7434..a4340489a5 100644 --- a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts +++ b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts @@ -57,7 +57,7 @@ describe('mergeTokenAccounts', () => { } }); - it.only('should merge all token accounts', async () => { + it('should merge all token accounts', async () => { const preAccounts = await rpc.getCompressedTokenAccountsByOwner( owner.publicKey, { mint }, diff --git a/js/compressed-token/tests/unit/unified-guards.test.ts b/js/compressed-token/tests/unit/unified-guards.test.ts index da122aca45..76fb19a81d 100644 --- a/js/compressed-token/tests/unit/unified-guards.test.ts +++ b/js/compressed-token/tests/unit/unified-guards.test.ts @@ -60,7 +60,7 @@ describe('unified guards', () => { await expect( unifiedCreateLoadAtaInstructions(rpc, wrongAta, owner, mint, owner), ).rejects.toThrow( - 'Unified loadAta expects ATA to be derived from c-token program. Derive it with getAssociatedTokenAddressInterface.', + 'For wrap=true, ata must be the c-token ATA. Got spl ATA instead.', ); }); }); diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index efb48d77df..b7a3e88bff 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -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 && 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 && vitest run tests/e2e/interface-methods.test.ts && 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 1b26e9c243..ab42e2371c 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -145,79 +145,6 @@ export const isLocalTest = (url: string) => { return url.includes('localhost') || url.includes('127.0.0.1'); }; -/** - * @internal - */ -export const localTestActiveStateTreeInfos = (): TreeInfo[] => { - return [ - { - tree: new PublicKey(merkletreePubkey), - queue: new PublicKey(nullifierQueuePubkey), - cpiContext: new PublicKey(cpiContextPubkey), - treeType: TreeType.StateV1, - nextTreeInfo: null, - }, - { - tree: new PublicKey(merkleTree2Pubkey), - queue: new PublicKey(nullifierQueue2Pubkey), - cpiContext: new PublicKey(cpiContext2Pubkey), - treeType: TreeType.StateV1, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchMerkleTree1), - queue: new PublicKey(batchQueue1), - cpiContext: new PublicKey(batchCpiContext1), - treeType: TreeType.StateV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchMerkleTree2), - queue: new PublicKey(batchQueue2), - cpiContext: new PublicKey(batchCpiContext2), - treeType: TreeType.StateV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchMerkleTree3), - queue: new PublicKey(batchQueue3), - cpiContext: new PublicKey(batchCpiContext3), - treeType: TreeType.StateV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchMerkleTree4), - queue: new PublicKey(batchQueue4), - cpiContext: new PublicKey(batchCpiContext4), - treeType: TreeType.StateV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchMerkleTree5), - queue: new PublicKey(batchQueue5), - cpiContext: new PublicKey(batchCpiContext5), - treeType: TreeType.StateV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(batchAddressTree), - queue: new PublicKey(batchAddressTree), // v2 address queue is part of the tree account. - cpiContext: PublicKey.default, - treeType: TreeType.AddressV2, - nextTreeInfo: null, - }, - { - tree: new PublicKey(testBatchAddressTree), - queue: new PublicKey(testBatchAddressTree), // v2 address queue is part of the tree account. - cpiContext: PublicKey.default, - treeType: TreeType.AddressV2, - nextTreeInfo: null, - }, - ].filter(info => - featureFlags.isV2() ? true : info.treeType === TreeType.StateV1, - ); -}; - export const getDefaultAddressSpace = () => { return getBatchAddressTreeInfo(); }; @@ -299,7 +226,7 @@ export const nullifierQueue2Pubkey = 'nfq2hgS7NYemXsFaFUCe3EMXSDSfnZnAe27jC6aPP1X'; export const cpiContext2Pubkey = 'cpi2cdhkH5roePvcudTgUL8ppEBfTay1desGh8G8QxK'; -// V2 testing - State Trees (5 triples) +// V2 export const batchMerkleTree1 = 'bmt1LryLZUMmF7ZtqESaw7wifBXLfXHQYoE4GAmrahU'; export const batchQueue1 = 'oq1na8gojfdUhsfCpyjNt6h4JaDWtHf1yQj4koBWfto'; export const batchCpiContext1 = 'cpi15BoVPKgEPw5o8wc2T816GE7b378nMXnhH3Xbq4y'; @@ -321,14 +248,66 @@ export const batchQueue5 = 'oq5oh5ZR3yGomuQgFduNDzjtGvVWfDRGLuDVjv9a96P'; export const batchCpiContext5 = 'cpi5ZTjdgYpZ1Xr7B1cMLLUE81oTtJbNNAyKary2nV6'; // V2 Address Trees -export const batchAddressTree = 'amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'; // v2 address tree (queue is part of the tree account) -export const testBatchAddressTree = - 'EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK'; // v2 address tree (queue is part of the tree account) +export const batchAddressTree = 'amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'; // Deprecated: Use batchMerkleTree1, batchQueue1, batchCpiContext1 instead export const batchMerkleTree = batchMerkleTree1; export const batchQueue = batchQueue1; +/** + * @internal + * Returns local test tree infos. + * V1: 2 state trees (smt/nfq/cpi pairs) + * V2: 5 batched state trees (bmt/oq/cpi triplets) + 1 address tree (amt2) + */ +export const localTestActiveStateTreeInfos = (): TreeInfo[] => { + // V1 State Trees: [tree, queue, cpi] + const V1_STATE_TREES: [string, string, string][] = [ + [merkletreePubkey, nullifierQueuePubkey, cpiContextPubkey], // smt1, nfq1, cpi1 + [merkleTree2Pubkey, nullifierQueue2Pubkey, cpiContext2Pubkey], // smt2, nfq2, cpi2 + ]; + + // V2 State Trees (batched): [bmt, oq, cpi] triplets + const V2_STATE_TREES: [string, string, string][] = [ + [batchMerkleTree1, batchQueue1, batchCpiContext1], // bmt1, oq1, cpi1 + [batchMerkleTree2, batchQueue2, batchCpiContext2], // bmt2, oq2, cpi2 + [batchMerkleTree3, batchQueue3, batchCpiContext3], // bmt3, oq3, cpi3 + [batchMerkleTree4, batchQueue4, batchCpiContext4], // bmt4, oq4, cpi4 + [batchMerkleTree5, batchQueue5, batchCpiContext5], // bmt5, oq5, cpi5 + ]; + + const V2_ADDRESS_TREE = batchAddressTree; // amt2 + + const v1Trees: TreeInfo[] = V1_STATE_TREES.map(([tree, queue, cpi]) => ({ + tree: new PublicKey(tree), + queue: new PublicKey(queue), + cpiContext: new PublicKey(cpi), + treeType: TreeType.StateV1, + nextTreeInfo: null, + })); + + const v2Trees: TreeInfo[] = V2_STATE_TREES.map(([tree, queue, cpi]) => ({ + tree: new PublicKey(tree), + queue: new PublicKey(queue), + cpiContext: new PublicKey(cpi), + treeType: TreeType.StateV2, + nextTreeInfo: null, + })); + + const v2AddressTree: TreeInfo = { + tree: new PublicKey(V2_ADDRESS_TREE), + queue: new PublicKey(V2_ADDRESS_TREE), // queue is part of the tree account + cpiContext: PublicKey.default, + treeType: TreeType.AddressV2, + nextTreeInfo: null, + }; + + if (featureFlags.isV2()) { + return [...v1Trees, ...v2Trees, v2AddressTree]; + } + return v1Trees; +}; + export const confirmConfig: ConfirmOptions = { commitment: 'confirmed', preflightCommitment: 'confirmed', diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 0acbb0b1bd..ee25b9d862 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -715,6 +715,7 @@ export class Rpc extends Connection implements CompressionApiInterface { /** * Get a list of all state tree infos. If not already cached, fetches from * the cluster. + * if featureFlags.isV2(), returns v2 trees too. */ async getStateTreeInfos(): Promise { if (isLocalTest(this.rpcEndpoint)) { From 37e25b241d091be347a7af2f2ddb4b6abf6406e4 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 11:58:16 +0100 Subject: [PATCH 07/28] default to v2 for alpha release --- js/stateless.js/src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index ab42e2371c..cd73f8be24 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -27,8 +27,8 @@ export const featureFlags = { ) { return process.env.LIGHT_PROTOCOL_VERSION as VERSION; } - // Default to V1 - return VERSION.V1; + // Default to V2 + return VERSION.V2; })(), isV2: () => featureFlags.version.replace(/['"]/g, '').toUpperCase() === 'V2', From 4adc02738ab8597c0e0e573171f7b55470536279 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 12:03:22 +0100 Subject: [PATCH 08/28] remove broken merge-token-accounts test from CI (has TODO: not required) --- js/compressed-token/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index e50304108c..c1b1016f58 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -127,7 +127,7 @@ "test:e2e:rpc-token-interop": "pnpm test-validator && vitest run tests/e2e/rpc-token-interop.test.ts --reporter=verbose", "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: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 && 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 && vitest run tests/e2e/wrap.test.ts --reporter=verbose", "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", From c1f16540a8a5ff2b33a3c9aa3e8962a2947fe721 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 12:14:54 +0100 Subject: [PATCH 09/28] fix: mergeTokenAccounts - process single batch per tx to avoid proof invalidation The original implementation tried to merge multiple batches in one transaction, but each batch's validity proof was based on state BEFORE any merges. This caused proofs to be invalid after the first batch executed. Fixed by processing only one batch of up to 4 accounts per call. Call repeatedly until 1 account remains if full consolidation is needed. --- js/compressed-token/package.json | 2 +- .../src/actions/merge-token-accounts.ts | 48 +++++++++---------- .../tests/e2e/merge-token-accounts.test.ts | 32 ------------- 3 files changed, 24 insertions(+), 58 deletions(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index c1b1016f58..e50304108c 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -127,7 +127,7 @@ "test:e2e:rpc-token-interop": "pnpm test-validator && vitest run tests/e2e/rpc-token-interop.test.ts --reporter=verbose", "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 && 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: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 && vitest run tests/e2e/wrap.test.ts --reporter=verbose", "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", diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 8ca4844c82..9902927d54 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -15,8 +15,9 @@ import { import { CompressedTokenProgram } from '../program'; /** - * Merge multiple compressed token accounts for a given mint into a single - * account + * Merge multiple compressed token accounts for a given mint into fewer + * accounts. Each call merges up to 4 accounts at a time. Call repeatedly + * until only 1 account remains if full consolidation is needed. * * @param rpc RPC connection to use * @param payer Fee payer @@ -44,33 +45,30 @@ export async function mergeTokenAccounts( ); } - const instructions = [ - ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), - ]; + if (compressedTokenAccounts.items.length === 1) { + throw new Error('Only one token account exists, nothing to merge'); + } - for ( - let i = 0; - i < compressedTokenAccounts.items.slice(0, 8).length; - i += 4 - ) { - const batch = compressedTokenAccounts.items.slice(i, i + 4); + // Take up to 4 accounts to merge in this transaction + const batch = compressedTokenAccounts.items.slice(0, 4); - const proof = await rpc.getValidityProof( - batch.map(account => bn(account.compressedAccount.hash)), - ); + const proof = await rpc.getValidityProof( + batch.map(account => bn(account.compressedAccount.hash)), + ); - const batchInstructions = - await CompressedTokenProgram.mergeTokenAccounts({ - payer: payer.publicKey, - owner: owner.publicKey, - inputCompressedTokenAccounts: batch, - mint, - recentValidityProof: proof.compressedProof, - recentInputStateRootIndices: proof.rootIndices, - }); + const mergeInstructions = await CompressedTokenProgram.mergeTokenAccounts({ + payer: payer.publicKey, + owner: owner.publicKey, + inputCompressedTokenAccounts: batch, + mint, + recentValidityProof: proof.compressedProof, + recentInputStateRootIndices: proof.rootIndices, + }); - instructions.push(...batchInstructions); - } + const instructions = [ + ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), + ...mergeInstructions, + ]; const { blockhash } = await rpc.getLatestBlockhash(); const additionalSigners = dedupeSigner(payer, [owner]); diff --git a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts index a4340489a5..06338f9e40 100644 --- a/js/compressed-token/tests/e2e/merge-token-accounts.test.ts +++ b/js/compressed-token/tests/e2e/merge-token-accounts.test.ts @@ -79,36 +79,4 @@ describe('mergeTokenAccounts', () => { ); expect(totalBalance.toNumber()).to.equal(500); // 5 accounts * 100 tokens each }); - - // TODO: add coverage for this apparent edge case. not required for now though. - it('should handle merging when there is only one account', async () => { - try { - await mergeTokenAccounts(rpc, payer, mint, owner); - console.log('First merge succeeded'); - - const postFirstMergeAccounts = - await rpc.getCompressedTokenAccountsByOwner(owner.publicKey, { - mint, - }); - console.log('Accounts after first merge:', postFirstMergeAccounts); - } catch (error) { - console.error('First merge failed:', error); - throw error; - } - - // Second merge attempt - try { - await mergeTokenAccounts(rpc, payer, mint, owner); - console.log('Second merge succeeded'); - } catch (error) { - console.error('Second merge failed:', error); - } - - const finalAccounts = await rpc.getCompressedTokenAccountsByOwner( - owner.publicKey, - { mint }, - ); - console.log('Final accounts:', finalAccounts); - expect(finalAccounts.items.length).to.equal(1); - }); }); From b5e8583941a8b1d13853902aecaed2931378e053 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 12:35:29 +0100 Subject: [PATCH 10/28] feat: mergeTokenAccounts supports up to 8 accounts for V2 (4 for V1) --- .../src/actions/merge-token-accounts.ts | 14 ++++++++++---- js/compressed-token/src/program.ts | 7 +++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 9902927d54..704232e4ba 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -11,13 +11,18 @@ import { buildAndSignTx, sendAndConfirmTx, bn, + featureFlags, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; +/** Max input accounts per merge: V1 supports 4, V2 supports 8 */ +const getMaxMergeAccounts = () => (featureFlags.isV2() ? 8 : 4); + /** * Merge multiple compressed token accounts for a given mint into fewer - * accounts. Each call merges up to 4 accounts at a time. Call repeatedly - * until only 1 account remains if full consolidation is needed. + * accounts. Each call merges up to 4 accounts (V1) or 8 accounts (V2) at a + * time. Call repeatedly until only 1 account remains if full consolidation + * is needed. * * @param rpc RPC connection to use * @param payer Fee payer @@ -49,8 +54,9 @@ export async function mergeTokenAccounts( throw new Error('Only one token account exists, nothing to merge'); } - // Take up to 4 accounts to merge in this transaction - const batch = compressedTokenAccounts.items.slice(0, 4); + const maxAccounts = getMaxMergeAccounts(); + // Take up to maxAccounts to merge in this transaction + const batch = compressedTokenAccounts.items.slice(0, maxAccounts); const proof = await rpc.getValidityProof( batch.map(account => bn(account.compressedAccount.hash)), diff --git a/js/compressed-token/src/program.ts b/js/compressed-token/src/program.ts index 5e9b7e5be3..c788912f07 100644 --- a/js/compressed-token/src/program.ts +++ b/js/compressed-token/src/program.ts @@ -1463,8 +1463,11 @@ export class CompressedTokenProgram { recentValidityProof, recentInputStateRootIndices, }: MergeTokenAccountsParams): Promise { - if (inputCompressedTokenAccounts.length > 4) { - throw new Error('Cannot merge more than 4 token accounts at once'); + const maxAccounts = featureFlags.isV2() ? 8 : 4; + if (inputCompressedTokenAccounts.length > maxAccounts) { + throw new Error( + `Cannot merge more than ${maxAccounts} token accounts at once`, + ); } checkMint(inputCompressedTokenAccounts, mint); From 75560f1b7df2f0b7b7dd958cc84171279ab5c81d Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 13:29:43 +0100 Subject: [PATCH 11/28] pass allowOwnerOffCurve along call chain --- .../v3/actions/get-or-create-ata-interface.ts | 5 ++++ js/compressed-token/src/v3/ata-utils.ts | 20 +++++++++------- .../src/v3/get-account-interface.ts | 24 ++++++++++++------- 3 files changed, 32 insertions(+), 17 deletions(-) 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 905400638d..68828a33b3 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 @@ -140,6 +140,7 @@ export async function _getOrCreateAtaInterface( commitment, confirmOptions, wrap, + allowOwnerOffCurve, ); } @@ -179,6 +180,7 @@ async function getOrCreateCTokenAta( commitment?: Commitment, confirmOptions?: ConfirmOptions, wrap = false, + allowOwnerOffCurve = false, ): Promise { const ownerPubkey = getOwnerPublicKey(owner); const ownerIsSigner = isSigner(owner); @@ -197,6 +199,7 @@ async function getOrCreateCTokenAta( commitment, CTOKEN_PROGRAM_ID, wrap, + allowOwnerOffCurve, ); // Check if we have a hot account @@ -228,6 +231,7 @@ async function getOrCreateCTokenAta( commitment, CTOKEN_PROGRAM_ID, wrap, + allowOwnerOffCurve, ); hasHotAccount = true; } else { @@ -300,6 +304,7 @@ async function getOrCreateCTokenAta( commitment, CTOKEN_PROGRAM_ID, wrap, + allowOwnerOffCurve, ); } } diff --git a/js/compressed-token/src/v3/ata-utils.ts b/js/compressed-token/src/v3/ata-utils.ts index 482d75f5e2..1ffb3e758a 100644 --- a/js/compressed-token/src/v3/ata-utils.ts +++ b/js/compressed-token/src/v3/ata-utils.ts @@ -34,24 +34,26 @@ export interface AtaValidationResult { * * Pass programId for fast path. * - * @param ata ATA address to check - * @param mint Mint address - * @param owner Owner address - * @param programId Optional: if known, only check this program's ATA - * @returns Result with detected type, or throws on mismatch + * @param ata ATA address to check + * @param mint Mint address + * @param owner Owner address + * @param programId Optional: if known, only check this program's ATA + * @param allowOwnerOffCurve Allow the owner to be off-curve (PDA) + * @returns Result with detected type, or throws on mismatch */ export function checkAtaAddress( ata: PublicKey, mint: PublicKey, owner: PublicKey, programId?: PublicKey, + allowOwnerOffCurve = false, ): AtaValidationResult { // fast path if (programId) { const expected = getAssociatedTokenAddressSync( mint, owner, - false, + allowOwnerOffCurve, programId, getAtaProgramId(programId), ); @@ -76,7 +78,7 @@ export function checkAtaAddress( ctokenExpected = getAssociatedTokenAddressSync( mint, owner, - false, + allowOwnerOffCurve, CTOKEN_PROGRAM_ID, getAtaProgramId(CTOKEN_PROGRAM_ID), ); @@ -92,7 +94,7 @@ export function checkAtaAddress( splExpected = getAssociatedTokenAddressSync( mint, owner, - false, + allowOwnerOffCurve, TOKEN_PROGRAM_ID, getAtaProgramId(TOKEN_PROGRAM_ID), ); @@ -104,7 +106,7 @@ export function checkAtaAddress( t22Expected = getAssociatedTokenAddressSync( mint, owner, - false, + allowOwnerOffCurve, TOKEN_2022_PROGRAM_ID, getAtaProgramId(TOKEN_2022_PROGRAM_ID), ); diff --git a/js/compressed-token/src/v3/get-account-interface.ts b/js/compressed-token/src/v3/get-account-interface.ts index c54c437410..4979c27e2a 100644 --- a/js/compressed-token/src/v3/get-account-interface.ts +++ b/js/compressed-token/src/v3/get-account-interface.ts @@ -220,13 +220,14 @@ export async function getAccountInterface( /** * Retrieve associated token account for a given owner and mint. * - * @param rpc RPC connection - * @param ata Associated token address - * @param owner Owner public key - * @param mint Mint public key - * @param commitment Optional commitment level - * @param programId Optional program ID - * @param wrap Include SPL/T22 balances (default: false) + * @param rpc RPC connection + * @param ata Associated token address + * @param owner Owner public key + * @param mint Mint public key + * @param commitment Optional commitment level + * @param programId Optional program ID + * @param wrap Include SPL/T22 balances (default: false) + * @param allowOwnerOffCurve Allow owner to be off-curve (PDA) * @returns AccountInterface with ATA metadata */ export async function getAtaInterface( @@ -237,11 +238,18 @@ export async function getAtaInterface( commitment?: Commitment, programId?: PublicKey, wrap = false, + allowOwnerOffCurve = false, ): Promise { // Invariant: ata MUST match a valid derivation from mint+owner. // Hot path: if programId provided, only validate against that program. // For wrap=true, additionally require c-token ATA. - const validation = checkAtaAddress(ata, mint, owner, programId); + const validation = checkAtaAddress( + ata, + mint, + owner, + programId, + allowOwnerOffCurve, + ); if (wrap && validation.type !== 'ctoken') { throw new Error( From d8ead5ef90051d3e3d35d5c2fdbafcba69ecc854 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 14:44:10 +0100 Subject: [PATCH 12/28] wip --- js/compressed-token/package.json | 7 ++--- .../src/v3/actions/create-mint-interface.ts | 26 ++++++++++++++----- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index e50304108c..3b351b564b 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -89,10 +89,10 @@ "vitest": "^2.1.1" }, "scripts": { - "test": "vitest run tests/unit && pnpm test:e2e:legacy:all", - "test-ci": "pnpm test:v1 && pnpm test:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:ctoken:all", + "test": "vitest run tests/unit && pnpm test:e2e:all", + "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 pnpm test:e2e:legacy:all", + "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:all", "test:v2:ctoken": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:ctoken:all", "test-all": "vitest run", "test:unit:all": "EXCLUDE_E2E=true vitest run", @@ -138,6 +138,7 @@ "test:e2e:load-ata-spl-t22": "pnpm test-validator && vitest run tests/e2e/load-ata-spl-t22.test.ts --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:ctoken:all": "pnpm test-validator && vitest run tests/e2e/create-compressed-mint.test.ts --bail=1 && vitest run tests/e2e/create-associated-ctoken.test.ts --bail=1 && vitest run tests/e2e/mint-to-ctoken.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 && 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 && 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 && vitest run tests/e2e/create-mint-interface.test.ts --bail=1 && vitest run tests/e2e/create-ata-interface.test.ts --bail=1 && vitest run tests/e2e/get-or-create-ata-interface.test.ts --bail=1 && vitest run tests/e2e/transfer-interface.test.ts --bail=1 && vitest run tests/e2e/unwrap.test.ts --bail=1 && vitest run tests/e2e/decompress2.test.ts --bail=1 && vitest run tests/e2e/payment-flows.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-spl-t22.test.ts --bail=1", + "test:e2e:all": "pnpm test:e2e:legacy:all && pnpm test:e2e:ctoken:all", "pull-idl": "../../scripts/push-compressed-token-idl.sh", "build": "if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V2\" ]; then LIGHT_PROTOCOL_VERSION=V2 pnpm build:bundle; else LIGHT_PROTOCOL_VERSION=V1 pnpm build:bundle; fi", "build:bundle": "rimraf dist && rollup -c", 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 81f7c0a0c2..3d87210068 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -17,13 +17,14 @@ import { getBatchAddressTreeInfo, DerivationMode, CTOKEN_PROGRAM_ID, + getDefaultAddressTreeInfo, } from '@lightprotocol/stateless.js'; import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token'; import { createMintInstruction, TokenMetadataInstructionData, } from '../instructions/create-mint'; -import { findMintAddress } from '../derivation'; +import { deriveCMintAddress, findMintAddress } from '../derivation'; import { createMint } from '../../actions/create-mint'; export { TokenMetadataInstructionData }; @@ -81,6 +82,14 @@ export async function createMintInterface( 'mintAuthority must be a Signer for compressed token mints', ); } + if ( + addressTreeInfo && + !addressTreeInfo.tree.equals(getDefaultAddressTreeInfo().tree) + ) { + throw new Error( + `addressTreeInfo ${addressTreeInfo?.tree.toString()} must be the default address tree info: ${getDefaultAddressTreeInfo().tree.toString()}`, + ); + } const resolvedFreezeAuthority = freezeAuthority && 'secretKey' in freezeAuthority @@ -92,15 +101,20 @@ export async function createMintInterface( outputStateTreeInfo ?? selectStateTreeInfo(await rpc.getStateTreeInfos()); + const compressedMintAddress = deriveCMintAddress( + keypair.publicKey, + addressTreeInfo, + ); + const validityProof = await rpc.getValidityProofV2( [], [ { - address: findMintAddress(keypair.publicKey)[0].toBytes(), + address: Uint8Array.from(compressedMintAddress), treeInfo: addressTreeInfo, }, ], - DerivationMode.compressible, + DerivationMode.standard, ); const ix = createMintInstruction( @@ -123,10 +137,8 @@ export async function createMintInterface( blockhash, additionalSigners, ); - const txId = await sendAndConfirmTx(rpc, tx, { - ...confirmOptions, - skipPreflight: true, - }); + const txId = await sendAndConfirmTx(rpc, tx); + console.log('txId', txId); const mint = findMintAddress(keypair.publicKey); return { mint: mint[0], transactionSignature: txId }; From d3395ae4de93e6af4efd14f6bcd35f0a47171efd Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 16:20:20 +0100 Subject: [PATCH 13/28] createLoadAtaInstructions shouldnt err on accountnotfound --- .../src/v3/actions/load-ata.ts | 43 +++++++++++-------- .../src/v3/get-account-interface.ts | 5 +-- .../tests/e2e/get-account-interface.test.ts | 6 +-- .../tests/e2e/load-ata-standard.test.ts | 3 +- .../tests/e2e/load-ata-unified.test.ts | 3 +- .../tests/e2e/transfer-interface.test.ts | 4 +- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/js/compressed-token/src/v3/actions/load-ata.ts b/js/compressed-token/src/v3/actions/load-ata.ts index d6a9f7868a..77338d2c26 100644 --- a/js/compressed-token/src/v3/actions/load-ata.ts +++ b/js/compressed-token/src/v3/actions/load-ata.ts @@ -18,6 +18,7 @@ import { TOKEN_2022_PROGRAM_ID, getAssociatedTokenAddressSync, createAssociatedTokenAccountIdempotentInstruction, + TokenAccountNotFoundError, } from '@solana/spl-token'; import { AccountInterface, @@ -77,23 +78,31 @@ export async function createLoadAtaInstructions( // Validation happens inside getAtaInterface via checkAtaAddress helper: // - Always validates ata matches mint+owner derivation // - For wrap=true, additionally requires c-token ATA - const ataInterface = await _getAtaInterface( - rpc, - ata, - owner, - mint, - undefined, - undefined, - wrap, - ); - return createLoadAtaInstructionsFromInterface( - rpc, - payer, - ataInterface, - options, - wrap, - ata, - ); + try { + const ataInterface = await _getAtaInterface( + rpc, + ata, + owner, + mint, + undefined, + undefined, + wrap, + ); + return createLoadAtaInstructionsFromInterface( + rpc, + payer, + ataInterface, + options, + wrap, + ata, + ); + } catch (error) { + // If account doesn't exist, there's nothing to load + if (error instanceof TokenAccountNotFoundError) { + return []; + } + throw error; + } } // Re-export AtaType for backwards compatibility diff --git a/js/compressed-token/src/v3/get-account-interface.ts b/js/compressed-token/src/v3/get-account-interface.ts index 4979c27e2a..a2f7ddf757 100644 --- a/js/compressed-token/src/v3/get-account-interface.ts +++ b/js/compressed-token/src/v3/get-account-interface.ts @@ -582,10 +582,7 @@ async function getUnifiedAccountInterface( // account not found if (sources.length === 0) { - const triedPrograms = wrap - ? 'TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, and CTOKEN_PROGRAM_ID (both onchain and compressed)' - : 'CTOKEN_PROGRAM_ID (both onchain and compressed)'; - throw new Error(`Token account not found. Tried ${triedPrograms}.`); + throw new TokenAccountNotFoundError(); } // priority order: c-token hot > c-token cold > SPL/T22 diff --git a/js/compressed-token/tests/e2e/get-account-interface.test.ts b/js/compressed-token/tests/e2e/get-account-interface.test.ts index 5700919869..e7e97876bb 100644 --- a/js/compressed-token/tests/e2e/get-account-interface.test.ts +++ b/js/compressed-token/tests/e2e/get-account-interface.test.ts @@ -421,7 +421,7 @@ describe('get-account-interface', () => { // getAccountInterface auto-detect cannot find minted-compressed tokens await expect( getAccountInterface(rpc, ctokenAta, 'confirmed'), - ).rejects.toThrow(/Token account not found/); + ).rejects.toThrow(TokenAccountNotFoundError); // Use getAtaInterface for minted-compressed tokens const result = await getAtaInterface( @@ -464,7 +464,7 @@ describe('get-account-interface', () => { await expect( getAccountInterface(rpc, nonExistentAta, 'confirmed'), - ).rejects.toThrow(/Token account not found/); + ).rejects.toThrow(TokenAccountNotFoundError); }); }); }); @@ -774,7 +774,7 @@ describe('get-account-interface', () => { owner.publicKey, ctokenMint, ), - ).rejects.toThrow(/Token account not found/); + ).rejects.toThrow(TokenAccountNotFoundError); }); }); diff --git a/js/compressed-token/tests/e2e/load-ata-standard.test.ts b/js/compressed-token/tests/e2e/load-ata-standard.test.ts index 0f8791cc24..42414a8c2b 100644 --- a/js/compressed-token/tests/e2e/load-ata-standard.test.ts +++ b/js/compressed-token/tests/e2e/load-ata-standard.test.ts @@ -22,6 +22,7 @@ import { TOKEN_PROGRAM_ID, createAssociatedTokenAccount, getAccount, + TokenAccountNotFoundError, } from '@solana/spl-token'; import { createMint, mintTo, decompress } from '../../src/actions'; import { @@ -301,7 +302,7 @@ describe('loadAta - Standard Path (wrap=false)', () => { mint, payer.publicKey, ), - ).rejects.toThrow('Token account not found'); + ).rejects.toThrow(TokenAccountNotFoundError); }); it('should return empty when hot exists but no cold', async () => { diff --git a/js/compressed-token/tests/e2e/load-ata-unified.test.ts b/js/compressed-token/tests/e2e/load-ata-unified.test.ts index 1cb71d3fd4..0b22c71cd0 100644 --- a/js/compressed-token/tests/e2e/load-ata-unified.test.ts +++ b/js/compressed-token/tests/e2e/load-ata-unified.test.ts @@ -22,6 +22,7 @@ import { createAssociatedTokenAccount, getOrCreateAssociatedTokenAccount, getAccount, + TokenAccountNotFoundError, } from '@solana/spl-token'; import { createMint, mintTo, decompress } from '../../src/actions'; import { @@ -361,7 +362,7 @@ describe('loadAta - Unified Path (wrap=true)', () => { owner as unknown as Signer, mint, ), - ).rejects.toThrow('Token account not found'); + ).rejects.toThrow(TokenAccountNotFoundError); }); it('should return null when only hot balance exists', async () => { diff --git a/js/compressed-token/tests/e2e/transfer-interface.test.ts b/js/compressed-token/tests/e2e/transfer-interface.test.ts index 90e03a5266..1d24104541 100644 --- a/js/compressed-token/tests/e2e/transfer-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-interface.test.ts @@ -162,7 +162,7 @@ describe('transfer-interface', () => { const ixs = await createLoadAtaInstructions( rpc, ata, - payer.publicKey, + owner.publicKey, mint, ); @@ -201,7 +201,7 @@ describe('transfer-interface', () => { const ixs = await createLoadAtaInstructions( rpc, ata, - payer.publicKey, + owner.publicKey, mint, ); From e9cedfa13e2f9c0ccbfcb5c5643ee9faa714f97a Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 18:32:32 +0100 Subject: [PATCH 14/28] add rpc readiness check --- cli/src/utils/initTestEnv.ts | 2 ++ cli/src/utils/process.ts | 70 ++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/cli/src/utils/initTestEnv.ts b/cli/src/utils/initTestEnv.ts index d48bb415d7..6438af9f41 100644 --- a/cli/src/utils/initTestEnv.ts +++ b/cli/src/utils/initTestEnv.ts @@ -11,6 +11,7 @@ import { import path from "path"; import { downloadBinIfNotExists } from "../psp-utils"; import { + confirmRpcReadiness, confirmServerStability, executeCommand, killProcess, @@ -172,6 +173,7 @@ export async function initTestEnv({ }); await waitForServers([{ port: rpcPort, path: "/health" }]); await confirmServerStability(`http://127.0.0.1:${rpcPort}/health`); + await confirmRpcReadiness(`http://127.0.0.1:${rpcPort}`); if (indexer) { const config = getConfig(); diff --git a/cli/src/utils/process.ts b/cli/src/utils/process.ts index ffdc4003e5..e3d1a241f5 100644 --- a/cli/src/utils/process.ts +++ b/cli/src/utils/process.ts @@ -285,3 +285,73 @@ export async function confirmServerStability( throw error; } } + +/** + * Confirms that the Solana RPC is fully ready to process requests. + * This goes beyond HTTP availability and verifies the RPC can handle actual Solana requests. + * + * @param rpcUrl - The RPC endpoint URL + * @param maxAttempts - Maximum number of attempts (default: 30) + * @param delayMs - Delay between attempts in milliseconds (default: 500ms) + * @throws Error if RPC doesn't become ready within maxAttempts + */ +export async function confirmRpcReadiness( + rpcUrl: string, + maxAttempts: number = 30, + delayMs: number = 500, +) { + let lastError: Error | unknown; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const response = await axios.post( + rpcUrl, + { + jsonrpc: "2.0", + id: 1, + method: "getHealth", + params: [], + }, + { + headers: { "Content-Type": "application/json" }, + timeout: 3000, + }, + ); + + if (response.data?.result === "ok") { + console.log( + `RPC is ready after ${attempt} attempt${attempt > 1 ? "s" : ""}.`, + ); + return; + } + + // Response received but not "ok" + lastError = new Error( + `RPC returned unexpected result: ${JSON.stringify(response.data)}`, + ); + } catch (error) { + lastError = error; + + // Log connection errors only on later attempts to reduce noise + if (attempt > 5 && attempt % 5 === 0) { + const errorMsg = error instanceof Error ? error.message : String(error); + console.log( + `RPC not ready yet (attempt ${attempt}/${maxAttempts}): ${errorMsg}`, + ); + } + } + + // Don't sleep after the last attempt + if (attempt < maxAttempts) { + await new Promise((resolve) => setTimeout(resolve, delayMs)); + } + } + + // If we get here, all attempts failed + const errorMsg = + lastError instanceof Error ? lastError.message : String(lastError); + const totalTime = Math.round((maxAttempts * delayMs) / 1000); + throw new Error( + `RPC failed to become ready after ${maxAttempts} attempts (~${totalTime}s). Last error: ${errorMsg}`, + ); +} From 78756bd2868e9d902ffdb00e7140620b1f6b552e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 21:02:35 +0100 Subject: [PATCH 15/28] fix ixdata and default to right token program on getOrCreateAtaInterface --- .../v3/actions/get-or-create-ata-interface.ts | 2 +- .../src/v3/actions/transfer-interface.ts | 1 - .../src/v3/instructions/transfer-interface.ts | 25 +++++-------------- .../tests/e2e/transfer-interface.test.ts | 25 +++---------------- 4 files changed, 11 insertions(+), 42 deletions(-) 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 68828a33b3..efcba8c649 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 @@ -67,7 +67,7 @@ export async function getOrCreateAtaInterface( allowOwnerOffCurve = false, commitment?: Commitment, confirmOptions?: ConfirmOptions, - programId = TOKEN_PROGRAM_ID, + programId = CTOKEN_PROGRAM_ID, associatedTokenProgramId = getAtaProgramId(programId), ): Promise { return _getOrCreateAtaInterface( diff --git a/js/compressed-token/src/v3/actions/transfer-interface.ts b/js/compressed-token/src/v3/actions/transfer-interface.ts index 4675f584ca..a742fc1afe 100644 --- a/js/compressed-token/src/v3/actions/transfer-interface.ts +++ b/js/compressed-token/src/v3/actions/transfer-interface.ts @@ -327,7 +327,6 @@ export async function transferInterface( destination, owner.publicKey, amountBigInt, - payer.publicKey, ), ); diff --git a/js/compressed-token/src/v3/instructions/transfer-interface.ts b/js/compressed-token/src/v3/instructions/transfer-interface.ts index 71aa2c57e1..8cefbae9ac 100644 --- a/js/compressed-token/src/v3/instructions/transfer-interface.ts +++ b/js/compressed-token/src/v3/instructions/transfer-interface.ts @@ -16,9 +16,8 @@ const CTOKEN_TRANSFER_DISCRIMINATOR = 3; * * @param source Source c-token account * @param destination Destination c-token account - * @param owner Owner of the source account (signer) + * @param owner Owner of the source account (signer, also pays for compressible extension top-ups) * @param amount Amount to transfer - * @param payer Payer for compressible extension top-up (optional) * @returns Transaction instruction for c-token transfer */ export function createCTokenTransferInstruction( @@ -26,29 +25,20 @@ export function createCTokenTransferInstruction( destination: PublicKey, owner: PublicKey, amount: number | bigint, - payer?: PublicKey, ): TransactionInstruction { - // Instruction data format (from CTOKEN_TRANSFER.md): + // Instruction data format: // byte 0: discriminator (3) - // byte 1: padding (0) - // bytes 2-9: amount (u64 LE) - SPL TokenInstruction::Transfer format - const data = Buffer.alloc(10); + // bytes 1-8: amount (u64 LE) + const data = Buffer.alloc(9); data.writeUInt8(CTOKEN_TRANSFER_DISCRIMINATOR, 0); - data.writeUInt8(0, 1); // padding - data.writeBigUInt64LE(BigInt(amount), 2); + data.writeBigUInt64LE(BigInt(amount), 1); const keys = [ { pubkey: source, isSigner: false, isWritable: true }, { pubkey: destination, isSigner: false, isWritable: true }, - { pubkey: owner, isSigner: true, isWritable: false }, + { pubkey: owner, isSigner: true, isWritable: true }, // owner is also payer for top-ups ]; - // Add payer as 4th account if provided and different from owner - // (for compressible extension top-up) - if (payer && !payer.equals(owner)) { - keys.push({ pubkey: payer, isSigner: true, isWritable: true }); - } - return new TransactionInstruction({ programId: CTOKEN_PROGRAM_ID, keys, @@ -64,7 +54,6 @@ export function createCTokenTransferInstruction( * @param destination Destination token account * @param owner Owner of the source account (signer) * @param amount Amount to transfer - * @param payer Payer for compressible top-up (optional) * @returns instruction for c-token transfer */ export function createTransferInterfaceInstruction( @@ -74,7 +63,6 @@ export function createTransferInterfaceInstruction( amount: number | bigint, multiSigners: (Signer | PublicKey)[] = [], programId: PublicKey = CTOKEN_PROGRAM_ID, - payer?: PublicKey, ): TransactionInstruction { if (programId.equals(CTOKEN_PROGRAM_ID)) { if (multiSigners.length > 0) { @@ -87,7 +75,6 @@ export function createTransferInterfaceInstruction( destination, owner, amount, - payer, ); } diff --git a/js/compressed-token/tests/e2e/transfer-interface.test.ts b/js/compressed-token/tests/e2e/transfer-interface.test.ts index 1d24104541..c50edfa5e0 100644 --- a/js/compressed-token/tests/e2e/transfer-interface.test.ts +++ b/js/compressed-token/tests/e2e/transfer-interface.test.ts @@ -84,11 +84,10 @@ describe('transfer-interface', () => { expect(ix.keys[2].pubkey.equals(owner)).toBe(true); }); - it('should add payer as 4th account when different from owner', () => { + it('should have owner as writable (pays for top-ups)', () => { const source = Keypair.generate().publicKey; const destination = Keypair.generate().publicKey; const owner = Keypair.generate().publicKey; - const payerPk = Keypair.generate().publicKey; const amount = BigInt(1000); const ix = createCTokenTransferInstruction( @@ -96,28 +95,12 @@ describe('transfer-interface', () => { destination, owner, amount, - payerPk, - ); - - expect(ix.keys.length).toBe(4); - expect(ix.keys[3].pubkey.equals(payerPk)).toBe(true); - }); - - it('should not add payer when same as owner', () => { - const source = Keypair.generate().publicKey; - const destination = Keypair.generate().publicKey; - const owner = Keypair.generate().publicKey; - const amount = BigInt(1000); - - const ix = createCTokenTransferInstruction( - source, - destination, - owner, - amount, - owner, // payer same as owner ); expect(ix.keys.length).toBe(3); + expect(ix.keys[2].pubkey.equals(owner)).toBe(true); + expect(ix.keys[2].isSigner).toBe(true); + expect(ix.keys[2].isWritable).toBe(true); // owner pays for top-ups }); }); From 625b9cd4cb3d538f217cdf12ad8fbedf865cee33 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 22:24:59 +0100 Subject: [PATCH 16/28] wip --- .../e2e/get-or-create-ata-interface.test.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts index 93c7c218f4..02f97327c3 100644 --- a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts +++ b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts @@ -806,31 +806,29 @@ describe('getOrCreateAtaInterface', () => { }); }); - describe('default programId (TOKEN_PROGRAM_ID)', () => { - let splMint: PublicKey; + describe('default programId (CTOKEN_PROGRAM_ID)', () => { + let ctokenMint: PublicKey; beforeAll(async () => { const mintAuthority = Keypair.generate(); - splMint = await createMint( + const result = await createMintInterface( rpc, payer, mintAuthority.publicKey, null, 9, - undefined, - undefined, - TOKEN_PROGRAM_ID, ); + ctokenMint = result.mint; }); - it('should default to TOKEN_PROGRAM_ID when programId not specified', async () => { + it('should default to CTOKEN_PROGRAM_ID when programId not specified', async () => { const owner = Keypair.generate(); const expectedAddress = getAssociatedTokenAddressSync( - splMint, + ctokenMint, owner.publicKey, false, - TOKEN_PROGRAM_ID, + CTOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID, ); @@ -838,7 +836,7 @@ describe('getOrCreateAtaInterface', () => { const account = await getOrCreateAtaInterface( rpc, payer, - splMint, + ctokenMint, owner.publicKey, ); @@ -846,9 +844,9 @@ describe('getOrCreateAtaInterface', () => { expectedAddress.toBase58(), ); - // Verify it's owned by TOKEN_PROGRAM_ID + // Verify it's owned by CTOKEN_PROGRAM_ID const info = await rpc.getAccountInfo(expectedAddress); - expect(info?.owner.toBase58()).toBe(TOKEN_PROGRAM_ID.toBase58()); + expect(info?.owner.toBase58()).toBe(CTOKEN_PROGRAM_ID.toBase58()); }); }); From 161ba7a699377dcc4ab29c645000e2b4b3d950ce Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 22:31:30 +0100 Subject: [PATCH 17/28] fix getOrCreateAtaInterface --- .../tests/e2e/get-or-create-ata-interface.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts index 02f97327c3..57be60ed29 100644 --- a/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts +++ b/js/compressed-token/tests/e2e/get-or-create-ata-interface.test.ts @@ -814,7 +814,7 @@ describe('getOrCreateAtaInterface', () => { const result = await createMintInterface( rpc, payer, - mintAuthority.publicKey, + mintAuthority, null, 9, ); @@ -829,7 +829,7 @@ describe('getOrCreateAtaInterface', () => { owner.publicKey, false, CTOKEN_PROGRAM_ID, - ASSOCIATED_TOKEN_PROGRAM_ID, + CTOKEN_PROGRAM_ID, // c-token uses CTOKEN_PROGRAM_ID as ATA program ); // Call without specifying programId From 9160e9d09594f3011dd91eaffa87329eec32cf45 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Fri, 19 Dec 2025 23:21:02 +0100 Subject: [PATCH 18/28] static 200k cu meter --- js/compressed-token/src/v3/actions/transfer-interface.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/js/compressed-token/src/v3/actions/transfer-interface.ts b/js/compressed-token/src/v3/actions/transfer-interface.ts index a742fc1afe..e16f5fb24d 100644 --- a/js/compressed-token/src/v3/actions/transfer-interface.ts +++ b/js/compressed-token/src/v3/actions/transfer-interface.ts @@ -68,7 +68,9 @@ function calculateComputeUnits( // SPL/T22 wrap operations cu += splWrapCount * 5_000; - return cu; + // TODO: dynamic + // return cu; + return 200_000; } /** From d0feeb0e2c5630e6e5e4efd51d68cba30e7c1956 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 20 Dec 2025 10:49:59 +0100 Subject: [PATCH 19/28] scope v1 correctly --- js/compressed-token/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 3b351b564b..69d70d3d94 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -89,7 +89,7 @@ "vitest": "^2.1.1" }, "scripts": { - "test": "vitest run tests/unit && pnpm test:e2e:all", + "test": "vitest run tests/unit && if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V1\" ]; then pnpm test:e2e:legacy:all; else pnpm test:e2e: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 pnpm test:e2e:all", From d6811e4d49e8ee71f4d30c9ea9e48cfed20e21ef Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 20 Dec 2025 13:17:01 +0100 Subject: [PATCH 20/28] test --- .github/workflows/js-v2.yml | 6 +++--- js/compressed-token/package.json | 4 ++-- .../test-rpc/get-parsed-events.ts | 20 +++++++++++++++---- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/.github/workflows/js-v2.yml b/.github/workflows/js-v2.yml index 01db3b449e..c3f71f74dd 100644 --- a/.github/workflows/js-v2.yml +++ b/.github/workflows/js-v2.yml @@ -79,12 +79,12 @@ jobs: done echo "Tests passed on attempt $attempt" - - name: Run compressed-token legacy tests with V2 + - name: Run compressed-token unit tests with V2 run: | - echo "Running compressed-token legacy tests with retry logic (max 2 attempts)..." + echo "Running compressed-token unit tests with retry logic (max 2 attempts)..." attempt=1 max_attempts=2 - until npx nx test @lightprotocol/compressed-token; do + until npx nx run @lightprotocol/compressed-token:test:unit:all:v2; do attempt=$((attempt + 1)) if [ $attempt -gt $max_attempts ]; then echo "Tests failed after $max_attempts attempts" diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 69d70d3d94..bd6aa45e8f 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -89,10 +89,10 @@ "vitest": "^2.1.1" }, "scripts": { - "test": "vitest run tests/unit && if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V1\" ]; then pnpm test:e2e:legacy:all; else pnpm test:e2e:all; fi", + "test": "vitest run tests/unit && if [ \"$LIGHT_PROTOCOL_VERSION\" = \"V1\" ]; then pnpm test:e2e:legacy:all; else pnpm test:e2e:ctoken: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 pnpm test:e2e:all", + "test:v2": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:ctoken:all", "test:v2:ctoken": "pnpm build:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm test:e2e:ctoken:all", "test-all": "vitest run", "test:unit:all": "EXCLUDE_E2E=true vitest run", diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts index 34b19af7da..4bdf35d25f 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-parsed-events.ts @@ -49,10 +49,22 @@ export async function getParsedEvents( 'confirmed', ) ).map(s => s.signature); - const txs = await rpc.getParsedTransactions(signatures, { - maxSupportedTransactionVersion: 0, - commitment: 'confirmed', - }); + const txs: (ParsedTransactionWithMeta | null)[] = []; + + // `getParsedTransactions` uses a JSON-RPC batch request under the hood. + // On some RPC servers (including local validators with strict limits), + // batching too many signatures can exceed the max request body size (413). + const maxSupportedTransactionVersion = 0; + const commitment = 'confirmed' as const; + const chunkSize = 100; + for (let i = 0; i < signatures.length; i += chunkSize) { + const chunk = signatures.slice(i, i + chunkSize); + const chunkTxs = await rpc.getParsedTransactions(chunk, { + maxSupportedTransactionVersion, + commitment, + }); + txs.push(...chunkTxs); + } for (const txParsed of txs) { if (!txParsed || !txParsed.transaction || !txParsed.meta) continue; From 96e9d3fe7f34658ca882e0e78e898dd96649752e Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 20 Dec 2025 15:12:04 +0100 Subject: [PATCH 21/28] fixes --- .../src/actions/merge-token-accounts.ts | 15 +-- js/compressed-token/src/actions/mint-to.ts | 95 +++++++++++++++---- .../src/v3/actions/load-ata.ts | 77 +++++++++++++-- js/compressed-token/src/v3/unified/index.ts | 14 ++- .../tests/e2e/load-ata-standard.test.ts | 19 ++-- .../src/test-helpers/test-rpc/test-rpc.ts | 61 +++++++++++- 6 files changed, 239 insertions(+), 42 deletions(-) diff --git a/js/compressed-token/src/actions/merge-token-accounts.ts b/js/compressed-token/src/actions/merge-token-accounts.ts index 704232e4ba..a38766a626 100644 --- a/js/compressed-token/src/actions/merge-token-accounts.ts +++ b/js/compressed-token/src/actions/merge-token-accounts.ts @@ -11,12 +11,16 @@ import { buildAndSignTx, sendAndConfirmTx, bn, - featureFlags, } from '@lightprotocol/stateless.js'; import { CompressedTokenProgram } from '../program'; -/** Max input accounts per merge: V1 supports 4, V2 supports 8 */ -const getMaxMergeAccounts = () => (featureFlags.isV2() ? 8 : 4); +/** + * Max input accounts per merge. + * + * Even though V2 supports larger merges, we keep this at 4 to avoid oversized + * transactions / RPC payload limits under heavy test load. + */ +const MAX_MERGE_ACCOUNTS = 4; /** * Merge multiple compressed token accounts for a given mint into fewer @@ -54,9 +58,8 @@ export async function mergeTokenAccounts( throw new Error('Only one token account exists, nothing to merge'); } - const maxAccounts = getMaxMergeAccounts(); - // Take up to maxAccounts to merge in this transaction - const batch = compressedTokenAccounts.items.slice(0, maxAccounts); + // Take up to MAX_MERGE_ACCOUNTS to merge in this transaction + const batch = compressedTokenAccounts.items.slice(0, MAX_MERGE_ACCOUNTS); const proof = await rpc.getValidityProof( batch.map(account => bn(account.compressedAccount.hash)), diff --git a/js/compressed-token/src/actions/mint-to.ts b/js/compressed-token/src/actions/mint-to.ts index bf6100950d..d491e78a92 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -21,6 +21,39 @@ import { SplInterfaceInfo, } from '../utils/get-token-pool-infos'; +function isBatchNotReadyError(error: unknown): boolean { + const message = error instanceof Error ? error.message : String(error); + // BatchedMerkleTreeError::BatchNotReady (14301) => custom program error 0x37dd + return ( + message.includes('0x37dd') || + message.includes('14301') || + message.includes('BatchNotReady') + ); +} + +async function selectAlternativeStateTreeInfo( + rpc: Rpc, + current: TreeInfo, +): Promise { + const infos = await rpc.getStateTreeInfos(); + + // Prefer a different active tree of the same type. + const candidates = infos.filter( + t => + t.treeType === current.treeType && + !t.nextTreeInfo && + !t.queue.equals(current.queue), + ); + + if (candidates.length > 0) { + const length = Math.min(5, candidates.length); + return candidates[Math.floor(Math.random() * length)]; + } + + // Fall back to normal selection (may return the same tree). + return selectStateTreeInfo(infos, current.treeType, true); +} + /** * Mint compressed tokens to a solana address * @@ -59,25 +92,51 @@ export async function mintTo( splInterfaceInfo ?? selectSplInterfaceInfo(await getSplInterfaceInfos(rpc, mint)); - const ix = await CompressedTokenProgram.mintTo({ - feePayer: payer.publicKey, - mint, - authority: authority.publicKey, - amount, - toPubkey, - outputStateTreeInfo, - tokenPoolInfo: splInterfaceInfo, - }); + // Retry on BatchNotReady (full output queue batch) by selecting a different + // active state tree. This can happen under heavy test load when one of the + // V2 output queues becomes blocked. + let selectedTree = outputStateTreeInfo; + let lastError: unknown; + for (let attempt = 0; attempt < 3; attempt++) { + try { + const ix = await CompressedTokenProgram.mintTo({ + feePayer: payer.publicKey, + mint, + authority: authority.publicKey, + amount, + toPubkey, + outputStateTreeInfo: selectedTree, + tokenPoolInfo: splInterfaceInfo, + }); - const { blockhash } = await rpc.getLatestBlockhash(); - const additionalSigners = dedupeSigner(payer, [authority]); + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); - const tx = buildAndSignTx( - [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], - payer, - blockhash, - additionalSigners, - ); + const tx = buildAndSignTx( + [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: 1_000_000, + }), + ix, + ], + payer, + blockhash, + additionalSigners, + ); + + return sendAndConfirmTx(rpc, tx, confirmOptions); + } catch (error) { + lastError = error; + if (!isBatchNotReadyError(error) || attempt === 2) { + throw error; + } + selectedTree = await selectAlternativeStateTreeInfo( + rpc, + selectedTree, + ); + } + } - return sendAndConfirmTx(rpc, tx, confirmOptions); + // Unreachable, but keeps TS happy. + throw lastError; } diff --git a/js/compressed-token/src/v3/actions/load-ata.ts b/js/compressed-token/src/v3/actions/load-ata.ts index 77338d2c26..4496732c9e 100644 --- a/js/compressed-token/src/v3/actions/load-ata.ts +++ b/js/compressed-token/src/v3/actions/load-ata.ts @@ -4,6 +4,8 @@ import { buildAndSignTx, sendAndConfirmTx, dedupeSigner, + bn, + ParsedTokenAccount, } from '@lightprotocol/stateless.js'; import { PublicKey, @@ -23,6 +25,8 @@ import { import { AccountInterface, getAtaInterface as _getAtaInterface, + TokenAccountSource, + TokenAccountSourceType, } from '../get-account-interface'; import { getAssociatedTokenAddressInterface } from '../get-associated-token-address-interface'; import { createAssociatedTokenAccountInterfaceIdempotentInstruction } from '../instructions/create-ata-interface'; @@ -35,6 +39,69 @@ import { import { getAtaProgramId, checkAtaAddress, AtaType } from '../ata-utils'; import { InterfaceOptions } from './transfer-interface'; +function getCompressedTokenAccountsFromAtaSources( + sources: TokenAccountSource[], +): ParsedTokenAccount[] { + const coldTypes = new Set([ + TokenAccountSourceType.CTokenCold, + TokenAccountSourceType.SplCold, + TokenAccountSourceType.Token2022Cold, + ]); + + return sources + .filter(source => source.loadContext !== undefined) + .filter(source => coldTypes.has(source.type)) + .map(source => { + const fullData = source.accountInfo.data; + const discriminatorBytes = fullData.subarray( + 0, + Math.min(8, fullData.length), + ); + const accountDataBytes = + fullData.length > 8 ? fullData.subarray(8) : Buffer.alloc(0); + + const compressedAccount = { + treeInfo: source.loadContext!.treeInfo, + hash: source.loadContext!.hash, + leafIndex: source.loadContext!.leafIndex, + proveByIndex: source.loadContext!.proveByIndex, + owner: source.accountInfo.owner, + lamports: bn(source.accountInfo.lamports), + address: null, + data: + fullData.length === 0 + ? null + : { + discriminator: Array.from(discriminatorBytes), + data: Buffer.from(accountDataBytes), + dataHash: new Array(32).fill(0), + }, + readOnly: false, + }; + + const state = !source.parsed.isInitialized + ? 0 + : source.parsed.isFrozen + ? 2 + : 1; + + return { + compressedAccount: compressedAccount as any, + parsed: { + mint: source.parsed.mint, + owner: source.parsed.owner, + amount: bn(source.parsed.amount.toString()), + delegate: source.parsed.delegate, + state, + tlv: + source.parsed.tlvData.length > 0 + ? source.parsed.tlvData + : null, + }, + } satisfies ParsedTokenAccount; + }); +} + // Re-export types moved to instructions export { ParsedAccountInfoInterface, @@ -265,9 +332,8 @@ export async function createLoadAtaInstructionsFromInterface( // 4. Decompress compressed tokens to c-token ATA if (coldBalance > BigInt(0) && ctokenColdSource) { - const compressedResult = - await rpc.getCompressedTokenAccountsByOwner(owner, { mint }); - const compressedAccounts = compressedResult.items; + const compressedAccounts = + getCompressedTokenAccountsFromAtaSources(sources); if (compressedAccounts.length > 0) { const proof = await rpc.getValidityProofV0( @@ -293,9 +359,8 @@ export async function createLoadAtaInstructionsFromInterface( // STANDARD MODE: Decompress to target ATA type if (coldBalance > BigInt(0) && ctokenColdSource) { - const compressedResult = - await rpc.getCompressedTokenAccountsByOwner(owner, { mint }); - const compressedAccounts = compressedResult.items; + const compressedAccounts = + getCompressedTokenAccountsFromAtaSources(sources); if (compressedAccounts.length > 0) { const proof = await rpc.getValidityProofV0( diff --git a/js/compressed-token/src/v3/unified/index.ts b/js/compressed-token/src/v3/unified/index.ts index 5571c1f918..d83d7dfa79 100644 --- a/js/compressed-token/src/v3/unified/index.ts +++ b/js/compressed-token/src/v3/unified/index.ts @@ -7,6 +7,7 @@ import { PublicKey, Signer, ConfirmOptions, Commitment } from '@solana/web3.js'; import { Rpc, CTOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; import BN from 'bn.js'; +import { TokenAccountNotFoundError } from '@solana/spl-token'; import { getAtaInterface as _getAtaInterface, @@ -133,7 +134,7 @@ export async function loadAta( confirmOptions?: ConfirmOptions, interfaceOptions?: InterfaceOptions, ) { - return _loadAta( + const signature = await _loadAta( rpc, ata, owner, @@ -143,6 +144,17 @@ export async function loadAta( interfaceOptions, true, ); + + // Unified semantics: if the canonical c-token ATA does not exist at all, + // treat this as an error (caller has no balances to load and no ATA). + if (signature === null) { + const accountInfo = await rpc.getAccountInfo(ata); + if (!accountInfo) { + throw new TokenAccountNotFoundError(); + } + } + + return signature; } /** diff --git a/js/compressed-token/tests/e2e/load-ata-standard.test.ts b/js/compressed-token/tests/e2e/load-ata-standard.test.ts index 42414a8c2b..87fe645aa0 100644 --- a/js/compressed-token/tests/e2e/load-ata-standard.test.ts +++ b/js/compressed-token/tests/e2e/load-ata-standard.test.ts @@ -287,22 +287,21 @@ describe('loadAta - Standard Path (wrap=false)', () => { }); describe('createLoadAtaInstructions', () => { - it('should throw when no accounts exist', async () => { + it('should return empty when no accounts exist', async () => { const owner = Keypair.generate(); const ata = getAssociatedTokenAddressInterface( mint, owner.publicKey, ); - await expect( - createLoadAtaInstructions( - rpc, - ata, - owner.publicKey, - mint, - payer.publicKey, - ), - ).rejects.toThrow(TokenAccountNotFoundError); + const ixs = await createLoadAtaInstructions( + rpc, + ata, + owner.publicKey, + mint, + payer.publicKey, + ); + expect(ixs.length).toBe(0); }); it('should return empty when hot exists but no cold', async () => { diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 0686510f02..19cb1138ab 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -174,7 +174,66 @@ export class TestRpc extends Connection implements CompressionApiInterface { * Returns local test state trees. */ async getStateTreeInfos(): Promise { - return localTestActiveStateTreeInfos(); + const infos = localTestActiveStateTreeInfos(); + + // In local test environments, some batched (V2) output queues can get + // stuck in a "Full" state (BatchNotReady=14301) if no background worker + // drains them. We must still return *all* TreeInfos (needed to resolve + // historical merkle contexts), but we mark blocked trees as "inactive" + // by setting nextTreeInfo, so selection logic skips them. + const V2_QUEUE_DISCRIMINATOR_LEN = 8; + const QUEUE_METADATA_LEN = 224; // QueueMetadata (see sdk-libs/program-test comment) + const QUEUE_BATCHES_OFFSET = + V2_QUEUE_DISCRIMINATOR_LEN + QUEUE_METADATA_LEN; // 232 + const U64_LEN = 8; + const BATCH_STRUCT_LEN = 96; // program-libs/batched-merkle-tree/src/batch.rs + const BATCHES_OFFSET = QUEUE_BATCHES_OFFSET + 7 * U64_LEN; // QueueBatches fields before batches + const BATCH_STATE_OFFSET_IN_BATCH = 1 * U64_LEN; // after num_inserted + const BATCH0_STATE_OFFSET = + BATCHES_OFFSET + BATCH_STATE_OFFSET_IN_BATCH; // 296 + const BATCH1_STATE_OFFSET = + BATCHES_OFFSET + BATCH_STRUCT_LEN + BATCH_STATE_OFFSET_IN_BATCH; // 392 + const BATCH_STATE_FULL = 2; + + const v2StateTrees = infos.filter(t => t.treeType === TreeType.StateV2); + if (v2StateTrees.length === 0) { + return infos; + } + + const queueAccountInfos = await this.getMultipleAccountsInfo( + v2StateTrees.map(t => t.queue), + ); + + const blockedQueues = new Set(); + for (let i = 0; i < v2StateTrees.length; i++) { + const ai = queueAccountInfos[i]; + if (!ai) continue; + const data = ai.data; + if (data.length < BATCH1_STATE_OFFSET + U64_LEN) continue; + const state0 = Number(data.readBigUInt64LE(BATCH0_STATE_OFFSET)); + const state1 = Number(data.readBigUInt64LE(BATCH1_STATE_OFFSET)); + if (state0 === BATCH_STATE_FULL && state1 === BATCH_STATE_FULL) { + blockedQueues.add(v2StateTrees[i].queue.toBase58()); + } + } + + // Pick a "next" tree for blocked ones (first non-blocked V2 tree). + const fallbackNextTree = + v2StateTrees.find(t => !blockedQueues.has(t.queue.toBase58())) ?? + null; + + if (blockedQueues.size === 0 || !fallbackNextTree) { + return infos; + } + + return infos.map(info => { + if (info.treeType !== TreeType.StateV2) return info; + if (!blockedQueues.has(info.queue.toBase58())) return info; + return { + ...info, + nextTreeInfo: fallbackNextTree, + }; + }); } async doFetch(): Promise { throw new Error('doFetch not supported in test-rpc'); From 8a0bfe905872fb803ccd14293b10d172a6e65839 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 20 Dec 2025 15:23:05 +0100 Subject: [PATCH 22/28] wip --- js/compressed-token/src/actions/mint-to.ts | 95 ++++--------------- .../src/test-helpers/test-rpc/test-rpc.ts | 61 +----------- 2 files changed, 19 insertions(+), 137 deletions(-) diff --git a/js/compressed-token/src/actions/mint-to.ts b/js/compressed-token/src/actions/mint-to.ts index d491e78a92..bf6100950d 100644 --- a/js/compressed-token/src/actions/mint-to.ts +++ b/js/compressed-token/src/actions/mint-to.ts @@ -21,39 +21,6 @@ import { SplInterfaceInfo, } from '../utils/get-token-pool-infos'; -function isBatchNotReadyError(error: unknown): boolean { - const message = error instanceof Error ? error.message : String(error); - // BatchedMerkleTreeError::BatchNotReady (14301) => custom program error 0x37dd - return ( - message.includes('0x37dd') || - message.includes('14301') || - message.includes('BatchNotReady') - ); -} - -async function selectAlternativeStateTreeInfo( - rpc: Rpc, - current: TreeInfo, -): Promise { - const infos = await rpc.getStateTreeInfos(); - - // Prefer a different active tree of the same type. - const candidates = infos.filter( - t => - t.treeType === current.treeType && - !t.nextTreeInfo && - !t.queue.equals(current.queue), - ); - - if (candidates.length > 0) { - const length = Math.min(5, candidates.length); - return candidates[Math.floor(Math.random() * length)]; - } - - // Fall back to normal selection (may return the same tree). - return selectStateTreeInfo(infos, current.treeType, true); -} - /** * Mint compressed tokens to a solana address * @@ -92,51 +59,25 @@ export async function mintTo( splInterfaceInfo ?? selectSplInterfaceInfo(await getSplInterfaceInfos(rpc, mint)); - // Retry on BatchNotReady (full output queue batch) by selecting a different - // active state tree. This can happen under heavy test load when one of the - // V2 output queues becomes blocked. - let selectedTree = outputStateTreeInfo; - let lastError: unknown; - for (let attempt = 0; attempt < 3; attempt++) { - try { - const ix = await CompressedTokenProgram.mintTo({ - feePayer: payer.publicKey, - mint, - authority: authority.publicKey, - amount, - toPubkey, - outputStateTreeInfo: selectedTree, - tokenPoolInfo: splInterfaceInfo, - }); - - const { blockhash } = await rpc.getLatestBlockhash(); - const additionalSigners = dedupeSigner(payer, [authority]); + const ix = await CompressedTokenProgram.mintTo({ + feePayer: payer.publicKey, + mint, + authority: authority.publicKey, + amount, + toPubkey, + outputStateTreeInfo, + tokenPoolInfo: splInterfaceInfo, + }); - const tx = buildAndSignTx( - [ - ComputeBudgetProgram.setComputeUnitLimit({ - units: 1_000_000, - }), - ix, - ], - payer, - blockhash, - additionalSigners, - ); + const { blockhash } = await rpc.getLatestBlockhash(); + const additionalSigners = dedupeSigner(payer, [authority]); - return sendAndConfirmTx(rpc, tx, confirmOptions); - } catch (error) { - lastError = error; - if (!isBatchNotReadyError(error) || attempt === 2) { - throw error; - } - selectedTree = await selectAlternativeStateTreeInfo( - rpc, - selectedTree, - ); - } - } + const tx = buildAndSignTx( + [ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000_000 }), ix], + payer, + blockhash, + additionalSigners, + ); - // Unreachable, but keeps TS happy. - throw lastError; + return sendAndConfirmTx(rpc, tx, confirmOptions); } diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 19cb1138ab..0686510f02 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -174,66 +174,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { * Returns local test state trees. */ async getStateTreeInfos(): Promise { - const infos = localTestActiveStateTreeInfos(); - - // In local test environments, some batched (V2) output queues can get - // stuck in a "Full" state (BatchNotReady=14301) if no background worker - // drains them. We must still return *all* TreeInfos (needed to resolve - // historical merkle contexts), but we mark blocked trees as "inactive" - // by setting nextTreeInfo, so selection logic skips them. - const V2_QUEUE_DISCRIMINATOR_LEN = 8; - const QUEUE_METADATA_LEN = 224; // QueueMetadata (see sdk-libs/program-test comment) - const QUEUE_BATCHES_OFFSET = - V2_QUEUE_DISCRIMINATOR_LEN + QUEUE_METADATA_LEN; // 232 - const U64_LEN = 8; - const BATCH_STRUCT_LEN = 96; // program-libs/batched-merkle-tree/src/batch.rs - const BATCHES_OFFSET = QUEUE_BATCHES_OFFSET + 7 * U64_LEN; // QueueBatches fields before batches - const BATCH_STATE_OFFSET_IN_BATCH = 1 * U64_LEN; // after num_inserted - const BATCH0_STATE_OFFSET = - BATCHES_OFFSET + BATCH_STATE_OFFSET_IN_BATCH; // 296 - const BATCH1_STATE_OFFSET = - BATCHES_OFFSET + BATCH_STRUCT_LEN + BATCH_STATE_OFFSET_IN_BATCH; // 392 - const BATCH_STATE_FULL = 2; - - const v2StateTrees = infos.filter(t => t.treeType === TreeType.StateV2); - if (v2StateTrees.length === 0) { - return infos; - } - - const queueAccountInfos = await this.getMultipleAccountsInfo( - v2StateTrees.map(t => t.queue), - ); - - const blockedQueues = new Set(); - for (let i = 0; i < v2StateTrees.length; i++) { - const ai = queueAccountInfos[i]; - if (!ai) continue; - const data = ai.data; - if (data.length < BATCH1_STATE_OFFSET + U64_LEN) continue; - const state0 = Number(data.readBigUInt64LE(BATCH0_STATE_OFFSET)); - const state1 = Number(data.readBigUInt64LE(BATCH1_STATE_OFFSET)); - if (state0 === BATCH_STATE_FULL && state1 === BATCH_STATE_FULL) { - blockedQueues.add(v2StateTrees[i].queue.toBase58()); - } - } - - // Pick a "next" tree for blocked ones (first non-blocked V2 tree). - const fallbackNextTree = - v2StateTrees.find(t => !blockedQueues.has(t.queue.toBase58())) ?? - null; - - if (blockedQueues.size === 0 || !fallbackNextTree) { - return infos; - } - - return infos.map(info => { - if (info.treeType !== TreeType.StateV2) return info; - if (!blockedQueues.has(info.queue.toBase58())) return info; - return { - ...info, - nextTreeInfo: fallbackNextTree, - }; - }); + return localTestActiveStateTreeInfos(); } async doFetch(): Promise { throw new Error('doFetch not supported in test-rpc'); From bcfaada67eaf2f44257da42aa3fa570e476fbf10 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sat, 20 Dec 2025 17:31:56 +0100 Subject: [PATCH 23/28] sequential e2e --- js/compressed-token/vitest.config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/compressed-token/vitest.config.ts b/js/compressed-token/vitest.config.ts index 3bbbf1ad7c..7f620c20b8 100644 --- a/js/compressed-token/vitest.config.ts +++ b/js/compressed-token/vitest.config.ts @@ -9,6 +9,9 @@ export default defineConfig({ : ['src/**/__tests__/*.test.ts', 'tests/**/*.test.ts'], includeSource: ['src/**/*.{js,ts}'], exclude: ['src/program.ts'], + // e2e tests share a single local validator instance; running files in parallel can + // overflow on-chain queues and lead to nondeterministic ProgramError failures. + fileParallelism: false, testTimeout: 350000, hookTimeout: 100000, reporters: ['verbose'], From ab2367f79edd3e7d23ea33317ff373c8481aeaa8 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Sun, 21 Dec 2025 20:03:31 +0100 Subject: [PATCH 24/28] bump to alpha.6 --- cli/package.json | 2 +- js/compressed-token/package.json | 2 +- js/stateless.js/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cli/package.json b/cli/package.json index 8656b9b5d9..94dea6432b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.5", + "version": "0.27.1-alpha.6", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index bd6aa45e8f..ab495af18e 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.1-alpha.3", + "version": "0.22.1-alpha.4", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index b7a3e88bff..ef40b9f1b1 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.1-alpha.2", + "version": "0.22.1-alpha.3", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", From d26791a244abfc3d8c4c9739ecff7fe5e80a0e9a Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Mon, 22 Dec 2025 11:34:18 +0100 Subject: [PATCH 25/28] fix getstatetreeinfos versioning --- Cargo.lock | 1 + .../tests/e2e/create-compressed-mint.test.ts | 3 +- .../src/utils/get-state-tree-infos.ts | 20 ++++++++- .../tests/unit/utils/tree-info.test.ts | 42 +++++++++++++++++++ 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45c2f9d0b8..db3011329d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2408,6 +2408,7 @@ dependencies = [ "light-registry", "light-sdk", "light-sparse-merkle-tree", + "light-verifier", "num-bigint 0.4.6", "num-traits", "serde", diff --git a/js/compressed-token/tests/e2e/create-compressed-mint.test.ts b/js/compressed-token/tests/e2e/create-compressed-mint.test.ts index 875f837458..fcb4cdfa24 100644 --- a/js/compressed-token/tests/e2e/create-compressed-mint.test.ts +++ b/js/compressed-token/tests/e2e/create-compressed-mint.test.ts @@ -16,6 +16,7 @@ import { CTOKEN_PROGRAM_ID, DerivationMode, selectStateTreeInfo, + TreeType, } from '@lightprotocol/stateless.js'; import { createMintInstruction, @@ -136,7 +137,7 @@ describe('createMintInterface (compressed)', () => { tree: new PublicKey('amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'), queue: new PublicKey('amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'), cpiContext: undefined, - treeType: 1, + treeType: TreeType.AddressV2, nextTreeInfo: null, }; diff --git a/js/stateless.js/src/utils/get-state-tree-infos.ts b/js/stateless.js/src/utils/get-state-tree-infos.ts index 8c13115de2..f163ecf598 100644 --- a/js/stateless.js/src/utils/get-state-tree-infos.ts +++ b/js/stateless.js/src/utils/get-state-tree-infos.ts @@ -194,6 +194,22 @@ export async function getAllStateTreeInfos({ if (!tree || !queue || !cpiContext) { throw new Error('Invalid state tree pubkeys structure'); } + // Detect tree type based on tree address prefix + // - bmt = batch merkle tree (StateV2) + // - amt = address merkle tree (AddressV2 if starts with amt2, AddressV1 if amt1) + // - smt = sparse merkle tree (StateV1) + const treeStr = tree.toBase58(); + let treeType: TreeType; + if (treeStr.startsWith('bmt')) { + treeType = TreeType.StateV2; + } else if (treeStr.startsWith('amt2')) { + treeType = TreeType.AddressV2; + } else if (treeStr.startsWith('amt')) { + treeType = TreeType.AddressV1; + } else { + treeType = TreeType.StateV1; + } + if ( nullifyLookupTablePubkeys .map(addr => addr.toBase58()) @@ -204,7 +220,7 @@ export async function getAllStateTreeInfos({ tree: PublicKey.default, queue: PublicKey.default, cpiContext: PublicKey.default, - treeType: TreeType.StateV1, + treeType, nextTreeInfo: null, }; } @@ -212,7 +228,7 @@ export async function getAllStateTreeInfos({ tree, queue, cpiContext, - treeType: TreeType.StateV1, + treeType, nextTreeInfo, }); } diff --git a/js/stateless.js/tests/unit/utils/tree-info.test.ts b/js/stateless.js/tests/unit/utils/tree-info.test.ts index 672f0d54a1..38f1bf03d3 100644 --- a/js/stateless.js/tests/unit/utils/tree-info.test.ts +++ b/js/stateless.js/tests/unit/utils/tree-info.test.ts @@ -9,6 +9,7 @@ import { merkletreePubkey, nullifierQueue2Pubkey, nullifierQueuePubkey, + localTestActiveStateTreeInfos, } from '../../../src'; describe('selectStateTreeInfo', () => { @@ -126,4 +127,45 @@ describe('selectStateTreeInfo', () => { 'Queue must not be null for state tree', ); }); + + it('should correctly set tree types for BMT (V2) trees', () => { + const allTrees = localTestActiveStateTreeInfos(); + + // Check BMT trees have StateV2 type + const bmtTrees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('bmt'), + ); + expect(bmtTrees.length).toBeGreaterThan(0); + + for (const tree of bmtTrees) { + expect(tree.treeType).toBe(TreeType.StateV2); + expect(tree.treeType).not.toBe(TreeType.StateV1); + // Verify the numeric value is 3 (StateV2) + expect(tree.treeType).toBe(3); + } + + // Check SMT trees have StateV1 type + const smtTrees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('smt'), + ); + expect(smtTrees.length).toBeGreaterThan(0); + + for (const tree of smtTrees) { + expect(tree.treeType).toBe(TreeType.StateV1); + // Verify the numeric value is 1 (StateV1) + expect(tree.treeType).toBe(1); + } + + // Check V2 address tree has AddressV2 type + const amt2Trees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('amt2'), + ); + expect(amt2Trees.length).toBe(1); + + for (const tree of amt2Trees) { + expect(tree.treeType).toBe(TreeType.AddressV2); + // Verify the numeric value is 4 (AddressV2) + expect(tree.treeType).toBe(4); + } + }); }); From d278cce61e2786d2e0cdb55f922ba5039047b9ef Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Mon, 22 Dec 2025 11:35:49 +0100 Subject: [PATCH 26/28] bump lockfile --- Cargo.lock | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index db3011329d..45c2f9d0b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2408,7 +2408,6 @@ dependencies = [ "light-registry", "light-sdk", "light-sparse-merkle-tree", - "light-verifier", "num-bigint 0.4.6", "num-traits", "serde", From c723dc424b7ce4cc5f5e96741f4e2a8205119c36 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Mon, 22 Dec 2025 17:29:40 +0100 Subject: [PATCH 27/28] add temp devnet backward compat helpers for mintaction --- cli/package.json | 2 +- js/compressed-token/package.json | 2 +- .../src/v3/actions/create-mint-interface.ts | 4 + .../src/v3/actions/mint-to-compressed.ts | 4 + js/compressed-token/src/v3/actions/mint-to.ts | 4 + .../src/v3/actions/update-mint.ts | 7 + .../src/v3/instructions/create-mint.ts | 19 + .../src/v3/layout/devnet-compat.ts | 29 ++ js/compressed-token/src/v3/layout/index.ts | 2 + .../src/v3/layout/layout-mint-action.ts | 160 +++++-- .../tests/unit/mint-action-layout.test.ts | 419 ++++++++++++++++++ js/stateless.js/package.json | 2 +- 12 files changed, 610 insertions(+), 44 deletions(-) create mode 100644 js/compressed-token/src/v3/layout/devnet-compat.ts create mode 100644 js/compressed-token/tests/unit/mint-action-layout.test.ts diff --git a/cli/package.json b/cli/package.json index 94dea6432b..8c173538fa 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.6", + "version": "0.27.1-alpha.7", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index ab495af18e..5a0c03e7fa 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.1-alpha.4", + "version": "0.22.1-alpha.5", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", 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 3d87210068..3b7ec2848f 100644 --- a/js/compressed-token/src/v3/actions/create-mint-interface.ts +++ b/js/compressed-token/src/v3/actions/create-mint-interface.ts @@ -24,6 +24,7 @@ import { createMintInstruction, TokenMetadataInstructionData, } from '../instructions/create-mint'; +import { setDevnetCompatFromEndpoint } from '../layout/devnet-compat'; import { deriveCMintAddress, findMintAddress } from '../derivation'; import { createMint } from '../../actions/create-mint'; @@ -117,6 +118,9 @@ export async function createMintInterface( DerivationMode.standard, ); + // TODO: Remove after devnet program update + setDevnetCompatFromEndpoint(rpc.rpcEndpoint); + const ix = createMintInstruction( keypair.publicKey, decimals, 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 db09d5bddc..8167ea2be9 100644 --- a/js/compressed-token/src/v3/actions/mint-to-compressed.ts +++ b/js/compressed-token/src/v3/actions/mint-to-compressed.ts @@ -16,6 +16,7 @@ import { TreeInfo, } from '@lightprotocol/stateless.js'; import { createMintToCompressedInstruction } from '../instructions/mint-to-compressed'; +import { setDevnetCompatFromEndpoint } from '../layout/devnet-compat'; import { getMintInterface } from '../get-mint-interface'; /** @@ -40,6 +41,9 @@ export async function mintToCompressed( tokenAccountVersion: number = 3, confirmOptions?: ConfirmOptions, ): Promise { + // TODO: Remove after devnet program update + setDevnetCompatFromEndpoint(rpc.rpcEndpoint); + const mintInfo = await getMintInterface( rpc, mint, diff --git a/js/compressed-token/src/v3/actions/mint-to.ts b/js/compressed-token/src/v3/actions/mint-to.ts index 70238a8ee6..dcc2f2355d 100644 --- a/js/compressed-token/src/v3/actions/mint-to.ts +++ b/js/compressed-token/src/v3/actions/mint-to.ts @@ -16,6 +16,7 @@ import { TreeInfo, } from '@lightprotocol/stateless.js'; import { createMintToInstruction } from '../instructions/mint-to'; +import { setDevnetCompatFromEndpoint } from '../layout/devnet-compat'; import { getMintInterface } from '../get-mint-interface'; export async function mintTo( @@ -28,6 +29,9 @@ export async function mintTo( outputQueue?: PublicKey, confirmOptions?: ConfirmOptions, ): Promise { + // TODO: Remove after devnet program update + setDevnetCompatFromEndpoint(rpc.rpcEndpoint); + const mintInfo = await getMintInterface( rpc, mint, diff --git a/js/compressed-token/src/v3/actions/update-mint.ts b/js/compressed-token/src/v3/actions/update-mint.ts index 838328e573..86c1d4c266 100644 --- a/js/compressed-token/src/v3/actions/update-mint.ts +++ b/js/compressed-token/src/v3/actions/update-mint.ts @@ -17,6 +17,7 @@ import { createUpdateMintAuthorityInstruction, createUpdateFreezeAuthorityInstruction, } from '../instructions/update-mint'; +import { setDevnetCompatFromEndpoint } from '../layout/devnet-compat'; import { getMintInterface } from '../get-mint-interface'; /** @@ -37,6 +38,9 @@ export async function updateMintAuthority( newMintAuthority: PublicKey | null, confirmOptions?: ConfirmOptions, ): Promise { + // TODO: Remove after devnet program update + setDevnetCompatFromEndpoint(rpc.rpcEndpoint); + const mintInterface = await getMintInterface( rpc, mint, @@ -104,6 +108,9 @@ export async function updateFreezeAuthority( newFreezeAuthority: PublicKey | null, confirmOptions?: ConfirmOptions, ): Promise { + // TODO: Remove after devnet program update + setDevnetCompatFromEndpoint(rpc.rpcEndpoint); + const mintInterface = await getMintInterface( rpc, mint, diff --git a/js/compressed-token/src/v3/instructions/create-mint.ts b/js/compressed-token/src/v3/instructions/create-mint.ts index df737eb7b0..cd15abb504 100644 --- a/js/compressed-token/src/v3/instructions/create-mint.ts +++ b/js/compressed-token/src/v3/instructions/create-mint.ts @@ -203,6 +203,25 @@ export function createMintInstruction( metadata, }); + return buildCreateMintIx( + mintSigner, + mintAuthority, + payer, + outputStateTreeInfo, + addressTreeInfo, + data, + ); +} + +/** @internal */ +function buildCreateMintIx( + mintSigner: PublicKey, + mintAuthority: PublicKey, + payer: PublicKey, + outputStateTreeInfo: TreeInfo, + addressTreeInfo: AddressTreeInfo, + data: Buffer, +): TransactionInstruction { const sys = defaultStaticAccountsStruct(); const keys = [ { diff --git a/js/compressed-token/src/v3/layout/devnet-compat.ts b/js/compressed-token/src/v3/layout/devnet-compat.ts new file mode 100644 index 0000000000..945c11e3d1 --- /dev/null +++ b/js/compressed-token/src/v3/layout/devnet-compat.ts @@ -0,0 +1,29 @@ +/** + * @internal + * Temporary devnet compatibility config. + * TODO: Remove after devnet program update (deployed at slot 426761768, Dec 8, 2025). + */ + +let _useDevnetFormat = false; + +/** + * Enable V1 instruction format for devnet compatibility. + * Call this before any mint operations when targeting devnet. + */ +export function setDevnetCompat(enabled: boolean): void { + _useDevnetFormat = enabled; +} + +/** + * Check if devnet compatibility mode is enabled. + */ +export function isDevnetCompat(): boolean { + return _useDevnetFormat; +} + +/** + * Auto-detect and set devnet compat from RPC endpoint. + */ +export function setDevnetCompatFromEndpoint(endpoint: string): void { + _useDevnetFormat = endpoint.toLowerCase().includes('devnet'); +} diff --git a/js/compressed-token/src/v3/layout/index.ts b/js/compressed-token/src/v3/layout/index.ts index 15d7eeae67..01dc4b8332 100644 --- a/js/compressed-token/src/v3/layout/index.ts +++ b/js/compressed-token/src/v3/layout/index.ts @@ -2,4 +2,6 @@ export * from './layout-mint'; export * from './layout-transfer2'; export * from './layout-token-metadata'; export * from './layout-mint-action'; +// TODO: Remove after devnet program update +export * from './devnet-compat'; export * from './serde'; diff --git a/js/compressed-token/src/v3/layout/layout-mint-action.ts b/js/compressed-token/src/v3/layout/layout-mint-action.ts index 4c2c8ce53d..275ab82c3b 100644 --- a/js/compressed-token/src/v3/layout/layout-mint-action.ts +++ b/js/compressed-token/src/v3/layout/layout-mint-action.ts @@ -23,6 +23,7 @@ import { rustEnum, } from '@coral-xyz/borsh'; import { bn } from '@lightprotocol/stateless.js'; +import { isDevnetCompat } from './devnet-compat'; export const MINT_ACTION_DISCRIMINATOR = Buffer.from([103]); @@ -171,6 +172,48 @@ export const MintActionCompressedInstructionDataLayout = struct([ option(CompressedMintInstructionDataLayout, 'mint'), ]); +// TODO: Remove V1 layouts after devnet program update +const ActionLayoutV1 = rustEnum([ + MintToCompressedActionLayout.replicate('mintToCompressed'), + UpdateAuthorityLayout.replicate('updateMintAuthority'), + UpdateAuthorityLayout.replicate('updateFreezeAuthority'), + CreateSplMintActionLayout.replicate('createSplMint'), + MintToCTokenActionLayout.replicate('mintToCToken'), + UpdateMetadataFieldActionLayout.replicate('updateMetadataField'), + UpdateMetadataAuthorityActionLayout.replicate('updateMetadataAuthority'), + RemoveMetadataKeyActionLayout.replicate('removeMetadataKey'), +]); +// TODO: Remove V1 layouts after devnet program update +const CompressedMintMetadataLayoutV1 = struct([ + u8('version'), + bool('splMintInitialized'), + publicKey('mint'), +]); +// TODO: Remove V1 layouts after devnet program update +const CompressedMintInstructionDataLayoutV1 = struct([ + u64('supply'), + u8('decimals'), + CompressedMintMetadataLayoutV1.replicate('metadata'), + option(publicKey(), 'mintAuthority'), + option(publicKey(), 'freezeAuthority'), + option(vec(ExtensionInstructionDataLayout), 'extensions'), +]); +// TODO: Remove V1 layouts after devnet program update +const MintActionCompressedInstructionDataLayoutV1 = struct([ + u32('leafIndex'), + bool('proveByIndex'), + u16('rootIndex'), + array(u8(), 32, 'compressedAddress'), + u8('tokenPoolBump'), + u8('tokenPoolIndex'), + u16('maxTopUp'), + option(CreateMintLayout, 'createMint'), + vec(ActionLayoutV1, 'actions'), + option(CompressedProofLayout, 'proof'), + option(CpiContextLayout, 'cpiContext'), + CompressedMintInstructionDataLayoutV1.replicate('mint'), // V1: not optional +]); + export interface ValidityProof { a: number[]; b: number[]; @@ -313,48 +356,83 @@ export interface MintActionCompressedInstructionData { export function encodeMintActionInstructionData( data: MintActionCompressedInstructionData, ): Buffer { + const useV1 = isDevnetCompat(); + // Convert bigint fields to BN for Borsh encoding - const encodableData = { - ...data, - mint: data.mint - ? { - ...data.mint, - supply: bn(data.mint.supply.toString()), - } - : null, - actions: data.actions.map(action => { - // Handle MintToCompressed action with recipients - if ('mintToCompressed' in action && action.mintToCompressed) { - return { - mintToCompressed: { - ...action.mintToCompressed, - recipients: action.mintToCompressed.recipients.map( - r => ({ - ...r, - amount: bn(r.amount.toString()), - }), - ), - }, - }; - } - // Handle MintToCToken action (c-token mint-to) - if ('mintToCToken' in action && action.mintToCToken) { - return { - mintToCToken: { - ...action.mintToCToken, - amount: bn(action.mintToCToken.amount.toString()), - }, - }; - } - return action; - }), - }; - - const buffer = Buffer.alloc(10000); // Generous allocation - const len = MintActionCompressedInstructionDataLayout.encode( - encodableData, - buffer, - ); + // TODO: Remove V1 layouts after devnet program update + const convertedActions = data.actions.map(action => { + if ('mintToCompressed' in action && action.mintToCompressed) { + return { + mintToCompressed: { + ...action.mintToCompressed, + recipients: action.mintToCompressed.recipients.map(r => ({ + ...r, + amount: bn(r.amount.toString()), + })), + }, + }; + } + if ('mintToCToken' in action && action.mintToCToken) { + return { + mintToCToken: { + ...action.mintToCToken, + amount: bn(action.mintToCToken.amount.toString()), + }, + }; + } + // V1 doesn't support new action types + if ( + useV1 && + ('decompressMint' in action || 'compressAndCloseCMint' in action) + ) { + throw new Error( + 'decompressMint/compressAndCloseCMint not supported on devnet', + ); + } + return action; + }); + + const buffer = Buffer.alloc(10000); + let len: number; + + // TODO: Remove V1 branch after devnet program update + if (useV1) { + if (!data.mint) { + throw new Error('V1 format requires mint data (cannot be null)'); + } + const encodableDataV1 = { + ...data, + actions: convertedActions, + mint: { + ...data.mint, + supply: bn(data.mint.supply.toString()), + metadata: { + version: data.mint.metadata.version, + splMintInitialized: data.mint.metadata.cmintDecompressed, + mint: data.mint.metadata.mint, + }, + }, + }; + len = MintActionCompressedInstructionDataLayoutV1.encode( + encodableDataV1, + buffer, + ); + } else { + const encodableData = { + ...data, + actions: convertedActions, + mint: data.mint + ? { + ...data.mint, + supply: bn(data.mint.supply.toString()), + } + : null, + }; + len = MintActionCompressedInstructionDataLayout.encode( + encodableData, + buffer, + ); + } return Buffer.concat([MINT_ACTION_DISCRIMINATOR, buffer.subarray(0, len)]); } diff --git a/js/compressed-token/tests/unit/mint-action-layout.test.ts b/js/compressed-token/tests/unit/mint-action-layout.test.ts new file mode 100644 index 0000000000..564012e314 --- /dev/null +++ b/js/compressed-token/tests/unit/mint-action-layout.test.ts @@ -0,0 +1,419 @@ +import { describe, it, expect } from 'vitest'; +import { PublicKey, Keypair } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { + encodeMintActionInstructionData, + decodeMintActionInstructionData, + MintActionCompressedInstructionData, + MINT_ACTION_DISCRIMINATOR, +} from '../../src/v3/layout/layout-mint-action'; +import { encodeCreateMintInstructionData } from '../../src/v3/instructions/create-mint'; +import { TokenDataVersion } from '../../src/constants'; +import { + deriveAddressV2, + CTOKEN_PROGRAM_ID, + getBatchAddressTreeInfo, +} from '@lightprotocol/stateless.js'; +import { findMintAddress } from '../../src/v3/derivation'; + +describe('MintActionCompressedInstructionData Layout', () => { + describe('encode/decode round-trip', () => { + it('should encode and decode createMint instruction data correctly', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + + // Create data matching what encodeCreateMintInstructionData produces + const instructionData: MintActionCompressedInstructionData = { + leafIndex: 0, + proveByIndex: false, + rootIndex: 42, + compressedAddress: Array.from(new Uint8Array(32).fill(1)), + tokenPoolBump: 0, + tokenPoolIndex: 0, + maxTopUp: 0, + createMint: { + readOnlyAddressTrees: [0, 0, 0, 0], + readOnlyAddressTreeRootIndices: [0, 0, 0, 0], + }, + actions: [], + proof: { + a: Array.from(new Uint8Array(32).fill(2)), + b: Array.from(new Uint8Array(64).fill(3)), + c: Array.from(new Uint8Array(32).fill(4)), + }, + cpiContext: null, + mint: { + supply: BigInt(0), + decimals: 9, + metadata: { + version: TokenDataVersion.ShaFlat, + cmintDecompressed: false, + mint: mintSigner.publicKey, + }, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + extensions: null, + }, + }; + + const encoded = encodeMintActionInstructionData(instructionData); + expect(encoded[0]).toBe(103); // MINT_ACTION_DISCRIMINATOR + + const decoded = decodeMintActionInstructionData(encoded); + + // Verify all fields match + expect(decoded.leafIndex).toBe(instructionData.leafIndex); + expect(decoded.proveByIndex).toBe(instructionData.proveByIndex); + expect(decoded.rootIndex).toBe(instructionData.rootIndex); + expect(decoded.compressedAddress).toEqual( + instructionData.compressedAddress, + ); + expect(decoded.tokenPoolBump).toBe(instructionData.tokenPoolBump); + expect(decoded.tokenPoolIndex).toBe(instructionData.tokenPoolIndex); + expect(decoded.maxTopUp).toBe(instructionData.maxTopUp); + expect(decoded.createMint).toEqual(instructionData.createMint); + expect(decoded.actions).toEqual([]); + expect(decoded.proof).toEqual(instructionData.proof); + expect(decoded.cpiContext).toBeNull(); + expect(decoded.mint).toBeDefined(); + expect(decoded.mint!.decimals).toBe(9); + }); + + it('should encode createMint without proof (null proof)', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + + const instructionData: MintActionCompressedInstructionData = { + leafIndex: 0, + proveByIndex: false, + rootIndex: 0, + compressedAddress: Array.from(new Uint8Array(32).fill(0)), + tokenPoolBump: 0, + tokenPoolIndex: 0, + maxTopUp: 0, + createMint: { + readOnlyAddressTrees: [0, 0, 0, 0], + readOnlyAddressTreeRootIndices: [0, 0, 0, 0], + }, + actions: [], + proof: null, + cpiContext: null, + mint: { + supply: BigInt(0), + decimals: 6, + metadata: { + version: TokenDataVersion.ShaFlat, + cmintDecompressed: false, + mint: mintSigner.publicKey, + }, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + extensions: null, + }, + }; + + const encoded = encodeMintActionInstructionData(instructionData); + const decoded = decodeMintActionInstructionData(encoded); + + expect(decoded.proof).toBeNull(); + expect(decoded.mint!.decimals).toBe(6); + }); + + it('should encode createMint with freeze authority', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + const freezeAuthority = Keypair.generate(); + + const instructionData: MintActionCompressedInstructionData = { + leafIndex: 0, + proveByIndex: false, + rootIndex: 100, + compressedAddress: Array.from(new Uint8Array(32).fill(5)), + tokenPoolBump: 0, + tokenPoolIndex: 0, + maxTopUp: 0, + createMint: { + readOnlyAddressTrees: [0, 0, 0, 0], + readOnlyAddressTreeRootIndices: [0, 0, 0, 0], + }, + actions: [], + proof: { + a: Array.from(new Uint8Array(32).fill(10)), + b: Array.from(new Uint8Array(64).fill(11)), + c: Array.from(new Uint8Array(32).fill(12)), + }, + cpiContext: null, + mint: { + supply: BigInt(0), + decimals: 9, + metadata: { + version: TokenDataVersion.ShaFlat, + cmintDecompressed: false, + mint: mintSigner.publicKey, + }, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: freezeAuthority.publicKey, + extensions: null, + }, + }; + + const encoded = encodeMintActionInstructionData(instructionData); + const decoded = decodeMintActionInstructionData(encoded); + + expect(decoded.mint!.freezeAuthority).toBeDefined(); + expect(decoded.mint!.freezeAuthority!.toBase58()).toBe( + freezeAuthority.publicKey.toBase58(), + ); + }); + + it('should encode createMint with token metadata extension', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + + const instructionData: MintActionCompressedInstructionData = { + leafIndex: 0, + proveByIndex: false, + rootIndex: 50, + compressedAddress: Array.from(new Uint8Array(32).fill(6)), + tokenPoolBump: 0, + tokenPoolIndex: 0, + maxTopUp: 0, + createMint: { + readOnlyAddressTrees: [0, 0, 0, 0], + readOnlyAddressTreeRootIndices: [0, 0, 0, 0], + }, + actions: [], + proof: { + a: Array.from(new Uint8Array(32).fill(20)), + b: Array.from(new Uint8Array(64).fill(21)), + c: Array.from(new Uint8Array(32).fill(22)), + }, + cpiContext: null, + mint: { + supply: BigInt(0), + decimals: 9, + metadata: { + version: TokenDataVersion.ShaFlat, + cmintDecompressed: false, + mint: mintSigner.publicKey, + }, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + extensions: [ + { + tokenMetadata: { + updateAuthority: mintAuthority.publicKey, + name: Buffer.from('Test Token'), + symbol: Buffer.from('TEST'), + uri: Buffer.from( + 'https://test.com/metadata.json', + ), + additionalMetadata: null, + }, + }, + ], + }, + }; + + const encoded = encodeMintActionInstructionData(instructionData); + const decoded = decodeMintActionInstructionData(encoded); + + expect(decoded.mint!.extensions).toBeDefined(); + expect(decoded.mint!.extensions!.length).toBe(1); + }); + }); + + describe('byte layout verification', () => { + it('should have correct discriminator', () => { + expect(MINT_ACTION_DISCRIMINATOR).toEqual(Buffer.from([103])); + }); + + it('should produce consistent byte output', () => { + const mintSigner = PublicKey.default; + const mintAuthority = Keypair.generate().publicKey; + + const instructionData: MintActionCompressedInstructionData = { + leafIndex: 0, + proveByIndex: false, + rootIndex: 0, + compressedAddress: Array(32).fill(0), + tokenPoolBump: 0, + tokenPoolIndex: 0, + maxTopUp: 0, + createMint: { + readOnlyAddressTrees: [0, 0, 0, 0], + readOnlyAddressTreeRootIndices: [0, 0, 0, 0], + }, + actions: [], + proof: null, + cpiContext: null, + mint: { + supply: BigInt(0), + decimals: 9, + metadata: { + version: 0, + cmintDecompressed: false, + mint: mintSigner, + }, + mintAuthority: mintAuthority, + freezeAuthority: null, + extensions: null, + }, + }; + + const encoded1 = encodeMintActionInstructionData(instructionData); + const encoded2 = encodeMintActionInstructionData(instructionData); + + // Should be deterministic + expect(encoded1).toEqual(encoded2); + + // Log hex for debugging + console.log( + 'Encoded bytes (hex):', + encoded1.toString('hex').slice(0, 200) + '...', + ); + console.log('Total encoded length:', encoded1.length); + + // First byte should be discriminator 103 + expect(encoded1[0]).toBe(103); + + // Next 4 bytes should be leafIndex (0 as u32 little-endian) + expect(encoded1.slice(1, 5)).toEqual(Buffer.from([0, 0, 0, 0])); + + // Next byte should be proveByIndex (false = 0) + expect(encoded1[5]).toBe(0); + + // Next 2 bytes should be rootIndex (0 as u16 little-endian) + expect(encoded1.slice(6, 8)).toEqual(Buffer.from([0, 0])); + + // Next 32 bytes should be compressedAddress (all zeros) + expect(encoded1.slice(8, 40)).toEqual(Buffer.alloc(32, 0)); + + // tokenPoolBump at byte 40 + expect(encoded1[40]).toBe(0); + + // tokenPoolIndex at byte 41 + expect(encoded1[41]).toBe(0); + + // maxTopUp at bytes 42-43 (u16 little-endian) + expect(encoded1.slice(42, 44)).toEqual(Buffer.from([0, 0])); + + // createMint Option: byte 44 should be 1 (Some) + expect(encoded1[44]).toBe(1); + + // createMint.readOnlyAddressTrees: bytes 45-48 + expect(encoded1.slice(45, 49)).toEqual(Buffer.from([0, 0, 0, 0])); + + // createMint.readOnlyAddressTreeRootIndices: bytes 49-56 (4 x u16) + expect(encoded1.slice(49, 57)).toEqual( + Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), + ); + }); + }); + + describe('encodeCreateMintInstructionData (integration)', () => { + it('should correctly encode create mint instruction data from params', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + const addressTreeInfo = getBatchAddressTreeInfo(); + + // Encode using the actual instruction builder + const encoded = encodeCreateMintInstructionData({ + mintSigner: mintSigner.publicKey, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + decimals: 9, + addressTree: addressTreeInfo.tree, + outputQueue: addressTreeInfo.queue, + rootIndex: 42, + proof: { + a: Array.from(new Uint8Array(32).fill(1)), + b: Array.from(new Uint8Array(64).fill(2)), + c: Array.from(new Uint8Array(32).fill(3)), + }, + }); + + // Discriminator check + expect(encoded[0]).toBe(103); + + // Should be decodable + const decoded = decodeMintActionInstructionData(encoded); + expect(decoded.leafIndex).toBe(0); + expect(decoded.proveByIndex).toBe(false); + expect(decoded.rootIndex).toBe(42); + expect(decoded.createMint).not.toBeNull(); + expect(decoded.mint).not.toBeNull(); + expect(decoded.mint!.decimals).toBe(9); + }); + + it('should correctly derive compressed mint address', () => { + const mintSigner = Keypair.generate(); + const addressTreeInfo = getBatchAddressTreeInfo(); + + // Get the mint PDA + const [mintPda] = findMintAddress(mintSigner.publicKey); + + // Derive the compressed address the same way createMintInterface does + const compressedAddress = deriveAddressV2( + mintPda.toBytes(), + addressTreeInfo.tree, + CTOKEN_PROGRAM_ID, + ); + + // Verify it's a valid 32-byte address + expect(compressedAddress.toBytes().length).toBe(32); + }); + + it('should encode create mint with null proof', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + const addressTreeInfo = getBatchAddressTreeInfo(); + + const encoded = encodeCreateMintInstructionData({ + mintSigner: mintSigner.publicKey, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + decimals: 6, + addressTree: addressTreeInfo.tree, + outputQueue: addressTreeInfo.queue, + rootIndex: 0, + proof: null, + }); + + const decoded = decodeMintActionInstructionData(encoded); + expect(decoded.proof).toBeNull(); + }); + + it('should encode create mint with metadata', () => { + const mintSigner = Keypair.generate(); + const mintAuthority = Keypair.generate(); + const addressTreeInfo = getBatchAddressTreeInfo(); + + const encoded = encodeCreateMintInstructionData({ + mintSigner: mintSigner.publicKey, + mintAuthority: mintAuthority.publicKey, + freezeAuthority: null, + decimals: 9, + addressTree: addressTreeInfo.tree, + outputQueue: addressTreeInfo.queue, + rootIndex: 100, + proof: { + a: Array.from(new Uint8Array(32).fill(10)), + b: Array.from(new Uint8Array(64).fill(20)), + c: Array.from(new Uint8Array(32).fill(30)), + }, + metadata: { + name: 'Test Token', + symbol: 'TEST', + uri: 'https://test.com/metadata.json', + updateAuthority: mintAuthority.publicKey, + additionalMetadata: null, + }, + }); + + const decoded = decodeMintActionInstructionData(encoded); + expect(decoded.mint!.extensions).not.toBeNull(); + expect(decoded.mint!.extensions!.length).toBe(1); + }); + }); +}); diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index ef40b9f1b1..a227609a5a 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.1-alpha.3", + "version": "0.22.1-alpha.4", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", From 085321c79c39e8d6037eb8a36e7211a29c15fc41 Mon Sep 17 00:00:00 2001 From: Swenschaeferjohann Date: Wed, 24 Dec 2025 00:46:28 +0100 Subject: [PATCH 28/28] fix ci test assert for v1 build, and bump pkgs --- cli/package.json | 2 +- js/compressed-token/package.json | 2 +- js/stateless.js/package.json | 2 +- .../tests/unit/utils/tree-info.test.ts | 63 ++++++++++++------- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/cli/package.json b/cli/package.json index 8c173538fa..69ab37fe8f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/zk-compression-cli", - "version": "0.27.1-alpha.7", + "version": "0.27.1-alpha.8", "description": "ZK Compression: Secure Scaling on Solana", "maintainers": [ { diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 5a0c03e7fa..0c78005ae7 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.1-alpha.5", + "version": "0.22.1-alpha.6", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index a227609a5a..31ea003d8b 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.1-alpha.4", + "version": "0.22.1-alpha.5", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", diff --git a/js/stateless.js/tests/unit/utils/tree-info.test.ts b/js/stateless.js/tests/unit/utils/tree-info.test.ts index 38f1bf03d3..b46eb4e410 100644 --- a/js/stateless.js/tests/unit/utils/tree-info.test.ts +++ b/js/stateless.js/tests/unit/utils/tree-info.test.ts @@ -10,6 +10,7 @@ import { nullifierQueue2Pubkey, nullifierQueuePubkey, localTestActiveStateTreeInfos, + featureFlags, } from '../../../src'; describe('selectStateTreeInfo', () => { @@ -131,20 +132,7 @@ describe('selectStateTreeInfo', () => { it('should correctly set tree types for BMT (V2) trees', () => { const allTrees = localTestActiveStateTreeInfos(); - // Check BMT trees have StateV2 type - const bmtTrees = allTrees.filter((t: TreeInfo) => - t.tree.toBase58().startsWith('bmt'), - ); - expect(bmtTrees.length).toBeGreaterThan(0); - - for (const tree of bmtTrees) { - expect(tree.treeType).toBe(TreeType.StateV2); - expect(tree.treeType).not.toBe(TreeType.StateV1); - // Verify the numeric value is 3 (StateV2) - expect(tree.treeType).toBe(3); - } - - // Check SMT trees have StateV1 type + // Check SMT trees have StateV1 type (always present) const smtTrees = allTrees.filter((t: TreeInfo) => t.tree.toBase58().startsWith('smt'), ); @@ -156,16 +144,43 @@ describe('selectStateTreeInfo', () => { expect(tree.treeType).toBe(1); } - // Check V2 address tree has AddressV2 type - const amt2Trees = allTrees.filter((t: TreeInfo) => - t.tree.toBase58().startsWith('amt2'), - ); - expect(amt2Trees.length).toBe(1); - - for (const tree of amt2Trees) { - expect(tree.treeType).toBe(TreeType.AddressV2); - // Verify the numeric value is 4 (AddressV2) - expect(tree.treeType).toBe(4); + // V2 trees (BMT state trees and AddressV2) are only present when featureFlags.isV2() + if (featureFlags.isV2()) { + // Check BMT trees have StateV2 type + const bmtTrees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('bmt'), + ); + expect(bmtTrees.length).toBeGreaterThan(0); + + for (const tree of bmtTrees) { + expect(tree.treeType).toBe(TreeType.StateV2); + expect(tree.treeType).not.toBe(TreeType.StateV1); + // Verify the numeric value is 3 (StateV2) + expect(tree.treeType).toBe(3); + } + + // Check V2 address tree has AddressV2 type + const amt2Trees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('amt2'), + ); + expect(amt2Trees.length).toBe(1); + + for (const tree of amt2Trees) { + expect(tree.treeType).toBe(TreeType.AddressV2); + // Verify the numeric value is 4 (AddressV2) + expect(tree.treeType).toBe(4); + } + } else { + // In V1 mode, BMT and AddressV2 trees should NOT be present + const bmtTrees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('bmt'), + ); + expect(bmtTrees.length).toBe(0); + + const amt2Trees = allTrees.filter((t: TreeInfo) => + t.tree.toBase58().startsWith('amt2'), + ); + expect(amt2Trees.length).toBe(0); } }); });