diff --git a/.github/actions/setup-and-build/action.yml b/.github/actions/setup-and-build/action.yml index 460362f5fc..1c4fd06593 100644 --- a/.github/actions/setup-and-build/action.yml +++ b/.github/actions/setup-and-build/action.yml @@ -1,6 +1,12 @@ name: Setup and build description: Checkout sources, install dependencies, build and prepare for tests +inputs: + components: + description: 'Comma-separated list of components to install (e.g., "node,pnpm,solana,anchor,jq,dependencies"). If not specified, all components are installed.' + required: false + default: "" + runs: using: "composite" steps: @@ -34,7 +40,11 @@ runs: if: steps.restore-local-cache.outputs.cache-hit != 'true' shell: bash run: | - ./scripts/install.sh + if [ -n "${{ inputs.components }}" ]; then + ./scripts/install.sh --components "${{ inputs.components }}" + else + ./scripts/install.sh + fi - name: Set local environment shell: bash diff --git a/.github/workflows/cli-v1.yml b/.github/workflows/cli-v1.yml new file mode 100644 index 0000000000..eca9daff02 --- /dev/null +++ b/.github/workflows/cli-v1.yml @@ -0,0 +1,89 @@ +on: + push: + branches: + - main + pull_request: + branches: + - "*" + types: + - opened + - synchronize + - reopened + - ready_for_review + +name: cli-tests-v1 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cli-v1: + name: cli-v1 + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + services: + redis: + image: redis:8.0.1 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + LIGHT_PROTOCOL_VERSION: V1 + REDIS_URL: redis://localhost:6379 + CI: true + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Cache nx + uses: actions/cache@v4 + with: + path: | + .nx/cache + node_modules/.cache/nx + js/stateless.js/node_modules/.cache/nx + js/compressed-token/node_modules/.cache/nx + cli/node_modules/.cache/nx + key: nx-cli-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}-${{ github.sha }} + restore-keys: | + nx-cli-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}- + nx-cli-v1-${{ runner.os }}- + + - name: Setup and build + uses: ./.github/actions/setup-and-build + + - name: Build stateless.js with V1 + run: | + source ./scripts/devenv.sh + cd js/stateless.js + pnpm build:v1 + + - name: Build compressed-token with V1 + run: | + source ./scripts/devenv.sh + cd js/compressed-token + pnpm build:v1 + + - name: Build CLI with V1 + run: | + source ./scripts/devenv.sh + npx nx build @lightprotocol/zk-compression-cli + + - name: Run CLI tests with V1 + run: | + source ./scripts/devenv.sh + npx nx test @lightprotocol/zk-compression-cli + + - name: Display prover logs on failure + if: failure() + run: | + echo "=== Displaying prover logs ===" + find cli/test-ledger -name "*prover*.log" -type f -exec echo "=== Contents of {} ===" \; -exec cat {} \; -exec echo "=== End of {} ===" \; || echo "No prover logs found" diff --git a/.github/workflows/cli-v2.yml b/.github/workflows/cli-v2.yml new file mode 100644 index 0000000000..d2e4683e70 --- /dev/null +++ b/.github/workflows/cli-v2.yml @@ -0,0 +1,89 @@ +on: + push: + branches: + - main + pull_request: + branches: + - "*" + types: + - opened + - synchronize + - reopened + - ready_for_review + +name: cli-tests-v2 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + cli-v2: + name: cli-v2 + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + services: + redis: + image: redis:8.0.1 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + LIGHT_PROTOCOL_VERSION: V2 + REDIS_URL: redis://localhost:6379 + CI: true + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Cache nx + uses: actions/cache@v4 + with: + path: | + .nx/cache + node_modules/.cache/nx + js/stateless.js/node_modules/.cache/nx + js/compressed-token/node_modules/.cache/nx + cli/node_modules/.cache/nx + key: nx-cli-v2-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}-${{ github.sha }} + restore-keys: | + nx-cli-v2-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}- + nx-cli-v2-${{ runner.os }}- + + - name: Setup and build + uses: ./.github/actions/setup-and-build + + - name: Build stateless.js with V2 + run: | + source ./scripts/devenv.sh + cd js/stateless.js + pnpm build:v2 + + - name: Build compressed-token with V2 + run: | + source ./scripts/devenv.sh + cd js/compressed-token + pnpm build:v2 + + - name: Build CLI with V2 + run: | + source ./scripts/devenv.sh + npx nx build @lightprotocol/zk-compression-cli + + - name: Run CLI tests with V2 + run: | + source ./scripts/devenv.sh + npx nx test @lightprotocol/zk-compression-cli + + - name: Display prover logs on failure + if: failure() + run: | + echo "=== Displaying prover logs ===" + find . -path "*/test-ledger/*prover*.log" -type f -exec echo "=== Contents of {} ===" \; -exec cat {} \; -exec echo "=== End of {} ===" \; || echo "No prover logs found" diff --git a/.github/workflows/js-v2.yml b/.github/workflows/js-v2.yml new file mode 100644 index 0000000000..f698603cac --- /dev/null +++ b/.github/workflows/js-v2.yml @@ -0,0 +1,94 @@ +on: + push: + branches: + - main + pull_request: + branches: + - "*" + types: + - opened + - synchronize + - reopened + - ready_for_review + +name: js-tests-v2 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + stateless-js-v2: + name: stateless-js-v2 + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + services: + redis: + image: redis:8.0.1 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + env: + LIGHT_PROTOCOL_VERSION: V2 + REDIS_URL: redis://localhost:6379 + CI: true + + steps: + - name: Checkout sources + uses: actions/checkout@v4 + + - name: Cache nx + uses: actions/cache@v4 + with: + path: | + .nx/cache + node_modules/.cache/nx + js/stateless.js/node_modules/.cache/nx + js/compressed-token/node_modules/.cache/nx + cli/node_modules/.cache/nx + key: nx-js-v2-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}-${{ github.sha }} + restore-keys: | + nx-js-v2-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}- + nx-js-v2-${{ runner.os }}- + + - name: Setup and build + uses: ./.github/actions/setup-and-build + + - name: Build stateless.js with V2 + run: | + source ./scripts/devenv.sh + cd js/stateless.js + pnpm build:v2 + + - name: Build compressed-token with V2 + run: | + source ./scripts/devenv.sh + cd js/compressed-token + pnpm build:v2 + + - name: Build CLI + run: | + source ./scripts/devenv.sh + npx nx build @lightprotocol/zk-compression-cli + + - name: Run stateless.js tests with V2 + run: | + source ./scripts/devenv.sh + npx nx test @lightprotocol/stateless.js + + - name: Run compressed-token tests with V2 + run: | + source ./scripts/devenv.sh + npx nx test @lightprotocol/compressed-token + + - name: Display prover logs on failure + if: failure() + run: | + echo "=== Displaying prover logs ===" + find . -path "*/test-ledger/*prover*.log" -type f -exec echo "=== Contents of {} ===" \; -exec cat {} \; -exec echo "=== End of {} ===" \; || echo "No prover logs found" diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index f8da21ae13..85169a05fb 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -11,15 +11,15 @@ on: - reopened - ready_for_review -name: js-tests +name: js-tests-v1 concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - stateless-js: - name: stateless-js + stateless-js-v1: + name: stateless-js-v1 if: github.event.pull_request.draft == false runs-on: ubuntu-latest @@ -35,32 +35,54 @@ jobs: --health-retries 5 env: + LIGHT_PROTOCOL_VERSION: V1 REDIS_URL: redis://localhost:6379 + CI: true steps: - name: Checkout sources uses: actions/checkout@v4 + - name: Cache nx + uses: actions/cache@v4 + with: + path: | + .nx/cache + node_modules/.cache/nx + js/stateless.js/node_modules/.cache/nx + js/compressed-token/node_modules/.cache/nx + cli/node_modules/.cache/nx + key: nx-js-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}-${{ github.sha }} + restore-keys: | + nx-js-v1-${{ runner.os }}-${{ hashFiles('pnpm-lock.yaml', 'js/**/package.json', 'cli/package.json') }}- + nx-js-v1-${{ runner.os }}- + - name: Setup and build uses: ./.github/actions/setup-and-build - - name: Build CLI + - name: Build stateless.js with V1 + run: | + source ./scripts/devenv.sh + cd js/stateless.js + pnpm build:v1 + + - name: Build compressed-token with V1 run: | source ./scripts/devenv.sh - npx nx build @lightprotocol/zk-compression-cli --skip-nx-cache + cd js/compressed-token + pnpm build:v1 - # Comment for breaking changes to Photon - - name: Run CLI tests + - name: Build CLI run: | source ./scripts/devenv.sh - npx nx test @lightprotocol/zk-compression-cli + npx nx build @lightprotocol/zk-compression-cli - - name: Run stateless.js tests + - name: Run stateless.js tests with V1 run: | source ./scripts/devenv.sh npx nx test @lightprotocol/stateless.js - - name: Run compressed-token tests + - name: Run compressed-token tests with V1 run: | source ./scripts/devenv.sh npx nx test @lightprotocol/compressed-token diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eec2218324..9f1bf76a3c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,6 +14,8 @@ jobs: - name: Setup and build uses: ./.github/actions/setup-and-build + with: + components: "rust,node,pnpm,solana,anchor,jq,keys,dependencies" - name: Install cargo-workspaces run: | diff --git a/cli/src/utils/initTestEnv.ts b/cli/src/utils/initTestEnv.ts index 47d16b6b35..a2356de889 100644 --- a/cli/src/utils/initTestEnv.ts +++ b/cli/src/utils/initTestEnv.ts @@ -149,7 +149,14 @@ export async function initTestEnv({ const config = getConfig(); config.proverUrl = `http://127.0.0.1:${proverPort}`; setConfig(config); - await startProver(proverPort, proverRunMode, circuits); + try { + // TODO: check if using redisUrl is better here. + await startProver(proverPort, proverRunMode, circuits); + } catch (error) { + console.error("Failed to start prover:", error); + // Prover logs will be automatically displayed by spawnBinary in process.ts + throw error; + } } } diff --git a/cli/src/utils/process.ts b/cli/src/utils/process.ts index ec70832a36..f3055ced41 100644 --- a/cli/src/utils/process.ts +++ b/cli/src/utils/process.ts @@ -7,6 +7,47 @@ import { promisify } from "util"; import axios from "axios"; const waitOn = require("wait-on"); +const readdir = promisify(fs.readdir); +const readFile = promisify(fs.readFile); + +/** + * Logs the contents of prover log files in test-ledger dir. + */ +export async function logProverFileContents() { + const testLedgerDir = path.join(__dirname, "../..", "test-ledger"); + + try { + if (!fs.existsSync(testLedgerDir)) { + console.log("test-ledger directory does not exist"); + return; + } + + const files = await readdir(testLedgerDir); + + const proverFiles = files.filter((file) => file.includes("prover")); + + if (proverFiles.length === 0) { + console.log("No prover log files found in test-ledger directory"); + return; + } + + for (const file of proverFiles) { + const filePath = path.join(testLedgerDir, file); + console.log(`\n========== Contents of ${file} ==========`); + + try { + const contents = await readFile(filePath, "utf8"); + console.log(contents); + console.log(`========== End of ${file} ==========\n`); + } catch (error) { + console.error(`Error reading ${file}:`, error); + } + } + } catch (error) { + console.error("Error accessing test-ledger directory:", error); + } +} + export async function killProcess(processName: string) { const processList = await find("name", processName); @@ -174,8 +215,13 @@ export function spawnBinary(command: string, args: string[] = []) { detached: true, }); - spawnedProcess.on("close", (code) => { + spawnedProcess.on("close", async (code) => { console.log(`${binaryName} process exited with code ${code}`); + // Log prover file contents if prover exits with non-zero code + if (code !== 0 && binaryName.includes("prover")) { + console.error(`Prover process failed with exit code ${code}`); + await logProverFileContents(); + } }); return spawnedProcess; diff --git a/cli/src/utils/processProverServer.ts b/cli/src/utils/processProverServer.ts index bdd7f0b84c..38359c2911 100644 --- a/cli/src/utils/processProverServer.ts +++ b/cli/src/utils/processProverServer.ts @@ -131,9 +131,17 @@ export async function startProver( args.push("--redis-url", redisUrl); } - spawnBinary(getProverPathByArch(), args); - await waitForServers([{ port: proverPort, path: "/" }]); - console.log(`Prover started successfully!`); + const proverProcess = spawnBinary(getProverPathByArch(), args); + + try { + await waitForServers([{ port: proverPort, path: "/" }]); + console.log(`Prover started successfully!`); + } catch (error) { + console.error( + "Failed to start prover - prover logs will be displayed above", + ); + throw error; + } } export function getProverNameByArch(): string { diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 9b0fab25b5..f8fc8bad4c 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -78,8 +78,12 @@ }, "scripts": { "test": "pnpm test:e2e:all", + "test:v1": "LIGHT_PROTOCOL_VERSION=V1 pnpm test", + "test:v2": "LIGHT_PROTOCOL_VERSION=V2 pnpm test", "test-all": "vitest run", "test:unit:all": "EXCLUDE_E2E=true vitest run", + "test:unit:all:v1": "LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit --reporter=verbose", + "test:unit:all:v2": "LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit --reporter=verbose", "test-all:verbose": "vitest run --reporter=verbose", "test-validator": "./../../cli/test_bin/run test-validator --prover-run-mode rpc", "test-validator-skip-prover": "./../../cli/test_bin/run test-validator --skip-prover", @@ -102,8 +106,12 @@ "test:e2e:multi-pool": "pnpm test-validator && vitest run tests/e2e/multi-pool.test.ts --reporter=verbose", "test:e2e: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", "pull-idl": "../../scripts/push-compressed-token-idl.sh", - "build": "rimraf dist && pnpm build:bundle", - "build:bundle": "rollup -c", + "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", + "build:v1": "LIGHT_PROTOCOL_VERSION=V1 pnpm build:stateless:v1 && LIGHT_PROTOCOL_VERSION=V1 pnpm build:bundle", + "build:v2": "LIGHT_PROTOCOL_VERSION=V2 pnpm build:stateless:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm build:bundle", + "build:stateless:v1": "cd ../stateless.js && pnpm build:v1", + "build:stateless:v2": "cd ../stateless.js && pnpm build:v2", "format": "prettier --write .", "lint": "eslint ." }, diff --git a/js/compressed-token/rollup.config.js b/js/compressed-token/rollup.config.js index 4fac8ae5ef..f19a4b3c29 100644 --- a/js/compressed-token/rollup.config.js +++ b/js/compressed-token/rollup.config.js @@ -1,3 +1,4 @@ +/* global process */ import typescript from '@rollup/plugin-typescript'; import nodePolyfills from 'rollup-plugin-polyfill-node'; import dts from 'rollup-plugin-dts'; @@ -6,6 +7,7 @@ import commonjs from '@rollup/plugin-commonjs'; import alias from '@rollup/plugin-alias'; import json from '@rollup/plugin-json'; import terser from '@rollup/plugin-terser'; +import replace from '@rollup/plugin-replace'; const rolls = (fmt, env) => ({ input: 'src/index.ts', @@ -22,6 +24,14 @@ const rolls = (fmt, env) => ({ '@lightprotocol/stateless.js', ], plugins: [ + replace({ + preventAssignment: true, + values: { + __BUILD_VERSION__: JSON.stringify( + process.env.LIGHT_PROTOCOL_VERSION || 'V1', + ), + }, + }), json(), typescript({ target: fmt === 'es' ? 'ES2022' : 'ES2017', diff --git a/js/compressed-token/src/types.ts b/js/compressed-token/src/types.ts index 65aa9fd6b1..09ff12f1cd 100644 --- a/js/compressed-token/src/types.ts +++ b/js/compressed-token/src/types.ts @@ -73,7 +73,6 @@ export type BatchCompressInstructionData = { bump: number; }; - export type MintToInstructionData = { recipients: PublicKey[]; amounts: BN[]; diff --git a/js/compressed-token/src/utils/version-check.ts b/js/compressed-token/src/utils/version-check.ts new file mode 100644 index 0000000000..41b0bc4f83 --- /dev/null +++ b/js/compressed-token/src/utils/version-check.ts @@ -0,0 +1,40 @@ +import { featureFlags, VERSION } from '@lightprotocol/stateless.js'; + +/** + * Validates that the built version of stateless.js matches the expected version. + * Throws an error if there's a mismatch. + * + * @param expectedVersion - The version expected (defaults to LIGHT_PROTOCOL_VERSION env var or V1) + * @throws Error if the versions don't match + */ +export function validateVersionConsistency(expectedVersion?: string): void { + const expected = + expectedVersion || process.env.LIGHT_PROTOCOL_VERSION || VERSION.V1; + const actual = featureFlags.version.replace(/['"]/g, ''); + + if (actual !== expected) { + throw new Error( + `Version mismatch detected!\n` + + `Expected: ${expected} (from ${expectedVersion ? 'parameter' : 'LIGHT_PROTOCOL_VERSION env var'})\n` + + `Actual: ${actual} (from built stateless.js)\n\n` + + `This means stateless.js was built with ${actual} but you're trying to use ${expected}.\n` + + `Please rebuild both packages with the same version:\n` + + ` pnpm build:${expected.toLowerCase()}\n` + + `This command will automatically build both stateless.js and compressed-token with ${expected}.`, + ); + } +} + +/** + * Gets the current version from stateless.js + */ +export function getCurrentVersion(): string { + return featureFlags.version.replace(/['"]/g, ''); +} + +/** + * Checks if the current version is V2 + */ +export function isV2(): boolean { + return featureFlags.isV2(); +} diff --git a/js/compressed-token/tests/setup/version-check.ts b/js/compressed-token/tests/setup/version-check.ts new file mode 100644 index 0000000000..9c1fdaedba --- /dev/null +++ b/js/compressed-token/tests/setup/version-check.ts @@ -0,0 +1,16 @@ +import { validateVersionConsistency } from '../../src/utils/version-check'; + +// Only used in tests. +export default function setup() { + console.log('Checking version consistency...'); + + try { + validateVersionConsistency(); + const expectedVersion = process.env.LIGHT_PROTOCOL_VERSION || 'V1'; + console.log(`✅ Version check passed: Using ${expectedVersion}`); + } catch (error) { + console.error('❌ Version check failed:'); + console.error(error.message); + process.exit(1); + } +} diff --git a/js/compressed-token/tests/unit/version.test.ts b/js/compressed-token/tests/unit/version.test.ts new file mode 100644 index 0000000000..fd6b369116 --- /dev/null +++ b/js/compressed-token/tests/unit/version.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect, beforeAll } from 'vitest'; +import { featureFlags, VERSION } from '@lightprotocol/stateless.js'; + +describe('Versioning', () => { + beforeAll(() => { + const expectedVersion = + process.env.LIGHT_PROTOCOL_VERSION || VERSION.V1; + const actualVersion = featureFlags.version.replace(/['"]/g, ''); + + if (actualVersion !== expectedVersion) { + throw new Error( + `Version mismatch detected!\n` + + `Expected: ${expectedVersion} (from LIGHT_PROTOCOL_VERSION env var)\n` + + `Actual: ${actualVersion} (from built stateless.js)\n\n` + + `This means stateless.js was built with ${actualVersion} but you're trying to test with ${expectedVersion}.\n` + + `Please rebuild stateless.js with the correct version:\n` + + ` cd ../stateless.js && pnpm build:${expectedVersion.toLowerCase()}\n` + + `Or use the compressed-token build command that handles this automatically:\n` + + ` pnpm build:${expectedVersion.toLowerCase()}`, + ); + } + }); + + it('should use version from stateless.js', () => { + console.log('Current version from stateless.js:', featureFlags.version); + console.log('isV2() from stateless.js:', featureFlags.isV2()); + console.log( + 'Environment variable:', + process.env.LIGHT_PROTOCOL_VERSION, + ); + + expect(featureFlags.version).toBeDefined(); + const actualVersion = featureFlags.version.replace(/['"]/g, ''); + expect([VERSION.V1, VERSION.V2]).toContain(actualVersion); + }); + + it('should respect LIGHT_PROTOCOL_VERSION environment variable', () => { + const expectedVersion = + process.env.LIGHT_PROTOCOL_VERSION || VERSION.V1; + const actualVersion = featureFlags.version.replace(/['"]/g, ''); + expect(actualVersion).toBe(expectedVersion); + }); + + it('isV2() should return correct value', () => { + const actualVersion = featureFlags.version.replace(/['"]/g, ''); + const expectedIsV2 = actualVersion === VERSION.V2; + expect(featureFlags.isV2()).toBe(expectedIsV2); + }); + + it('compressed-token should use the same version as stateless.js', () => { + const actualVersion = featureFlags.version.replace(/['"]/g, ''); + const isV2 = featureFlags.isV2(); + + if (process.env.LIGHT_PROTOCOL_VERSION === 'V2') { + expect(actualVersion).toBe(VERSION.V2); + expect(isV2).toBe(true); + } else { + expect(actualVersion).toBe(VERSION.V1); + expect(isV2).toBe(false); + } + }); +}); diff --git a/js/compressed-token/vitest.config.ts b/js/compressed-token/vitest.config.ts index 26a728edbe..3bbbf1ad7c 100644 --- a/js/compressed-token/vitest.config.ts +++ b/js/compressed-token/vitest.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ testTimeout: 350000, hookTimeout: 100000, reporters: ['verbose'], + globalSetup: './tests/setup/version-check.ts', }, define: { 'import.meta.vitest': false, diff --git a/js/stateless.js/.eslintignore b/js/stateless.js/.eslintignore index d2939734e4..26caad229d 100644 --- a/js/stateless.js/.eslintignore +++ b/js/stateless.js/.eslintignore @@ -1,3 +1,4 @@ node_modules lib dist +test-version.js diff --git a/js/stateless.js/BUILD.md b/js/stateless.js/BUILD.md new file mode 100644 index 0000000000..a3a6a37a08 --- /dev/null +++ b/js/stateless.js/BUILD.md @@ -0,0 +1,16 @@ +Stateless.js compiles with V1 API by default. To switch over to V2 endpoints (with backward compatibility for V1 state), run: + +```bash +pnpm build:v2 +# or +LIGHT_PROTOCOL_VERSION=V2 pnpm build +``` + +## Usage in Code + +```typescript +// From rpc.ts +const endpoint = featureFlags.isV2() + ? versionedEndpoint('getCompressedAccountV2') + : versionedEndpoint('getCompressedAccount'); +``` diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index f5430e0bb9..b8a18226a2 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -87,7 +87,11 @@ "scripts": { "test": "pnpm test:unit:all && pnpm test:e2e:all", "test-all": "vitest run", + "test:v1": "LIGHT_PROTOCOL_VERSION=V1 pnpm test", + "test:v2": "LIGHT_PROTOCOL_VERSION=V2 pnpm test", "test:unit:all": "vitest run tests/unit --reporter=verbose", + "test:unit:all:v1": "LIGHT_PROTOCOL_VERSION=V1 vitest run tests/unit --reporter=verbose", + "test:unit:all:v2": "LIGHT_PROTOCOL_VERSION=V2 vitest run tests/unit --reporter=verbose", "test:unit:tree-info": "vitest run tests/unit/utils/tree-info.test.ts --reporter=verbose", "test:conversions": "vitest run tests/unit/utils/conversion.test.ts --reporter=verbose", "test-validator": "./../../cli/test_bin/run test-validator --prover-run-mode rpc", @@ -96,7 +100,7 @@ "test:e2e:compress": "pnpm test-validator && vitest run tests/e2e/compress.test.ts --reporter=verbose", "test:e2e:test-rpc": "pnpm test-validator && vitest run tests/e2e/test-rpc.test.ts --reporter=verbose --bail=1", "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", + "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:index": "vitest run tests/e2e/program.test.ts", @@ -105,8 +109,10 @@ "test:verbose": "vitest run --reporter=verbose", "test:testnet": "vitest run tests/e2e/testnet.test.ts --reporter=verbose", "pull-idls": "../../scripts/push-stateless-js-idls.sh && ../../scripts/push-compressed-token-idl.sh", - "build": "rimraf dist && pnpm build:bundle", - "build:bundle": "rollup -c", + "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", + "build:v1": "LIGHT_PROTOCOL_VERSION=V1 pnpm build:bundle", + "build:v2": "LIGHT_PROTOCOL_VERSION=V2 pnpm build:bundle", "format": "prettier --write .", "lint": "eslint ." }, diff --git a/js/stateless.js/rollup.config.js b/js/stateless.js/rollup.config.js index c78a4a1ea1..c42b978ea9 100644 --- a/js/stateless.js/rollup.config.js +++ b/js/stateless.js/rollup.config.js @@ -1,11 +1,13 @@ +/* global process */ import typescript from '@rollup/plugin-typescript'; import nodePolyfills from 'rollup-plugin-polyfill-node'; import dts from 'rollup-plugin-dts'; import resolve from '@rollup/plugin-node-resolve'; import commonjs from '@rollup/plugin-commonjs'; import terser from '@rollup/plugin-terser'; - +import replace from '@rollup/plugin-replace'; import json from '@rollup/plugin-json'; + const rolls = (fmt, env) => ({ input: 'src/index.ts', output: { @@ -16,6 +18,14 @@ const rolls = (fmt, env) => ({ }, external: ['@solana/web3.js'], plugins: [ + replace({ + preventAssignment: true, + values: { + __BUILD_VERSION__: JSON.stringify( + process.env.LIGHT_PROTOCOL_VERSION || 'V1', + ), + }, + }), typescript({ target: fmt === 'es' ? 'ES2022' : 'ES2017', outDir: `dist/${fmt}/${env}`, diff --git a/js/stateless.js/src/constants.ts b/js/stateless.js/src/constants.ts index af9041e6f4..b34cfaaff5 100644 --- a/js/stateless.js/src/constants.ts +++ b/js/stateless.js/src/constants.ts @@ -14,8 +14,24 @@ export enum VERSION { * Feature flags. Only use if you know what you are doing. */ export const featureFlags = { - version: VERSION.V1, - isV2: () => featureFlags.version.toUpperCase() === 'V2', + version: ((): VERSION => { + // Check if we're in a build environment (replaced by rollup) + // eslint-disable-next-line no-constant-condition + if ('__BUILD_VERSION__' !== '__BUILD_' + 'VERSION__') { + return '__BUILD_VERSION__' as VERSION; + } + // Otherwise, check runtime environment variable (for tests) + if ( + typeof process !== 'undefined' && + process.env?.LIGHT_PROTOCOL_VERSION + ) { + return process.env.LIGHT_PROTOCOL_VERSION as VERSION; + } + // Default to V1 + return VERSION.V1; + })(), + isV2: () => + featureFlags.version.replace(/['"]/g, '').toUpperCase() === 'V2', }; /** @@ -24,7 +40,7 @@ export const featureFlags = { * or 'getCompressedAccountV2' (V2) */ export const versionedEndpoint = (base: string) => - featureFlags.version.toUpperCase() === 'V1' ? base : `${base}V2`; + featureFlags.isV2() ? `${base}V2` : base; export const FIELD_SIZE = new BN( '21888242871839275222246405745257275088548364400416034343698204186575808495617', diff --git a/js/stateless.js/src/globals.d.ts b/js/stateless.js/src/globals.d.ts new file mode 100644 index 0000000000..b2ba4b3072 --- /dev/null +++ b/js/stateless.js/src/globals.d.ts @@ -0,0 +1 @@ +declare const __BUILD_VERSION__: 'V1' | 'V2'; diff --git a/js/stateless.js/src/rpc.ts b/js/stateless.js/src/rpc.ts index 9d57305700..9bc3660e5f 100644 --- a/js/stateless.js/src/rpc.ts +++ b/js/stateless.js/src/rpc.ts @@ -339,7 +339,7 @@ export const proverRequest = async ( method: 'inclusion' | 'new-address' | 'combined', params: any = [], log = false, - publicInputHash: BN | undefined = undefined, + _publicInputHash: BN | undefined = undefined, // Not supported. ): Promise => { let logMsg: string = ''; @@ -1065,7 +1065,7 @@ export class Rpc extends Connection implements CompressionApiInterface { stateTreeInfo, bn(item.hash.toArray('be', 32)), item.leafIndex, - false, + featureFlags.isV2() ? item.proveByIndex : false, ), item.owner, bn(item.lamports), @@ -1892,7 +1892,7 @@ export class Rpc extends Connection implements CompressionApiInterface { .concat(value.addresses.map((r: any) => r.rootIndex)), proveByIndices: value.accounts .map((r: any) => r.rootIndex.proveByIndex) - .concat(value.addresses.map((r: any) => false)), + .concat(value.addresses.map((r: any) => false)), // addresses.proveByIndex is always false. treeInfos: value.accounts .map((r: any) => r.merkleContext) .concat( diff --git a/js/stateless.js/test-version.js b/js/stateless.js/test-version.js new file mode 100644 index 0000000000..9f76abe3ec --- /dev/null +++ b/js/stateless.js/test-version.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +// Test script to verify the compiled version is correctly set +import { featureFlags } from './dist/cjs/node/index.cjs'; + +console.log('Testing build version...'); +console.log('featureFlags.version:', featureFlags.version); +console.log('featureFlags.isV2():', featureFlags.isV2()); + +const expectedVersion = process.env.EXPECTED_VERSION || 'V1'; +const actualVersion = featureFlags.version.replace(/['"]/g, ''); +if (actualVersion === expectedVersion) { + console.log(`✅ Success: Version is correctly set to ${expectedVersion}`); + process.exit(0); +} else { + console.error( + `❌ Error: Expected version ${expectedVersion} but got ${actualVersion}`, + ); + process.exit(1); +} diff --git a/js/stateless.js/tests/unit/version.test.ts b/js/stateless.js/tests/unit/version.test.ts new file mode 100644 index 0000000000..76285d96fd --- /dev/null +++ b/js/stateless.js/tests/unit/version.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect } from 'vitest'; +import { featureFlags, VERSION } from '../../src/constants'; + +describe('Version System', () => { + it('should have version set', () => { + console.log('Current version:', featureFlags.version); + console.log('isV2():', featureFlags.isV2()); + console.log( + 'Environment variable:', + process.env.LIGHT_PROTOCOL_VERSION, + ); + + expect(featureFlags.version).toBeDefined(); + expect([VERSION.V1, VERSION.V2]).toContain(featureFlags.version); + }); + + it('should respect LIGHT_PROTOCOL_VERSION environment variable', () => { + const expectedVersion = + process.env.LIGHT_PROTOCOL_VERSION || VERSION.V1; + expect(featureFlags.version).toBe(expectedVersion); + }); + + it('isV2() should return correct value', () => { + const expectedIsV2 = featureFlags.version === VERSION.V2; + expect(featureFlags.isV2()).toBe(expectedIsV2); + }); +}); diff --git a/nx.json b/nx.json index 3c8bc1b96c..e5f23b2f79 100644 --- a/nx.json +++ b/nx.json @@ -9,7 +9,7 @@ }, "targetDefaults": { "build": { - "cache": false, + "cache": true, "inputs": [ "noMarkdown", "^noMarkdown", @@ -22,11 +22,14 @@ "outputs": [ "{workspaceRoot}/target/deploy", "{workspaceRoot}/target/idl", - "{workspaceRoot}/target/types" + "{workspaceRoot}/target/types", + "{projectRoot}/dist", + "{projectRoot}/lib", + "{projectRoot}/bin" ] }, "test": { - "cache": false, + "cache": true, "inputs": [ "noMarkdown", "^noMarkdown", @@ -65,5 +68,17 @@ "^noTestLedger" ] } + }, + "daemon": { + "enabled": false + }, + "tasksRunnerOptions": { + "default": { + "runner": "nx/tasks-runners/default", + "options": { + "cacheableOperations": ["build", "test"], + "cacheDirectory": ".nx/cache" + } + } } } diff --git a/scripts/INSTALL.md b/scripts/INSTALL.md new file mode 100644 index 0000000000..525626e8b9 --- /dev/null +++ b/scripts/INSTALL.md @@ -0,0 +1,40 @@ +# Component-Based Installation + +The `install.sh` script now supports selective component installation to reduce CI/CD times. + +## Usage + +```bash +# Install all components (default) +./scripts/install.sh + +# Install specific components only +./scripts/install.sh --components "node,pnpm,dependencies" + +# Install with full keys +./scripts/install.sh --full-keys --components "rust,solana,anchor" +``` + +## Available Components + +- `go` - Golang +- `rust` - Rust toolchain +- `node` - Node.js runtime +- `pnpm` - Package manager +- `solana` - Solana CLI tools +- `anchor` - Anchor +- `jq` - JSON processor +- `keys` - Gnark proving keys +- `dependencies` - all PNPM deps +- `redis` - Redis server (not needed for some tests) + +## GitHub Actions Usage + +In workflow files, specify components via the `setup-and-build` action: + +```yaml +- name: Setup and build + uses: ./.github/actions/setup-and-build + with: + components: "node,pnpm,solana,anchor,jq,keys,dependencies" +``` diff --git a/scripts/devenv.sh b/scripts/devenv.sh index 66c3867751..c164e39e98 100755 --- a/scripts/devenv.sh +++ b/scripts/devenv.sh @@ -66,6 +66,7 @@ PATH="${CARGO_HOME}/bin:${PATH}" # Export the modified PATH export PATH +# Comment to start prover without redis export REDIS_URL="redis://localhost:6379" # Enable small_ix feature by default in devenv diff --git a/scripts/install.sh b/scripts/install.sh index bfed6da5a3..5dbe444988 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -337,6 +337,9 @@ main() { # Parse command line arguments local key_type="light" local reset_log=true + local components="" + local install_all=true + while [[ $# -gt 0 ]]; do case $1 in --full-keys) @@ -347,6 +350,11 @@ main() { reset_log=false shift ;; + --components) + components="$2" + install_all=false + shift 2 + ;; *) echo "Unknown option: $1" exit 1 @@ -358,18 +366,28 @@ main() { rm -f "$INSTALL_LOG" fi - install_go - install_rust - install_node - install_pnpm - install_solana - install_anchor - install_jq - download_gnark_keys "$key_type" - install_dependencies - install_redis + # Helper function to check if component should be installed + should_install() { + local component=$1 + if $install_all; then + return 0 + fi + [[ ",$components," == *",$component,"* ]] + } + + # Install components based on selection + should_install "go" && install_go + should_install "rust" && install_rust + should_install "node" && install_node + should_install "pnpm" && install_pnpm + should_install "solana" && install_solana + should_install "anchor" && install_anchor + should_install "jq" && install_jq + should_install "keys" && download_gnark_keys "$key_type" + should_install "dependencies" && install_dependencies + should_install "redis" && install_redis echo "✨ Light Protocol development dependencies installed" } -main +main "$@"