diff --git a/.github/workflows/js-token-interface-v2.yml b/.github/workflows/js-token-interface-v2.yml new file mode 100644 index 0000000000..c95d46f298 --- /dev/null +++ b/.github/workflows/js-token-interface-v2.yml @@ -0,0 +1,102 @@ +on: + push: + branches: + - main + pull_request: + branches: + - "**" + types: + - opened + - synchronize + - reopened + - ready_for_review + +name: js-token-interface-v2 + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + token-interface-js-v2: + name: token-interface-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@v6 + with: + submodules: true + + - name: Setup and build + uses: ./.github/actions/setup-and-build + with: + skip-components: "redis,disk-cleanup,go" + cache-key: "js" + + - name: Build token-interface with V2 + run: | + cd js/token-interface + pnpm build:v2 + + - name: Build CLI + run: | + just cli build + + - name: Run token-interface unit tests with V2 + run: | + echo "Running token-interface unit tests with retry logic (max 2 attempts)..." + attempt=1 + max_attempts=2 + until just js test-token-interface-unit-v2; do + attempt=$((attempt + 1)) + if [ $attempt -gt $max_attempts ]; then + echo "Tests failed after $max_attempts attempts" + exit 1 + fi + echo "Attempt $attempt/$max_attempts failed, retrying..." + sleep 5 + done + echo "Tests passed on attempt $attempt" + + - name: Run token-interface e2e tests with V2 + run: | + echo "Running token-interface e2e tests with retry logic (max 2 attempts)..." + attempt=1 + max_attempts=2 + until just js test-token-interface-e2e-v2; do + attempt=$((attempt + 1)) + if [ $attempt -gt $max_attempts ]; then + echo "Tests failed after $max_attempts attempts" + exit 1 + fi + echo "Attempt $attempt/$max_attempts failed, retrying..." + sleep 5 + done + echo "Tests passed on attempt $attempt" + + - 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 index 2408996181..8a0da02be2 100644 --- a/.github/workflows/js-v2.yml +++ b/.github/workflows/js-v2.yml @@ -4,7 +4,7 @@ on: - main pull_request: branches: - - "*" + - "**" types: - opened - synchronize diff --git a/.gitignore b/.gitignore index 17c82f39d6..c297e14be5 100644 --- a/.gitignore +++ b/.gitignore @@ -66,6 +66,7 @@ api-docs/ # Third-party dependencies /.local /.vscode +.cursor/ **/.idea **/*.iml diff --git a/js/justfile b/js/justfile index 892573cd7c..151dcf5b1e 100644 --- a/js/justfile +++ b/js/justfile @@ -6,8 +6,9 @@ default: build: cd stateless.js && pnpm build cd compressed-token && pnpm build + cd token-interface && pnpm build -test: test-stateless test-compressed-token +test: test-stateless test-compressed-token test-token-interface test-stateless: cd stateless.js && pnpm test @@ -15,13 +16,24 @@ test-stateless: test-compressed-token: cd compressed-token && pnpm test +test-token-interface: + cd token-interface && pnpm test + +test-token-interface-unit-v2: + cd token-interface && pnpm test:unit:all + +test-token-interface-e2e-v2: + cd token-interface && pnpm test:e2e:all + test-compressed-token-unit-v2: cd compressed-token && pnpm test:unit:all:v2 lint: cd stateless.js && pnpm lint cd compressed-token && pnpm lint + cd token-interface && pnpm lint format: cd stateless.js && pnpm format cd compressed-token && pnpm format + cd token-interface && pnpm format diff --git a/js/token-interface/.prettierrc b/js/token-interface/.prettierrc new file mode 100644 index 0000000000..59be93e26f --- /dev/null +++ b/js/token-interface/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "useTabs": false, + "tabWidth": 4, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/js/token-interface/README.md b/js/token-interface/README.md new file mode 100644 index 0000000000..34e7b08d20 --- /dev/null +++ b/js/token-interface/README.md @@ -0,0 +1,104 @@ +# `@lightprotocol/token-interface` + +Payments-focused helpers for Light rent-free token flows. + +Use this when you want SPL-style transfers with unified sender handling: + +- sender side auto wraps/loads into light ATA +- recipient ATA can be light (default), SPL, or Token-2022 via `tokenProgram` + +## RPC client (required) + +All builders expect `createRpc()` from `@lightprotocol/stateless.js`. + +```ts +import { createRpc } from '@lightprotocol/stateless.js'; + +// Add this to your client. It is a superset of web3.js Connection RPC plus Light APIs. +const rpc = createRpc(); +// Optional: createRpc(clusterUrl) +``` + +## Canonical for Kit users + +Use `createTransferInstructionPlan` from `/kit`. + +```ts +import { createTransferInstructionPlan } from '@lightprotocol/token-interface/kit'; + +const transferPlan = await createTransferInstructionPlan({ + rpc, + payer: payer.publicKey, + mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: customer.publicKey, + // Optional destination program: + // tokenProgram: TOKEN_PROGRAM_ID + amount: 25n, +}); +``` + +If you prefer Kit instruction arrays instead of plans: + +```ts +import { createTransferInstructions } from '@lightprotocol/token-interface/kit'; +``` + +## Canonical for web3.js users + +Use `createTransferInstructions` from the root export. + +```ts +import { createTransferInstructions } from '@lightprotocol/token-interface'; + +const instructions = await createTransferInstructions({ + rpc, + payer: payer.publicKey, + mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: customer.publicKey, + amount: 25n, +}); + +// add memo if needed, then build/sign/send transaction +``` + +## Raw single-instruction helpers + +Use these when you want manual orchestration: + +```ts +import { + createAtaInstruction, + createLoadInstruction, + createTransferCheckedInstruction, +} from '@lightprotocol/token-interface/instructions'; +``` + +## No-wrap instruction-flow builders (advanced) + +If you explicitly want to disable automatic sender wrapping, use: + +```ts +import { createTransferInstructionsNowrap } from '@lightprotocol/token-interface/instructions'; +``` + +## Read account + +```ts +import { getAta } from '@lightprotocol/token-interface'; + +const account = await getAta({ rpc, owner: customer.publicKey, mint }); +console.log(account.amount, account.hotAmount, account.compressedAmount); +``` + +## Important rules + +- Only one compressed sender account is loaded per call; smaller ones are ignored for that call. +- Transfer always builds checked semantics. +- Canonical builders always use wrap-enabled sender setup (`createTransferInstructions`, `createLoadInstructions`, `createApproveInstructions`, `createRevokeInstructions`). +- If sender SPL/T22 balances are wrapped by the flow, source SPL/T22 ATAs are not auto-closed. +- Recipient ATA is derived from `(recipient, mint, tokenProgram)`; default is light token program. +- Recipient-side load is still intentionally disabled. diff --git a/js/token-interface/cucumber.js b/js/token-interface/cucumber.js new file mode 100644 index 0000000000..f181c869c6 --- /dev/null +++ b/js/token-interface/cucumber.js @@ -0,0 +1,26 @@ +const common = { + requireModule: ['tsx'], + require: [ + 'tests/support/**/*.ts', + 'tests/step-definitions/**/*.steps.ts', + ], + format: ['progress'], + formatOptions: { snippetInterface: 'async-await' }, +}; + +const defaultProfile = { + ...common, + paths: ['features/**/*.feature'], +}; + +export { defaultProfile as default }; + +export const unit = { + ...common, + paths: ['features/unit/**/*.feature'], +}; + +export const e2e = { + ...common, + paths: ['features/e2e/**/*.feature'], +}; diff --git a/js/token-interface/eslint.config.cjs b/js/token-interface/eslint.config.cjs new file mode 100644 index 0000000000..15c2201db4 --- /dev/null +++ b/js/token-interface/eslint.config.cjs @@ -0,0 +1,102 @@ +const js = require('@eslint/js'); +const tseslint = require('@typescript-eslint/eslint-plugin'); +const tsParser = require('@typescript-eslint/parser'); + +module.exports = [ + { + ignores: [ + 'node_modules/**', + 'dist/**', + 'build/**', + 'coverage/**', + '*.config.js', + 'eslint.config.js', + 'jest.config.js', + 'rollup.config.js', + ], + }, + js.configs.recommended, + { + files: ['**/*.js', '**/*.cjs', '**/*.mjs'], + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + require: 'readonly', + module: 'readonly', + process: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + exports: 'readonly', + console: 'readonly', + Buffer: 'readonly', + }, + }, + }, + { + files: [ + 'tests/**/*.ts', + '**/*.steps.ts', + ], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, + 'no-unused-vars': 0, + }, + }, + { + files: ['src/**/*.ts', 'src/**/*.tsx'], + languageOptions: { + parser: tsParser, + parserOptions: { + project: './tsconfig.json', + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, + 'no-unused-vars': 0, + }, + }, +]; diff --git a/js/token-interface/features/e2e/approve-revoke.feature b/js/token-interface/features/e2e/approve-revoke.feature new file mode 100644 index 0000000000..929a07fb9f --- /dev/null +++ b/js/token-interface/features/e2e/approve-revoke.feature @@ -0,0 +1,13 @@ +@e2e +Feature: Approve and revoke delegation + + Scenario: Full approve then revoke cycle + Given a fresh mint fixture + And an owner with 4000 compressed tokens + And a new delegate + When the owner approves the delegate for 1500 tokens + Then the delegate is set on the token account with amount 1500 + And no compute budget instructions were included + When the owner revokes the delegation + Then the delegate is cleared and delegated amount is 0 + And no compute budget instructions were included in revoke diff --git a/js/token-interface/features/e2e/ata-read.feature b/js/token-interface/features/e2e/ata-read.feature new file mode 100644 index 0000000000..3c118cf973 --- /dev/null +++ b/js/token-interface/features/e2e/ata-read.feature @@ -0,0 +1,8 @@ +@e2e +Feature: ATA creation and reads + + Scenario: Create and read back an ATA + Given a fresh mint fixture + And a new owner + When the owner creates an ATA + Then reading the ATA returns the correct address, owner, mint, and zero balance diff --git a/js/token-interface/features/e2e/freeze-thaw.feature b/js/token-interface/features/e2e/freeze-thaw.feature new file mode 100644 index 0000000000..fe9dfd5732 --- /dev/null +++ b/js/token-interface/features/e2e/freeze-thaw.feature @@ -0,0 +1,10 @@ +@e2e +Feature: Freeze and thaw token accounts + + Scenario: Freeze then thaw a hot account + Given a fresh mint fixture with freeze authority + And an owner with a created ATA and 2500 compressed tokens + When the freeze authority freezes the account + Then the account state is "Frozen" + When the freeze authority thaws the account + Then the account state is "Initialized" diff --git a/js/token-interface/features/e2e/load.feature b/js/token-interface/features/e2e/load.feature new file mode 100644 index 0000000000..39a9e3d389 --- /dev/null +++ b/js/token-interface/features/e2e/load.feature @@ -0,0 +1,21 @@ +@e2e +Feature: Load compressed balances into hot account + + Scenario: getAta exposes the biggest compressed balance + Given a fresh mint fixture + And an owner with compressed mints of 400, 300, and 200 tokens + When I read the owner ATA + Then the ATA amount is 400 + And the ATA compressed amount is 400 + And the ATA requires load + And there are 2 ignored compressed accounts totaling 500 + + Scenario: Load one compressed balance per call + Given a fresh mint fixture + And an owner with compressed mints of 500, 300, and 200 tokens + When I load the first compressed balance + Then the hot balance is 500 + And compressed accounts are 300 and 200 + When I load the next compressed balance + Then the hot balance is 800 + And compressed accounts are 200 diff --git a/js/token-interface/features/e2e/transfer.feature b/js/token-interface/features/e2e/transfer.feature new file mode 100644 index 0000000000..3e7910e98b --- /dev/null +++ b/js/token-interface/features/e2e/transfer.feature @@ -0,0 +1,48 @@ +@e2e +Feature: Token transfers + + Scenario: Single-transaction transfer between light-token accounts + Given a fresh mint fixture + And a funded sender with 5000 compressed tokens + And a new recipient + When the sender transfers 2000 tokens to the recipient + Then the recipient ATA balance is 2000 + And the sender ATA balance is 3000 + + Scenario: Transfer to an SPL ATA recipient + Given a fresh mint fixture + And a funded sender with 3000 compressed tokens + And a funded recipient with SOL + When the sender transfers 1250 tokens to the recipient SPL ATA + Then the recipient SPL ATA balance is 1250 + + Scenario: Insufficient funds error propagates from on-chain + Given a fresh mint fixture + And a sender with compressed mints of 500, 300, and 200 tokens + And a new recipient + When the sender attempts to transfer 600 tokens + Then the transaction fails with "custom program error" + + Scenario: Zero-amount transfer preserves balance + Given a fresh mint fixture + And a funded sender with 500 compressed tokens + And a new recipient + When the sender transfers 0 tokens to the recipient + Then the sender ATA balance is 500 + + Scenario: Recipient compressed balance is not loaded during transfer + Given a fresh mint fixture + And a funded sender with 400 compressed tokens + And a funded recipient with 300 compressed tokens + When the sender transfers 200 tokens to the recipient + Then the recipient hot balance is 200 + And the recipient still has 300 in compressed accounts + And the recipient total ATA amount is 500 with 300 compressed + + Scenario: Delegated transfer after approval + Given a fresh mint fixture + And an owner with 500 compressed tokens + And a delegate approved for 300 tokens + When the delegate transfers 250 tokens to a new recipient + Then the recipient ATA balance is 250 + And the owner ATA balance is 250 diff --git a/js/token-interface/features/unit/instruction-builders.feature b/js/token-interface/features/unit/instruction-builders.feature new file mode 100644 index 0000000000..4a51320397 --- /dev/null +++ b/js/token-interface/features/unit/instruction-builders.feature @@ -0,0 +1,28 @@ +@unit +Feature: Light-token instruction builders + Low-level instruction builders produce correct program IDs, + discriminators, and account orderings without any RPC calls. + + Scenario: Create a canonical light-token ATA instruction + Given random keypairs for "payer", "owner", and "mint" + When I build a create-ATA instruction for "payer", "owner", and "mint" + Then the instruction program ID is the light-token program + And account key 0 is "owner" + And account key 1 is "mint" + And account key 2 is "payer" + + Scenario: Create a checked transfer instruction + Given random keypairs for "source", "destination", "mint", "authority", and "payer" + When I build a checked transfer instruction for 42 tokens with 9 decimals + Then the instruction program ID is the light-token program + And the instruction discriminator byte is 12 + And account key 0 is "source" + And account key 2 is "destination" + + Scenario: Create approve, revoke, freeze, and thaw instructions + Given random keypairs for "tokenAccount", "owner", "delegate", "mint", and "freezeAuthority" + When I build approve, revoke, freeze, and thaw instructions + Then the approve instruction targets the light-token program + And the revoke instruction targets the light-token program + And the freeze discriminator byte is 10 + And the thaw discriminator byte is 11 diff --git a/js/token-interface/features/unit/kit-adapter.feature b/js/token-interface/features/unit/kit-adapter.feature new file mode 100644 index 0000000000..ce41afa344 --- /dev/null +++ b/js/token-interface/features/unit/kit-adapter.feature @@ -0,0 +1,18 @@ +@unit +Feature: Solana Kit adapter + The kit module converts legacy web3.js instructions to + Solana Kit compatible instruction objects. + + Scenario: Convert legacy instructions to kit instructions + Given a legacy create-ATA instruction + When I convert it to kit instructions + Then the result is a list of 1 kit instruction object + + Scenario: Wrap canonical builders for kit consumers + Given random keypairs for "payer", "owner", and "mint" + When I call the kit createAtaInstructions builder + Then the result is a list of 1 kit instruction + + Scenario: Transfer and plan builders are exported + Then createTransferInstructions from kit is a function + And createTransferInstructionPlan from kit is a function diff --git a/js/token-interface/features/unit/public-api.feature b/js/token-interface/features/unit/public-api.feature new file mode 100644 index 0000000000..91ec5d0b00 --- /dev/null +++ b/js/token-interface/features/unit/public-api.feature @@ -0,0 +1,30 @@ +@unit +Feature: Public API surface + The root package export exposes address derivation, instruction + builders, discriminators, and error types. + + Scenario: Derive the canonical light-token ATA address + Given random keypairs for "owner" and "mint" + When I derive the ATA address for "owner" and "mint" + Then it matches the low-level getAssociatedTokenAddress result + + Scenario: Build one canonical ATA instruction + Given random keypairs for "payer", "owner", and "mint" + When I build an ATA instruction list for "payer", "owner", and "mint" + Then the result is a list of 1 instruction + And the first instruction program ID is the light-token program + + Scenario: Raw freeze and thaw discriminators + Given random keypairs for "tokenAccount", "mint", and "freezeAuthority" + When I build raw freeze and thaw instructions + Then the freeze discriminator byte is 10 + And the thaw discriminator byte is 11 + + Scenario: Single-transaction error is clear + When I create a MultiTransactionNotSupportedError for "createLoadInstructions" with batch count 2 + Then the error name is "MultiTransactionNotSupportedError" + And the error message contains "single-transaction" + And the error message contains "createLoadInstructions" + + Scenario: Canonical transfer builder is exported + Then createTransferInstructions is a function diff --git a/js/token-interface/package.json b/js/token-interface/package.json new file mode 100644 index 0000000000..1ce3fa9bf0 --- /dev/null +++ b/js/token-interface/package.json @@ -0,0 +1,90 @@ +{ + "name": "@lightprotocol/token-interface", + "version": "0.23.0", + "description": "JS enhancement layer for SPL and Token-2022 flows using Light rent-free token accounts", + "sideEffects": false, + "main": "dist/cjs/index.cjs", + "type": "module", + "exports": { + ".": { + "require": "./dist/cjs/index.cjs", + "import": "./dist/es/index.js", + "types": "./dist/types/index.d.ts" + }, + "./instructions": { + "require": "./dist/cjs/instructions/index.cjs", + "import": "./dist/es/instructions/index.js", + "types": "./dist/types/instructions/index.d.ts" + }, + "./kit": { + "require": "./dist/cjs/kit/index.cjs", + "import": "./dist/es/kit/index.js", + "types": "./dist/types/kit/index.d.ts" + } + }, + "types": "./dist/types/index.d.ts", + "files": [ + "dist" + ], + "maintainers": [ + { + "name": "Light Protocol Maintainers", + "email": "friends@lightprotocol.com" + } + ], + "license": "Apache-2.0", + "peerDependencies": { + "@coral-xyz/borsh": "^0.29.0", + "@lightprotocol/stateless.js": "workspace:*", + "@solana/spl-token": ">=0.3.9", + "@solana/web3.js": ">=1.73.5" + }, + "dependencies": { + "@solana/buffer-layout": "^4.0.1", + "@solana/buffer-layout-utils": "^0.2.0", + "@solana/compat": "^6.5.0", + "@solana/instruction-plans": "^6.5.0", + "@solana/kit": "^6.5.0", + "bn.js": "^5.2.1", + "buffer": "6.0.3" + }, + "devDependencies": { + "@lightprotocol/compressed-token": "workspace:*", + "@eslint/js": "9.36.0", + "@rollup/plugin-commonjs": "^26.0.1", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-typescript": "^11.1.6", + "@types/bn.js": "^5.1.5", + "@types/node": "^22.5.5", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "eslint": "^9.36.0", + "@cucumber/cucumber": "^11.0.0", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "rollup": "^4.21.3", + "rollup-plugin-dts": "^6.1.1", + "tslib": "^2.7.0", + "tsx": "^4.19.0", + "typescript": "^5.6.2" + }, + "scripts": { + "build": "pnpm build:v2", + "build:v2": "pnpm build:deps:v2 && pnpm build:bundle", + "build:deps:v2": "pnpm --dir ../stateless.js build:v2", + "build:bundle": "rimraf dist && rollup -c", + "test": "pnpm test:unit:all && pnpm test:e2e:all", + "test:unit:all": "pnpm build:deps:v2 && LIGHT_PROTOCOL_VERSION=V2 pnpm exec cucumber-js --profile unit", + "test:e2e:all": "pnpm build:deps:v2 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 pnpm exec cucumber-js --profile e2e", + "test-validator": "./../../cli/test_bin/run test-validator", + "lint": "eslint .", + "format": "prettier --write ." + }, + "keywords": [ + "light", + "solana", + "token", + "interface", + "payments" + ] +} diff --git a/js/token-interface/rollup.config.js b/js/token-interface/rollup.config.js new file mode 100644 index 0000000000..f1a41daa1a --- /dev/null +++ b/js/token-interface/rollup.config.js @@ -0,0 +1,71 @@ +import typescript from '@rollup/plugin-typescript'; +import dts from 'rollup-plugin-dts'; +import resolve from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; + +const inputs = { + index: 'src/index.ts', + 'instructions/index': 'src/instructions/index.ts', + 'kit/index': 'src/kit/index.ts', +}; + +const external = [ + '@coral-xyz/borsh', + '@lightprotocol/stateless.js', + '@solana/buffer-layout', + '@solana/buffer-layout-utils', + '@solana/compat', + '@solana/instruction-plans', + '@solana/kit', + '@solana/spl-token', + '@solana/web3.js', + 'bn.js', + 'buffer', +]; + +const jsConfig = format => ({ + input: inputs, + output: { + dir: `dist/${format}`, + format, + entryFileNames: `[name].${format === 'cjs' ? 'cjs' : 'js'}`, + chunkFileNames: `[name]-[hash].${format === 'cjs' ? 'cjs' : 'js'}`, + sourcemap: true, + }, + external, + plugins: [ + typescript({ + target: format === 'es' ? 'ES2022' : 'ES2017', + outDir: `dist/${format}`, + }), + commonjs(), + resolve({ + extensions: ['.mjs', '.js', '.json', '.ts'], + }), + ], + onwarn(warning, warn) { + if (warning.code !== 'CIRCULAR_DEPENDENCY') { + warn(warning); + } + }, +}); + +const dtsEntry = (input, file) => ({ + input, + output: [{ file, format: 'es' }], + external, + plugins: [ + dts({ + respectExternal: true, + tsconfig: './tsconfig.json', + }), + ], +}); + +export default [ + jsConfig('cjs'), + jsConfig('es'), + dtsEntry('src/index.ts', 'dist/types/index.d.ts'), + dtsEntry('src/instructions/index.ts', 'dist/types/instructions/index.d.ts'), + dtsEntry('src/kit/index.ts', 'dist/types/kit/index.d.ts'), +]; diff --git a/js/token-interface/src/account.ts b/js/token-interface/src/account.ts new file mode 100644 index 0000000000..68d2aa6de6 --- /dev/null +++ b/js/token-interface/src/account.ts @@ -0,0 +1,227 @@ +import { getAssociatedTokenAddress } from './read/associated-token-address'; +import { parseLightTokenCold, parseLightTokenHot } from './read/get-account'; +import { Buffer } from 'buffer'; +import { + LIGHT_TOKEN_PROGRAM_ID, + type ParsedTokenAccount, + type Rpc, +} from '@lightprotocol/stateless.js'; +import { TokenAccountNotFoundError } from '@solana/spl-token'; +import type { PublicKey } from '@solana/web3.js'; +import type { + GetAtaInput, + TokenInterfaceAccount, + TokenInterfaceParsedAta, +} from './types'; + +const ZERO = BigInt(0); + +function toBufferAccountInfo( + accountInfo: T, +): Omit & { data: Buffer } { + if (Buffer.isBuffer(accountInfo.data)) { + return accountInfo as Omit & { data: Buffer }; + } + return { + ...accountInfo, + data: Buffer.from(accountInfo.data), + }; +} + +function toBigIntAmount(account: ParsedTokenAccount): bigint { + return BigInt(account.parsed.amount.toString()); +} + +function sortCompressedAccounts( + accounts: ParsedTokenAccount[], +): ParsedTokenAccount[] { + return [...accounts].sort((left, right) => { + const leftAmount = toBigIntAmount(left); + const rightAmount = toBigIntAmount(right); + + if (rightAmount > leftAmount) { + return 1; + } + + if (rightAmount < leftAmount) { + return -1; + } + + return ( + right.compressedAccount.leafIndex - left.compressedAccount.leafIndex + ); + }); +} + +function clampDelegatedAmount(amount: bigint, delegatedAmount: bigint): bigint { + return delegatedAmount < amount ? delegatedAmount : amount; +} + +function buildParsedAta( + address: PublicKey, + owner: PublicKey, + mint: PublicKey, + hotParsed: ReturnType['parsed'] | null, + coldParsed: ReturnType['parsed'] | null, +): TokenInterfaceParsedAta { + const hotAmount = hotParsed?.amount ?? ZERO; + const compressedAmount = coldParsed?.amount ?? ZERO; + const amount = hotAmount + compressedAmount; + + let delegate: PublicKey | null = null; + let delegatedAmount = ZERO; + + if (hotParsed?.delegate) { + delegate = hotParsed.delegate; + delegatedAmount = hotParsed.delegatedAmount ?? ZERO; + + if (coldParsed?.delegate?.equals(delegate)) { + delegatedAmount += clampDelegatedAmount( + coldParsed.amount, + coldParsed.delegatedAmount ?? coldParsed.amount, + ); + } + } else if (coldParsed?.delegate) { + delegate = coldParsed.delegate; + delegatedAmount = clampDelegatedAmount( + coldParsed.amount, + coldParsed.delegatedAmount ?? coldParsed.amount, + ); + } + + return { + address, + owner, + mint, + amount, + delegate, + delegatedAmount: clampDelegatedAmount(amount, delegatedAmount), + isInitialized: hotParsed?.isInitialized === true || coldParsed !== null, + isFrozen: hotParsed?.isFrozen === true || coldParsed?.isFrozen === true, + }; +} + +function selectPrimaryCompressedAccount(accounts: ParsedTokenAccount[]): { + selected: ParsedTokenAccount | null; + ignored: ParsedTokenAccount[]; +} { + const candidates = sortCompressedAccounts( + accounts.filter(account => { + return ( + account.compressedAccount.owner.equals( + LIGHT_TOKEN_PROGRAM_ID, + ) && + account.compressedAccount.data !== null && + account.compressedAccount.data.data.length > 0 && + toBigIntAmount(account) > ZERO + ); + }), + ); + + return { + selected: candidates[0] ?? null, + ignored: candidates.slice(1), + }; +} + +export async function getAtaOrNull({ + rpc, + owner, + mint, + commitment, +}: GetAtaInput): Promise { + const address = getAssociatedTokenAddress(mint, owner); + + const [hotInfo, compressedResult] = await Promise.all([ + rpc.getAccountInfo(address, commitment), + rpc.getCompressedTokenAccountsByOwner(owner, { mint }), + ]); + + const hotParsed = + hotInfo && hotInfo.owner.equals(LIGHT_TOKEN_PROGRAM_ID) + ? parseLightTokenHot(address, toBufferAccountInfo(hotInfo)).parsed + : null; + + const { selected, ignored } = selectPrimaryCompressedAccount( + compressedResult.items, + ); + const coldParsed = selected + ? parseLightTokenCold(address, selected.compressedAccount).parsed + : null; + + if (!hotParsed && !coldParsed) { + return null; + } + + const parsed = buildParsedAta(address, owner, mint, hotParsed, coldParsed); + const ignoredCompressedAmount = ignored.reduce( + (sum, account) => sum + toBigIntAmount(account), + ZERO, + ); + + return { + address, + owner, + mint, + amount: parsed.amount, + hotAmount: hotParsed?.amount ?? ZERO, + compressedAmount: coldParsed?.amount ?? ZERO, + hasHotAccount: hotParsed !== null, + requiresLoad: coldParsed !== null, + parsed, + compressedAccount: selected, + ignoredCompressedAccounts: ignored, + ignoredCompressedAmount, + }; +} + +export async function getAta( + input: GetAtaInput, +): Promise { + const account = await getAtaOrNull(input); + + if (!account) { + throw new TokenAccountNotFoundError(); + } + + return account; +} + +export function getSpendableAmount( + account: TokenInterfaceAccount, + authority: PublicKey, +): bigint { + if (authority.equals(account.owner)) { + return account.amount; + } + + if ( + account.parsed.delegate !== null && + authority.equals(account.parsed.delegate) + ) { + return clampDelegatedAmount( + account.amount, + account.parsed.delegatedAmount, + ); + } + + return ZERO; +} + +export function assertAccountNotFrozen( + account: TokenInterfaceAccount, + operation: 'load' | 'transfer' | 'approve' | 'revoke' | 'burn' | 'freeze', +): void { + if (account.parsed.isFrozen) { + throw new Error(`Account is frozen; ${operation} is not allowed.`); + } +} + +export function assertAccountFrozen( + account: TokenInterfaceAccount, + operation: 'thaw', +): void { + if (!account.parsed.isFrozen) { + throw new Error(`Account is not frozen; ${operation} is not allowed.`); + } +} diff --git a/js/token-interface/src/constants.ts b/js/token-interface/src/constants.ts new file mode 100644 index 0000000000..2514305c51 --- /dev/null +++ b/js/token-interface/src/constants.ts @@ -0,0 +1,43 @@ +import { Buffer } from 'buffer'; +import { PublicKey } from '@solana/web3.js'; + +export const LIGHT_TOKEN_CONFIG = new PublicKey( + 'ACXg8a7VaqecBWrSbdu73W4Pg9gsqXJ3EXAqkHyhvVXg', +); + +export const LIGHT_TOKEN_RENT_SPONSOR = new PublicKey( + 'r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti', +); + +export enum TokenDataVersion { + V1 = 1, + V2 = 2, + ShaFlat = 3, +} + +export const POOL_SEED = Buffer.from('pool'); +export const CPI_AUTHORITY_SEED = Buffer.from('cpi_authority'); +export const MAX_TOP_UP = 65535; + +export const COMPRESSED_TOKEN_PROGRAM_ID = new PublicKey( + 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', +); + +export function deriveSplInterfacePdaWithIndex( + mint: PublicKey, + index: number, +): [PublicKey, number] { + const indexSeed = + index === 0 ? Buffer.from([]) : Buffer.from([index & 0xff]); + return PublicKey.findProgramAddressSync( + [POOL_SEED, mint.toBuffer(), indexSeed], + COMPRESSED_TOKEN_PROGRAM_ID, + ); +} + +export function deriveCpiAuthorityPda(): PublicKey { + return PublicKey.findProgramAddressSync( + [CPI_AUTHORITY_SEED], + COMPRESSED_TOKEN_PROGRAM_ID, + )[0]; +} diff --git a/js/token-interface/src/errors.ts b/js/token-interface/src/errors.ts new file mode 100644 index 0000000000..88607e73dc --- /dev/null +++ b/js/token-interface/src/errors.ts @@ -0,0 +1,16 @@ +export const ERR_FETCH_BY_OWNER_REQUIRED = 'fetchByOwner is required'; + +export class MultiTransactionNotSupportedError extends Error { + readonly operation: string; + readonly batchCount: number; + + constructor(operation: string, batchCount: number) { + super( + `${operation} requires ${batchCount} transactions with the current underlying interface builders. ` + + '@lightprotocol/token-interface only exposes single-transaction instruction builders.', + ); + this.name = 'MultiTransactionNotSupportedError'; + this.operation = operation; + this.batchCount = batchCount; + } +} diff --git a/js/token-interface/src/helpers.ts b/js/token-interface/src/helpers.ts new file mode 100644 index 0000000000..94f4d8e4d6 --- /dev/null +++ b/js/token-interface/src/helpers.ts @@ -0,0 +1,56 @@ +import type { LoadOptions } from './load-options'; +import { getMint } from './read'; +import { ComputeBudgetProgram, PublicKey } from '@solana/web3.js'; +import type { Rpc } from '@lightprotocol/stateless.js'; +import type { TransactionInstruction } from '@solana/web3.js'; +import { MultiTransactionNotSupportedError } from './errors'; + +export async function getMintDecimals( + rpc: Rpc, + mint: PublicKey, +): Promise { + const mintInfo = await getMint(rpc, mint); + return mintInfo.mint.decimals; +} + +export function toLoadOptions( + owner: PublicKey, + authority?: PublicKey, + wrap = false, +): LoadOptions | undefined { + if ((!authority || authority.equals(owner)) && !wrap) { + return undefined; + } + + const options: LoadOptions = {}; + if (wrap) { + options.wrap = true; + } + if (authority && !authority.equals(owner)) { + options.delegatePubkey = authority; + } + + return options; +} + +export function normalizeInstructionBatches( + operation: string, + batches: TransactionInstruction[][], +): TransactionInstruction[] { + if (batches.length === 0) { + return []; + } + + if (batches.length > 1) { + throw new MultiTransactionNotSupportedError(operation, batches.length); + } + + return batches[0].filter( + instruction => + !instruction.programId.equals(ComputeBudgetProgram.programId), + ); +} + +export function toBigIntAmount(amount: number | bigint): bigint { + return BigInt(amount.toString()); +} diff --git a/js/token-interface/src/index.ts b/js/token-interface/src/index.ts new file mode 100644 index 0000000000..b4e87ded27 --- /dev/null +++ b/js/token-interface/src/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export * from './errors'; +export * from './read'; +export * from './instructions'; diff --git a/js/token-interface/src/instructions/_plan.ts b/js/token-interface/src/instructions/_plan.ts new file mode 100644 index 0000000000..4d30b546b7 --- /dev/null +++ b/js/token-interface/src/instructions/_plan.ts @@ -0,0 +1,24 @@ +import { fromLegacyTransactionInstruction } from '@solana/compat'; +import { + sequentialInstructionPlan, + type InstructionPlan, +} from '@solana/instruction-plans'; +import type { TransactionInstruction } from '@solana/web3.js'; + +export type KitInstruction = ReturnType< + typeof fromLegacyTransactionInstruction +>; + +export function toKitInstructions( + instructions: TransactionInstruction[], +): KitInstruction[] { + return instructions.map(instruction => + fromLegacyTransactionInstruction(instruction), + ); +} + +export function toInstructionPlan( + instructions: TransactionInstruction[], +): InstructionPlan { + return sequentialInstructionPlan(toKitInstructions(instructions)); +} diff --git a/js/token-interface/src/instructions/approve.ts b/js/token-interface/src/instructions/approve.ts new file mode 100644 index 0000000000..0c6f5c4b99 --- /dev/null +++ b/js/token-interface/src/instructions/approve.ts @@ -0,0 +1,111 @@ +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import type { + CreateApproveInstructionsInput, + CreateRawApproveInstructionInput, +} from '../types'; +import { toBigIntAmount } from '../helpers'; +import { getAtaAddress } from '../read'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_APPROVE_DISCRIMINATOR = 4; + +export function createApproveInstruction({ + tokenAccount, + delegate, + owner, + amount, + payer, +}: CreateRawApproveInstructionInput): TransactionInstruction { + const data = Buffer.alloc(9); + data.writeUInt8(LIGHT_TOKEN_APPROVE_DISCRIMINATOR, 0); + data.writeBigUInt64LE(BigInt(amount), 1); + + const effectiveFeePayer = payer ?? owner; + + const keys = [ + { pubkey: tokenAccount, isSigner: false, isWritable: true }, + { pubkey: delegate, isSigner: false, isWritable: false }, + { + pubkey: owner, + isSigner: true, + isWritable: effectiveFeePayer.equals(owner), + }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { + pubkey: effectiveFeePayer, + isSigner: !effectiveFeePayer.equals(owner), + isWritable: true, + }, + ]; + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys, + data, + }); +} + +export async function createApproveInstructions({ + rpc, + payer, + owner, + mint, + delegate, + amount, +}: CreateApproveInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: true, + })), + createApproveInstruction({ + tokenAccount, + delegate, + owner, + amount: toBigIntAmount(amount), + payer, + }), + ]; +} + +export async function createApproveInstructionsNowrap({ + rpc, + payer, + owner, + mint, + delegate, + amount, +}: CreateApproveInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: false, + })), + createApproveInstruction({ + tokenAccount, + delegate, + owner, + amount: toBigIntAmount(amount), + payer, + }), + ]; +} + +export async function createApproveInstructionPlan( + input: CreateApproveInstructionsInput, +) { + return toInstructionPlan(await createApproveInstructions(input)); +} diff --git a/js/token-interface/src/instructions/ata.ts b/js/token-interface/src/instructions/ata.ts new file mode 100644 index 0000000000..490634276a --- /dev/null +++ b/js/token-interface/src/instructions/ata.ts @@ -0,0 +1,441 @@ +import { + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { + TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction as createSplAssociatedTokenAccountInstruction, + createAssociatedTokenAccountIdempotentInstruction as createSplAssociatedTokenAccountIdempotentInstruction, +} from '@solana/spl-token'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { struct, u8, u32, option, vec, array } from '@coral-xyz/borsh'; +import { LIGHT_TOKEN_CONFIG, LIGHT_TOKEN_RENT_SPONSOR } from '../constants'; +import { getAtaProgramId } from '../read/ata-utils'; +import { getAtaAddress } from '../read'; +import type { CreateRawAtaInstructionInput } from '../types'; +import { toInstructionPlan } from './_plan'; + +const CREATE_ASSOCIATED_TOKEN_ACCOUNT_DISCRIMINATOR = Buffer.from([100]); +const CREATE_ASSOCIATED_TOKEN_ACCOUNT_IDEMPOTENT_DISCRIMINATOR = Buffer.from([ + 102, +]); + +// Matches Rust CompressToPubkey struct +const CompressToPubkeyLayout = struct([ + u8('bump'), + array(u8(), 32, 'programId'), + vec(vec(u8()), 'seeds'), +]); + +// Matches Rust CompressibleExtensionInstructionData struct +// From: program-libs/token-interface/src/instructions/extensions/compressible.rs +const CompressibleExtensionInstructionDataLayout = struct([ + u8('tokenAccountVersion'), + u8('rentPayment'), + u8('compressionOnly'), + u32('writeTopUp'), + option(CompressToPubkeyLayout, 'compressToAccountPubkey'), +]); + +const CreateAssociatedTokenAccountInstructionDataLayout = struct([ + option(CompressibleExtensionInstructionDataLayout, 'compressibleConfig'), +]); + +export interface CompressToPubkey { + bump: number; + programId: number[]; + seeds: number[][]; +} + +export interface CompressibleConfig { + tokenAccountVersion: number; + rentPayment: number; + compressionOnly: number; + writeTopUp: number; + compressToAccountPubkey?: CompressToPubkey | null; +} + +export interface CreateAssociatedLightTokenAccountParams { + compressibleConfig?: CompressibleConfig | null; +} + +/** + * Default compressible config for light-token ATAs - matches Rust SDK defaults. + * + * - tokenAccountVersion: 3 (ShaFlat) - latest hashing scheme + * - rentPayment: 16 - prepay 16 epochs (~24 hours rent) + * - compressionOnly: 1 - required for ATAs + * - writeTopUp: 766 - per-write top-up (~2 epochs rent) when rent < 2 epochs + * - compressToAccountPubkey: null - required for ATAs + * + * Cost breakdown at associated token account creation: + * - Rent sponsor PDA (LIGHT_TOKEN_RENT_SPONSOR) pays: rent exemption (~890,880 lamports) + * - Fee payer pays: compression_cost (11K) + 16 epochs rent (~6,400) = ~17,400 lamports + tx fees + * + * Per-write top-up (transfers): + * - When account rent is below 2 epochs, fee payer pays 766 lamports top-up + * - This keeps the account perpetually funded when actively used + * + * Rent calculation (272-byte compressible lightToken account): + * - rent_per_epoch = base_rent (128) + bytes * rent_per_byte (272 * 1) = 400 lamports + * - 16 epochs = 16 * 400 = 6,400 lamports (24 hours) + * - 2 epochs = 2 * 400 = 800 lamports (~3 hours, writeTopUp = 766 is conservative) + * + * Account size breakdown (272 bytes): + * - 165 bytes: SPL token base layout + * - 1 byte: account_type discriminator + * - 1 byte: Option discriminator for extensions + * - 4 bytes: Vec length prefix + * - 1 byte: extension type discriminant + * - 4 bytes: CompressibleExtension header (decimals_option, decimals, compression_only, is_ata) + * - 96 bytes: CompressionInfo struct + */ +export const DEFAULT_COMPRESSIBLE_CONFIG: CompressibleConfig = { + tokenAccountVersion: 3, // ShaFlat (latest hashing scheme) + rentPayment: 16, // 16 epochs (~24 hours) - matches Rust SDK + compressionOnly: 1, // Required for ATAs + writeTopUp: 766, // Per-write top-up (~2 epochs) - matches Rust SDK + compressToAccountPubkey: null, // Required null for ATAs +}; + +/** @internal */ +function getAssociatedLightTokenAddress( + owner: PublicKey, + mint: PublicKey, +): PublicKey { + return PublicKey.findProgramAddressSync( + [owner.toBuffer(), LIGHT_TOKEN_PROGRAM_ID.toBuffer(), mint.toBuffer()], + LIGHT_TOKEN_PROGRAM_ID, + )[0]; +} + +/** @internal */ +function encodeCreateAssociatedLightTokenAccountData( + params: CreateAssociatedLightTokenAccountParams, + idempotent: boolean, +): Buffer { + const discriminator = idempotent + ? CREATE_ASSOCIATED_TOKEN_ACCOUNT_IDEMPOTENT_DISCRIMINATOR + : CREATE_ASSOCIATED_TOKEN_ACCOUNT_DISCRIMINATOR; + const payload = { compressibleConfig: params.compressibleConfig || null }; + let size = 64; + + for (;;) { + const buffer = Buffer.alloc(size); + try { + const len = + CreateAssociatedTokenAccountInstructionDataLayout.encode( + payload, + buffer, + ); + return Buffer.concat([discriminator, buffer.subarray(0, len)]); + } catch (error) { + if (!(error instanceof RangeError) || size >= 4096) { + throw error; + } + size *= 2; + } + } +} + +export interface CreateAssociatedLightTokenAccountInstructionParams { + feePayer?: PublicKey; + owner: PublicKey; + mint: PublicKey; + compressibleConfig?: CompressibleConfig | null; + configAccount?: PublicKey; + rentPayerPda?: PublicKey; +} + +/** + * Create instruction for creating an associated light-token account. + * Uses the default rent sponsor PDA by default. + * + * @param input Associated light-token account input. + * @param input.feePayer Optional fee payer public key. Defaults to owner. + * @param input.owner Owner of the associated token account. + * @param input.mint Mint address. + * @param input.compressibleConfig Compressible configuration (defaults to rent sponsor config). + * @param input.configAccount Config account (defaults to LIGHT_TOKEN_CONFIG). + * @param input.rentPayerPda Rent payer PDA (defaults to LIGHT_TOKEN_RENT_SPONSOR). + */ +export function createAssociatedLightTokenAccountInstruction({ + feePayer, + owner, + mint, + compressibleConfig = DEFAULT_COMPRESSIBLE_CONFIG, + configAccount = LIGHT_TOKEN_CONFIG, + rentPayerPda = LIGHT_TOKEN_RENT_SPONSOR, +}: CreateAssociatedLightTokenAccountInstructionParams): TransactionInstruction { + const effectiveFeePayer = feePayer ?? owner; + const associatedTokenAccount = getAssociatedLightTokenAddress(owner, mint); + + const data = encodeCreateAssociatedLightTokenAccountData( + { + compressibleConfig, + }, + false, + ); + + // Account order per Rust processor: + // 0. owner (non-mut, non-signer) + // 1. mint (non-mut, non-signer) + // 2. fee_payer (signer, mut) + // 3. associated_token_account (mut) + // 4. system_program + // Optional (only when compressibleConfig is non-null): + // 5. config account + // 6. rent_payer PDA + const keys: { + pubkey: PublicKey; + isSigner: boolean; + isWritable: boolean; + }[] = [ + { pubkey: owner, isSigner: false, isWritable: false }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: effectiveFeePayer, isSigner: true, isWritable: true }, + { + pubkey: associatedTokenAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + if (compressibleConfig) { + keys.push( + { pubkey: configAccount, isSigner: false, isWritable: false }, + { pubkey: rentPayerPda, isSigner: false, isWritable: true }, + ); + } + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys, + data, + }); +} + +/** + * Create idempotent instruction for creating an associated light-token account. + * Uses the default rent sponsor PDA by default. + * + * @param input Associated light-token account input. + * @param input.feePayer Optional fee payer public key. Defaults to owner. + * @param input.owner Owner of the associated token account. + * @param input.mint Mint address. + * @param input.compressibleConfig Compressible configuration (defaults to rent sponsor config). + * @param input.configAccount Config account (defaults to LIGHT_TOKEN_CONFIG). + * @param input.rentPayerPda Rent payer PDA (defaults to LIGHT_TOKEN_RENT_SPONSOR). + */ +export function createAssociatedLightTokenAccountIdempotentInstruction({ + feePayer, + owner, + mint, + compressibleConfig = DEFAULT_COMPRESSIBLE_CONFIG, + configAccount = LIGHT_TOKEN_CONFIG, + rentPayerPda = LIGHT_TOKEN_RENT_SPONSOR, +}: CreateAssociatedLightTokenAccountInstructionParams): TransactionInstruction { + const effectiveFeePayer = feePayer ?? owner; + const associatedTokenAccount = getAssociatedLightTokenAddress(owner, mint); + + const data = encodeCreateAssociatedLightTokenAccountData( + { + compressibleConfig, + }, + true, + ); + + const keys: { + pubkey: PublicKey; + isSigner: boolean; + isWritable: boolean; + }[] = [ + { pubkey: owner, isSigner: false, isWritable: false }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: effectiveFeePayer, isSigner: true, isWritable: true }, + { + pubkey: associatedTokenAccount, + isSigner: false, + isWritable: true, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + if (compressibleConfig) { + keys.push( + { pubkey: configAccount, isSigner: false, isWritable: false }, + { pubkey: rentPayerPda, isSigner: false, isWritable: true }, + ); + } + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys, + data, + }); +} + +/** + * light-token-specific config for createAssociatedTokenAccountInstruction + */ +export interface LightTokenConfig { + compressibleConfig?: CompressibleConfig | null; + configAccount?: PublicKey; + rentPayerPda?: PublicKey; +} + +export interface CreateAssociatedTokenAccountInstructionInput { + payer?: PublicKey; + associatedToken: PublicKey; + owner: PublicKey; + mint: PublicKey; + programId?: PublicKey; + associatedTokenProgramId?: PublicKey; + lightTokenConfig?: LightTokenConfig; +} + +/** + * Create instruction for creating an associated token account (SPL, Token-2022, + * or light-token). + * + * @param input Associated token account input. + * @param input.payer Fee payer public key. + * @param input.associatedToken Associated token account address. + * @param input.owner Owner of the associated token account. + * @param input.mint Mint address. + * @param input.programId Token program ID (default: TOKEN_PROGRAM_ID). + * @param input.associatedTokenProgramId Associated token program ID. + * @param input.lightTokenConfig Optional light-token-specific configuration. + */ +function createAssociatedTokenAccountInstruction({ + payer, + associatedToken, + owner, + mint, + programId = TOKEN_PROGRAM_ID, + associatedTokenProgramId, + lightTokenConfig, +}: CreateAssociatedTokenAccountInstructionInput): TransactionInstruction { + const effectivePayer = payer ?? owner; + const effectiveAssociatedTokenProgramId = + associatedTokenProgramId ?? getAtaProgramId(programId); + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return createAssociatedLightTokenAccountInstruction({ + feePayer: effectivePayer, + owner, + mint, + compressibleConfig: lightTokenConfig?.compressibleConfig, + configAccount: lightTokenConfig?.configAccount, + rentPayerPda: lightTokenConfig?.rentPayerPda, + }); + } else { + return createSplAssociatedTokenAccountInstruction( + effectivePayer, + associatedToken, + owner, + mint, + programId, + effectiveAssociatedTokenProgramId, + ); + } +} + +/** + * Create idempotent instruction for creating an associated token account (SPL, + * Token-2022, or light-token). + * + * @param input Associated token account input. + * @param input.payer Fee payer public key. + * @param input.associatedToken Associated token account address. + * @param input.owner Owner of the associated token account. + * @param input.mint Mint address. + * @param input.programId Token program ID (default: TOKEN_PROGRAM_ID). + * @param input.associatedTokenProgramId Associated token program ID. + * @param input.lightTokenConfig Optional light-token-specific configuration. + */ +function createAssociatedTokenAccountIdempotentInstruction({ + payer, + associatedToken, + owner, + mint, + programId = TOKEN_PROGRAM_ID, + associatedTokenProgramId, + lightTokenConfig, +}: CreateAssociatedTokenAccountInstructionInput): TransactionInstruction { + const effectivePayer = payer ?? owner; + const effectiveAssociatedTokenProgramId = + associatedTokenProgramId ?? getAtaProgramId(programId); + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return createAssociatedLightTokenAccountIdempotentInstruction({ + feePayer: effectivePayer, + owner, + mint, + compressibleConfig: lightTokenConfig?.compressibleConfig, + configAccount: lightTokenConfig?.configAccount, + rentPayerPda: lightTokenConfig?.rentPayerPda, + }); + } else { + return createSplAssociatedTokenAccountIdempotentInstruction( + effectivePayer, + associatedToken, + owner, + mint, + programId, + effectiveAssociatedTokenProgramId, + ); + } +} + +export const createAta = createAssociatedTokenAccountInstruction; +export const createAtaIdempotent = + createAssociatedTokenAccountIdempotentInstruction; + +export function createAtaInstruction({ + payer, + owner, + mint, + programId, +}: CreateRawAtaInstructionInput): TransactionInstruction { + const targetProgramId = programId ?? LIGHT_TOKEN_PROGRAM_ID; + const associatedToken = getAtaAddress({ + owner, + mint, + programId: targetProgramId, + }); + + return createAtaIdempotent({ + payer, + associatedToken, + owner, + mint, + programId: targetProgramId, + }); +} + +export async function createAtaInstructions({ + payer, + owner, + mint, + programId, +}: CreateRawAtaInstructionInput): Promise { + return [createAtaInstruction({ payer, owner, mint, programId })]; +} + +export async function createAtaInstructionPlan( + input: CreateRawAtaInstructionInput, +) { + return toInstructionPlan(await createAtaInstructions(input)); +} diff --git a/js/token-interface/src/instructions/burn.ts b/js/token-interface/src/instructions/burn.ts new file mode 100644 index 0000000000..6a7483a351 --- /dev/null +++ b/js/token-interface/src/instructions/burn.ts @@ -0,0 +1,185 @@ +import { Buffer } from 'buffer'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import type { + CreateBurnInstructionsInput, + CreateRawBurnCheckedInstructionInput, + CreateRawBurnInstructionInput, +} from '../types'; +import { toBigIntAmount } from '../helpers'; +import { getAtaAddress } from '../read'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_BURN_DISCRIMINATOR = 8; +const LIGHT_TOKEN_BURN_CHECKED_DISCRIMINATOR = 15; + +export function createBurnInstruction({ + source, + mint, + authority, + amount, + payer, +}: CreateRawBurnInstructionInput): TransactionInstruction { + const data = Buffer.alloc(9); + data.writeUInt8(LIGHT_TOKEN_BURN_DISCRIMINATOR, 0); + data.writeBigUInt64LE(BigInt(amount), 1); + + const effectivePayer = payer ?? authority; + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: true }, + { + pubkey: authority, + isSigner: true, + isWritable: effectivePayer.equals(authority), + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: effectivePayer, + isSigner: !effectivePayer.equals(authority), + isWritable: true, + }, + ], + data, + }); +} + +export function createBurnCheckedInstruction({ + source, + mint, + authority, + amount, + decimals, + payer, +}: CreateRawBurnCheckedInstructionInput): TransactionInstruction { + const data = Buffer.alloc(10); + data.writeUInt8(LIGHT_TOKEN_BURN_CHECKED_DISCRIMINATOR, 0); + data.writeBigUInt64LE(BigInt(amount), 1); + data.writeUInt8(decimals, 9); + + const effectivePayer = payer ?? authority; + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: true }, + { + pubkey: authority, + isSigner: true, + isWritable: effectivePayer.equals(authority), + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: effectivePayer, + isSigner: !effectivePayer.equals(authority), + isWritable: true, + }, + ], + data, + }); +} + +export async function createBurnInstructions({ + rpc, + payer, + owner, + mint, + authority, + amount, + decimals, +}: CreateBurnInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + const amountBn = toBigIntAmount(amount); + const burnIx = + decimals !== undefined + ? createBurnCheckedInstruction({ + source: tokenAccount, + mint, + authority, + amount: amountBn, + decimals, + payer, + }) + : createBurnInstruction({ + source: tokenAccount, + mint, + authority, + amount: amountBn, + payer, + }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + authority, + wrap: true, + })), + burnIx, + ]; +} + +export async function createBurnInstructionsNowrap({ + rpc, + payer, + owner, + mint, + authority, + amount, + decimals, +}: CreateBurnInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + const amountBn = toBigIntAmount(amount); + const burnIx = + decimals !== undefined + ? createBurnCheckedInstruction({ + source: tokenAccount, + mint, + authority, + amount: amountBn, + decimals, + payer, + }) + : createBurnInstruction({ + source: tokenAccount, + mint, + authority, + amount: amountBn, + payer, + }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + authority, + wrap: false, + })), + burnIx, + ]; +} + +export async function createBurnInstructionPlan( + input: CreateBurnInstructionsInput, +) { + return toInstructionPlan(await createBurnInstructions(input)); +} diff --git a/js/token-interface/src/instructions/freeze.ts b/js/token-interface/src/instructions/freeze.ts new file mode 100644 index 0000000000..a8fab97f1c --- /dev/null +++ b/js/token-interface/src/instructions/freeze.ts @@ -0,0 +1,87 @@ +import { TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import type { + CreateFreezeInstructionsInput, + CreateRawFreezeInstructionInput, +} from '../types'; +import { getAtaAddress } from '../read'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_FREEZE_ACCOUNT_DISCRIMINATOR = 10; + +export function createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, +}: CreateRawFreezeInstructionInput): TransactionInstruction { + const data = Buffer.alloc(1); + data.writeUInt8(LIGHT_TOKEN_FREEZE_ACCOUNT_DISCRIMINATOR, 0); + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: tokenAccount, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: freezeAuthority, isSigner: true, isWritable: false }, + ], + data, + }); +} + +export async function createFreezeInstructions({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateFreezeInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: true, + })), + createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, + }), + ]; +} + +export async function createFreezeInstructionsNowrap({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateFreezeInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: false, + })), + createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, + }), + ]; +} + +export async function createFreezeInstructionPlan( + input: CreateFreezeInstructionsInput, +) { + return toInstructionPlan(await createFreezeInstructions(input)); +} diff --git a/js/token-interface/src/instructions/index.ts b/js/token-interface/src/instructions/index.ts new file mode 100644 index 0000000000..dcaca06122 --- /dev/null +++ b/js/token-interface/src/instructions/index.ts @@ -0,0 +1,9 @@ +export * from './_plan'; +export * from './ata'; +export * from './approve'; +export * from './revoke'; +export * from './transfer'; +export * from './load'; +export * from './burn'; +export * from './freeze'; +export * from './thaw'; diff --git a/js/token-interface/src/instructions/layout/layout-mint-action.ts b/js/token-interface/src/instructions/layout/layout-mint-action.ts new file mode 100644 index 0000000000..04a0ef8c9e --- /dev/null +++ b/js/token-interface/src/instructions/layout/layout-mint-action.ts @@ -0,0 +1,362 @@ +/** + * Borsh layouts for MintAction instruction data + * + * These layouts match the Rust structs in: + * program-libs/light-token-types/src/instructions/mint_action/ + * + * @module mint-action-layout + */ +import { PublicKey } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { + struct, + option, + vec, + bool, + u8, + u16, + u32, + u64, + array, + vecU8, + publicKey, + rustEnum, +} from '@coral-xyz/borsh'; +import { bn } from '@lightprotocol/stateless.js'; + +export const MINT_ACTION_DISCRIMINATOR = Buffer.from([103]); + +export const RecipientLayout = struct([publicKey('recipient'), u64('amount')]); + +export const MintToCompressedActionLayout = struct([ + u8('tokenAccountVersion'), + vec(RecipientLayout, 'recipients'), +]); + +export const UpdateAuthorityLayout = struct([ + option(publicKey(), 'newAuthority'), +]); + +export const MintToLightTokenActionLayout = struct([ + u8('accountIndex'), + u64('amount'), +]); + +export const UpdateMetadataFieldActionLayout = struct([ + u8('extensionIndex'), + u8('fieldType'), + vecU8('key'), + vecU8('value'), +]); + +export const UpdateMetadataAuthorityActionLayout = struct([ + u8('extensionIndex'), + publicKey('newAuthority'), +]); + +export const RemoveMetadataKeyActionLayout = struct([ + u8('extensionIndex'), + vecU8('key'), + u8('idempotent'), +]); + +export const DecompressMintActionLayout = struct([ + u8('rentPayment'), + u32('writeTopUp'), +]); + +export const CompressAndCloseCMintActionLayout = struct([u8('idempotent')]); + +export const ActionLayout = rustEnum([ + MintToCompressedActionLayout.replicate('mintToCompressed'), + UpdateAuthorityLayout.replicate('updateMintAuthority'), + UpdateAuthorityLayout.replicate('updateFreezeAuthority'), + MintToLightTokenActionLayout.replicate('mintToLightToken'), + UpdateMetadataFieldActionLayout.replicate('updateMetadataField'), + UpdateMetadataAuthorityActionLayout.replicate('updateMetadataAuthority'), + RemoveMetadataKeyActionLayout.replicate('removeMetadataKey'), + DecompressMintActionLayout.replicate('decompressMint'), + CompressAndCloseCMintActionLayout.replicate('compressAndCloseCMint'), +]); + +export const CompressedProofLayout = struct([ + array(u8(), 32, 'a'), + array(u8(), 64, 'b'), + array(u8(), 32, 'c'), +]); + +export const CpiContextLayout = struct([ + bool('setContext'), + bool('firstSetContext'), + u8('inTreeIndex'), + u8('inQueueIndex'), + u8('outQueueIndex'), + u8('tokenOutQueueIndex'), + u8('assignedAccountIndex'), + array(u8(), 4, 'readOnlyAddressTrees'), + array(u8(), 32, 'addressTreePubkey'), +]); + +export const CreateMintLayout = struct([ + array(u8(), 4, 'readOnlyAddressTrees'), + array(u16(), 4, 'readOnlyAddressTreeRootIndices'), +]); + +export const AdditionalMetadataLayout = struct([vecU8('key'), vecU8('value')]); + +export const TokenMetadataInstructionDataLayout = struct([ + option(publicKey(), 'updateAuthority'), + vecU8('name'), + vecU8('symbol'), + vecU8('uri'), + option(vec(AdditionalMetadataLayout), 'additionalMetadata'), +]); + +const PlaceholderLayout = struct([]); + +export const ExtensionInstructionDataLayout = rustEnum([ + PlaceholderLayout.replicate('placeholder0'), + PlaceholderLayout.replicate('placeholder1'), + PlaceholderLayout.replicate('placeholder2'), + PlaceholderLayout.replicate('placeholder3'), + PlaceholderLayout.replicate('placeholder4'), + PlaceholderLayout.replicate('placeholder5'), + PlaceholderLayout.replicate('placeholder6'), + PlaceholderLayout.replicate('placeholder7'), + PlaceholderLayout.replicate('placeholder8'), + PlaceholderLayout.replicate('placeholder9'), + PlaceholderLayout.replicate('placeholder10'), + PlaceholderLayout.replicate('placeholder11'), + PlaceholderLayout.replicate('placeholder12'), + PlaceholderLayout.replicate('placeholder13'), + PlaceholderLayout.replicate('placeholder14'), + PlaceholderLayout.replicate('placeholder15'), + PlaceholderLayout.replicate('placeholder16'), + PlaceholderLayout.replicate('placeholder17'), + PlaceholderLayout.replicate('placeholder18'), + TokenMetadataInstructionDataLayout.replicate('tokenMetadata'), +]); + +export const CompressedMintMetadataLayout = struct([ + u8('version'), + bool('cmintDecompressed'), + publicKey('mint'), + array(u8(), 32, 'mintSigner'), + u8('bump'), +]); + +export const MintInstructionDataLayout = struct([ + u64('supply'), + u8('decimals'), + CompressedMintMetadataLayout.replicate('metadata'), + option(publicKey(), 'mintAuthority'), + option(publicKey(), 'freezeAuthority'), + option(vec(ExtensionInstructionDataLayout), 'extensions'), +]); + +export const MintActionCompressedInstructionDataLayout = struct([ + u32('leafIndex'), + bool('proveByIndex'), + u16('rootIndex'), + u16('maxTopUp'), + option(CreateMintLayout, 'createMint'), + vec(ActionLayout, 'actions'), + option(CompressedProofLayout, 'proof'), + option(CpiContextLayout, 'cpiContext'), + option(MintInstructionDataLayout, 'mint'), +]); + +export interface ValidityProof { + a: number[]; + b: number[]; + c: number[]; +} + +export interface Recipient { + recipient: PublicKey; + amount: bigint; +} + +export interface MintToCompressedAction { + tokenAccountVersion: number; + recipients: Recipient[]; +} + +export interface UpdateAuthority { + newAuthority: PublicKey | null; +} + +export interface MintToLightTokenAction { + accountIndex: number; + amount: bigint; +} + +export interface UpdateMetadataFieldAction { + extensionIndex: number; + fieldType: number; + key: Buffer; + value: Buffer; +} + +export interface UpdateMetadataAuthorityAction { + extensionIndex: number; + newAuthority: PublicKey; +} + +export interface RemoveMetadataKeyAction { + extensionIndex: number; + key: Buffer; + idempotent: number; +} + +export interface DecompressMintAction { + rentPayment: number; + writeTopUp: number; +} + +export interface CompressAndCloseCMintAction { + idempotent: number; +} + +export type Action = + | { mintToCompressed: MintToCompressedAction } + | { updateMintAuthority: UpdateAuthority } + | { updateFreezeAuthority: UpdateAuthority } + | { mintToLightToken: MintToLightTokenAction } + | { updateMetadataField: UpdateMetadataFieldAction } + | { updateMetadataAuthority: UpdateMetadataAuthorityAction } + | { removeMetadataKey: RemoveMetadataKeyAction } + | { decompressMint: DecompressMintAction } + | { compressAndCloseCMint: CompressAndCloseCMintAction }; + +export interface CpiContext { + setContext: boolean; + firstSetContext: boolean; + inTreeIndex: number; + inQueueIndex: number; + outQueueIndex: number; + tokenOutQueueIndex: number; + assignedAccountIndex: number; + readOnlyAddressTrees: number[]; + addressTreePubkey: number[]; +} + +export interface CreateMint { + readOnlyAddressTrees: number[]; + readOnlyAddressTreeRootIndices: number[]; +} + +export interface AdditionalMetadata { + key: Buffer; + value: Buffer; +} + +export interface TokenMetadataLayoutData { + updateAuthority: PublicKey | null; + name: Buffer; + symbol: Buffer; + uri: Buffer; + additionalMetadata: AdditionalMetadata[] | null; +} + +export type ExtensionInstructionData = { + tokenMetadata: TokenMetadataLayoutData; +}; + +export interface CompressedMintMetadata { + version: number; + cmintDecompressed: boolean; + mint: PublicKey; + mintSigner: number[]; + bump: number; +} + +export interface MintLayoutData { + supply: bigint; + decimals: number; + metadata: CompressedMintMetadata; + mintAuthority: PublicKey | null; + freezeAuthority: PublicKey | null; + extensions: ExtensionInstructionData[] | null; +} + +export interface MintActionCompressedInstructionData { + leafIndex: number; + proveByIndex: boolean; + rootIndex: number; + maxTopUp: number; + createMint: CreateMint | null; + actions: Action[]; + proof: ValidityProof | null; + cpiContext: CpiContext | null; + mint: MintLayoutData | null; +} + +/** + * Encode MintActionCompressedInstructionData to buffer + * + * @param data - The instruction data to encode + * @returns Encoded buffer with discriminator prepended + * @internal + */ +export function encodeMintActionInstructionData( + data: MintActionCompressedInstructionData, +): Buffer { + // Convert bigint fields to BN for Borsh encoding + 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 ('mintToLightToken' in action && action.mintToLightToken) { + return { + mintToLightToken: { + ...action.mintToLightToken, + amount: bn(action.mintToLightToken.amount.toString()), + }, + }; + } + return action; + }); + + const buffer = Buffer.alloc(10000); + + const encodableData = { + ...data, + actions: convertedActions, + mint: data.mint + ? { + ...data.mint, + supply: bn(data.mint.supply.toString()), + } + : null, + }; + const len = MintActionCompressedInstructionDataLayout.encode( + encodableData, + buffer, + ); + + return Buffer.concat([MINT_ACTION_DISCRIMINATOR, buffer.subarray(0, len)]); +} + +/** + * Decode MintActionCompressedInstructionData from buffer + * + * @param buffer - The buffer to decode (including discriminator) + * @returns Decoded instruction data + * @internal + */ +export function decodeMintActionInstructionData( + buffer: Buffer, +): MintActionCompressedInstructionData { + return MintActionCompressedInstructionDataLayout.decode( + buffer.subarray(MINT_ACTION_DISCRIMINATOR.length), + ) as MintActionCompressedInstructionData; +} diff --git a/js/token-interface/src/instructions/layout/layout-mint.ts b/js/token-interface/src/instructions/layout/layout-mint.ts new file mode 100644 index 0000000000..692e0c501a --- /dev/null +++ b/js/token-interface/src/instructions/layout/layout-mint.ts @@ -0,0 +1,502 @@ +import { MINT_SIZE, MintLayout } from '@solana/spl-token'; +import { PublicKey } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { struct, u8 } from '@solana/buffer-layout'; +import { publicKey } from '@solana/buffer-layout-utils'; +import { + struct as borshStruct, + vec, + vecU8, + publicKey as borshPublicKey, +} from '@coral-xyz/borsh'; + +/** + * SPL-compatible base mint structure + */ +export interface BaseMint { + /** Optional authority used to mint new tokens */ + mintAuthority: PublicKey | null; + /** Total supply of tokens */ + supply: bigint; + /** Number of base 10 digits to the right of the decimal place */ + decimals: number; + /** Is initialized - for SPL compatibility */ + isInitialized: boolean; + /** Optional authority to freeze token accounts */ + freezeAuthority: PublicKey | null; +} + +/** + * Light mint context (protocol version, SPL mint reference) + */ +export interface MintContext { + /** Protocol version for upgradability */ + version: number; + /** Whether the compressed light mint has been decompressed to a light mint account */ + cmintDecompressed: boolean; + /** PDA of the associated SPL mint */ + splMint: PublicKey; + /** Signer pubkey used to derive the mint PDA */ + mintSigner: Uint8Array; + /** Bump seed for the mint PDA */ + bump: number; +} + +/** + * Raw extension data as stored on-chain + */ +export interface MintExtension { + extensionType: number; + data: Uint8Array; +} + +/** + * Parsed token metadata matching on-chain TokenMetadata extension. + * Fields: updateAuthority, mint, name, symbol, uri, additionalMetadata + */ +export interface TokenMetadata { + /** Authority that can update metadata (None if zero pubkey) */ + updateAuthority?: PublicKey | null; + /** Associated mint pubkey */ + mint: PublicKey; + /** Token name */ + name: string; + /** Token symbol */ + symbol: string; + /** URI pointing to off-chain metadata JSON */ + uri: string; + /** Additional key-value metadata pairs */ + additionalMetadata?: { key: string; value: string }[]; +} + +/** + * Borsh layout for TokenMetadata extension data + * Format: updateAuthority (32) + mint (32) + name + symbol + uri + additional_metadata + */ +export const TokenMetadataLayout = borshStruct([ + borshPublicKey('updateAuthority'), + borshPublicKey('mint'), + vecU8('name'), + vecU8('symbol'), + vecU8('uri'), + vec(borshStruct([vecU8('key'), vecU8('value')]), 'additionalMetadata'), +]); + +/** + * Complete light mint structure (raw format) + */ +export interface CompressedMint { + base: BaseMint; + mintContext: MintContext; + /** Reserved bytes for T22 layout compatibility */ + reserved: Uint8Array; + /** Account type discriminator (1 = Mint) */ + accountType: number; + /** Compression info embedded in mint */ + compression: CompressionInfo; + extensions: MintExtension[] | null; +} + +/** MintContext as stored by the program */ +/** + * Raw mint context for layout encoding (mintSigner and bump are encoded separately) + */ +export interface RawMintContext { + version: number; + cmintDecompressed: number; // bool as u8 + splMint: PublicKey; +} + +/** Buffer layout for de/serializing MintContext */ +export const MintContextLayout = struct([ + u8('version'), + u8('cmintDecompressed'), + publicKey('splMint'), +]); + +/** Byte length of MintContext (excluding mintSigner and bump which are read separately) */ +export const MINT_CONTEXT_SIZE = MintContextLayout.span; // 34 bytes + +/** Additional bytes for mintSigner (32) + bump (1) */ +export const MINT_SIGNER_SIZE = 32; +export const BUMP_SIZE = 1; + +/** Reserved bytes for T22 layout compatibility (padding to reach byte 165) */ +export const RESERVED_SIZE = 16; + +/** Account type discriminator size */ +export const ACCOUNT_TYPE_SIZE = 1; + +/** Account type value for light mint */ +export const ACCOUNT_TYPE_MINT = 1; + +/** + * Rent configuration for compressible accounts + */ +export interface RentConfig { + /** Base rent constant per epoch */ + baseRent: number; + /** Compression cost in lamports */ + compressionCost: number; + /** Lamports per byte per epoch */ + lamportsPerBytePerEpoch: number; + /** Maximum epochs that can be pre-funded */ + maxFundedEpochs: number; + /** Maximum lamports for top-up operation */ + maxTopUp: number; +} + +/** Byte length of RentConfig */ +export const RENT_CONFIG_SIZE = 8; // 2 + 2 + 1 + 1 + 2 + +/** + * Compression info embedded in light mint + */ +export interface CompressionInfo { + /** Config account version (0 = uninitialized) */ + configAccountVersion: number; + /** Whether to compress to pubkey instead of owner */ + compressToPubkey: number; + /** Account version for hashing scheme */ + accountVersion: number; + /** Lamports to top up per write */ + lamportsPerWrite: number; + /** Authority that can compress the account */ + compressionAuthority: PublicKey; + /** Recipient for rent on closure */ + rentSponsor: PublicKey; + /** Last slot rent was claimed */ + lastClaimedSlot: bigint; + /** Rent exemption lamports paid at account creation */ + rentExemptionPaid: number; + /** Reserved for future use */ + reserved: number; + /** Rent configuration */ + rentConfig: RentConfig; +} + +/** Byte length of CompressionInfo */ +export const COMPRESSION_INFO_SIZE = 96; // 2 + 1 + 1 + 4 + 32 + 32 + 8 + 4 + 4 + 8 + +/** + * Calculate the byte length of a TokenMetadata extension from buffer. + * Format: updateAuthority (32) + mint (32) + name (4+len) + symbol (4+len) + uri (4+len) + additional (4 + items) + * @internal + */ +function getTokenMetadataByteLength( + buffer: Buffer, + startOffset: number, +): number { + let offset = startOffset; + + // updateAuthority: 32 bytes + offset += 32; + // mint: 32 bytes + offset += 32; + + // name: Vec + const nameLen = buffer.readUInt32LE(offset); + offset += 4 + nameLen; + + // symbol: Vec + const symbolLen = buffer.readUInt32LE(offset); + offset += 4 + symbolLen; + + // uri: Vec + const uriLen = buffer.readUInt32LE(offset); + offset += 4 + uriLen; + + // additional_metadata: Vec + const additionalCount = buffer.readUInt32LE(offset); + offset += 4; + for (let i = 0; i < additionalCount; i++) { + const keyLen = buffer.readUInt32LE(offset); + offset += 4 + keyLen; + const valueLen = buffer.readUInt32LE(offset); + offset += 4 + valueLen; + } + + return offset - startOffset; +} + +/** + * Get the byte length of an extension based on its type. + * Returns the length of the extension data (excluding the 1-byte discriminant). + * @internal + */ +function getExtensionByteLength( + extensionType: number, + buffer: Buffer, + dataStartOffset: number, +): number { + switch (extensionType) { + case ExtensionType.TokenMetadata: + return getTokenMetadataByteLength(buffer, dataStartOffset); + default: + // For unknown extensions, we can't determine the length + // Return remaining buffer length as fallback + return buffer.length - dataStartOffset; + } +} + +/** + * Deserialize CompressionInfo from buffer at given offset + * @returns Tuple of [CompressionInfo, bytesRead] + * @internal + */ +function deserializeCompressionInfo( + buffer: Buffer, + offset: number, +): [CompressionInfo, number] { + const startOffset = offset; + + const configAccountVersion = buffer.readUInt16LE(offset); + offset += 2; + + const compressToPubkey = buffer.readUInt8(offset); + offset += 1; + + const accountVersion = buffer.readUInt8(offset); + offset += 1; + + const lamportsPerWrite = buffer.readUInt32LE(offset); + offset += 4; + + const compressionAuthority = new PublicKey( + buffer.slice(offset, offset + 32), + ); + offset += 32; + + const rentSponsor = new PublicKey(buffer.slice(offset, offset + 32)); + offset += 32; + + const lastClaimedSlot = buffer.readBigUInt64LE(offset); + offset += 8; + + // Read rent_exemption_paid (u32) and _reserved (u32) + const rentExemptionPaid = buffer.readUInt32LE(offset); + offset += 4; + const reserved = buffer.readUInt32LE(offset); + offset += 4; + + // Read RentConfig (8 bytes) + const baseRent = buffer.readUInt16LE(offset); + offset += 2; + const compressionCost = buffer.readUInt16LE(offset); + offset += 2; + const lamportsPerBytePerEpoch = buffer.readUInt8(offset); + offset += 1; + const maxFundedEpochs = buffer.readUInt8(offset); + offset += 1; + const maxTopUp = buffer.readUInt16LE(offset); + offset += 2; + + const rentConfig: RentConfig = { + baseRent, + compressionCost, + lamportsPerBytePerEpoch, + maxFundedEpochs, + maxTopUp, + }; + + const compressionInfo: CompressionInfo = { + configAccountVersion, + compressToPubkey, + accountVersion, + lamportsPerWrite, + compressionAuthority, + rentSponsor, + lastClaimedSlot, + rentExemptionPaid, + reserved, + rentConfig, + }; + + return [compressionInfo, offset - startOffset]; +} + +/** + * Deserialize a light mint from buffer + * Uses SPL's MintLayout for BaseMint and buffer-layout struct for context + * + * @param data - The raw account data buffer + * @returns The deserialized light mint + */ +export function deserializeMint(data: Buffer | Uint8Array): CompressedMint { + const buffer = data instanceof Buffer ? data : Buffer.from(data); + let offset = 0; + + // 1. Decode BaseMint using SPL's MintLayout (82 bytes) + const rawMint = MintLayout.decode(buffer.slice(offset, offset + MINT_SIZE)); + offset += MINT_SIZE; + + // 2. Decode MintContext using our layout (34 bytes) + const rawContext = MintContextLayout.decode( + buffer.slice(offset, offset + MINT_CONTEXT_SIZE), + ); + offset += MINT_CONTEXT_SIZE; + + // 2b. Read mintSigner (32 bytes) and bump (1 byte) + const mintSigner = buffer.slice(offset, offset + MINT_SIGNER_SIZE); + offset += MINT_SIGNER_SIZE; + const bump = buffer.readUInt8(offset); + offset += BUMP_SIZE; + + // 3. Read reserved bytes (16 bytes) for T22 compatibility + const reserved = buffer.slice(offset, offset + RESERVED_SIZE); + offset += RESERVED_SIZE; + + // 4. Read account_type discriminator (1 byte) + const accountType = buffer.readUInt8(offset); + offset += ACCOUNT_TYPE_SIZE; + + // 5. Read CompressionInfo (96 bytes) + const [compression, compressionBytesRead] = deserializeCompressionInfo( + buffer, + offset, + ); + offset += compressionBytesRead; + + // 6. Parse extensions: Option> + // Borsh format: Option byte + Vec length + (discriminant + variant data) for each + const hasExtensions = buffer.readUInt8(offset) === 1; + offset += 1; + + let extensions: MintExtension[] | null = null; + if (hasExtensions) { + const vecLen = buffer.readUInt32LE(offset); + offset += 4; + + extensions = []; + for (let i = 0; i < vecLen; i++) { + const extensionType = buffer.readUInt8(offset); + offset += 1; + + // Calculate extension data length based on type + const dataLength = getExtensionByteLength( + extensionType, + buffer, + offset, + ); + const extensionData = buffer.slice(offset, offset + dataLength); + offset += dataLength; + + extensions.push({ + extensionType, + data: extensionData, + }); + } + } + + // Convert raw types to our interface with proper null handling + const baseMint: BaseMint = { + mintAuthority: + rawMint.mintAuthorityOption === 1 ? rawMint.mintAuthority : null, + supply: rawMint.supply, + decimals: rawMint.decimals, + isInitialized: rawMint.isInitialized, + freezeAuthority: + rawMint.freezeAuthorityOption === 1 + ? rawMint.freezeAuthority + : null, + }; + + const mintContext: MintContext = { + version: rawContext.version, + cmintDecompressed: rawContext.cmintDecompressed !== 0, + splMint: rawContext.splMint, + mintSigner, + bump, + }; + + const mint: CompressedMint = { + base: baseMint, + mintContext, + reserved, + accountType, + compression, + extensions, + }; + + return mint; +} + +/** + * Extension type constants + */ +export enum ExtensionType { + TokenMetadata = 19, // Name, symbol, uri + // Add more extension types as needed +} + +/** + * Decode TokenMetadata from raw extension data using Borsh layout + * Extension format: updateAuthority (32) + mint (32) + name (Vec) + symbol (Vec) + uri (Vec) + additional (Vec) + */ +function decodeTokenMetadata(data: Uint8Array): TokenMetadata | null { + try { + const buffer = Buffer.from(data); + // Minimum size: 32 (updateAuthority) + 32 (mint) + 4 (name len) + 4 (symbol len) + 4 (uri len) + 4 (additional len) = 80 + if (buffer.length < 80) { + return null; + } + + // Decode using Borsh layout + const decoded = TokenMetadataLayout.decode(buffer) as { + updateAuthority: PublicKey; + mint: PublicKey; + name: Buffer; + symbol: Buffer; + uri: Buffer; + additionalMetadata: { key: Buffer; value: Buffer }[]; + }; + + // Convert zero pubkey to undefined for updateAuthority + const updateAuthorityBytes = decoded.updateAuthority.toBuffer(); + const isZero = updateAuthorityBytes.every((b: number) => b === 0); + const updateAuthority = isZero ? undefined : decoded.updateAuthority; + + // Convert Buffer fields to strings + const name = Buffer.from(decoded.name).toString('utf-8'); + const symbol = Buffer.from(decoded.symbol).toString('utf-8'); + const uri = Buffer.from(decoded.uri).toString('utf-8'); + + // Convert additional metadata + let additionalMetadata: { key: string; value: string }[] | undefined; + if ( + decoded.additionalMetadata && + decoded.additionalMetadata.length > 0 + ) { + additionalMetadata = decoded.additionalMetadata.map(item => ({ + key: Buffer.from(item.key).toString('utf-8'), + value: Buffer.from(item.value).toString('utf-8'), + })); + } + + return { + updateAuthority, + mint: decoded.mint, + name, + symbol, + uri, + additionalMetadata, + }; + } catch { + return null; + } +} + +/** + * Extract and parse TokenMetadata from extensions array + * @param extensions - Array of raw extensions + * @returns Parsed TokenMetadata or null if not found + */ +export function extractTokenMetadata( + extensions: MintExtension[] | null, +): TokenMetadata | null { + if (!extensions) return null; + const metadataExt = extensions.find( + ext => ext.extensionType === ExtensionType.TokenMetadata, + ); + return metadataExt ? decodeTokenMetadata(metadataExt.data) : null; +} diff --git a/js/token-interface/src/instructions/layout/layout-transfer2.ts b/js/token-interface/src/instructions/layout/layout-transfer2.ts new file mode 100644 index 0000000000..0f17dfb141 --- /dev/null +++ b/js/token-interface/src/instructions/layout/layout-transfer2.ts @@ -0,0 +1,540 @@ +import { + struct, + option, + vec, + bool, + u64, + u8, + u16, + u32, + array, +} from '@coral-xyz/borsh'; +import { Buffer } from 'buffer'; +import { bn } from '@lightprotocol/stateless.js'; +import { PublicKey } from '@solana/web3.js'; +import { CompressionInfo } from './layout-mint'; +import { + AdditionalMetadata, + CompressedProofLayout, + TokenMetadataInstructionDataLayout, +} from './layout-mint-action'; + +// Transfer2 discriminator = 101 +export const TRANSFER2_DISCRIMINATOR = Buffer.from([101]); + +// Extension discriminant values (matching Rust enum) +export const EXTENSION_DISCRIMINANT_TOKEN_METADATA = 19; +export const EXTENSION_DISCRIMINANT_COMPRESSED_ONLY = 31; +export const EXTENSION_DISCRIMINANT_COMPRESSIBLE = 32; + +// CompressionMode enum values +export const COMPRESSION_MODE_COMPRESS = 0; +export const COMPRESSION_MODE_DECOMPRESS = 1; +export const COMPRESSION_MODE_COMPRESS_AND_CLOSE = 2; + +/** + * Compression struct for Transfer2 instruction + */ +export interface Compression { + mode: number; + amount: bigint; + mint: number; + sourceOrRecipient: number; + authority: number; + poolAccountIndex: number; + poolIndex: number; + bump: number; + decimals: number; +} + +/** + * Packed merkle context for compressed accounts + */ +export interface PackedMerkleContext { + merkleTreePubkeyIndex: number; + queuePubkeyIndex: number; + leafIndex: number; + proveByIndex: boolean; +} + +/** + * Input token data with context for Transfer2 + */ +export interface MultiInputTokenDataWithContext { + owner: number; + amount: bigint; + hasDelegate: boolean; + delegate: number; + mint: number; + version: number; + merkleContext: PackedMerkleContext; + rootIndex: number; +} + +/** + * Output token data for Transfer2 + */ +export interface MultiTokenTransferOutputData { + owner: number; + amount: bigint; + hasDelegate: boolean; + delegate: number; + mint: number; + version: number; +} + +/** + * CPI context for Transfer2 + */ +export interface CompressedCpiContext { + setContext: boolean; + firstSetContext: boolean; + cpiContextAccountIndex: number; +} + +/** + * Token metadata extension instruction data for Transfer2 TLV + */ +export interface Transfer2TokenMetadata { + updateAuthority: PublicKey | null; + name: Uint8Array; + symbol: Uint8Array; + uri: Uint8Array; + additionalMetadata: AdditionalMetadata[] | null; +} + +/** + * CompressedOnly extension instruction data for Transfer2 TLV + */ +export interface Transfer2CompressedOnly { + delegatedAmount: bigint; + withheldTransferFee: bigint; + isFrozen: boolean; + compressionIndex: number; + isAta: boolean; + bump: number; + ownerIndex: number; +} + +/** + * Extension instruction data types for Transfer2 in_tlv/out_tlv + */ +export type Transfer2ExtensionData = + | { type: 'TokenMetadata'; data: Transfer2TokenMetadata } + | { type: 'CompressedOnly'; data: Transfer2CompressedOnly } + | { type: 'Compressible'; data: CompressionInfo }; + +/** + * Full Transfer2 instruction data + * + * Note on `decimals` field in Compression: + * - For SPL compress/decompress: actual token decimals + * - For CompressAndClose mode: used as `rent_sponsor_is_signer` flag + */ +export interface Transfer2InstructionData { + withTransactionHash: boolean; + withLamportsChangeAccountMerkleTreeIndex: boolean; + lamportsChangeAccountMerkleTreeIndex: number; + lamportsChangeAccountOwnerIndex: number; + outputQueue: number; + maxTopUp: number; + cpiContext: CompressedCpiContext | null; + compressions: Compression[] | null; + proof: { a: number[]; b: number[]; c: number[] } | null; + inTokenData: MultiInputTokenDataWithContext[]; + outTokenData: MultiTokenTransferOutputData[]; + inLamports: bigint[] | null; + outLamports: bigint[] | null; + /** Extensions for input light-token accounts (one array per input account) */ + inTlv: Transfer2ExtensionData[][] | null; + /** Extensions for output light-token accounts (one array per output account) */ + outTlv: Transfer2ExtensionData[][] | null; +} + +// Borsh layouts for extension data + +const CompressedOnlyExtensionInstructionDataLayout = struct([ + u64('delegatedAmount'), + u64('withheldTransferFee'), + bool('isFrozen'), + u8('compressionIndex'), + bool('isAta'), + u8('bump'), + u8('ownerIndex'), +]); + +const RentConfigLayout = struct([ + u16('baseRent'), + u16('compressionCost'), + u8('lamportsPerBytePerEpoch'), + u8('maxFundedEpochs'), + u16('maxTopUp'), +]); + +const CompressionInfoLayout = struct([ + u16('configAccountVersion'), + u8('compressToPubkey'), + u8('accountVersion'), + u32('lamportsPerWrite'), + array(u8(), 32, 'compressionAuthority'), + array(u8(), 32, 'rentSponsor'), + u64('lastClaimedSlot'), + u32('rentExemptionPaid'), + u32('reserved'), + RentConfigLayout.replicate('rentConfig'), +]); + +/** + * Serialize a single Transfer2ExtensionData to bytes + * @internal + */ +function serializeExtensionInstructionData( + ext: Transfer2ExtensionData, +): Uint8Array { + const buffer = Buffer.alloc(1024); + let offset = 0; + + // Write discriminant + if (ext.type === 'TokenMetadata') { + buffer.writeUInt8(EXTENSION_DISCRIMINANT_TOKEN_METADATA, offset); + offset += 1; + const data = { + updateAuthority: ext.data.updateAuthority, + name: ext.data.name, + symbol: ext.data.symbol, + uri: ext.data.uri, + additionalMetadata: ext.data.additionalMetadata + ? ext.data.additionalMetadata.map(m => ({ + key: Buffer.from(m.key), + value: Buffer.from(m.value), + })) + : null, + }; + offset += TokenMetadataInstructionDataLayout.encode( + data, + buffer, + offset, + ); + } else if (ext.type === 'CompressedOnly') { + buffer.writeUInt8(EXTENSION_DISCRIMINANT_COMPRESSED_ONLY, offset); + offset += 1; + const data = { + delegatedAmount: bn(ext.data.delegatedAmount.toString()), + withheldTransferFee: bn(ext.data.withheldTransferFee.toString()), + isFrozen: ext.data.isFrozen, + compressionIndex: ext.data.compressionIndex, + isAta: ext.data.isAta, + bump: ext.data.bump, + ownerIndex: ext.data.ownerIndex, + }; + offset += CompressedOnlyExtensionInstructionDataLayout.encode( + data, + buffer, + offset, + ); + } else if (ext.type === 'Compressible') { + buffer.writeUInt8(EXTENSION_DISCRIMINANT_COMPRESSIBLE, offset); + offset += 1; + const data = { + configAccountVersion: ext.data.configAccountVersion, + compressToPubkey: ext.data.compressToPubkey, + accountVersion: ext.data.accountVersion, + lamportsPerWrite: ext.data.lamportsPerWrite, + compressionAuthority: Array.from( + ext.data.compressionAuthority.toBytes(), + ), + rentSponsor: Array.from(ext.data.rentSponsor.toBytes()), + lastClaimedSlot: bn(ext.data.lastClaimedSlot.toString()), + rentExemptionPaid: ext.data.rentExemptionPaid, + reserved: ext.data.reserved, + rentConfig: ext.data.rentConfig, + }; + offset += CompressionInfoLayout.encode(data, buffer, offset); + } + + return buffer.subarray(0, offset); +} + +/** + * Serialize Vec> to bytes for Borsh + * @internal + */ +function serializeExtensionTlv( + tlv: Transfer2ExtensionData[][] | null, +): Uint8Array | null { + if (tlv === null) { + return null; + } + + const chunks: Uint8Array[] = []; + + // Write outer vec length (4 bytes, little-endian) + const outerLenBuf = Buffer.alloc(4); + outerLenBuf.writeUInt32LE(tlv.length, 0); + chunks.push(outerLenBuf); + + for (const innerVec of tlv) { + // Write inner vec length (4 bytes, little-endian) + const innerLenBuf = Buffer.alloc(4); + innerLenBuf.writeUInt32LE(innerVec.length, 0); + chunks.push(innerLenBuf); + + for (const ext of innerVec) { + chunks.push(serializeExtensionInstructionData(ext)); + } + } + + return Buffer.concat(chunks); +} + +// Borsh layouts +const CompressionLayout = struct([ + u8('mode'), + u64('amount'), + u8('mint'), + u8('sourceOrRecipient'), + u8('authority'), + u8('poolAccountIndex'), + u8('poolIndex'), + u8('bump'), + u8('decimals'), +]); + +const PackedMerkleContextLayout = struct([ + u8('merkleTreePubkeyIndex'), + u8('queuePubkeyIndex'), + u32('leafIndex'), + bool('proveByIndex'), +]); + +const MultiInputTokenDataWithContextLayout = struct([ + u8('owner'), + u64('amount'), + bool('hasDelegate'), + u8('delegate'), + u8('mint'), + u8('version'), + PackedMerkleContextLayout.replicate('merkleContext'), + u16('rootIndex'), +]); + +const MultiTokenTransferOutputDataLayout = struct([ + u8('owner'), + u64('amount'), + bool('hasDelegate'), + u8('delegate'), + u8('mint'), + u8('version'), +]); + +const CompressedCpiContextLayout = struct([ + bool('setContext'), + bool('firstSetContext'), + u8('cpiContextAccountIndex'), +]); + +// Layout without TLV fields - we'll serialize those manually +const Transfer2InstructionDataBaseLayout = struct([ + bool('withTransactionHash'), + bool('withLamportsChangeAccountMerkleTreeIndex'), + u8('lamportsChangeAccountMerkleTreeIndex'), + u8('lamportsChangeAccountOwnerIndex'), + u8('outputQueue'), + u16('maxTopUp'), + option(CompressedCpiContextLayout, 'cpiContext'), + option(vec(CompressionLayout), 'compressions'), + option(CompressedProofLayout, 'proof'), + vec(MultiInputTokenDataWithContextLayout, 'inTokenData'), + vec(MultiTokenTransferOutputDataLayout, 'outTokenData'), + option(vec(u64()), 'inLamports'), + option(vec(u64()), 'outLamports'), +]); + +/** + * Encode Transfer2 instruction data using Borsh + * @internal + */ +export function encodeTransfer2InstructionData( + data: Transfer2InstructionData, +): Buffer { + // Convert bigint values to BN for Borsh encoding + const baseData = { + withTransactionHash: data.withTransactionHash, + withLamportsChangeAccountMerkleTreeIndex: + data.withLamportsChangeAccountMerkleTreeIndex, + lamportsChangeAccountMerkleTreeIndex: + data.lamportsChangeAccountMerkleTreeIndex, + lamportsChangeAccountOwnerIndex: data.lamportsChangeAccountOwnerIndex, + outputQueue: data.outputQueue, + maxTopUp: data.maxTopUp, + cpiContext: data.cpiContext, + compressions: + data.compressions?.map(c => ({ + ...c, + amount: bn(c.amount.toString()), + })) ?? null, + proof: data.proof, + inTokenData: data.inTokenData.map(t => ({ + ...t, + amount: bn(t.amount.toString()), + })), + outTokenData: data.outTokenData.map(t => ({ + ...t, + amount: bn(t.amount.toString()), + })), + inLamports: data.inLamports?.map(v => bn(v.toString())) ?? null, + outLamports: data.outLamports?.map(v => bn(v.toString())) ?? null, + }; + + // Encode base layout + const baseBuffer = Buffer.alloc(4000); + const baseLen = Transfer2InstructionDataBaseLayout.encode( + baseData, + baseBuffer, + ); + + // Manually serialize TLV fields + const chunks: Buffer[] = [ + TRANSFER2_DISCRIMINATOR, + baseBuffer.subarray(0, baseLen), + ]; + + // Serialize inTlv as Option>> + if (data.inTlv === null) { + // Option::None = 0 + chunks.push(Buffer.from([0])); + } else { + // Option::Some = 1 + chunks.push(Buffer.from([1])); + const serialized = serializeExtensionTlv(data.inTlv); + if (serialized) { + chunks.push(Buffer.from(serialized)); + } + } + + // Serialize outTlv as Option>> + if (data.outTlv === null) { + // Option::None = 0 + chunks.push(Buffer.from([0])); + } else { + // Option::Some = 1 + chunks.push(Buffer.from([1])); + const serialized = serializeExtensionTlv(data.outTlv); + if (serialized) { + chunks.push(Buffer.from(serialized)); + } + } + + return Buffer.concat(chunks); +} + +/** + * @internal + * Create a compression struct for wrapping SPL tokens to light-token + * (compress from SPL associated token account) + */ +export function createCompressSpl( + amount: bigint, + mintIndex: number, + sourceIndex: number, + authorityIndex: number, + poolAccountIndex: number, + poolIndex: number, + bump: number, + decimals: number, +): Compression { + return { + mode: COMPRESSION_MODE_COMPRESS, + amount, + mint: mintIndex, + sourceOrRecipient: sourceIndex, + authority: authorityIndex, + poolAccountIndex, + poolIndex, + bump, + decimals, + }; +} + +/** + * @internal + * Create a compression struct for decompressing to light-token associated token account + * @param amount - Amount to decompress + * @param mintIndex - Index of mint in packed accounts + * @param recipientIndex - Index of recipient light-token account in packed accounts + * @param tokenProgramIndex - Index of light-token program in packed accounts (for CPI) + */ +export function createDecompressLightToken( + amount: bigint, + mintIndex: number, + recipientIndex: number, + tokenProgramIndex?: number, +): Compression { + return { + mode: COMPRESSION_MODE_DECOMPRESS, + amount, + mint: mintIndex, + sourceOrRecipient: recipientIndex, + authority: 0, + poolAccountIndex: tokenProgramIndex ?? 0, + poolIndex: 0, + bump: 0, + decimals: 0, + }; +} + +/** + * @internal + * Create a compression struct for compressing light-token (burn from light-token associated token account) + * Used in unwrap flow: light-token associated token account -> pool -> SPL associated token account + * @param amount - Amount to compress (burn from light-token) + * @param mintIndex - Index of mint in packed accounts + * @param sourceIndex - Index of source light-token account in packed accounts + * @param authorityIndex - Index of authority/owner in packed accounts (must sign) + * @param tokenProgramIndex - Index of light-token program in packed accounts (for CPI) + */ +export function createCompressLightToken( + amount: bigint, + mintIndex: number, + sourceIndex: number, + authorityIndex: number, + tokenProgramIndex?: number, +): Compression { + return { + mode: COMPRESSION_MODE_COMPRESS, + amount, + mint: mintIndex, + sourceOrRecipient: sourceIndex, + authority: authorityIndex, + poolAccountIndex: tokenProgramIndex ?? 0, + poolIndex: 0, + bump: 0, + decimals: 0, + }; +} + +/** + * Create a compression struct for decompressing SPL tokens + * @internal + */ +export function createDecompressSpl( + amount: bigint, + mintIndex: number, + recipientIndex: number, + poolAccountIndex: number, + poolIndex: number, + bump: number, + decimals: number, +): Compression { + return { + mode: COMPRESSION_MODE_DECOMPRESS, + amount, + mint: mintIndex, + sourceOrRecipient: recipientIndex, + authority: 0, + poolAccountIndex, + poolIndex, + bump, + decimals, + }; +} diff --git a/js/token-interface/src/instructions/layout/layout.ts b/js/token-interface/src/instructions/layout/layout.ts new file mode 100644 index 0000000000..8641d5c93b --- /dev/null +++ b/js/token-interface/src/instructions/layout/layout.ts @@ -0,0 +1,3 @@ +export * from './layout-mint'; +export * from './layout-mint-action'; +export * from './layout-transfer2'; diff --git a/js/token-interface/src/instructions/load.ts b/js/token-interface/src/instructions/load.ts new file mode 100644 index 0000000000..24d4ea926c --- /dev/null +++ b/js/token-interface/src/instructions/load.ts @@ -0,0 +1,410 @@ +import { + Rpc, + LIGHT_TOKEN_PROGRAM_ID, + assertV2Enabled, +} from '@lightprotocol/stateless.js'; +import { + ComputeBudgetProgram, + PublicKey, + TransactionInstruction, +} from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, + TokenAccountNotFoundError, +} from '@solana/spl-token'; +import { + AccountView, + checkNotFrozen, + getAtaView as _getAtaView, + isAuthorityForAccount, + filterAccountForAuthority, +} from '../read/get-account'; +import { getAssociatedTokenAddress } from '../read/associated-token-address'; +import { createAtaIdempotent } from './ata'; +import { createWrapInstruction } from './wrap'; +import { getSplInterfaces, type SplInterface } from '../spl-interface'; +import { getAtaProgramId, checkAtaAddress, AtaType } from '../read/ata-utils'; +import type { LoadOptions } from '../load-options'; +import { getMint } from '../read/get-mint'; +import { toLoadOptions } from '../helpers'; +import { getAtaAddress } from '../read'; +import type { CreateLoadInstructionsInput } from '../types'; +import { toInstructionPlan } from './_plan'; +import { createDecompressInstruction } from './load/decompress'; +import { selectPrimaryColdCompressedAccountForLoad } from './load/select-primary-cold-account'; +export { createDecompressInstruction } from './load/decompress'; + +async function _buildLoadInstructions( + rpc: Rpc, + payer: PublicKey, + ata: AccountView, + options: LoadOptions | undefined, + wrap: boolean, + targetAta: PublicKey, + targetAmount: bigint | undefined, + authority: PublicKey | undefined, + decimals: number, + allowFrozen: boolean, +): Promise { + if (!ata._isAta || !ata._owner || !ata._mint) { + throw new Error( + 'AccountView must be from getAtaView (requires _isAta, _owner, _mint)', + ); + } + + if (!allowFrozen) { + checkNotFrozen(ata, 'load'); + } + + const owner = ata._owner; + const mint = ata._mint; + const sources = ata._sources ?? []; + + const primaryColdCompressedAccount = + selectPrimaryColdCompressedAccountForLoad(sources); + + const lightTokenAtaAddress = getAssociatedTokenAddress(mint, owner); + const splAta = getAssociatedTokenAddressSync( + mint, + owner, + false, + TOKEN_PROGRAM_ID, + getAtaProgramId(TOKEN_PROGRAM_ID), + ); + const t22Ata = getAssociatedTokenAddressSync( + mint, + owner, + false, + TOKEN_2022_PROGRAM_ID, + getAtaProgramId(TOKEN_2022_PROGRAM_ID), + ); + + let ataType: AtaType = 'light-token'; + const validation = checkAtaAddress(targetAta, mint, owner); + ataType = validation.type; + if (wrap && ataType !== 'light-token') { + throw new Error( + `For wrap=true, targetAta must be light-token associated token account. Got ${ataType} associated token account.`, + ); + } + + const splSource = sources.find(s => s.type === 'spl'); + const t22Source = sources.find(s => s.type === 'token2022'); + const lightTokenHotSource = sources.find(s => s.type === 'light-token-hot'); + const splBalance = splSource?.amount ?? BigInt(0); + const t22Balance = t22Source?.amount ?? BigInt(0); + const coldBalance = primaryColdCompressedAccount + ? BigInt(primaryColdCompressedAccount.parsed.amount.toString()) + : BigInt(0); + + if ( + splBalance === BigInt(0) && + t22Balance === BigInt(0) && + coldBalance === BigInt(0) + ) { + return []; + } + + const needsSplProgram = ataType === 'spl' || splBalance > BigInt(0); + const needsToken2022Program = + ataType === 'token2022' || t22Balance > BigInt(0); + + let splProgramInterface: SplInterface | undefined; + let token2022ProgramInterface: SplInterface | undefined; + if (needsSplProgram || needsToken2022Program) { + const splInterfaces = + options?.splInterfaces ?? (await getSplInterfaces(rpc, mint)); + splProgramInterface = splInterfaces.find( + info => + info.isInitialized && + info.tokenProgramId.equals(TOKEN_PROGRAM_ID), + ); + token2022ProgramInterface = splInterfaces.find( + info => + info.isInitialized && + info.tokenProgramId.equals(TOKEN_2022_PROGRAM_ID), + ); + + if (needsSplProgram && !splProgramInterface) { + throw new Error( + `No initialized SPL interface found for mint ${mint.toBase58()} and token program ${TOKEN_PROGRAM_ID.toBase58()}.`, + ); + } + if (needsToken2022Program && !token2022ProgramInterface) { + throw new Error( + `No initialized SPL interface found for mint ${mint.toBase58()} and token program ${TOKEN_2022_PROGRAM_ID.toBase58()}.`, + ); + } + } + + const setupInstructions: TransactionInstruction[] = []; + + let decompressTarget: PublicKey = lightTokenAtaAddress; + let decompressSplInfo: SplInterface | undefined; + let canDecompress = false; + + if (wrap) { + decompressTarget = lightTokenAtaAddress; + decompressSplInfo = undefined; + canDecompress = true; + + if (!lightTokenHotSource) { + setupInstructions.push( + createAtaIdempotent({ + payer, + associatedToken: lightTokenAtaAddress, + owner, + mint, + programId: LIGHT_TOKEN_PROGRAM_ID, + }), + ); + } + + if (splBalance > BigInt(0)) { + setupInstructions.push( + createWrapInstruction({ + source: splAta, + destination: lightTokenAtaAddress, + owner, + mint, + amount: splBalance, + splInterface: splProgramInterface!, + decimals, + payer, + }), + ); + } + + if (t22Balance > BigInt(0)) { + setupInstructions.push( + createWrapInstruction({ + source: t22Ata, + destination: lightTokenAtaAddress, + owner, + mint, + amount: t22Balance, + splInterface: token2022ProgramInterface!, + decimals, + payer, + }), + ); + } + } else { + if (ataType === 'light-token') { + decompressTarget = lightTokenAtaAddress; + decompressSplInfo = undefined; + canDecompress = true; + if (!lightTokenHotSource) { + setupInstructions.push( + createAtaIdempotent({ + payer, + associatedToken: lightTokenAtaAddress, + owner, + mint, + programId: LIGHT_TOKEN_PROGRAM_ID, + }), + ); + } + } else if (ataType === 'spl') { + decompressTarget = splAta; + decompressSplInfo = splProgramInterface!; + canDecompress = true; + if (!splSource) { + setupInstructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + splAta, + owner, + mint, + TOKEN_PROGRAM_ID, + ), + ); + } + } else if (ataType === 'token2022') { + decompressTarget = t22Ata; + decompressSplInfo = token2022ProgramInterface!; + canDecompress = true; + if (!t22Source) { + setupInstructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + t22Ata, + owner, + mint, + TOKEN_2022_PROGRAM_ID, + ), + ); + } + } + } + + let accountToLoad = primaryColdCompressedAccount; + + if ( + targetAmount !== undefined && + canDecompress && + primaryColdCompressedAccount + ) { + const isDelegate = authority !== undefined && !authority.equals(owner); + const hotBalance = (() => { + if (!lightTokenHotSource) return BigInt(0); + if (isDelegate) { + const delegated = + lightTokenHotSource.parsed.delegatedAmount ?? BigInt(0); + return delegated < lightTokenHotSource.amount + ? delegated + : lightTokenHotSource.amount; + } + return lightTokenHotSource.amount; + })(); + let effectiveHotAfterSetup: bigint; + + if (wrap) { + effectiveHotAfterSetup = hotBalance + splBalance + t22Balance; + } else if (ataType === 'light-token') { + effectiveHotAfterSetup = hotBalance; + } else if (ataType === 'spl') { + effectiveHotAfterSetup = splBalance; + } else { + effectiveHotAfterSetup = t22Balance; + } + + const neededFromCold = + targetAmount > effectiveHotAfterSetup + ? targetAmount - effectiveHotAfterSetup + : BigInt(0); + + if (neededFromCold === BigInt(0)) { + accountToLoad = null; + } + } + + if (!canDecompress || !accountToLoad) { + return setupInstructions; + } + + const proof = await rpc.getValidityProofV0([ + { + hash: accountToLoad.compressedAccount.hash, + tree: accountToLoad.compressedAccount.treeInfo.tree, + queue: accountToLoad.compressedAccount.treeInfo.queue, + }, + ]); + const authorityForDecompress = authority ?? owner; + const amountToDecompress = BigInt(accountToLoad.parsed.amount.toString()); + + return [ + ...setupInstructions, + createDecompressInstruction({ + payer, + inputCompressedTokenAccounts: [accountToLoad], + toAddress: decompressTarget, + amount: amountToDecompress, + validityProof: proof, + splInterface: decompressSplInfo, + decimals, + authority: authorityForDecompress, + }), + ]; +} + +export interface CreateLoadInstructionOptions + extends CreateLoadInstructionsInput { + authority?: PublicKey; + wrap?: boolean; + allowFrozen?: boolean; + splInterfaces?: SplInterface[]; + decimals?: number; +} + +function buildLoadOptions( + owner: PublicKey, + authority: PublicKey | undefined, + wrap: boolean, + splInterfaces: SplInterface[] | undefined, +): LoadOptions | undefined { + const options = toLoadOptions(owner, authority, wrap) ?? {}; + if (splInterfaces) { + options.splInterfaces = splInterfaces; + } + return Object.keys(options).length === 0 ? undefined : options; +} + +export async function createLoadInstructions({ + rpc, + payer, + owner, + mint, + authority, + wrap = true, + allowFrozen = false, + splInterfaces, + decimals, +}: CreateLoadInstructionOptions): Promise { + const targetAta = getAtaAddress({ owner, mint }); + const loadOptions = buildLoadOptions(owner, authority, wrap, splInterfaces); + + assertV2Enabled(); + payer ??= owner; + const authorityPubkey = loadOptions?.delegatePubkey ?? owner; + + let accountView: AccountView; + try { + accountView = await _getAtaView( + rpc, + targetAta, + owner, + mint, + undefined, + undefined, + wrap, + ); + } catch (e) { + if (e instanceof TokenAccountNotFoundError) { + return []; + } + throw e; + } + + const resolvedDecimals = + decimals ?? (await getMint(rpc, mint)).mint.decimals; + + if (!owner.equals(authorityPubkey)) { + if (!isAuthorityForAccount(accountView, authorityPubkey)) { + throw new Error( + 'Signer is not the owner or a delegate of the account.', + ); + } + accountView = filterAccountForAuthority(accountView, authorityPubkey); + } + + const instructions = await _buildLoadInstructions( + rpc, + payer, + accountView, + loadOptions, + wrap, + targetAta, + undefined, + authorityPubkey, + resolvedDecimals, + allowFrozen, + ); + + if (instructions.length === 0) { + return []; + } + return instructions.filter( + instruction => + !instruction.programId.equals(ComputeBudgetProgram.programId), + ); +} + +export async function createLoadInstructionPlan( + input: CreateLoadInstructionsInput, +) { + return toInstructionPlan(await createLoadInstructions(input)); +} diff --git a/js/token-interface/src/instructions/load/decompress.ts b/js/token-interface/src/instructions/load/decompress.ts new file mode 100644 index 0000000000..e001419bfd --- /dev/null +++ b/js/token-interface/src/instructions/load/decompress.ts @@ -0,0 +1,524 @@ +import { + LIGHT_TOKEN_PROGRAM_ID, + LightSystemProgram, + ParsedTokenAccount, + ValidityProofWithContext, + defaultStaticAccountsStruct, +} from '@lightprotocol/stateless.js'; +import { Buffer } from 'buffer'; +import { + PublicKey, + SystemProgram, + TransactionInstruction, +} from '@solana/web3.js'; +import { + COMPRESSED_TOKEN_PROGRAM_ID, + MAX_TOP_UP, + TokenDataVersion, + deriveCpiAuthorityPda, +} from '../../constants'; +import { + COMPRESSION_MODE_DECOMPRESS, + Compression, + MultiInputTokenDataWithContext, + Transfer2ExtensionData, + Transfer2InstructionData, + encodeTransfer2InstructionData, +} from '../layout/layout-transfer2'; +import { SplInterface } from '../../spl-interface'; + +const COMPRESSED_ONLY_DISC = 31; +const COMPRESSED_ONLY_SIZE = 17; // u64 + u64 + u8 + +interface ParsedCompressedOnly { + delegatedAmount: bigint; + withheldTransferFee: bigint; + isAta: boolean; +} + +/** + * Parse CompressedOnly extension from a Borsh-serialized TLV buffer + * (Vec). Returns null if no CompressedOnly found. + * @internal + */ +function parseCompressedOnlyFromTlv( + tlv: Buffer | null, +): ParsedCompressedOnly | null { + if (!tlv || tlv.length < 5) return null; + try { + let offset = 0; + const vecLen = tlv.readUInt32LE(offset); + offset += 4; + for (let i = 0; i < vecLen; i++) { + if (offset >= tlv.length) return null; + const disc = tlv[offset]; + offset += 1; + if (disc === COMPRESSED_ONLY_DISC) { + if (offset + COMPRESSED_ONLY_SIZE > tlv.length) return null; + const loDA = BigInt(tlv.readUInt32LE(offset)); + const hiDA = BigInt(tlv.readUInt32LE(offset + 4)); + const delegatedAmount = loDA | (hiDA << BigInt(32)); + const loFee = BigInt(tlv.readUInt32LE(offset + 8)); + const hiFee = BigInt(tlv.readUInt32LE(offset + 12)); + const withheldTransferFee = loFee | (hiFee << BigInt(32)); + const isAta = tlv[offset + 16] !== 0; + return { delegatedAmount, withheldTransferFee, isAta }; + } + const SIZES: Record = { + 29: 8, + 30: 1, + 31: 17, + }; + const size = SIZES[disc]; + if (size === undefined) { + throw new Error( + `parseCompressedOnlyFromTlv: unknown TLV extension discriminant ${disc}`, + ); + } + offset += size; + } + } catch { + // Ignoring unknown TLV extensions. + return null; + } + return null; +} + +/** + * Build inTlv array for Transfer2 from input compressed accounts. + * For each account, if CompressedOnly TLV is present, converts it to + * the instruction format (enriched with is_frozen, compression_index, + * bump, owner_index). Returns null if no accounts have TLV. + * @internal + */ +function buildInTlv( + accounts: ParsedTokenAccount[], + ownerIndex: number, + owner: PublicKey, + mint: PublicKey, +): Transfer2ExtensionData[][] | null { + let hasAny = false; + const result: Transfer2ExtensionData[][] = []; + + for (const acc of accounts) { + const co = parseCompressedOnlyFromTlv(acc.parsed.tlv); + if (!co) { + result.push([]); + continue; + } + hasAny = true; + let bump = 0; + if (co.isAta) { + const seeds = [ + owner.toBuffer(), + LIGHT_TOKEN_PROGRAM_ID.toBuffer(), + mint.toBuffer(), + ]; + const [, b] = PublicKey.findProgramAddressSync( + seeds, + LIGHT_TOKEN_PROGRAM_ID, + ); + bump = b; + } + const isFrozen = acc.parsed.state === 2; + result.push([ + { + type: 'CompressedOnly', + data: { + delegatedAmount: co.delegatedAmount, + withheldTransferFee: co.withheldTransferFee, + isFrozen, + // This builder emits a single decompress compression per batch. + // Keep index at 0 unless multi-compression output is added here. + compressionIndex: 0, + isAta: co.isAta, + bump, + ownerIndex, + }, + }, + ]); + } + return hasAny ? result : null; +} + +/** + * Get token data version from compressed account discriminator. + * @internal + */ +function getVersionFromDiscriminator( + discriminator: number[] | undefined, +): number { + if (!discriminator || discriminator.length < 8) { + // Default to ShaFlat for new accounts without discriminator + return TokenDataVersion.ShaFlat; + } + + // V1 has discriminator[0] = 2 + if (discriminator[0] === 2) { + return TokenDataVersion.V1; + } + + // V2 and ShaFlat have version in discriminator[7] + const versionByte = discriminator[7]; + if (versionByte === 3) { + return TokenDataVersion.V2; + } + if (versionByte === 4) { + return TokenDataVersion.ShaFlat; + } + + // Default to ShaFlat + return TokenDataVersion.ShaFlat; +} + +/** + * Build input token data for Transfer2 from parsed token accounts + * @internal + */ +function buildInputTokenData( + accounts: ParsedTokenAccount[], + rootIndices: number[], + packedAccountIndices: Map, +): MultiInputTokenDataWithContext[] { + return accounts.map((acc, i) => { + const ownerKey = acc.parsed.owner.toBase58(); + const mintKey = acc.parsed.mint.toBase58(); + + const version = getVersionFromDiscriminator( + acc.compressedAccount.data?.discriminator, + ); + + return { + owner: packedAccountIndices.get(ownerKey)!, + amount: BigInt(acc.parsed.amount.toString()), + hasDelegate: acc.parsed.delegate !== null, + delegate: acc.parsed.delegate + ? (packedAccountIndices.get(acc.parsed.delegate.toBase58()) ?? + 0) + : 0, + mint: packedAccountIndices.get(mintKey)!, + version, + merkleContext: { + merkleTreePubkeyIndex: packedAccountIndices.get( + acc.compressedAccount.treeInfo.tree.toBase58(), + )!, + queuePubkeyIndex: packedAccountIndices.get( + acc.compressedAccount.treeInfo.queue.toBase58(), + )!, + leafIndex: acc.compressedAccount.leafIndex, + proveByIndex: acc.compressedAccount.proveByIndex, + }, + rootIndex: rootIndices[i], + }; + }); +} + +/** + * Create decompress instruction using Transfer2. + * + * @internal Use createLoadInstructions instead. + * + * Supports decompressing to both light-token accounts and SPL token accounts: + * - For light-token destinations: No splInterface needed + * - For SPL destinations: Provide splInterface and decimals + */ +export function createDecompressInstruction({ + payer, + inputCompressedTokenAccounts, + toAddress, + amount, + validityProof, + splInterface, + decimals, + maxTopUp, + authority, +}: { + payer?: PublicKey; + inputCompressedTokenAccounts: ParsedTokenAccount[]; + toAddress: PublicKey; + amount: bigint; + validityProof: ValidityProofWithContext; + splInterface?: SplInterface; + decimals: number; + maxTopUp?: number; + authority?: PublicKey; +}): TransactionInstruction { + if (inputCompressedTokenAccounts.length === 0) { + throw new Error('No input light-token accounts provided'); + } + + const mint = inputCompressedTokenAccounts[0].parsed.mint; + const owner = inputCompressedTokenAccounts[0].parsed.owner; + for (const account of inputCompressedTokenAccounts) { + if (!account.parsed.mint.equals(mint)) { + throw new Error( + 'All input light-token accounts must have the same mint for decompress.', + ); + } + if (!account.parsed.owner.equals(owner)) { + throw new Error( + 'All input light-token accounts must have the same owner for decompress.', + ); + } + } + + // Build packed accounts map + // Order: trees/queues first, then mint, owner, light-token account, light-token program + const packedAccountIndices = new Map(); + const packedAccounts: PublicKey[] = []; + + // Collect unique trees and queues + const treeSet = new Set(); + const queueSet = new Set(); + for (const acc of inputCompressedTokenAccounts) { + treeSet.add(acc.compressedAccount.treeInfo.tree.toBase58()); + queueSet.add(acc.compressedAccount.treeInfo.queue.toBase58()); + } + + // Add trees first (owned by account compression program) + for (const tree of treeSet) { + packedAccountIndices.set(tree, packedAccounts.length); + packedAccounts.push(new PublicKey(tree)); + } + + let firstQueueIndex = 0; + let isFirstQueue = true; + for (const queue of queueSet) { + if (isFirstQueue) { + firstQueueIndex = packedAccounts.length; + isFirstQueue = false; + } + packedAccountIndices.set(queue, packedAccounts.length); + packedAccounts.push(new PublicKey(queue)); + } + + // Add mint + const mintIndex = packedAccounts.length; + packedAccountIndices.set(mint.toBase58(), mintIndex); + packedAccounts.push(mint); + + // Add owner + const ownerIndex = packedAccounts.length; + packedAccountIndices.set(owner.toBase58(), ownerIndex); + packedAccounts.push(owner); + + // Add destination token account (light-token or SPL) + const destinationIndex = packedAccounts.length; + packedAccountIndices.set(toAddress.toBase58(), destinationIndex); + packedAccounts.push(toAddress); + + // Add unique delegate pubkeys from input accounts + for (const acc of inputCompressedTokenAccounts) { + if (acc.parsed.delegate) { + const delegateKey = acc.parsed.delegate.toBase58(); + if (!packedAccountIndices.has(delegateKey)) { + packedAccountIndices.set(delegateKey, packedAccounts.length); + packedAccounts.push(acc.parsed.delegate); + } + } + } + + // For SPL decompression, add pool account and token program + let poolAccountIndex = 0; + let poolIndex = 0; + let poolBump = 0; + let tokenProgramIndex = 0; + + if (splInterface) { + // Add SPL interface PDA (token pool) + poolAccountIndex = packedAccounts.length; + packedAccountIndices.set( + splInterface.poolPda.toBase58(), + poolAccountIndex, + ); + packedAccounts.push(splInterface.poolPda); + + // Add SPL token program + tokenProgramIndex = packedAccounts.length; + packedAccountIndices.set( + splInterface.tokenProgramId.toBase58(), + tokenProgramIndex, + ); + packedAccounts.push(splInterface.tokenProgramId); + + poolIndex = splInterface.derivationIndex; + poolBump = splInterface.bump; + } + + // Keep token program index materialized to preserve packed account ordering + // and index side effects for instruction construction. + void tokenProgramIndex; + + // Build input token data + const inTokenData = buildInputTokenData( + inputCompressedTokenAccounts, + validityProof.rootIndices, + packedAccountIndices, + ); + + // Calculate total input amount and change + const totalInputAmount = inputCompressedTokenAccounts.reduce( + (sum, acc) => sum + BigInt(acc.parsed.amount.toString()), + BigInt(0), + ); + if (amount > totalInputAmount) { + throw new Error( + `Decompress amount ${amount.toString()} exceeds total input amount ${totalInputAmount.toString()}.`, + ); + } + const changeAmount = totalInputAmount - amount; + + const outTokenData: { + owner: number; + amount: bigint; + hasDelegate: boolean; + delegate: number; + mint: number; + version: number; + }[] = []; + + if (changeAmount > 0) { + const version = getVersionFromDiscriminator( + inputCompressedTokenAccounts[0].compressedAccount.data + ?.discriminator, + ); + + outTokenData.push({ + owner: ownerIndex, + amount: changeAmount, + hasDelegate: false, + delegate: 0, + mint: mintIndex, + version, + }); + } + + // Build decompress compression + // For light-token: pool values are 0 (unused) + // For SPL: pool values point to SPL interface PDA + const compressions: Compression[] = [ + { + mode: COMPRESSION_MODE_DECOMPRESS, + amount, + mint: mintIndex, + sourceOrRecipient: destinationIndex, + authority: 0, // Not needed for decompress + poolAccountIndex: splInterface ? poolAccountIndex : 0, + poolIndex: splInterface ? poolIndex : 0, + bump: splInterface ? poolBump : 0, + decimals, + }, + ]; + + // Build Transfer2 instruction data + const instructionData: Transfer2InstructionData = { + withTransactionHash: false, + withLamportsChangeAccountMerkleTreeIndex: false, + lamportsChangeAccountMerkleTreeIndex: 0, + lamportsChangeAccountOwnerIndex: 0, + outputQueue: firstQueueIndex, // First queue in packed accounts + maxTopUp: maxTopUp ?? MAX_TOP_UP, + cpiContext: null, + compressions, + proof: validityProof.compressedProof + ? { + a: Array.from(validityProof.compressedProof.a), + b: Array.from(validityProof.compressedProof.b), + c: Array.from(validityProof.compressedProof.c), + } + : null, + inTokenData, + outTokenData, + inLamports: null, + outLamports: null, + inTlv: buildInTlv( + inputCompressedTokenAccounts, + ownerIndex, + owner, + mint, + ), + outTlv: null, + }; + + const data = encodeTransfer2InstructionData(instructionData); + + // Build accounts for Transfer2 with compressed accounts (full path) + const { + accountCompressionAuthority, + registeredProgramPda, + accountCompressionProgram, + } = defaultStaticAccountsStruct(); + const signerIndex = (() => { + if (!authority || authority.equals(owner)) { + return ownerIndex; + } + const authorityIndex = packedAccountIndices.get(authority.toBase58()); + if (authorityIndex === undefined) { + throw new Error( + `Authority ${authority.toBase58()} is not present in packed accounts`, + ); + } + return authorityIndex; + })(); + const effectivePayer = payer ?? authority ?? owner; + + const keys = [ + // 0: light_system_program (non-mutable) + { + pubkey: LightSystemProgram.programId, + isSigner: false, + isWritable: false, + }, + // 1: fee_payer (signer, mutable) + { pubkey: effectivePayer, isSigner: true, isWritable: true }, + // 2: cpi_authority_pda + { + pubkey: deriveCpiAuthorityPda(), + isSigner: false, + isWritable: false, + }, + // 3: registered_program_pda + { + pubkey: registeredProgramPda, + isSigner: false, + isWritable: false, + }, + // 4: account_compression_authority + { + pubkey: accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + // 5: account_compression_program + { + pubkey: accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + // 6: system_program + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + // 7+: packed_accounts (trees/queues come first) + ...packedAccounts.map((pubkey, i) => { + const isTreeOrQueue = i < treeSet.size + queueSet.size; + const isDestination = pubkey.equals(toAddress); + const isPool = + splInterface !== undefined && + pubkey.equals(splInterface.poolPda); + return { + pubkey, + isSigner: i === signerIndex, + isWritable: isTreeOrQueue || isDestination || isPool, + }; + }), + ]; + + return new TransactionInstruction({ + programId: COMPRESSED_TOKEN_PROGRAM_ID, + keys, + data, + }); +} diff --git a/js/token-interface/src/instructions/load/select-primary-cold-account.ts b/js/token-interface/src/instructions/load/select-primary-cold-account.ts new file mode 100644 index 0000000000..183220b822 --- /dev/null +++ b/js/token-interface/src/instructions/load/select-primary-cold-account.ts @@ -0,0 +1,80 @@ +import { ParsedTokenAccount, bn } from '@lightprotocol/stateless.js'; +import { Buffer } from 'buffer'; +import { COLD_SOURCE_TYPES, TokenAccountSource } from '../../read/get-account'; + +/** + * Default load policy: select one deterministic cold compressed account. + * Priority is highest amount, then highest leaf index. + */ +export function selectPrimaryColdCompressedAccountForLoad( + sources: TokenAccountSource[], +): ParsedTokenAccount | null { + const candidates: ParsedTokenAccount[] = []; + for (const source of sources) { + if (!COLD_SOURCE_TYPES.has(source.type) || !source.loadContext) { + continue; + } + const loadContext = source.loadContext; + 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: loadContext.treeInfo, + hash: loadContext.hash, + leafIndex: loadContext.leafIndex, + proveByIndex: 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, + } satisfies ParsedTokenAccount['compressedAccount']; + + const state = !source.parsed.isInitialized + ? 0 + : source.parsed.isFrozen + ? 2 + : 1; + + candidates.push({ + compressedAccount, + 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, + }, + }); + } + + if (candidates.length === 0) { + return null; + } + + candidates.sort((a, b) => { + const amountA = BigInt(a.parsed.amount.toString()); + const amountB = BigInt(b.parsed.amount.toString()); + if (amountB > amountA) return 1; + if (amountB < amountA) return -1; + return b.compressedAccount.leafIndex - a.compressedAccount.leafIndex; + }); + + return candidates[0]; +} diff --git a/js/token-interface/src/instructions/revoke.ts b/js/token-interface/src/instructions/revoke.ts new file mode 100644 index 0000000000..e81b1c31e0 --- /dev/null +++ b/js/token-interface/src/instructions/revoke.ts @@ -0,0 +1,95 @@ +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import type { + CreateRawRevokeInstructionInput, + CreateRevokeInstructionsInput, +} from '../types'; +import { getAtaAddress } from '../read'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_REVOKE_DISCRIMINATOR = 5; + +export function createRevokeInstruction({ + tokenAccount, + owner, + payer, +}: CreateRawRevokeInstructionInput): TransactionInstruction { + const effectiveFeePayer = payer ?? owner; + + const keys = [ + { pubkey: tokenAccount, isSigner: false, isWritable: true }, + { + pubkey: owner, + isSigner: true, + isWritable: effectiveFeePayer.equals(owner), + }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { + pubkey: effectiveFeePayer, + isSigner: !effectiveFeePayer.equals(owner), + isWritable: true, + }, + ]; + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys, + data: Buffer.from([LIGHT_TOKEN_REVOKE_DISCRIMINATOR]), + }); +} + +export async function createRevokeInstructions({ + rpc, + payer, + owner, + mint, +}: CreateRevokeInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: true, + })), + createRevokeInstruction({ + tokenAccount, + owner, + payer, + }), + ]; +} + +export async function createRevokeInstructionsNowrap({ + rpc, + payer, + owner, + mint, +}: CreateRevokeInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: false, + })), + createRevokeInstruction({ + tokenAccount, + owner, + payer, + }), + ]; +} + +export async function createRevokeInstructionPlan( + input: CreateRevokeInstructionsInput, +) { + return toInstructionPlan(await createRevokeInstructions(input)); +} diff --git a/js/token-interface/src/instructions/thaw.ts b/js/token-interface/src/instructions/thaw.ts new file mode 100644 index 0000000000..01263c5ab2 --- /dev/null +++ b/js/token-interface/src/instructions/thaw.ts @@ -0,0 +1,89 @@ +import { TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import type { + CreateRawThawInstructionInput, + CreateThawInstructionsInput, +} from '../types'; +import { getAtaAddress } from '../read'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_THAW_ACCOUNT_DISCRIMINATOR = 11; + +export function createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, +}: CreateRawThawInstructionInput): TransactionInstruction { + const data = Buffer.alloc(1); + data.writeUInt8(LIGHT_TOKEN_THAW_ACCOUNT_DISCRIMINATOR, 0); + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: tokenAccount, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: freezeAuthority, isSigner: true, isWritable: false }, + ], + data, + }); +} + +export async function createThawInstructions({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateThawInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: true, + allowFrozen: true, + })), + createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, + }), + ]; +} + +export async function createThawInstructionsNowrap({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateThawInstructionsInput): Promise { + const tokenAccount = getAtaAddress({ owner, mint }); + + return [ + ...(await createLoadInstructions({ + rpc, + payer, + owner, + mint, + wrap: false, + allowFrozen: true, + })), + createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, + }), + ]; +} + +export async function createThawInstructionPlan( + input: CreateThawInstructionsInput, +) { + return toInstructionPlan(await createThawInstructions(input)); +} diff --git a/js/token-interface/src/instructions/transfer.ts b/js/token-interface/src/instructions/transfer.ts new file mode 100644 index 0000000000..8682b03aaa --- /dev/null +++ b/js/token-interface/src/instructions/transfer.ts @@ -0,0 +1,243 @@ +import { Buffer } from 'buffer'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { getSplInterfaces } from '../spl-interface'; +import { createUnwrapInstruction } from './unwrap'; +import { getMintDecimals, toBigIntAmount } from '../helpers'; +import { getAtaAddress } from '../read'; +import type { + CreateRawTransferInstructionInput, + CreateTransferInstructionsInput, +} from '../types'; +import { createLoadInstructions } from './load'; +import { toInstructionPlan } from './_plan'; +import { createAtaInstruction } from './ata'; + +const LIGHT_TOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12; + +export function createTransferCheckedInstruction({ + source, + destination, + mint, + authority, + payer, + amount, + decimals, +}: CreateRawTransferInstructionInput): TransactionInstruction { + const data = Buffer.alloc(10); + data.writeUInt8(LIGHT_TOKEN_TRANSFER_CHECKED_DISCRIMINATOR, 0); + data.writeBigUInt64LE(BigInt(amount), 1); + data.writeUInt8(decimals, 9); + + const effectivePayer = payer ?? authority; + + return new TransactionInstruction({ + programId: LIGHT_TOKEN_PROGRAM_ID, + keys: [ + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: destination, isSigner: false, isWritable: true }, + { + pubkey: authority, + isSigner: true, + isWritable: effectivePayer.equals(authority), + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + { + pubkey: effectivePayer, + isSigner: !effectivePayer.equals(authority), + isWritable: true, + }, + ], + data, + }); +} + +/** + * Canonical web3.js transfer flow builder. + * Returns an instruction array for a single transfer flow (setup + transfer). + */ +export async function createTransferInstructions({ + rpc, + payer, + mint, + sourceOwner, + authority, + recipient, + tokenProgram, + amount, +}: CreateTransferInstructionsInput): Promise { + const effectivePayer = payer ?? authority; + const amountBigInt = toBigIntAmount(amount); + const recipientTokenProgramId = tokenProgram ?? LIGHT_TOKEN_PROGRAM_ID; + const [decimals, transferSplInterfaces] = await Promise.all([ + getMintDecimals(rpc, mint), + recipientTokenProgramId.equals(LIGHT_TOKEN_PROGRAM_ID) + ? Promise.resolve(undefined) + : getSplInterfaces(rpc, mint), + ]); + const senderLoadInstructions = await createLoadInstructions({ + rpc, + payer: effectivePayer, + owner: sourceOwner, + mint, + authority, + wrap: true, + decimals, + splInterfaces: transferSplInterfaces, + }); + const recipientAta = getAtaAddress({ + owner: recipient, + mint, + programId: recipientTokenProgramId, + }); + const recipientLoadInstructions: TransactionInstruction[] = []; + const senderAta = getAtaAddress({ + owner: sourceOwner, + mint, + }); + let transferInstruction: TransactionInstruction; + if (recipientTokenProgramId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + transferInstruction = createTransferCheckedInstruction({ + source: senderAta, + destination: recipientAta, + mint, + authority, + payer: effectivePayer, + amount: amountBigInt, + decimals, + }); + } else { + if (!transferSplInterfaces) { + throw new Error( + 'Missing SPL interfaces for non-light transfer path.', + ); + } + const splInterface = transferSplInterfaces.find( + info => + info.isInitialized && + info.tokenProgramId.equals(recipientTokenProgramId), + ); + if (!splInterface) { + throw new Error( + `No initialized SPL pool found for tokenProgram ${recipientTokenProgramId.toBase58()}.`, + ); + } + transferInstruction = createUnwrapInstruction({ + source: senderAta, + destination: recipientAta, + owner: authority, + mint, + amount: amountBigInt, + splInterface, + decimals, + payer: effectivePayer, + }); + } + + return [ + ...senderLoadInstructions, + createAtaInstruction({ + payer: effectivePayer, + owner: recipient, + mint, + programId: recipientTokenProgramId, + }), + ...recipientLoadInstructions, + transferInstruction, + ]; +} + +/** + * No-wrap transfer flow builder (advanced). + */ +export async function createTransferInstructionsNowrap({ + rpc, + payer, + mint, + sourceOwner, + authority, + recipient, + tokenProgram, + amount, +}: CreateTransferInstructionsInput): Promise { + const effectivePayer = payer ?? authority; + const amountBigInt = toBigIntAmount(amount); + const recipientTokenProgramId = tokenProgram ?? LIGHT_TOKEN_PROGRAM_ID; + const [decimals, transferSplInterfaces] = await Promise.all([ + getMintDecimals(rpc, mint), + recipientTokenProgramId.equals(LIGHT_TOKEN_PROGRAM_ID) + ? Promise.resolve(undefined) + : getSplInterfaces(rpc, mint), + ]); + const senderLoadInstructions = await createLoadInstructions({ + rpc, + payer: effectivePayer, + owner: sourceOwner, + mint, + authority, + wrap: false, + decimals, + splInterfaces: transferSplInterfaces, + }); + const recipientAta = getAtaAddress({ + owner: recipient, + mint, + programId: recipientTokenProgramId, + }); + const senderAta = getAtaAddress({ + owner: sourceOwner, + mint, + }); + + let transferInstruction: TransactionInstruction; + if (recipientTokenProgramId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + transferInstruction = createTransferCheckedInstruction({ + source: senderAta, + destination: recipientAta, + mint, + authority, + payer: effectivePayer, + amount: amountBigInt, + decimals, + }); + } else { + if (!transferSplInterfaces) { + throw new Error( + 'Missing SPL interfaces for non-light transfer path.', + ); + } + const splInterface = transferSplInterfaces.find( + info => + info.isInitialized && + info.tokenProgramId.equals(recipientTokenProgramId), + ); + if (!splInterface) { + throw new Error( + `No initialized SPL pool found for tokenProgram ${recipientTokenProgramId.toBase58()}.`, + ); + } + transferInstruction = createUnwrapInstruction({ + source: senderAta, + destination: recipientAta, + owner: authority, + mint, + amount: amountBigInt, + splInterface, + decimals, + payer: effectivePayer, + }); + } + + return [...senderLoadInstructions, transferInstruction]; +} + +export async function createTransferInstructionPlan( + input: CreateTransferInstructionsInput, +) { + return toInstructionPlan(await createTransferInstructions(input)); +} diff --git a/js/token-interface/src/instructions/unwrap.ts b/js/token-interface/src/instructions/unwrap.ts new file mode 100644 index 0000000000..c70a9ccff4 --- /dev/null +++ b/js/token-interface/src/instructions/unwrap.ts @@ -0,0 +1,144 @@ +import { + PublicKey, + TransactionInstruction, + SystemProgram, +} from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + COMPRESSED_TOKEN_PROGRAM_ID, + deriveCpiAuthorityPda, + MAX_TOP_UP, +} from '../constants'; +import type { SplInterface } from '../spl-interface'; +import { + encodeTransfer2InstructionData, + createCompressLightToken, + createDecompressSpl, + type Transfer2InstructionData, + type Compression, +} from './layout/layout-transfer2'; + +export interface CreateUnwrapInstructionInput { + source: PublicKey; + destination: PublicKey; + owner: PublicKey; + mint: PublicKey; + amount: bigint; + splInterface: SplInterface; + decimals: number; + payer?: PublicKey; + maxTopUp?: number; +} + +/** + * Create an unwrap instruction that moves tokens from a light-token account to an + * SPL/T22 account. + * + * @param input Unwrap instruction input. + * @param input.source Source light-token account. + * @param input.destination Destination SPL/T22 token account. + * @param input.owner Owner/authority of the source account (signer). + * @param input.mint Mint address. + * @param input.amount Amount to unwrap. + * @param input.splInterface SPL interface info for the decompression. + * @param input.decimals Mint decimals (required for transfer_checked). + * @param input.payer Fee payer (defaults to owner). + * @param input.maxTopUp Optional cap on rent top-up (units of 1k lamports; default no cap). + * @returns Instruction to unwrap tokens + */ +export function createUnwrapInstruction({ + source, + destination, + owner, + mint, + amount, + splInterface, + decimals, + payer = owner, + maxTopUp, +}: CreateUnwrapInstructionInput): TransactionInstruction { + const MINT_INDEX = 0; + const OWNER_INDEX = 1; + const SOURCE_INDEX = 2; + const DESTINATION_INDEX = 3; + const POOL_INDEX = 4; + const LIGHT_TOKEN_PROGRAM_INDEX = 6; + + const compressions: Compression[] = [ + createCompressLightToken( + amount, + MINT_INDEX, + SOURCE_INDEX, + OWNER_INDEX, + LIGHT_TOKEN_PROGRAM_INDEX, + ), + createDecompressSpl( + amount, + MINT_INDEX, + DESTINATION_INDEX, + POOL_INDEX, + splInterface.derivationIndex, + splInterface.bump, + decimals, + ), + ]; + + const instructionData: Transfer2InstructionData = { + withTransactionHash: false, + withLamportsChangeAccountMerkleTreeIndex: false, + lamportsChangeAccountMerkleTreeIndex: 0, + lamportsChangeAccountOwnerIndex: 0, + outputQueue: 0, + maxTopUp: maxTopUp ?? MAX_TOP_UP, + cpiContext: null, + compressions, + proof: null, + inTokenData: [], + outTokenData: [], + inLamports: null, + outLamports: null, + inTlv: null, + outTlv: null, + }; + + const data = encodeTransfer2InstructionData(instructionData); + + const keys = [ + { + pubkey: deriveCpiAuthorityPda(), + isSigner: false, + isWritable: false, + }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: owner, isSigner: true, isWritable: false }, + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + { + pubkey: splInterface.poolPda, + isSigner: false, + isWritable: true, + }, + { + pubkey: splInterface.tokenProgramId, + isSigner: false, + isWritable: false, + }, + { + pubkey: LIGHT_TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + return new TransactionInstruction({ + programId: COMPRESSED_TOKEN_PROGRAM_ID, + keys, + data, + }); +} diff --git a/js/token-interface/src/instructions/wrap.ts b/js/token-interface/src/instructions/wrap.ts new file mode 100644 index 0000000000..2b53bcd58a --- /dev/null +++ b/js/token-interface/src/instructions/wrap.ts @@ -0,0 +1,146 @@ +import { + PublicKey, + TransactionInstruction, + SystemProgram, +} from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + COMPRESSED_TOKEN_PROGRAM_ID, + deriveCpiAuthorityPda, + MAX_TOP_UP, +} from '../constants'; +import type { SplInterface } from '../spl-interface'; +import { + encodeTransfer2InstructionData, + createCompressSpl, + createDecompressLightToken, + type Transfer2InstructionData, + type Compression, +} from './layout/layout-transfer2'; + +export interface CreateWrapInstructionInput { + source: PublicKey; + destination: PublicKey; + owner: PublicKey; + mint: PublicKey; + amount: bigint; + splInterface: SplInterface; + decimals: number; + payer?: PublicKey; + maxTopUp?: number; +} + +/** + * Create a wrap instruction that moves tokens from an SPL/T22 account to a + * light-token account. + * + * @param input Wrap instruction input. + * @param input.source Source SPL/T22 token account. + * @param input.destination Destination light-token account. + * @param input.owner Owner of the source account (signer). + * @param input.mint Mint address. + * @param input.amount Amount to wrap. + * @param input.splInterface SPL interface info for the compression. + * @param input.decimals Mint decimals (required for transfer_checked). + * @param input.payer Fee payer (defaults to owner). + * @param input.maxTopUp Optional cap on rent top-up (units of 1k lamports; default no cap). + * @returns Instruction to wrap tokens + */ +export function createWrapInstruction({ + source, + destination, + owner, + mint, + amount, + splInterface, + decimals, + payer = owner, + maxTopUp, +}: CreateWrapInstructionInput): TransactionInstruction { + const MINT_INDEX = 0; + const OWNER_INDEX = 1; + const SOURCE_INDEX = 2; + const DESTINATION_INDEX = 3; + const POOL_INDEX = 4; + const _SPL_TOKEN_PROGRAM_INDEX = 5; + const LIGHT_TOKEN_PROGRAM_INDEX = 6; + + const compressions: Compression[] = [ + createCompressSpl( + amount, + MINT_INDEX, + SOURCE_INDEX, + OWNER_INDEX, + POOL_INDEX, + splInterface.derivationIndex, + splInterface.bump, + decimals, + ), + createDecompressLightToken( + amount, + MINT_INDEX, + DESTINATION_INDEX, + LIGHT_TOKEN_PROGRAM_INDEX, + ), + ]; + + const instructionData: Transfer2InstructionData = { + withTransactionHash: false, + withLamportsChangeAccountMerkleTreeIndex: false, + lamportsChangeAccountMerkleTreeIndex: 0, + lamportsChangeAccountOwnerIndex: 0, + outputQueue: 0, + maxTopUp: maxTopUp ?? MAX_TOP_UP, + cpiContext: null, + compressions, + proof: null, + inTokenData: [], + outTokenData: [], + inLamports: null, + outLamports: null, + inTlv: null, + outTlv: null, + }; + + const data = encodeTransfer2InstructionData(instructionData); + + const keys = [ + { + pubkey: deriveCpiAuthorityPda(), + isSigner: false, + isWritable: false, + }, + { pubkey: payer, isSigner: true, isWritable: true }, + { pubkey: mint, isSigner: false, isWritable: false }, + { pubkey: owner, isSigner: true, isWritable: false }, + { pubkey: source, isSigner: false, isWritable: true }, + { pubkey: destination, isSigner: false, isWritable: true }, + { + pubkey: splInterface.poolPda, + isSigner: false, + isWritable: true, + }, + { + pubkey: splInterface.tokenProgramId, + isSigner: false, + isWritable: false, + }, + { + pubkey: LIGHT_TOKEN_PROGRAM_ID, + isSigner: false, + isWritable: false, + }, + // System program needed for top-up CPIs when destination has compressible extension + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ]; + + return new TransactionInstruction({ + programId: COMPRESSED_TOKEN_PROGRAM_ID, + keys, + data, + }); +} diff --git a/js/token-interface/src/kit/index.ts b/js/token-interface/src/kit/index.ts new file mode 100644 index 0000000000..6aee74ebc3 --- /dev/null +++ b/js/token-interface/src/kit/index.ts @@ -0,0 +1,145 @@ +import type { TransactionInstruction } from '@solana/web3.js'; +import { + createTransferInstructions as createTransferInstructionsTx, + createTransferInstructionsNowrap as createTransferInstructionsNowrapTx, + createApproveInstructions as createApproveInstructionsTx, + createApproveInstructionsNowrap as createApproveInstructionsNowrapTx, + createAtaInstructions as createAtaInstructionsTx, + createBurnInstructions as createBurnInstructionsTx, + createBurnInstructionsNowrap as createBurnInstructionsNowrapTx, + createFreezeInstructions as createFreezeInstructionsTx, + createFreezeInstructionsNowrap as createFreezeInstructionsNowrapTx, + createLoadInstructions as createLoadInstructionsTx, + createRevokeInstructions as createRevokeInstructionsTx, + createRevokeInstructionsNowrap as createRevokeInstructionsNowrapTx, + createThawInstructions as createThawInstructionsTx, + createThawInstructionsNowrap as createThawInstructionsNowrapTx, +} from '../instructions'; +import type { KitInstruction } from '../instructions/_plan'; +import { toKitInstructions } from '../instructions/_plan'; +import type { + CreateApproveInstructionsInput, + CreateAtaInstructionsInput, + CreateBurnInstructionsInput, + CreateFreezeInstructionsInput, + CreateLoadInstructionsInput, + CreateRevokeInstructionsInput, + CreateThawInstructionsInput, + CreateTransferInstructionsInput, +} from '../types'; + +export type { KitInstruction }; + +export { + createApproveInstructionPlan, + createAtaInstructionPlan, + createBurnInstructionPlan, + createFreezeInstructionPlan, + createLoadInstructionPlan, + createRevokeInstructionPlan, + createThawInstructionPlan, + createTransferInstructionPlan, + toInstructionPlan, + toKitInstructions, +} from '../instructions'; + +function wrap( + instructions: Promise, +): Promise { + return instructions.then(ixs => toKitInstructions(ixs)); +} + +export async function createAtaInstructions( + input: CreateAtaInstructionsInput, +): Promise { + return wrap(createAtaInstructionsTx(input)); +} + +export async function createLoadInstructions( + input: CreateLoadInstructionsInput, +): Promise { + return wrap(createLoadInstructionsTx(input)); +} + +export async function createTransferInstructions( + input: CreateTransferInstructionsInput, +): Promise { + return wrap(createTransferInstructionsTx(input)); +} + +export async function createTransferInstructionsNowrap( + input: CreateTransferInstructionsInput, +): Promise { + return wrap(createTransferInstructionsNowrapTx(input)); +} + +export async function createApproveInstructions( + input: CreateApproveInstructionsInput, +): Promise { + return wrap(createApproveInstructionsTx(input)); +} + +export async function createApproveInstructionsNowrap( + input: CreateApproveInstructionsInput, +): Promise { + return wrap(createApproveInstructionsNowrapTx(input)); +} + +export async function createRevokeInstructions( + input: CreateRevokeInstructionsInput, +): Promise { + return wrap(createRevokeInstructionsTx(input)); +} + +export async function createRevokeInstructionsNowrap( + input: CreateRevokeInstructionsInput, +): Promise { + return wrap(createRevokeInstructionsNowrapTx(input)); +} + +export async function createFreezeInstructions( + input: CreateFreezeInstructionsInput, +): Promise { + return wrap(createFreezeInstructionsTx(input)); +} + +export async function createFreezeInstructionsNowrap( + input: CreateFreezeInstructionsInput, +): Promise { + return wrap(createFreezeInstructionsNowrapTx(input)); +} + +export async function createThawInstructions( + input: CreateThawInstructionsInput, +): Promise { + return wrap(createThawInstructionsTx(input)); +} + +export async function createThawInstructionsNowrap( + input: CreateThawInstructionsInput, +): Promise { + return wrap(createThawInstructionsNowrapTx(input)); +} + +export async function createBurnInstructions( + input: CreateBurnInstructionsInput, +): Promise { + return wrap(createBurnInstructionsTx(input)); +} + +export async function createBurnInstructionsNowrap( + input: CreateBurnInstructionsInput, +): Promise { + return wrap(createBurnInstructionsNowrapTx(input)); +} + +export type { + CreateApproveInstructionsInput, + CreateAtaInstructionsInput, + CreateBurnInstructionsInput, + CreateFreezeInstructionsInput, + CreateLoadInstructionsInput, + CreateRevokeInstructionsInput, + CreateThawInstructionsInput, + CreateTransferInstructionsInput, +}; diff --git a/js/token-interface/src/load-options.ts b/js/token-interface/src/load-options.ts new file mode 100644 index 0000000000..4495cbf272 --- /dev/null +++ b/js/token-interface/src/load-options.ts @@ -0,0 +1,8 @@ +import type { PublicKey } from '@solana/web3.js'; +import type { SplInterface } from './spl-interface'; + +export interface LoadOptions { + splInterfaces?: SplInterface[]; + wrap?: boolean; + delegatePubkey?: PublicKey; +} diff --git a/js/token-interface/src/read/associated-token-address.ts b/js/token-interface/src/read/associated-token-address.ts new file mode 100644 index 0000000000..0283b9d0c8 --- /dev/null +++ b/js/token-interface/src/read/associated-token-address.ts @@ -0,0 +1,23 @@ +import { PublicKey } from '@solana/web3.js'; +import { getAssociatedTokenAddressSync } from '@solana/spl-token'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { getAtaProgramId } from './ata-utils'; + +export function getAssociatedTokenAddress( + mint: PublicKey, + owner: PublicKey, + allowOwnerOffCurve = false, + programId: PublicKey = LIGHT_TOKEN_PROGRAM_ID, + associatedTokenProgramId?: PublicKey, +): PublicKey { + const effectiveAssociatedProgramId = + associatedTokenProgramId ?? getAtaProgramId(programId); + + return getAssociatedTokenAddressSync( + mint, + owner, + allowOwnerOffCurve, + programId, + effectiveAssociatedProgramId, + ); +} diff --git a/js/token-interface/src/read/ata-utils.ts b/js/token-interface/src/read/ata-utils.ts new file mode 100644 index 0000000000..a4b81d2fc9 --- /dev/null +++ b/js/token-interface/src/read/ata-utils.ts @@ -0,0 +1,108 @@ +import { + ASSOCIATED_TOKEN_PROGRAM_ID, + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddressSync, +} from '@solana/spl-token'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { PublicKey } from '@solana/web3.js'; + +export function getAtaProgramId(tokenProgramId: PublicKey): PublicKey { + if (tokenProgramId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return LIGHT_TOKEN_PROGRAM_ID; + } + return ASSOCIATED_TOKEN_PROGRAM_ID; +} + +export type AtaType = 'spl' | 'token2022' | 'light-token'; + +export interface AtaValidationResult { + valid: true; + type: AtaType; + programId: PublicKey; +} + +export function checkAtaAddress( + ata: PublicKey, + mint: PublicKey, + owner: PublicKey, + programId?: PublicKey, + allowOwnerOffCurve = false, +): AtaValidationResult { + if (programId) { + const expected = getAssociatedTokenAddressSync( + mint, + owner, + allowOwnerOffCurve, + programId, + getAtaProgramId(programId), + ); + if (ata.equals(expected)) { + return { + valid: true, + type: programIdToAtaType(programId), + programId, + }; + } + throw new Error( + `ATA address mismatch for ${programId.toBase58()}. ` + + `Expected: ${expected.toBase58()}, got: ${ata.toBase58()}`, + ); + } + + const lightTokenExpected = getAssociatedTokenAddressSync( + mint, + owner, + allowOwnerOffCurve, + LIGHT_TOKEN_PROGRAM_ID, + getAtaProgramId(LIGHT_TOKEN_PROGRAM_ID), + ); + if (ata.equals(lightTokenExpected)) { + return { + valid: true, + type: 'light-token', + programId: LIGHT_TOKEN_PROGRAM_ID, + }; + } + + const splExpected = getAssociatedTokenAddressSync( + mint, + owner, + allowOwnerOffCurve, + TOKEN_PROGRAM_ID, + getAtaProgramId(TOKEN_PROGRAM_ID), + ); + if (ata.equals(splExpected)) { + return { valid: true, type: 'spl', programId: TOKEN_PROGRAM_ID }; + } + + const t22Expected = getAssociatedTokenAddressSync( + mint, + owner, + allowOwnerOffCurve, + TOKEN_2022_PROGRAM_ID, + getAtaProgramId(TOKEN_2022_PROGRAM_ID), + ); + if (ata.equals(t22Expected)) { + return { + valid: true, + type: 'token2022', + programId: TOKEN_2022_PROGRAM_ID, + }; + } + + throw new Error( + `ATA address does not match any valid derivation from mint+owner. ` + + `Got: ${ata.toBase58()}, expected one of: ` + + `light-token=${lightTokenExpected.toBase58()}, ` + + `SPL=${splExpected.toBase58()}, ` + + `T22=${t22Expected.toBase58()}`, + ); +} + +function programIdToAtaType(programId: PublicKey): AtaType { + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) return 'light-token'; + if (programId.equals(TOKEN_PROGRAM_ID)) return 'spl'; + if (programId.equals(TOKEN_2022_PROGRAM_ID)) return 'token2022'; + throw new Error(`Unknown program ID: ${programId.toBase58()}`); +} diff --git a/js/token-interface/src/read/get-account.ts b/js/token-interface/src/read/get-account.ts new file mode 100644 index 0000000000..fe29cd312f --- /dev/null +++ b/js/token-interface/src/read/get-account.ts @@ -0,0 +1,1117 @@ +import { AccountInfo, Commitment, PublicKey } from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + unpackAccount as unpackAccountSPL, + TokenAccountNotFoundError, + TokenInvalidAccountOwnerError, + getAssociatedTokenAddressSync, + AccountState, + Account, +} from '@solana/spl-token'; +import { + Rpc, + LIGHT_TOKEN_PROGRAM_ID, + MerkleContext, + CompressedAccountWithMerkleContext, + assertV2Enabled, +} from '@lightprotocol/stateless.js'; +import { Buffer } from 'buffer'; +import BN from 'bn.js'; +import { getAtaProgramId, checkAtaAddress } from './ata-utils'; +import { ERR_FETCH_BY_OWNER_REQUIRED } from '../errors'; + +export const TokenAccountSourceType = { + Spl: 'spl', + Token2022: 'token2022', + SplCold: 'spl-cold', + Token2022Cold: 'token2022-cold', + LightTokenHot: 'light-token-hot', + LightTokenCold: 'light-token-cold', +} as const; + +export type TokenAccountSourceTypeValue = + (typeof TokenAccountSourceType)[keyof typeof TokenAccountSourceType]; + +/** Cold (compressed) source types. Used for load/decompress and isCold. */ +export const COLD_SOURCE_TYPES: ReadonlySet = + new Set([ + TokenAccountSourceType.LightTokenCold, + TokenAccountSourceType.SplCold, + TokenAccountSourceType.Token2022Cold, + ]); + +function isColdSourceType(type: TokenAccountSourceTypeValue): boolean { + return COLD_SOURCE_TYPES.has(type); +} + +/** @internal */ +export interface TokenAccountSource { + type: TokenAccountSourceTypeValue; + address: PublicKey; + amount: bigint; + accountInfo: AccountInfo; + loadContext?: MerkleContext; + parsed: Account; +} + +export interface AccountView { + accountInfo: AccountInfo; + parsed: Account; + isCold: boolean; + loadContext?: MerkleContext; + _sources?: TokenAccountSource[]; + _needsConsolidation?: boolean; + _hasDelegate?: boolean; + _anyFrozen?: boolean; + /** True when fetched via getAtaView */ + _isAta?: boolean; + /** Associated token account owner - set by getAtaView */ + _owner?: PublicKey; + /** Associated token account mint - set by getAtaView */ + _mint?: PublicKey; +} + +type CompressedByOwnerResult = Awaited< + ReturnType +>; + +function toErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +function throwRpcFetchFailure(context: string, error: unknown): never { + throw new Error(`${context}: ${toErrorMessage(error)}`); +} + +function throwIfUnexpectedRpcErrors( + context: string, + unexpectedErrors: unknown[], +): void { + if (unexpectedErrors.length > 0) { + throwRpcFetchFailure(context, unexpectedErrors[0]); + } +} + +export type FrozenOperation = + | 'load' + | 'transfer' + | 'unwrap' + | 'approve' + | 'revoke'; + +export function checkNotFrozen( + iface: AccountView, + operation: FrozenOperation, +): void { + if (iface._anyFrozen) { + throw new Error( + `Account is frozen. One or more sources (hot or cold) are frozen; ${operation} is not allowed.`, + ); + } +} + +/** @internal */ +function parseTokenData(data: Buffer): { + mint: PublicKey; + owner: PublicKey; + amount: BN; + delegate: PublicKey | null; + state: number; + tlv: Buffer | null; +} | null { + if (!data || data.length === 0) return null; + + try { + let offset = 0; + const mint = new PublicKey(data.slice(offset, offset + 32)); + offset += 32; + const owner = new PublicKey(data.slice(offset, offset + 32)); + offset += 32; + const amount = new BN(data.slice(offset, offset + 8), 'le'); + offset += 8; + const delegateOption = data[offset]; + offset += 1; + const delegate = delegateOption + ? new PublicKey(data.slice(offset, offset + 32)) + : null; + offset += 32; + const state = data[offset]; + offset += 1; + const tlvOption = data[offset]; + offset += 1; + const tlv = tlvOption ? data.slice(offset) : null; + + return { + mint, + owner, + amount, + delegate, + state, + tlv, + }; + } catch { + return null; + } +} + +/** + * Known extension data sizes by Borsh enum discriminator. + * undefined = variable-length (cannot skip without full parsing). + * @internal + */ +const EXTENSION_DATA_SIZES: Record = { + 0: 0, + 1: 0, + 2: 0, + 3: 0, + 4: 0, + 5: 0, + 6: 0, + 7: 0, + 8: 0, + 9: 0, + 10: 0, + 11: 0, + 12: 0, + 13: 0, + 14: 0, + 15: 0, + 16: 0, + 17: 0, + 18: 0, + 19: undefined, // TokenMetadata (variable) + 20: 0, + 21: 0, + 22: 0, + 23: 0, + 24: 0, + 25: 0, + 26: 0, + 27: 0, // PausableAccountExtension (unit struct) + 28: 0, // PermanentDelegateAccountExtension (unit struct) + 29: 8, // TransferFeeAccountExtension (u64) + 30: 1, // TransferHookAccountExtension (u8) + 31: 17, // CompressedOnlyExtension (u64 + u64 + u8) + 32: undefined, // CompressibleExtension (variable) +}; + +const COMPRESSED_ONLY_DISCRIMINATOR = 31; + +/** + * Extract delegated_amount from CompressedOnly extension in Borsh-serialized + * TLV data (Vec). + * @internal + */ +function extractDelegatedAmountFromTlv(tlv: Buffer | null): bigint | null { + if (!tlv || tlv.length < 5) return null; + + try { + let offset = 0; + const vecLen = tlv.readUInt32LE(offset); + offset += 4; + + for (let i = 0; i < vecLen; i++) { + if (offset >= tlv.length) return null; + + const discriminator = tlv[offset]; + offset += 1; + + if (discriminator === COMPRESSED_ONLY_DISCRIMINATOR) { + if (offset + 8 > tlv.length) return null; + // delegated_amount is the first u64 field + const lo = BigInt(tlv.readUInt32LE(offset)); + const hi = BigInt(tlv.readUInt32LE(offset + 4)); + return lo | (hi << BigInt(32)); + } + + const size = EXTENSION_DATA_SIZES[discriminator]; + if (size === undefined) return null; + offset += size; + } + } catch { + return null; + } + + return null; +} + +/** @internal */ +function convertTokenDataToAccount( + address: PublicKey, + tokenData: { + mint: PublicKey; + owner: PublicKey; + amount: BN; + delegate: PublicKey | null; + state: number; + tlv: Buffer | null; + }, +): Account { + // Determine delegatedAmount for compressed TokenData: + // 1. If CompressedOnly extension present in TLV, use its delegated_amount + // 2. If delegate is set (regular compressed approve), the entire compressed + // account's amount is the delegation (change goes to a separate account) + // 3. Otherwise, 0 + let delegatedAmount = BigInt(0); + const extensionDelegatedAmount = extractDelegatedAmountFromTlv( + tokenData.tlv, + ); + if (extensionDelegatedAmount !== null) { + delegatedAmount = extensionDelegatedAmount; + } else if (tokenData.delegate) { + delegatedAmount = BigInt(tokenData.amount.toString()); + } + + return { + address, + mint: tokenData.mint, + owner: tokenData.owner, + amount: BigInt(tokenData.amount.toString()), + delegate: tokenData.delegate, + delegatedAmount, + isInitialized: tokenData.state !== AccountState.Uninitialized, + isFrozen: tokenData.state === AccountState.Frozen, + isNative: false, + rentExemptReserve: null, + closeAuthority: null, + tlvData: tokenData.tlv ? Buffer.from(tokenData.tlv) : Buffer.alloc(0), + }; +} + +function requireCompressedAccountData( + compressedAccount: CompressedAccountWithMerkleContext, +): NonNullable { + const data = compressedAccount.data; + if (!data) { + throw new Error('Compressed account is missing token data'); + } + return data; +} + +/** Convert compressed account to AccountInfo */ +function toAccountInfo( + compressedAccount: CompressedAccountWithMerkleContext, +): AccountInfo { + const compressedData = requireCompressedAccountData(compressedAccount); + const dataDiscriminatorBuffer: Buffer = Buffer.from( + compressedData.discriminator, + ); + const dataBuffer: Buffer = Buffer.from(compressedData.data); + const data: Buffer = Buffer.concat([dataDiscriminatorBuffer, dataBuffer]); + + return { + executable: false, + owner: compressedAccount.owner, + lamports: compressedAccount.lamports.toNumber(), + data, + rentEpoch: undefined, + }; +} + +/** @internal */ +export function parseLightTokenHot( + address: PublicKey, + accountInfo: AccountInfo, +): { + accountInfo: AccountInfo; + loadContext: undefined; + parsed: Account; + isCold: false; +} { + // Hot light-token accounts use SPL-compatible layout with 4-byte COption tags. + // unpackAccountSPL correctly parses all fields including delegatedAmount, + // isNative, and closeAuthority. + const parsed = unpackAccountSPL( + address, + accountInfo, + LIGHT_TOKEN_PROGRAM_ID, + ); + return { + accountInfo, + loadContext: undefined, + parsed, + isCold: false, + }; +} + +/** @internal */ +export function parseLightTokenCold( + address: PublicKey, + compressedAccount: CompressedAccountWithMerkleContext, +): { + accountInfo: AccountInfo; + loadContext: MerkleContext; + parsed: Account; + isCold: true; +} { + const parsed = parseTokenData( + requireCompressedAccountData(compressedAccount).data, + ); + if (!parsed) throw new Error('Invalid token data'); + return { + accountInfo: toAccountInfo(compressedAccount), + loadContext: { + treeInfo: compressedAccount.treeInfo, + hash: compressedAccount.hash, + leafIndex: compressedAccount.leafIndex, + proveByIndex: compressedAccount.proveByIndex, + }, + parsed: convertTokenDataToAccount(address, parsed), + isCold: true, + }; +} + +/** + * 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 allowOwnerOffCurve Allow owner to be off-curve (PDA) + * @returns AccountView with associated token account metadata + */ +export async function getAtaView( + rpc: Rpc, + ata: PublicKey, + owner: PublicKey, + mint: PublicKey, + commitment?: Commitment, + programId?: PublicKey, + wrap = false, + allowOwnerOffCurve = false, +): Promise { + assertV2Enabled(); + + // 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 light-token associated token account. + const validation = checkAtaAddress( + ata, + mint, + owner, + programId, + allowOwnerOffCurve, + ); + + if (wrap && validation.type !== 'light-token') { + throw new Error( + `For wrap=true, ata must be the light-token ATA. Got ${validation.type} ATA instead.`, + ); + } + + // Pass both ata address AND fetchByOwner for proper lookups: + // - address is used for on-chain account fetching + // - fetchByOwner is used for light-token lookup by owner+mint + const result = await _getAccountView( + rpc, + ata, + commitment, + programId, + { + owner, + mint, + }, + wrap, + ); + result._isAta = true; + result._owner = owner; + result._mint = mint; + return result; +} + +/** + * @internal + */ +async function _getAccountView( + rpc: Rpc, + address: PublicKey | undefined, + commitment: Commitment | undefined, + programId: PublicKey | undefined, + fetchByOwner: { owner: PublicKey; mint: PublicKey } | undefined, + wrap: boolean, +): Promise { + if (!programId) { + return getUnifiedAccountView( + rpc, + address, + commitment, + fetchByOwner, + wrap, + ); + } + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return getLightTokenAccountView(rpc, address, commitment, fetchByOwner); + } + + if ( + programId.equals(TOKEN_PROGRAM_ID) || + programId.equals(TOKEN_2022_PROGRAM_ID) + ) { + return getSplOrToken2022AccountView( + rpc, + address, + commitment, + programId, + fetchByOwner, + ); + } + + throw new TokenInvalidAccountOwnerError(); +} + +/** + * @internal + */ +async function _tryFetchSpl( + rpc: Rpc, + address: PublicKey, + commitment?: Commitment, +): Promise<{ + accountInfo: AccountInfo; + parsed: Account; + isCold: false; + loadContext: undefined; +} | null> { + const info = await rpc.getAccountInfo(address, commitment); + if (!info) { + return null; + } + if (!info.owner.equals(TOKEN_PROGRAM_ID)) { + throw new TokenInvalidAccountOwnerError(); + } + const account = unpackAccountSPL(address, info, TOKEN_PROGRAM_ID); + return { + accountInfo: info, + parsed: account, + isCold: false, + loadContext: undefined, + }; +} + +/** + * @internal + */ +async function _tryFetchToken2022( + rpc: Rpc, + address: PublicKey, + commitment?: Commitment, +): Promise<{ + accountInfo: AccountInfo; + parsed: Account; + isCold: false; + loadContext: undefined; +} | null> { + const info = await rpc.getAccountInfo(address, commitment); + if (!info) { + return null; + } + if (!info.owner.equals(TOKEN_2022_PROGRAM_ID)) { + throw new TokenInvalidAccountOwnerError(); + } + const account = unpackAccountSPL(address, info, TOKEN_2022_PROGRAM_ID); + return { + accountInfo: info, + parsed: account, + isCold: false, + loadContext: undefined, + }; +} + +/** + * @internal + */ +async function _tryFetchLightTokenHot( + rpc: Rpc, + address: PublicKey, + commitment?: Commitment, +): Promise<{ + accountInfo: AccountInfo; + loadContext: undefined; + parsed: Account; + isCold: false; +} | null> { + const info = await rpc.getAccountInfo(address, commitment); + if (!info) { + return null; + } + if (!info.owner.equals(LIGHT_TOKEN_PROGRAM_ID)) { + throw new TokenInvalidAccountOwnerError(); + } + return parseLightTokenHot(address, info); +} + +/** @internal */ +async function getUnifiedAccountView( + rpc: Rpc, + address: PublicKey | undefined, + commitment: Commitment | undefined, + fetchByOwner: { owner: PublicKey; mint: PublicKey } | undefined, + wrap: boolean, +): Promise { + if (!address && !fetchByOwner) { + throw new Error(ERR_FETCH_BY_OWNER_REQUIRED); + } + + // Canonical address for unified mode is always the light-token associated token account + let lightTokenAta: PublicKey; + if (address) { + lightTokenAta = address; + } else { + if (!fetchByOwner) { + throw new Error(ERR_FETCH_BY_OWNER_REQUIRED); + } + lightTokenAta = getAssociatedTokenAddressSync( + fetchByOwner.mint, + fetchByOwner.owner, + false, + LIGHT_TOKEN_PROGRAM_ID, + getAtaProgramId(LIGHT_TOKEN_PROGRAM_ID), + ); + } + + const fetchPromises: Promise<{ + accountInfo: AccountInfo; + parsed: Account; + isCold: boolean; + loadContext?: MerkleContext; + } | null>[] = []; + const fetchTypes: TokenAccountSource['type'][] = []; + const fetchAddresses: PublicKey[] = []; + + // light-token hot + fetchPromises.push(_tryFetchLightTokenHot(rpc, lightTokenAta, commitment)); + fetchTypes.push(TokenAccountSourceType.LightTokenHot); + fetchAddresses.push(lightTokenAta); + + // SPL / Token-2022 (only when wrap is enabled) + if (wrap) { + // Always derive SPL/T22 addresses from owner+mint, not from the passed + // light-token address. SPL and T22 associated token accounts are different from light-token associated token accounts. + if (!fetchByOwner) { + throw new Error( + 'fetchByOwner is required for wrap=true to derive SPL/T22 addresses', + ); + } + const splTokenAta = getAssociatedTokenAddressSync( + fetchByOwner.mint, + fetchByOwner.owner, + false, + TOKEN_PROGRAM_ID, + getAtaProgramId(TOKEN_PROGRAM_ID), + ); + const token2022Ata = getAssociatedTokenAddressSync( + fetchByOwner.mint, + fetchByOwner.owner, + false, + TOKEN_2022_PROGRAM_ID, + getAtaProgramId(TOKEN_2022_PROGRAM_ID), + ); + + fetchPromises.push(_tryFetchSpl(rpc, splTokenAta, commitment)); + fetchTypes.push(TokenAccountSourceType.Spl); + fetchAddresses.push(splTokenAta); + + fetchPromises.push(_tryFetchToken2022(rpc, token2022Ata, commitment)); + fetchTypes.push(TokenAccountSourceType.Token2022); + fetchAddresses.push(token2022Ata); + } + + // Fetch ALL cold light-token accounts (not just one) - important for V1/V2 detection + const coldAccountsPromise = fetchByOwner + ? rpc.getCompressedTokenAccountsByOwner(fetchByOwner.owner, { + mint: fetchByOwner.mint, + }) + : rpc.getCompressedTokenAccountsByOwner(lightTokenAta); + + const hotResults = await Promise.allSettled(fetchPromises); + const ownerMismatchErrors: TokenInvalidAccountOwnerError[] = []; + const unexpectedErrors: unknown[] = []; + + let coldResult: Awaited | null = null; + try { + coldResult = await coldAccountsPromise; + } catch (error) { + unexpectedErrors.push(error); + } + + // collect all successful hot results + const sources: TokenAccountSource[] = []; + + for (let i = 0; i < hotResults.length; i++) { + const result = hotResults[i]; + if (result.status === 'fulfilled') { + const value = result.value; + if (!value) { + continue; + } + sources.push({ + type: fetchTypes[i], + address: fetchAddresses[i], + amount: value.parsed.amount, + accountInfo: value.accountInfo, + loadContext: value.loadContext, + parsed: value.parsed, + }); + } else if (result.reason instanceof TokenInvalidAccountOwnerError) { + ownerMismatchErrors.push(result.reason); + } else { + unexpectedErrors.push(result.reason); + } + } + + // Add ALL cold light-token accounts (handles both V1 and V2) + if (coldResult) { + for (const item of coldResult.items) { + const compressedAccount = item.compressedAccount; + if ( + compressedAccount && + compressedAccount.data && + compressedAccount.data.data.length > 0 && + compressedAccount.owner.equals(LIGHT_TOKEN_PROGRAM_ID) + ) { + const parsed = parseLightTokenCold( + lightTokenAta, + compressedAccount, + ); + sources.push({ + type: TokenAccountSourceType.LightTokenCold, + address: lightTokenAta, + amount: parsed.parsed.amount, + accountInfo: parsed.accountInfo, + loadContext: parsed.loadContext, + parsed: parsed.parsed, + }); + } + } + } + + throwIfUnexpectedRpcErrors( + 'Failed to fetch token account data from RPC', + unexpectedErrors, + ); + + // account not found + if (sources.length === 0) { + if (ownerMismatchErrors.length > 0) { + throw ownerMismatchErrors[0]; + } + throw new TokenAccountNotFoundError(); + } + + // priority order: light-token hot > light-token cold > SPL/T22 + const priority: TokenAccountSource['type'][] = [ + TokenAccountSourceType.LightTokenHot, + TokenAccountSourceType.LightTokenCold, + TokenAccountSourceType.Spl, + TokenAccountSourceType.Token2022, + ]; + + sources.sort((a, b) => { + const aIdx = priority.indexOf(a.type); + const bIdx = priority.indexOf(b.type); + return aIdx - bIdx; + }); + + return buildAccountViewFromSources(sources, lightTokenAta); +} + +/** @internal */ +async function getLightTokenAccountView( + rpc: Rpc, + address: PublicKey | undefined, + commitment: Commitment | undefined, + fetchByOwner?: { owner: PublicKey; mint: PublicKey }, +): Promise { + // Derive address if not provided + if (!address) { + if (!fetchByOwner) { + throw new Error(ERR_FETCH_BY_OWNER_REQUIRED); + } + address = getAssociatedTokenAddressSync( + fetchByOwner.mint, + fetchByOwner.owner, + false, + LIGHT_TOKEN_PROGRAM_ID, + getAtaProgramId(LIGHT_TOKEN_PROGRAM_ID), + ); + } + + const [onchainResult, compressedResult] = await Promise.allSettled([ + rpc.getAccountInfo(address, commitment), + // Fetch compressed: by owner+mint for associated token accounts, by address for non-ATAs + fetchByOwner + ? rpc.getCompressedTokenAccountsByOwner(fetchByOwner.owner, { + mint: fetchByOwner.mint, + }) + : rpc.getCompressedTokenAccountsByOwner(address), + ]); + const unexpectedErrors: unknown[] = []; + const ownerMismatchErrors: TokenInvalidAccountOwnerError[] = []; + + const onchainAccount = + onchainResult.status === 'fulfilled' ? onchainResult.value : null; + if (onchainResult.status === 'rejected') { + unexpectedErrors.push(onchainResult.reason); + } + const compressedAccounts = + compressedResult.status === 'fulfilled' + ? compressedResult.value.items.map(item => item.compressedAccount) + : []; + if (compressedResult.status === 'rejected') { + unexpectedErrors.push(compressedResult.reason); + } + + const sources: TokenAccountSource[] = []; + + // Collect light-token associated token account (hot balance) + if (onchainAccount) { + if (!onchainAccount.owner.equals(LIGHT_TOKEN_PROGRAM_ID)) { + ownerMismatchErrors.push(new TokenInvalidAccountOwnerError()); + } else { + const parsed = parseLightTokenHot(address, onchainAccount); + sources.push({ + type: TokenAccountSourceType.LightTokenHot, + address, + amount: parsed.parsed.amount, + accountInfo: onchainAccount, + parsed: parsed.parsed, + }); + } + } + + // Collect compressed light-token accounts (cold balance) + for (const compressedAccount of compressedAccounts) { + if ( + compressedAccount && + compressedAccount.data && + compressedAccount.data.data.length > 0 && + compressedAccount.owner.equals(LIGHT_TOKEN_PROGRAM_ID) + ) { + const parsed = parseLightTokenCold(address, compressedAccount); + sources.push({ + type: TokenAccountSourceType.LightTokenCold, + address, + amount: parsed.parsed.amount, + accountInfo: parsed.accountInfo, + loadContext: parsed.loadContext, + parsed: parsed.parsed, + }); + } + } + + throwIfUnexpectedRpcErrors( + 'Failed to fetch token account data from RPC', + unexpectedErrors, + ); + + if (sources.length === 0) { + if (ownerMismatchErrors.length > 0) { + throw ownerMismatchErrors[0]; + } + throw new TokenAccountNotFoundError(); + } + + // Priority: hot > cold + sources.sort((a, b) => { + if (a.type === 'light-token-hot' && b.type === 'light-token-cold') + return -1; + if (a.type === 'light-token-cold' && b.type === 'light-token-hot') + return 1; + return 0; + }); + + return buildAccountViewFromSources(sources, address); +} + +/** @internal */ +async function getSplOrToken2022AccountView( + rpc: Rpc, + address: PublicKey | undefined, + commitment: Commitment | undefined, + programId: PublicKey, + fetchByOwner?: { owner: PublicKey; mint: PublicKey }, +): Promise { + if (!address) { + if (!fetchByOwner) { + throw new Error(ERR_FETCH_BY_OWNER_REQUIRED); + } + address = getAssociatedTokenAddressSync( + fetchByOwner.mint, + fetchByOwner.owner, + false, + programId, + getAtaProgramId(programId), + ); + } + + const hotType: TokenAccountSource['type'] = programId.equals( + TOKEN_PROGRAM_ID, + ) + ? TokenAccountSourceType.Spl + : TokenAccountSourceType.Token2022; + + const coldType: TokenAccountSource['type'] = programId.equals( + TOKEN_PROGRAM_ID, + ) + ? TokenAccountSourceType.SplCold + : TokenAccountSourceType.Token2022Cold; + + // Fetch hot and cold in parallel (neither is required individually) + const [hotResult, coldResult] = await Promise.allSettled([ + rpc.getAccountInfo(address, commitment), + fetchByOwner + ? rpc.getCompressedTokenAccountsByOwner(fetchByOwner.owner, { + mint: fetchByOwner.mint, + }) + : Promise.resolve(null as CompressedByOwnerResult | null), + ]); + + const sources: TokenAccountSource[] = []; + const unexpectedErrors: unknown[] = []; + const ownerMismatchErrors: TokenInvalidAccountOwnerError[] = []; + + const hotInfo = hotResult.status === 'fulfilled' ? hotResult.value : null; + if (hotResult.status === 'rejected') + unexpectedErrors.push(hotResult.reason); + const coldAccounts = + coldResult.status === 'fulfilled' ? coldResult.value : null; + if (coldResult.status === 'rejected') + unexpectedErrors.push(coldResult.reason); + + // Hot SPL/T22 account (may not exist) + if (hotInfo) { + if (!hotInfo.owner.equals(programId)) { + ownerMismatchErrors.push(new TokenInvalidAccountOwnerError()); + } else { + try { + const account = unpackAccountSPL(address, hotInfo, programId); + sources.push({ + type: hotType, + address, + amount: account.amount, + accountInfo: hotInfo, + parsed: account, + }); + } catch (error) { + unexpectedErrors.push(error); + } + } + } + + // Cold (compressed) accounts + for (const item of coldAccounts?.items ?? []) { + const compressedAccount = item.compressedAccount; + if ( + compressedAccount && + compressedAccount.data && + compressedAccount.data.data.length > 0 && + compressedAccount.owner.equals(LIGHT_TOKEN_PROGRAM_ID) + ) { + const parsedCold = parseLightTokenCold(address, compressedAccount); + sources.push({ + type: coldType, + address, + amount: parsedCold.parsed.amount, + accountInfo: parsedCold.accountInfo, + loadContext: parsedCold.loadContext, + parsed: parsedCold.parsed, + }); + } + } + + throwIfUnexpectedRpcErrors( + 'Failed to fetch token account data from RPC', + unexpectedErrors, + ); + + if (sources.length === 0) { + if (ownerMismatchErrors.length > 0) { + throw ownerMismatchErrors[0]; + } + throw new TokenAccountNotFoundError(); + } + + return buildAccountViewFromSources(sources, address); +} + +/** @internal */ +function buildAccountViewFromSources( + sources: TokenAccountSource[], + canonicalAddress: PublicKey, +): AccountView { + const totalAmount = sources.reduce( + (sum, src) => sum + src.amount, + BigInt(0), + ); + + const primarySource = sources[0]; + + const hasDelegate = sources.some(src => src.parsed.delegate !== null); + const anyFrozen = sources.some(src => src.parsed.isFrozen); + const hasColdSource = sources.some(src => isColdSourceType(src.type)); + const needsConsolidation = sources.length > 1; + const delegatedContribution = (src: TokenAccountSource): bigint => { + const delegated = src.parsed.delegatedAmount ?? src.amount; + return src.amount < delegated ? src.amount : delegated; + }; + + const sumForDelegate = ( + candidate: PublicKey, + scope: (src: TokenAccountSource) => boolean, + ): bigint => + sources.reduce((sum, src) => { + if (!scope(src)) return sum; + const delegate = src.parsed.delegate; + if (!delegate || !delegate.equals(candidate)) return sum; + return sum + delegatedContribution(src); + }, BigInt(0)); + + const hotDelegatedSource = sources.find( + src => !isColdSourceType(src.type) && src.parsed.delegate !== null, + ); + const coldDelegatedSources = sources.filter( + src => isColdSourceType(src.type) && src.parsed.delegate !== null, + ); + + let canonicalDelegate: PublicKey | null = null; + let canonicalDelegatedAmount = BigInt(0); + + if (hotDelegatedSource?.parsed.delegate) { + // If any hot source is delegated, it always determines canonical delegate. + // Cold delegates only contribute when they match this hot delegate. + canonicalDelegate = hotDelegatedSource.parsed.delegate; + canonicalDelegatedAmount = sumForDelegate( + canonicalDelegate, + () => true, + ); + } else if (coldDelegatedSources.length > 0) { + // No hot delegate: canonical delegate is taken from the most recent + // delegated cold source in source order (source[0] is most recent). + const firstColdDelegate = coldDelegatedSources[0].parsed.delegate; + if (firstColdDelegate) { + canonicalDelegate = firstColdDelegate; + canonicalDelegatedAmount = sumForDelegate(canonicalDelegate, src => + isColdSourceType(src.type), + ); + } + } + + const unifiedAccount: Account = { + ...primarySource.parsed, + address: canonicalAddress, + amount: totalAmount, + // Synthetic ATA view models post-load state; any cold source implies initialized. + isInitialized: primarySource.parsed.isInitialized || hasColdSource, + delegate: canonicalDelegate, + delegatedAmount: canonicalDelegatedAmount, + ...(anyFrozen ? { state: AccountState.Frozen, isFrozen: true } : {}), + }; + + return { + accountInfo: primarySource.accountInfo, + parsed: unifiedAccount, + isCold: isColdSourceType(primarySource.type), + loadContext: primarySource.loadContext, + _sources: sources, + _needsConsolidation: needsConsolidation, + _hasDelegate: hasDelegate, + _anyFrozen: anyFrozen, + }; +} + +/** + * Spendable amount for a given authority (owner or delegate). + * - If authority equals the ATA owner: full parsed.amount. + * - If authority is the canonical delegate: parsed.delegatedAmount (bounded by parsed.amount). + * - Otherwise: 0. + * @internal + */ +function spendableAmountForAuthority( + iface: AccountView, + authority: PublicKey, +): bigint { + const owner = iface._owner; + if (owner && authority.equals(owner)) { + return iface.parsed.amount; + } + const delegate = iface.parsed.delegate; + if (delegate && authority.equals(delegate)) { + const delegated = iface.parsed.delegatedAmount ?? BigInt(0); + return delegated < iface.parsed.amount + ? delegated + : iface.parsed.amount; + } + return BigInt(0); +} + +/** + * Whether the given authority can sign for this ATA (owner or canonical delegate). + * @internal + */ +export function isAuthorityForAccount( + iface: AccountView, + authority: PublicKey, +): boolean { + const owner = iface._owner; + if (owner && authority.equals(owner)) return true; + const delegate = iface.parsed.delegate; + return delegate !== null && authority.equals(delegate); +} + +/** + * @internal + * Canonical authority projection for owner/delegate checks. + */ +export function filterAccountForAuthority( + iface: AccountView, + authority: PublicKey, +): AccountView { + const owner = iface._owner; + if (owner && authority.equals(owner)) { + return iface; + } + const spendable = spendableAmountForAuthority(iface, authority); + const canonicalDelegate = iface.parsed.delegate; + if ( + spendable === BigInt(0) || + canonicalDelegate === null || + !authority.equals(canonicalDelegate) + ) { + return { + ...iface, + _sources: [], + _needsConsolidation: false, + parsed: { ...iface.parsed, amount: BigInt(0) }, + }; + } + const sources = iface._sources ?? []; + const filtered = sources.filter( + src => + src.parsed.delegate !== null && + src.parsed.delegate.equals(canonicalDelegate), + ); + const primary = filtered[0]; + return { + ...iface, + ...(primary + ? { + accountInfo: primary.accountInfo, + isCold: isColdSourceType(primary.type), + loadContext: primary.loadContext, + } + : {}), + _sources: filtered, + _needsConsolidation: filtered.length > 1, + parsed: { + ...iface.parsed, + amount: spendable, + }, + }; +} diff --git a/js/token-interface/src/read/get-mint.ts b/js/token-interface/src/read/get-mint.ts new file mode 100644 index 0000000000..d979b42dbf --- /dev/null +++ b/js/token-interface/src/read/get-mint.ts @@ -0,0 +1,225 @@ +import { PublicKey, Commitment } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { + Rpc, + bn, + deriveAddressV2, + LIGHT_TOKEN_PROGRAM_ID, + getDefaultAddressTreeInfo, + MerkleContext, + assertV2Enabled, +} from '@lightprotocol/stateless.js'; +import { + Mint, + getMint as getSplMint, + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + TokenAccountNotFoundError, + TokenInvalidAccountOwnerError, +} from '@solana/spl-token'; +import { + deserializeMint, + MintContext, + TokenMetadata, + MintExtension, + extractTokenMetadata, + CompressionInfo, + CompressedMint, +} from '../instructions/layout/layout-mint'; + +export interface MintInfo { + mint: Mint; + programId: PublicKey; + merkleContext?: MerkleContext; + mintContext?: MintContext; + tokenMetadata?: TokenMetadata; + extensions?: MintExtension[]; + /** Compression info for light-token mints */ + compression?: CompressionInfo; +} + +function toErrorMessage(error: unknown): string { + if (error instanceof Error) return error.message; + return String(error); +} + +/** + * Get unified mint info for SPL/T22/light-token mints. + * + * @param rpc RPC connection + * @param address The mint address + * @param commitment Optional commitment level + * @param programId Token program ID. If not provided, tries all programs to + * auto-detect. + * @returns Object with mint, optional merkleContext, mintContext, and + * tokenMetadata + */ +export async function getMint( + rpc: Rpc, + address: PublicKey, + commitment?: Commitment, + programId?: PublicKey, +): Promise { + assertV2Enabled(); + + // try all three programs in parallel + if (!programId) { + const [tokenResult, token2022Result, compressedResult] = + await Promise.allSettled([ + getMint(rpc, address, commitment, TOKEN_PROGRAM_ID), + getMint(rpc, address, commitment, TOKEN_2022_PROGRAM_ID), + getMint(rpc, address, commitment, LIGHT_TOKEN_PROGRAM_ID), + ]); + + if (tokenResult.status === 'fulfilled') { + return tokenResult.value; + } + if (token2022Result.status === 'fulfilled') { + return token2022Result.value; + } + if (compressedResult.status === 'fulfilled') { + return compressedResult.value; + } + + const errors = [tokenResult, token2022Result, compressedResult] + .filter( + (result): result is PromiseRejectedResult => + result.status === 'rejected', + ) + .map(result => result.reason); + + const ownerMismatch = errors.find( + error => error instanceof TokenInvalidAccountOwnerError, + ); + if (ownerMismatch) { + throw ownerMismatch; + } + + const allNotFound = + errors.length > 0 && + errors.every(error => error instanceof TokenAccountNotFoundError); + if (allNotFound) { + throw new TokenAccountNotFoundError( + `Mint not found: ${address.toString()}. ` + + `Tried TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, and LIGHT_TOKEN_PROGRAM_ID.`, + ); + } + + const unexpected = errors.find( + error => + !(error instanceof TokenAccountNotFoundError) && + !(error instanceof TokenInvalidAccountOwnerError), + ); + if (unexpected) { + throw new Error( + `Failed to fetch mint data from RPC: ${toErrorMessage(unexpected)}`, + ); + } + + throw new TokenAccountNotFoundError( + `Mint not found: ${address.toString()}. ` + + `Tried TOKEN_PROGRAM_ID, TOKEN_2022_PROGRAM_ID, and LIGHT_TOKEN_PROGRAM_ID.`, + ); + } + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + const addressTree = getDefaultAddressTreeInfo().tree; + const compressedAddress = deriveAddressV2( + address.toBytes(), + addressTree, + LIGHT_TOKEN_PROGRAM_ID, + ); + const compressedAccount = await rpc.getCompressedAccount( + bn(compressedAddress.toBytes()), + ); + + if (!compressedAccount?.data?.data) { + throw new TokenAccountNotFoundError( + `Light mint not found for ${address.toString()}`, + ); + } + if (!compressedAccount.owner.equals(LIGHT_TOKEN_PROGRAM_ID)) { + throw new TokenInvalidAccountOwnerError(); + } + + const compressedData = Buffer.from(compressedAccount.data.data); + + // After decompressMint, the compressed account contains sentinel data (just hash ~32 bytes). + // The actual mint data lives in the light mint account. + // Minimum light mint size is 82 (base) + 34 (context) + 33 (signer+bump) = 149+ bytes. + const SENTINEL_THRESHOLD = 64; + const isDecompressed = compressedData.length < SENTINEL_THRESHOLD; + + let compressedMintData: CompressedMint; + + if (isDecompressed) { + // Light mint account exists - read from light mint account + const cmintAccountInfo = await rpc.getAccountInfo( + address, + commitment, + ); + if (!cmintAccountInfo?.data) { + throw new TokenAccountNotFoundError( + `Decompressed light mint account not found on-chain for ${address.toString()}`, + ); + } + if (!cmintAccountInfo.owner.equals(LIGHT_TOKEN_PROGRAM_ID)) { + throw new TokenInvalidAccountOwnerError(); + } + compressedMintData = deserializeMint( + Buffer.from(cmintAccountInfo.data), + ); + } else { + // Mint is still compressed - use compressed account data + compressedMintData = deserializeMint(compressedData); + } + + const mint: Mint = { + address, + mintAuthority: compressedMintData.base.mintAuthority, + supply: compressedMintData.base.supply, + decimals: compressedMintData.base.decimals, + isInitialized: compressedMintData.base.isInitialized, + freezeAuthority: compressedMintData.base.freezeAuthority, + tlvData: Buffer.alloc(0), + }; + + const merkleContext: MerkleContext = { + treeInfo: compressedAccount.treeInfo, + hash: compressedAccount.hash, + leafIndex: compressedAccount.leafIndex, + proveByIndex: compressedAccount.proveByIndex, + }; + + const tokenMetadata = extractTokenMetadata( + compressedMintData.extensions, + ); + + const result: MintInfo = { + mint, + programId, + merkleContext, + mintContext: compressedMintData.mintContext, + tokenMetadata: tokenMetadata || undefined, + extensions: compressedMintData.extensions || undefined, + compression: compressedMintData.compression, + }; + + if (!result.merkleContext) { + throw new Error( + `Invalid light mint: merkleContext is required for LIGHT_TOKEN_PROGRAM_ID`, + ); + } + if (!result.mintContext) { + throw new Error( + `Invalid light mint: mintContext is required for LIGHT_TOKEN_PROGRAM_ID`, + ); + } + + return result; + } + + // Otherwise, fetch SPL/T22 mint + const mint = await getSplMint(rpc, address, commitment, programId); + return { mint, programId }; +} diff --git a/js/token-interface/src/read/index.ts b/js/token-interface/src/read/index.ts new file mode 100644 index 0000000000..5ab0196d71 --- /dev/null +++ b/js/token-interface/src/read/index.ts @@ -0,0 +1,36 @@ +import type { PublicKey } from '@solana/web3.js'; +import { getAta as getTokenInterfaceAta } from '../account'; +import type { + AtaOwnerInput, + GetAtaInput, + TokenInterfaceAccount, +} from '../types'; +import { getAssociatedTokenAddress } from './associated-token-address'; + +export { getAssociatedTokenAddress } from './associated-token-address'; +export * from './ata-utils'; +export { getMint } from './get-mint'; +export type { MintInfo } from './get-mint'; +export * from './get-account'; + +export function getAtaAddress({ + mint, + owner, + programId, +}: AtaOwnerInput): PublicKey { + return getAssociatedTokenAddress(mint, owner, false, programId); +} + +export async function getAta({ + rpc, + owner, + mint, + commitment, +}: GetAtaInput): Promise { + return getTokenInterfaceAta({ + rpc, + owner, + mint, + commitment, + }); +} diff --git a/js/token-interface/src/spl-interface.ts b/js/token-interface/src/spl-interface.ts new file mode 100644 index 0000000000..5786b77f91 --- /dev/null +++ b/js/token-interface/src/spl-interface.ts @@ -0,0 +1,101 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + TokenAccountNotFoundError, + TokenInvalidAccountOwnerError, + unpackAccount, +} from '@solana/spl-token'; +import { bn, Rpc } from '@lightprotocol/stateless.js'; +import BN from 'bn.js'; +import { deriveSplInterfacePdaWithIndex } from './constants'; + +export type SplInterface = { + mint: PublicKey; + poolPda: PublicKey; + tokenProgramId: PublicKey; + activity?: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; + isInitialized: boolean; + balance: BN; + derivationIndex: number; + bump: number; +}; + +function isSupportedTokenProgramId(programId: PublicKey): boolean { + return ( + programId.equals(TOKEN_PROGRAM_ID) || + programId.equals(TOKEN_2022_PROGRAM_ID) + ); +} + +export async function getSplInterfaces( + rpc: Rpc, + mint: PublicKey, + commitment?: Commitment, +): Promise { + const addressesAndBumps = Array.from({ length: 5 }, (_, i) => + deriveSplInterfacePdaWithIndex(mint, i), + ); + + const accountInfos = await rpc.getMultipleAccountsInfo( + addressesAndBumps.map(([address]) => address), + commitment, + ); + + const anchorIndex = accountInfos.findIndex( + accountInfo => accountInfo !== null, + ); + if (anchorIndex === -1) { + throw new TokenAccountNotFoundError( + `SPL interface not found for mint ${mint.toBase58()}.`, + ); + } + const anchorAccountInfo = accountInfos[anchorIndex]; + if (!anchorAccountInfo) { + throw new TokenAccountNotFoundError( + `SPL interface not found for mint ${mint.toBase58()}.`, + ); + } + const tokenProgramId = anchorAccountInfo.owner; + if (!isSupportedTokenProgramId(tokenProgramId)) { + throw new TokenInvalidAccountOwnerError( + `Invalid token program owner for SPL interface mint ${mint.toBase58()}: ${tokenProgramId.toBase58()}`, + ); + } + + const parsedInfos = addressesAndBumps.map(([address], i) => + accountInfos[i] + ? unpackAccount(address, accountInfos[i], accountInfos[i].owner) + : null, + ); + + return parsedInfos.map((parsedInfo, i) => { + if (!parsedInfo) { + return { + mint, + poolPda: addressesAndBumps[i][0], + tokenProgramId, + activity: undefined, + balance: bn(0), + isInitialized: false, + derivationIndex: i, + bump: addressesAndBumps[i][1], + }; + } + + return { + mint, + poolPda: parsedInfo.address, + tokenProgramId, + activity: undefined, + balance: bn(parsedInfo.amount.toString()), + isInitialized: true, + derivationIndex: i, + bump: addressesAndBumps[i][1], + }; + }); +} diff --git a/js/token-interface/src/types.ts b/js/token-interface/src/types.ts new file mode 100644 index 0000000000..93f24ada64 --- /dev/null +++ b/js/token-interface/src/types.ts @@ -0,0 +1,149 @@ +import type { ParsedTokenAccount, Rpc } from '@lightprotocol/stateless.js'; +import type { Commitment, PublicKey } from '@solana/web3.js'; + +export interface TokenInterfaceParsedAta { + address: PublicKey; + owner: PublicKey; + mint: PublicKey; + amount: bigint; + delegate: PublicKey | null; + delegatedAmount: bigint; + isInitialized: boolean; + isFrozen: boolean; +} + +export interface TokenInterfaceAccount { + address: PublicKey; + owner: PublicKey; + mint: PublicKey; + amount: bigint; + hotAmount: bigint; + compressedAmount: bigint; + hasHotAccount: boolean; + requiresLoad: boolean; + parsed: TokenInterfaceParsedAta; + compressedAccount: ParsedTokenAccount | null; + ignoredCompressedAccounts: ParsedTokenAccount[]; + ignoredCompressedAmount: bigint; +} + +export interface AtaOwnerInput { + owner: PublicKey; + mint: PublicKey; + programId?: PublicKey; +} + +export interface GetAtaInput extends AtaOwnerInput { + rpc: Rpc; + commitment?: Commitment; +} + +export interface CreateAtaInstructionsInput extends AtaOwnerInput { + payer?: PublicKey; + programId?: PublicKey; +} + +export interface CreateLoadInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; +} + +export interface CreateTransferInstructionsInput { + rpc: Rpc; + payer?: PublicKey; + mint: PublicKey; + sourceOwner: PublicKey; + authority: PublicKey; + recipient: PublicKey; + tokenProgram?: PublicKey; + amount: number | bigint; +} + +export interface CreateApproveInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; + delegate: PublicKey; + amount: number | bigint; +} + +export interface CreateRevokeInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; +} + +export interface CreateBurnInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; + authority: PublicKey; + amount: number | bigint; + /** When set, emits BurnChecked; otherwise Burn. */ + decimals?: number; +} + +/** Single freeze ix (hot token account address already known). */ +export interface CreateRawFreezeInstructionInput { + tokenAccount: PublicKey; + mint: PublicKey; + freezeAuthority: PublicKey; +} + +/** Single thaw ix (hot token account address already known). */ +export interface CreateRawThawInstructionInput { + tokenAccount: PublicKey; + mint: PublicKey; + freezeAuthority: PublicKey; +} + +export interface CreateFreezeInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; + freezeAuthority: PublicKey; +} + +export interface CreateThawInstructionsInput extends AtaOwnerInput { + rpc: Rpc; + payer?: PublicKey; + freezeAuthority: PublicKey; +} + +export type CreateRawAtaInstructionInput = CreateAtaInstructionsInput; +export type CreateRawLoadInstructionInput = CreateLoadInstructionsInput; + +export interface CreateRawTransferInstructionInput { + source: PublicKey; + destination: PublicKey; + mint: PublicKey; + authority: PublicKey; + payer?: PublicKey; + amount: number | bigint; + decimals: number; +} + +/** Light-token CTokenBurn (hot account only). `mint` is the CMint account. */ +export interface CreateRawBurnInstructionInput { + source: PublicKey; + mint: PublicKey; + authority: PublicKey; + amount: number | bigint; + payer?: PublicKey; +} + +/** Light-token CTokenBurnChecked (hot account only). */ +export interface CreateRawBurnCheckedInstructionInput + extends CreateRawBurnInstructionInput { + decimals: number; +} + +export interface CreateRawApproveInstructionInput { + tokenAccount: PublicKey; + delegate: PublicKey; + owner: PublicKey; + amount: number | bigint; + payer?: PublicKey; +} + +export interface CreateRawRevokeInstructionInput { + tokenAccount: PublicKey; + owner: PublicKey; + payer?: PublicKey; +} diff --git a/js/token-interface/tests/e2e/burn.test.ts b/js/token-interface/tests/e2e/burn.test.ts new file mode 100644 index 0000000000..264b9b5cbc --- /dev/null +++ b/js/token-interface/tests/e2e/burn.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; +import { Keypair } from '@solana/web3.js'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { createBurnInstructions } from '../../src'; +import { + TEST_TOKEN_DECIMALS, + createMintFixture, + mintCompressedToOwner, + sendInstructions, +} from './helpers'; + +describe('burn instructions', () => { + it('surfaces on-chain burn failure for unsupported mint/account combinations', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + + await mintCompressedToOwner(fixture, owner.publicKey, 1_000n); + + const burnInstructions = await createBurnInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + authority: owner.publicKey, + amount: 300n, + }); + + await expect( + sendInstructions(fixture.rpc, fixture.payer, burnInstructions, [ + owner, + ]), + ).rejects.toThrow( + 'instruction modified data of an account it does not own', + ); + }); + + it('fails checked burn with wrong mint decimals', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + + await mintCompressedToOwner(fixture, owner.publicKey, 500n); + + const burnInstructions = await createBurnInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + authority: owner.publicKey, + amount: 100n, + decimals: TEST_TOKEN_DECIMALS + 1, + }); + + await expect( + sendInstructions(fixture.rpc, fixture.payer, burnInstructions, [ + owner, + ]), + ).rejects.toThrow(); + }); + + it('rejects burn build for signer that is neither owner nor delegate', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const unauthorized = Keypair.generate(); + + await mintCompressedToOwner(fixture, owner.publicKey, 900n); + + await expect( + createBurnInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + authority: unauthorized.publicKey, + amount: 250n, + }), + ).rejects.toThrow( + 'Signer is not the owner or a delegate of the account.', + ); + }); + + it('builds burn instructions when payer is omitted', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + + await mintCompressedToOwner(fixture, owner.publicKey, 500n); + + const burnInstructions = await createBurnInstructions({ + rpc: fixture.rpc, + owner: owner.publicKey, + mint: fixture.mint, + authority: owner.publicKey, + amount: 100n, + }); + + expect(burnInstructions.length).toBeGreaterThan(0); + }); +}); diff --git a/js/token-interface/tests/e2e/helpers.ts b/js/token-interface/tests/e2e/helpers.ts new file mode 100644 index 0000000000..55b44a5692 --- /dev/null +++ b/js/token-interface/tests/e2e/helpers.ts @@ -0,0 +1,203 @@ +import { AccountState } from '@solana/spl-token'; +import { + Keypair, + PublicKey, + Signer, + TransactionInstruction, +} from '@solana/web3.js'; +import { + Rpc, + TreeInfo, + VERSION, + bn, + buildAndSignTx, + createRpc, + featureFlags, + newAccountWithLamports, + selectStateTreeInfo, + sendAndConfirmTx, +} from '@lightprotocol/stateless.js'; +import { createMint, mintTo } from '@lightprotocol/compressed-token'; +import { parseLightTokenHot } from '../../src/read'; +import { getSplInterfaces } from '../../src/spl-interface'; + +featureFlags.version = VERSION.V2; + +export const TEST_TOKEN_DECIMALS = 9; + +export interface MintFixture { + rpc: Rpc; + payer: Signer; + mint: PublicKey; + mintAuthority: Keypair; + stateTreeInfo: TreeInfo; + tokenPoolInfos: Awaited>; + freezeAuthority?: Keypair; +} + +export async function createMintFixture(options?: { + withFreezeAuthority?: boolean; + payerLamports?: number; +}): Promise { + const rpc = createRpc(); + const payer = await newAccountWithLamports( + rpc, + options?.payerLamports ?? 20e9, + ); + const mintAuthority = Keypair.generate(); + const mintKeypair = Keypair.generate(); + const freezeAuthority = options?.withFreezeAuthority + ? Keypair.generate() + : undefined; + + const mint = ( + await createMint( + rpc, + payer, + mintAuthority.publicKey, + TEST_TOKEN_DECIMALS, + mintKeypair, + undefined, + undefined, + freezeAuthority?.publicKey ?? null, + ) + ).mint; + + const stateTreeInfo = selectStateTreeInfo(await rpc.getStateTreeInfos()); + const tokenPoolInfos = await getSplInterfaces(rpc, mint); + + return { + rpc, + payer, + mint, + mintAuthority, + stateTreeInfo, + tokenPoolInfos, + freezeAuthority, + }; +} + +export async function mintCompressedToOwner( + fixture: MintFixture, + owner: PublicKey, + amount: bigint, +): Promise { + const selectedSplInterfaceInfo = fixture.tokenPoolInfos.find( + info => info.isInitialized, + ); + if (!selectedSplInterfaceInfo) { + throw new Error('No initialized SPL interface info found.'); + } + + const selectedSplInterfaceForMintTo = { + ...selectedSplInterfaceInfo, + splInterfacePda: selectedSplInterfaceInfo.poolPda, + tokenProgram: selectedSplInterfaceInfo.tokenProgramId, + poolIndex: selectedSplInterfaceInfo.derivationIndex, + }; + + await mintTo( + fixture.rpc, + fixture.payer, + fixture.mint, + owner, + fixture.mintAuthority, + bn(amount.toString()), + fixture.stateTreeInfo, + selectedSplInterfaceForMintTo, + ); +} + +export async function mintMultipleColdAccounts( + fixture: MintFixture, + owner: PublicKey, + count: number, + amountPerAccount: bigint, +): Promise { + for (let i = 0; i < count; i += 1) { + await mintCompressedToOwner(fixture, owner, amountPerAccount); + } +} + +export async function sendInstructions( + rpc: Rpc, + payer: Signer, + instructions: TransactionInstruction[], + additionalSigners: Signer[] = [], +): Promise { + const { blockhash } = await rpc.getLatestBlockhash(); + const tx = buildAndSignTx( + instructions, + payer, + blockhash, + additionalSigners, + ); + return sendAndConfirmTx(rpc, tx); +} + +export async function getHotBalance( + rpc: Rpc, + tokenAccount: PublicKey, +): Promise { + const info = await rpc.getAccountInfo(tokenAccount); + if (!info) { + return BigInt(0); + } + + return parseLightTokenHot(tokenAccount, info).parsed.amount; +} + +export async function getHotDelegate( + rpc: Rpc, + tokenAccount: PublicKey, +): Promise<{ delegate: PublicKey | null; delegatedAmount: bigint }> { + const info = await rpc.getAccountInfo(tokenAccount); + if (!info) { + return { delegate: null, delegatedAmount: BigInt(0) }; + } + + const { parsed } = parseLightTokenHot(tokenAccount, info); + return { + delegate: parsed.delegate, + delegatedAmount: parsed.delegatedAmount ?? BigInt(0), + }; +} + +export async function getHotState( + rpc: Rpc, + tokenAccount: PublicKey, +): Promise { + const info = await rpc.getAccountInfo(tokenAccount); + if (!info) { + throw new Error(`Account not found: ${tokenAccount.toBase58()}`); + } + + const { parsed } = parseLightTokenHot(tokenAccount, info); + return parsed.isFrozen + ? AccountState.Frozen + : parsed.isInitialized + ? AccountState.Initialized + : AccountState.Uninitialized; +} + +export async function getCompressedAmounts( + rpc: Rpc, + owner: PublicKey, + mint: PublicKey, +): Promise { + const result = await rpc.getCompressedTokenAccountsByOwner(owner, { mint }); + + return result.items + .map(account => BigInt(account.parsed.amount.toString())) + .sort((left, right) => { + if (right > left) { + return 1; + } + + if (right < left) { + return -1; + } + + return 0; + }); +} diff --git a/js/token-interface/tests/step-definitions/e2e/approve-revoke.steps.ts b/js/token-interface/tests/step-definitions/e2e/approve-revoke.steps.ts new file mode 100644 index 0000000000..5b0f1e33d5 --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/approve-revoke.steps.ts @@ -0,0 +1,127 @@ +import assert from 'node:assert/strict'; +import { When, Then } from '@cucumber/cucumber'; +import { ComputeBudgetProgram } from '@solana/web3.js'; +import { + createApproveInstructions, + createRevokeInstructions, + getAtaAddress, +} from '../../../src/index.js'; +import { getHotDelegate, sendInstructions } from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +When( + 'the owner approves the delegate for {int} tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + assert.ok(this.delegate, 'delegate must be created first'); + + const instructions = await createApproveInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + delegate: this.delegate.publicKey, + amount: BigInt(amount), + }); + + this.lastApproveInstructions = instructions; + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.owner], + ); + }, +); + +Then( + 'the delegate is set on the token account with amount {int}', + async function (this: TokenInterfaceWorld, expectedAmount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + assert.ok(this.delegate, 'delegate must be created first'); + + const tokenAccount = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const delegateInfo = await getHotDelegate( + this.fixture.rpc, + tokenAccount, + ); + + assert.strictEqual( + delegateInfo.delegate?.toBase58(), + this.delegate.publicKey.toBase58(), + ); + assert.strictEqual(delegateInfo.delegatedAmount, BigInt(expectedAmount)); + }, +); + +Then( + 'no compute budget instructions were included', + function (this: TokenInterfaceWorld) { + const hasComputeBudget = this.lastApproveInstructions.some((ix) => + ix.programId.equals(ComputeBudgetProgram.programId), + ); + assert.strictEqual(hasComputeBudget, false); + }, +); + +When( + 'the owner revokes the delegation', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const instructions = await createRevokeInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + this.lastRevokeInstructions = instructions; + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.owner], + ); + }, +); + +Then( + 'the delegate is cleared and delegated amount is 0', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const tokenAccount = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const delegateInfo = await getHotDelegate( + this.fixture.rpc, + tokenAccount, + ); + + assert.strictEqual(delegateInfo.delegate, null); + assert.strictEqual(delegateInfo.delegatedAmount, BigInt(0)); + }, +); + +Then( + 'no compute budget instructions were included in revoke', + function (this: TokenInterfaceWorld) { + const hasComputeBudget = this.lastRevokeInstructions.some((ix) => + ix.programId.equals(ComputeBudgetProgram.programId), + ); + assert.strictEqual(hasComputeBudget, false); + }, +); diff --git a/js/token-interface/tests/step-definitions/e2e/ata-read.steps.ts b/js/token-interface/tests/step-definitions/e2e/ata-read.steps.ts new file mode 100644 index 0000000000..14d40d2431 --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/ata-read.steps.ts @@ -0,0 +1,62 @@ +import assert from 'node:assert/strict'; +import { When, Then } from '@cucumber/cucumber'; +import { + createAtaInstructions, + getAta, + getAtaAddress, +} from '../../../src/index.js'; +import { sendInstructions } from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +When( + 'the owner creates an ATA', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const instructions = await createAtaInstructions({ + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + ); + }, +); + +Then( + 'reading the ATA returns the correct address, owner, mint, and zero balance', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const expectedAddress = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const account = await getAta({ + rpc: this.fixture.rpc, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + assert.strictEqual( + account.parsed.address.toBase58(), + expectedAddress.toBase58(), + ); + assert.strictEqual( + account.parsed.owner.toBase58(), + this.owner.publicKey.toBase58(), + ); + assert.strictEqual( + account.parsed.mint.toBase58(), + this.fixture.mint.toBase58(), + ); + assert.strictEqual(account.parsed.amount, BigInt(0)); + }, +); diff --git a/js/token-interface/tests/step-definitions/e2e/common.steps.ts b/js/token-interface/tests/step-definitions/e2e/common.steps.ts new file mode 100644 index 0000000000..cc14c32555 --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/common.steps.ts @@ -0,0 +1,200 @@ +import assert from 'node:assert/strict'; +import { Given } from '@cucumber/cucumber'; +import { Keypair } from '@solana/web3.js'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createAtaInstructions, + createApproveInstructions, + getAtaAddress, +} from '../../../src/index.js'; +import { + createMintFixture, + mintCompressedToOwner, + sendInstructions, +} from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +Given( + 'a fresh mint fixture', + async function (this: TokenInterfaceWorld) { + this.fixture = await createMintFixture(); + }, +); + +Given( + 'a fresh mint fixture with freeze authority', + async function (this: TokenInterfaceWorld) { + this.fixture = await createMintFixture({ withFreezeAuthority: true }); + }, +); + +Given( + 'a new owner', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + this.owner = await newAccountWithLamports(this.fixture.rpc, 1e9); + }, +); + +Given( + 'a new recipient', + function (this: TokenInterfaceWorld) { + this.recipient = Keypair.generate(); + }, +); + +Given( + 'a new delegate', + function (this: TokenInterfaceWorld) { + this.delegate = Keypair.generate(); + }, +); + +Given( + 'a funded sender with {int} compressed tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + this.sender = await newAccountWithLamports(this.fixture.rpc, 1e9); + await mintCompressedToOwner( + this.fixture, + this.sender.publicKey, + BigInt(amount), + ); + }, +); + +Given( + 'a funded recipient with SOL', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + this.recipient = await newAccountWithLamports(this.fixture.rpc, 1e9); + }, +); + +Given( + 'a funded recipient with {int} compressed tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + this.recipient = await newAccountWithLamports(this.fixture.rpc, 1e9); + await mintCompressedToOwner( + this.fixture, + this.recipient.publicKey, + BigInt(amount), + ); + }, +); + +Given( + 'an owner with {int} compressed tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + this.owner = await newAccountWithLamports(this.fixture.rpc, 1e9); + await mintCompressedToOwner( + this.fixture, + this.owner.publicKey, + BigInt(amount), + ); + }, +); + +Given( + 'an owner with a created ATA and {int} compressed tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + this.owner = await newAccountWithLamports(this.fixture.rpc, 1e9); + + const ataIxs = await createAtaInstructions({ + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + await sendInstructions(this.fixture.rpc, this.fixture.payer, ataIxs); + + await mintCompressedToOwner( + this.fixture, + this.owner.publicKey, + BigInt(amount), + ); + }, +); + +Given( + 'an owner with compressed mints of {int}, {int}, and {int} tokens', + async function ( + this: TokenInterfaceWorld, + amount1: number, + amount2: number, + amount3: number, + ) { + assert.ok(this.fixture, 'fixture must be created first'); + this.owner = await newAccountWithLamports(this.fixture.rpc, 1e9); + await mintCompressedToOwner( + this.fixture, + this.owner.publicKey, + BigInt(amount1), + ); + await mintCompressedToOwner( + this.fixture, + this.owner.publicKey, + BigInt(amount2), + ); + await mintCompressedToOwner( + this.fixture, + this.owner.publicKey, + BigInt(amount3), + ); + }, +); + +Given( + 'a sender with compressed mints of {int}, {int}, and {int} tokens', + async function ( + this: TokenInterfaceWorld, + amount1: number, + amount2: number, + amount3: number, + ) { + assert.ok(this.fixture, 'fixture must be created first'); + this.sender = await newAccountWithLamports(this.fixture.rpc, 1e9); + await mintCompressedToOwner( + this.fixture, + this.sender.publicKey, + BigInt(amount1), + ); + await mintCompressedToOwner( + this.fixture, + this.sender.publicKey, + BigInt(amount2), + ); + await mintCompressedToOwner( + this.fixture, + this.sender.publicKey, + BigInt(amount3), + ); + }, +); + +Given( + 'a delegate approved for {int} tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + this.delegate = await newAccountWithLamports(this.fixture.rpc, 1e9); + + const approveIxs = await createApproveInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + delegate: this.delegate.publicKey, + amount: BigInt(amount), + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + approveIxs, + [this.owner], + ); + }, +); diff --git a/js/token-interface/tests/step-definitions/e2e/freeze-thaw.steps.ts b/js/token-interface/tests/step-definitions/e2e/freeze-thaw.steps.ts new file mode 100644 index 0000000000..606b6bd153 --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/freeze-thaw.steps.ts @@ -0,0 +1,90 @@ +import assert from 'node:assert/strict'; +import { When, Then } from '@cucumber/cucumber'; +import { AccountState } from '@solana/spl-token'; +import { + createFreezeInstructions, + createThawInstructions, + getAtaAddress, +} from '../../../src/index.js'; +import { getHotState, sendInstructions } from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +When( + 'the freeze authority freezes the account', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + assert.ok( + this.fixture.freezeAuthority, + 'freeze authority must exist on fixture', + ); + + const instructions = await createFreezeInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + freezeAuthority: this.fixture.freezeAuthority.publicKey, + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.owner, this.fixture.freezeAuthority], + ); + }, +); + +Then( + 'the account state is {string}', + async function (this: TokenInterfaceWorld, expectedState: string) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const tokenAccount = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const state = await getHotState(this.fixture.rpc, tokenAccount); + + const stateMap: Record = { + Frozen: AccountState.Frozen, + Initialized: AccountState.Initialized, + Uninitialized: AccountState.Uninitialized, + }; + + assert.strictEqual(state, stateMap[expectedState]); + }, +); + +When( + 'the freeze authority thaws the account', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + assert.ok( + this.fixture.freezeAuthority, + 'freeze authority must exist on fixture', + ); + + const instructions = await createThawInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + freezeAuthority: this.fixture.freezeAuthority.publicKey, + }); + + // Thaw only needs freezeAuthority as signer. The account is frozen + // so createLoadInstructions inside createThawInstructions produces + // no load instructions (nothing to load into a frozen account). + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.fixture.freezeAuthority], + ); + }, +); diff --git a/js/token-interface/tests/step-definitions/e2e/load.steps.ts b/js/token-interface/tests/step-definitions/e2e/load.steps.ts new file mode 100644 index 0000000000..ae065f74d7 --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/load.steps.ts @@ -0,0 +1,169 @@ +import assert from 'node:assert/strict'; +import { When, Then } from '@cucumber/cucumber'; +import { + createLoadInstructions, + getAta, + getAtaAddress, +} from '../../../src/index.js'; +import { + getCompressedAmounts, + getHotBalance, + sendInstructions, +} from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +When( + 'I read the owner ATA', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + this.resultAccount = await getAta({ + rpc: this.fixture.rpc, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + }, +); + +Then( + 'the ATA amount is {int}', + function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.resultAccount, 'resultAccount must be set'); + assert.strictEqual(this.resultAccount.parsed.amount, BigInt(expected)); + }, +); + +Then( + 'the ATA compressed amount is {int}', + function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.resultAccount, 'resultAccount must be set'); + assert.strictEqual( + this.resultAccount.compressedAmount, + BigInt(expected), + ); + }, +); + +Then( + 'the ATA requires load', + function (this: TokenInterfaceWorld) { + assert.ok(this.resultAccount, 'resultAccount must be set'); + assert.strictEqual(this.resultAccount.requiresLoad, true); + }, +); + +Then( + 'there are {int} ignored compressed accounts totaling {int}', + function ( + this: TokenInterfaceWorld, + count: number, + totalIgnored: number, + ) { + assert.ok(this.resultAccount, 'resultAccount must be set'); + assert.strictEqual( + this.resultAccount.ignoredCompressedAccounts.length, + count, + ); + assert.strictEqual( + this.resultAccount.ignoredCompressedAmount, + BigInt(totalIgnored), + ); + }, +); + +When( + 'I load the first compressed balance', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const instructions = await createLoadInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.owner], + ); + }, +); + +Then( + 'the hot balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const tokenAccount = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const balance = await getHotBalance(this.fixture.rpc, tokenAccount); + assert.strictEqual(balance, BigInt(expected)); + }, +); + +Then( + 'compressed accounts are {int} and {int}', + async function ( + this: TokenInterfaceWorld, + amount1: number, + amount2: number, + ) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const amounts = await getCompressedAmounts( + this.fixture.rpc, + this.owner.publicKey, + this.fixture.mint, + ); + + assert.deepStrictEqual(amounts, [BigInt(amount1), BigInt(amount2)]); + }, +); + +When( + 'I load the next compressed balance', + async function (this: TokenInterfaceWorld) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const instructions = await createLoadInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.owner], + ); + }, +); + +Then( + 'compressed accounts are {int}', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const amounts = await getCompressedAmounts( + this.fixture.rpc, + this.owner.publicKey, + this.fixture.mint, + ); + + assert.deepStrictEqual(amounts, [BigInt(amount)]); + }, +); diff --git a/js/token-interface/tests/step-definitions/e2e/transfer.steps.ts b/js/token-interface/tests/step-definitions/e2e/transfer.steps.ts new file mode 100644 index 0000000000..f95f91b02d --- /dev/null +++ b/js/token-interface/tests/step-definitions/e2e/transfer.steps.ts @@ -0,0 +1,275 @@ +import assert from 'node:assert/strict'; +import { When, Then } from '@cucumber/cucumber'; +import { Keypair } from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + unpackAccount, +} from '@solana/spl-token'; +import { + createTransferInstructions, + getAta, + getAtaAddress, +} from '../../../src/index.js'; +import { + getCompressedAmounts, + getHotBalance, + sendInstructions, +} from '../../e2e/helpers.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +When( + 'the sender transfers {int} tokens to the recipient', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.sender, 'sender must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const instructions = await createTransferInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + mint: this.fixture.mint, + sourceOwner: this.sender.publicKey, + authority: this.sender.publicKey, + recipient: this.recipient.publicKey, + amount: BigInt(amount), + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.sender], + ); + }, +); + +Then( + 'the recipient ATA balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const account = await getAta({ + rpc: this.fixture.rpc, + owner: this.recipient.publicKey, + mint: this.fixture.mint, + }); + + assert.strictEqual(account.parsed.amount, BigInt(expected)); + }, +); + +Then( + 'the sender ATA balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.sender, 'sender must be created first'); + + const senderAta = getAtaAddress({ + owner: this.sender.publicKey, + mint: this.fixture.mint, + }); + + const balance = await getHotBalance(this.fixture.rpc, senderAta); + assert.strictEqual(balance, BigInt(expected)); + }, +); + +When( + 'the sender transfers {int} tokens to the recipient SPL ATA', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.sender, 'sender must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const instructions = await createTransferInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + mint: this.fixture.mint, + sourceOwner: this.sender.publicKey, + authority: this.sender.publicKey, + recipient: this.recipient.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + amount: BigInt(amount), + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.sender], + ); + }, +); + +Then( + 'the recipient SPL ATA balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const recipientSplAta = getAssociatedTokenAddressSync( + this.fixture.mint, + this.recipient.publicKey, + false, + TOKEN_PROGRAM_ID, + ); + + const recipientSplInfo = await this.fixture.rpc.getAccountInfo( + recipientSplAta, + ); + assert.ok(recipientSplInfo, 'SPL ATA account should exist'); + + const recipientSpl = unpackAccount( + recipientSplAta, + recipientSplInfo, + TOKEN_PROGRAM_ID, + ); + assert.strictEqual(recipientSpl.amount, BigInt(expected)); + }, +); + +When( + 'the sender attempts to transfer {int} tokens', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.sender, 'sender must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + try { + const instructions = await createTransferInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + mint: this.fixture.mint, + sourceOwner: this.sender.publicKey, + authority: this.sender.publicKey, + recipient: this.recipient.publicKey, + amount: BigInt(amount), + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.sender], + ); + } catch (error) { + this.resultError = error as Error; + } + }, +); + +Then( + 'the transaction fails with {string}', + function (this: TokenInterfaceWorld, expectedMessage: string) { + assert.ok(this.resultError, 'an error should have been thrown'); + assert.ok( + this.resultError.message.includes(expectedMessage), + `expected error message to contain "${expectedMessage}", got: "${this.resultError.message}"`, + ); + }, +); + +Then( + 'the recipient hot balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const recipientAtaAddress = getAtaAddress({ + owner: this.recipient.publicKey, + mint: this.fixture.mint, + }); + + const balance = await getHotBalance( + this.fixture.rpc, + recipientAtaAddress, + ); + assert.strictEqual(balance, BigInt(expected)); + }, +); + +Then( + 'the recipient still has {int} in compressed accounts', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const amounts = await getCompressedAmounts( + this.fixture.rpc, + this.recipient.publicKey, + this.fixture.mint, + ); + + assert.deepStrictEqual(amounts, [BigInt(expected)]); + }, +); + +Then( + 'the recipient total ATA amount is {int} with {int} compressed', + async function ( + this: TokenInterfaceWorld, + totalAmount: number, + compressedAmount: number, + ) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.recipient, 'recipient must be created first'); + + const recipientAta = await getAta({ + rpc: this.fixture.rpc, + owner: this.recipient.publicKey, + mint: this.fixture.mint, + }); + + assert.strictEqual(recipientAta.parsed.amount, BigInt(totalAmount)); + assert.strictEqual( + recipientAta.compressedAmount, + BigInt(compressedAmount), + ); + }, +); + +When( + 'the delegate transfers {int} tokens to a new recipient', + async function (this: TokenInterfaceWorld, amount: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + assert.ok(this.delegate, 'delegate must be created first'); + + this.recipient = Keypair.generate(); + + const instructions = await createTransferInstructions({ + rpc: this.fixture.rpc, + payer: this.fixture.payer.publicKey, + mint: this.fixture.mint, + sourceOwner: this.owner.publicKey, + authority: this.delegate.publicKey, + recipient: this.recipient.publicKey, + amount: BigInt(amount), + }); + + await sendInstructions( + this.fixture.rpc, + this.fixture.payer, + instructions, + [this.delegate], + ); + }, +); + +Then( + 'the owner ATA balance is {int}', + async function (this: TokenInterfaceWorld, expected: number) { + assert.ok(this.fixture, 'fixture must be created first'); + assert.ok(this.owner, 'owner must be created first'); + + const ownerAta = getAtaAddress({ + owner: this.owner.publicKey, + mint: this.fixture.mint, + }); + + const balance = await getHotBalance(this.fixture.rpc, ownerAta); + assert.strictEqual(balance, BigInt(expected)); + }, +); diff --git a/js/token-interface/tests/step-definitions/unit/instruction-builders.steps.ts b/js/token-interface/tests/step-definitions/unit/instruction-builders.steps.ts new file mode 100644 index 0000000000..35d0278e5c --- /dev/null +++ b/js/token-interface/tests/step-definitions/unit/instruction-builders.steps.ts @@ -0,0 +1,129 @@ +import assert from 'node:assert/strict'; +import { Given, When, Then } from '@cucumber/cucumber'; +import { Keypair } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + createApproveInstruction, + createAtaInstruction, + createFreezeInstruction, + createRevokeInstruction, + createThawInstruction, + createTransferCheckedInstruction, +} from '../../../src/instructions/index.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +Given( + 'random keypairs for {string}, {string}, {string}, {string}, and {string}', + function ( + this: TokenInterfaceWorld, + n1: string, + n2: string, + n3: string, + n4: string, + n5: string, + ) { + for (const name of [n1, n2, n3, n4, n5]) { + this.keypairs[name] = Keypair.generate().publicKey; + } + }, +); + +When( + 'I build a create-ATA instruction for {string}, {string}, and {string}', + function ( + this: TokenInterfaceWorld, + payerKey: string, + ownerKey: string, + mintKey: string, + ) { + this.instruction = createAtaInstruction({ + payer: this.keypairs[payerKey], + owner: this.keypairs[ownerKey], + mint: this.keypairs[mintKey], + }); + }, +); + +Then( + 'the instruction program ID is the light-token program', + function (this: TokenInterfaceWorld) { + assert.ok(this.instruction!.programId.equals(LIGHT_TOKEN_PROGRAM_ID)); + }, +); + +Then( + 'account key {int} is {string}', + function (this: TokenInterfaceWorld, index: number, name: string) { + assert.ok(this.instruction!.keys[index].pubkey.equals(this.keypairs[name])); + }, +); + +When( + 'I build a checked transfer instruction for {int} tokens with {int} decimals', + function (this: TokenInterfaceWorld, amount: number, decimals: number) { + this.instruction = createTransferCheckedInstruction({ + source: this.keypairs['source'], + destination: this.keypairs['destination'], + mint: this.keypairs['mint'], + authority: this.keypairs['authority'], + payer: this.keypairs['payer'], + amount: BigInt(amount), + decimals, + }); + }, +); + +Then( + 'the instruction discriminator byte is {int}', + function (this: TokenInterfaceWorld, disc: number) { + assert.strictEqual(this.instruction!.data[0], disc); + }, +); + +When( + 'I build approve, revoke, freeze, and thaw instructions', + function (this: TokenInterfaceWorld) { + this.builtInstructions['approve'] = createApproveInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + delegate: this.keypairs['delegate'], + owner: this.keypairs['owner'], + amount: 10n, + }); + this.builtInstructions['revoke'] = createRevokeInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + owner: this.keypairs['owner'], + }); + this.builtInstructions['freeze'] = createFreezeInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + mint: this.keypairs['mint'], + freezeAuthority: this.keypairs['freezeAuthority'], + }); + this.builtInstructions['thaw'] = createThawInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + mint: this.keypairs['mint'], + freezeAuthority: this.keypairs['freezeAuthority'], + }); + }, +); + +Then( + 'the approve instruction targets the light-token program', + function (this: TokenInterfaceWorld) { + assert.ok( + this.builtInstructions['approve'].programId.equals( + LIGHT_TOKEN_PROGRAM_ID, + ), + ); + }, +); + +Then( + 'the revoke instruction targets the light-token program', + function (this: TokenInterfaceWorld) { + assert.ok( + this.builtInstructions['revoke'].programId.equals( + LIGHT_TOKEN_PROGRAM_ID, + ), + ); + }, +); diff --git a/js/token-interface/tests/step-definitions/unit/kit-adapter.steps.ts b/js/token-interface/tests/step-definitions/unit/kit-adapter.steps.ts new file mode 100644 index 0000000000..570c5eb021 --- /dev/null +++ b/js/token-interface/tests/step-definitions/unit/kit-adapter.steps.ts @@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { Given, When, Then } from '@cucumber/cucumber'; +import { Keypair } from '@solana/web3.js'; +import { createAtaInstruction } from '../../../src/instructions/index.js'; +import { + createTransferInstructions, + createAtaInstructions, + createTransferInstructionPlan, + toKitInstructions, +} from '../../../src/kit/index.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +Given( + 'a legacy create-ATA instruction', + function (this: TokenInterfaceWorld) { + this.instruction = createAtaInstruction({ + payer: Keypair.generate().publicKey, + owner: Keypair.generate().publicKey, + mint: Keypair.generate().publicKey, + }); + }, +); + +When( + 'I convert it to kit instructions', + function (this: TokenInterfaceWorld) { + this.kitInstructions = toKitInstructions([this.instruction!]); + }, +); + +Then( + 'the result is a list of {int} kit instruction object(s)', + function (this: TokenInterfaceWorld, count: number) { + assert.strictEqual(this.kitInstructions!.length, count); + assert.ok(this.kitInstructions![0] !== undefined); + assert.strictEqual(typeof this.kitInstructions![0], 'object'); + }, +); + +When( + 'I call the kit createAtaInstructions builder', + async function (this: TokenInterfaceWorld) { + this.kitInstructions = await createAtaInstructions({ + payer: this.keypairs['payer'], + owner: this.keypairs['owner'], + mint: this.keypairs['mint'], + }); + }, +); + +Then( + 'the result is a list of {int} kit instruction(s)', + function (this: TokenInterfaceWorld, count: number) { + assert.strictEqual(this.kitInstructions!.length, count); + assert.ok(this.kitInstructions![0] !== undefined); + }, +); + +Then( + 'createTransferInstructions from kit is a function', + function () { + assert.strictEqual(typeof createTransferInstructions, 'function'); + }, +); + +Then( + 'createTransferInstructionPlan from kit is a function', + function () { + assert.strictEqual(typeof createTransferInstructionPlan, 'function'); + }, +); diff --git a/js/token-interface/tests/step-definitions/unit/public-api.steps.ts b/js/token-interface/tests/step-definitions/unit/public-api.steps.ts new file mode 100644 index 0000000000..9fa3eaf723 --- /dev/null +++ b/js/token-interface/tests/step-definitions/unit/public-api.steps.ts @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import { Given, When, Then } from '@cucumber/cucumber'; +import { Keypair } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { getAssociatedTokenAddress } from '../../../src/read/index.js'; +import { + createTransferInstructions, + MultiTransactionNotSupportedError, + createAtaInstructions, + createFreezeInstruction, + createThawInstruction, + getAtaAddress, +} from '../../../src/index.js'; +import type { TokenInterfaceWorld } from '../../support/world.js'; + +Given( + 'random keypairs for {string} and {string}', + function (this: TokenInterfaceWorld, name1: string, name2: string) { + this.keypairs[name1] = Keypair.generate().publicKey; + this.keypairs[name2] = Keypair.generate().publicKey; + }, +); + +Given( + 'random keypairs for {string}, {string}, and {string}', + function ( + this: TokenInterfaceWorld, + name1: string, + name2: string, + name3: string, + ) { + this.keypairs[name1] = Keypair.generate().publicKey; + this.keypairs[name2] = Keypair.generate().publicKey; + this.keypairs[name3] = Keypair.generate().publicKey; + }, +); + +When( + 'I derive the ATA address for {string} and {string}', + function (this: TokenInterfaceWorld, ownerKey: string, mintKey: string) { + const derived = getAtaAddress({ + owner: this.keypairs[ownerKey], + mint: this.keypairs[mintKey], + }); + this.keypairs['derivedAta'] = derived; + }, +); + +Then( + 'it matches the low-level getAssociatedTokenAddress result', + function (this: TokenInterfaceWorld) { + const expected = getAssociatedTokenAddress( + this.keypairs['mint'], + this.keypairs['owner'], + ); + assert.ok(this.keypairs['derivedAta'].equals(expected)); + }, +); + +When( + 'I build an ATA instruction list for {string}, {string}, and {string}', + async function ( + this: TokenInterfaceWorld, + payerKey: string, + ownerKey: string, + mintKey: string, + ) { + this.instructions = await createAtaInstructions({ + payer: this.keypairs[payerKey], + owner: this.keypairs[ownerKey], + mint: this.keypairs[mintKey], + }); + }, +); + +Then( + 'the result is a list of {int} instruction(s)', + function (this: TokenInterfaceWorld, count: number) { + assert.strictEqual(this.instructions.length, count); + }, +); + +Then( + 'the first instruction program ID is the light-token program', + function (this: TokenInterfaceWorld) { + assert.ok(this.instructions[0].programId.equals(LIGHT_TOKEN_PROGRAM_ID)); + }, +); + +When( + 'I build raw freeze and thaw instructions', + function (this: TokenInterfaceWorld) { + this.builtInstructions['freeze'] = createFreezeInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + mint: this.keypairs['mint'], + freezeAuthority: this.keypairs['freezeAuthority'], + }); + this.builtInstructions['thaw'] = createThawInstruction({ + tokenAccount: this.keypairs['tokenAccount'], + mint: this.keypairs['mint'], + freezeAuthority: this.keypairs['freezeAuthority'], + }); + }, +); + +Then( + 'the freeze discriminator byte is {int}', + function (this: TokenInterfaceWorld, disc: number) { + assert.strictEqual(this.builtInstructions['freeze'].data[0], disc); + }, +); + +Then( + 'the thaw discriminator byte is {int}', + function (this: TokenInterfaceWorld, disc: number) { + assert.strictEqual(this.builtInstructions['thaw'].data[0], disc); + }, +); + +When( + 'I create a MultiTransactionNotSupportedError for {string} with batch count {int}', + function (this: TokenInterfaceWorld, funcName: string, count: number) { + this.errorInstance = new MultiTransactionNotSupportedError( + funcName, + count, + ); + }, +); + +Then( + 'the error name is {string}', + function (this: TokenInterfaceWorld, name: string) { + assert.strictEqual(this.errorInstance!.name, name); + }, +); + +Then( + 'the error message contains {string}', + function (this: TokenInterfaceWorld, substring: string) { + assert.ok(this.errorInstance!.message.includes(substring)); + }, +); + +Then('createTransferInstructions is a function', function () { + assert.strictEqual(typeof createTransferInstructions, 'function'); +}); diff --git a/js/token-interface/tests/support/hooks.ts b/js/token-interface/tests/support/hooks.ts new file mode 100644 index 0000000000..b0d315a1b2 --- /dev/null +++ b/js/token-interface/tests/support/hooks.ts @@ -0,0 +1,5 @@ +import { setDefaultTimeout } from '@cucumber/cucumber'; + +// E2E tests interact with on-chain state and need long timeouts (350 seconds). +// Unit tests complete in <100ms naturally. +setDefaultTimeout(350_000); diff --git a/js/token-interface/tests/support/world.ts b/js/token-interface/tests/support/world.ts new file mode 100644 index 0000000000..54c35fae53 --- /dev/null +++ b/js/token-interface/tests/support/world.ts @@ -0,0 +1,42 @@ +import { World, IWorldOptions, setWorldConstructor } from '@cucumber/cucumber'; +import type { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; +import type { MintFixture } from '../e2e/helpers.js'; + +export class TokenInterfaceWorld extends World { + // ---- Fixture state (e2e) ---- + fixture?: MintFixture; + owner?: Keypair; + sender?: Keypair; + recipient?: Keypair; + delegate?: Keypair; + + // ---- Instruction results ---- + instructions: TransactionInstruction[] = []; + lastApproveInstructions: TransactionInstruction[] = []; + lastRevokeInstructions: TransactionInstruction[] = []; + transactionSignature?: string; + + // ---- Assertion targets ---- + resultBalance?: bigint; + resultAmounts?: bigint[]; + resultError?: Error; + resultAccount?: any; + resultDelegateInfo?: { + delegate: PublicKey | null; + delegatedAmount: bigint; + }; + resultState?: number; + + // ---- Unit test state ---- + keypairs: Record = {}; + instruction?: TransactionInstruction; + builtInstructions: Record = {}; + kitInstructions?: any[]; + errorInstance?: Error; + + constructor(options: IWorldOptions) { + super(options); + } +} + +setWorldConstructor(TokenInterfaceWorld); diff --git a/js/token-interface/tests/unit/account.test.ts b/js/token-interface/tests/unit/account.test.ts new file mode 100644 index 0000000000..7a78fd6899 --- /dev/null +++ b/js/token-interface/tests/unit/account.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, it } from 'vitest'; +import { Keypair } from '@solana/web3.js'; +import type { TokenInterfaceAccount } from '../../src'; +import { + assertAccountFrozen, + assertAccountNotFrozen, + getSpendableAmount, +} from '../../src/account'; + +function buildAccount(input: { + owner: ReturnType['publicKey']; + delegate?: ReturnType['publicKey'] | null; + amount: bigint; + delegatedAmount?: bigint; + isFrozen?: boolean; +}): TokenInterfaceAccount { + const mint = Keypair.generate().publicKey; + const parsedDelegate = input.delegate ?? null; + const delegatedAmount = input.delegatedAmount ?? 0n; + const isFrozen = input.isFrozen ?? false; + + return { + address: Keypair.generate().publicKey, + owner: input.owner, + mint, + amount: input.amount, + hotAmount: input.amount, + compressedAmount: 0n, + hasHotAccount: true, + requiresLoad: false, + parsed: { + address: Keypair.generate().publicKey, + owner: input.owner, + mint, + amount: input.amount, + delegate: parsedDelegate, + delegatedAmount, + isInitialized: true, + isFrozen, + }, + compressedAccount: null, + ignoredCompressedAccounts: [], + ignoredCompressedAmount: 0n, + }; +} + +describe('account helpers', () => { + it('returns full amount for owner and delegated amount for delegate', () => { + const owner = Keypair.generate().publicKey; + const delegate = Keypair.generate().publicKey; + const outsider = Keypair.generate().publicKey; + const account = buildAccount({ + owner, + delegate, + amount: 100n, + delegatedAmount: 30n, + }); + + expect(getSpendableAmount(account, owner)).toBe(100n); + expect(getSpendableAmount(account, delegate)).toBe(30n); + expect(getSpendableAmount(account, outsider)).toBe(0n); + }); + + it('clamps delegated spendable amount to total balance', () => { + const owner = Keypair.generate().publicKey; + const delegate = Keypair.generate().publicKey; + const account = buildAccount({ + owner, + delegate, + amount: 20n, + delegatedAmount: 500n, + }); + + expect(getSpendableAmount(account, delegate)).toBe(20n); + }); + + it('throws explicit frozen-state assertion errors', () => { + const owner = Keypair.generate().publicKey; + const frozenAccount = buildAccount({ + owner, + amount: 1n, + isFrozen: true, + }); + const activeAccount = buildAccount({ + owner, + amount: 1n, + isFrozen: false, + }); + + expect(() => assertAccountNotFrozen(frozenAccount, 'transfer')).toThrow( + 'Account is frozen; transfer is not allowed.', + ); + expect(() => assertAccountFrozen(activeAccount, 'thaw')).toThrow( + 'Account is not frozen; thaw is not allowed.', + ); + }); +}); diff --git a/js/token-interface/tsconfig.json b/js/token-interface/tsconfig.json new file mode 100644 index 0000000000..7542e4510d --- /dev/null +++ b/js/token-interface/tsconfig.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "importHelpers": true, + "outDir": "./dist", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "declaration": false, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Node", + "lib": ["ESNext", "DOM"], + "types": ["node"], + "skipLibCheck": true + }, + "include": ["./src/**/*.ts", "rollup.config.js"] +} diff --git a/js/token-interface/tsconfig.test.json b/js/token-interface/tsconfig.test.json new file mode 100644 index 0000000000..eea543a7dc --- /dev/null +++ b/js/token-interface/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "rootDirs": ["src", "tests"] + }, + "extends": "./tsconfig.json", + "include": ["./tests/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 158713b83f..1ea03ff484 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,6 +435,97 @@ importers: specifier: ^2.1.1 version: 2.1.1(@types/node@22.16.5)(terser@5.43.1) + js/token-interface: + dependencies: + '@coral-xyz/borsh': + specifier: ^0.29.0 + version: 0.29.0(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)) + '@lightprotocol/stateless.js': + specifier: workspace:* + version: link:../stateless.js + '@solana/buffer-layout': + specifier: ^4.0.1 + version: 4.0.1 + '@solana/buffer-layout-utils': + specifier: ^0.2.0 + version: 0.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/compat': + specifier: ^6.5.0 + version: 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instruction-plans': + specifier: ^6.5.0 + version: 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit': + specifier: ^6.5.0 + version: 6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/spl-token': + specifier: '>=0.3.9' + version: 0.3.11(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/web3.js': + specifier: '>=1.73.5' + version: 1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + bn.js: + specifier: ^5.2.1 + version: 5.2.1 + buffer: + specifier: 6.0.3 + version: 6.0.3 + devDependencies: + '@cucumber/cucumber': + specifier: ^11.0.0 + version: 11.3.0 + '@eslint/js': + specifier: 9.36.0 + version: 9.36.0 + '@lightprotocol/compressed-token': + specifier: workspace:* + version: link:../compressed-token + '@rollup/plugin-commonjs': + specifier: ^26.0.1 + version: 26.0.1(rollup@4.21.3) + '@rollup/plugin-node-resolve': + specifier: ^15.2.3 + version: 15.2.3(rollup@4.21.3) + '@rollup/plugin-typescript': + specifier: ^11.1.6 + version: 11.1.6(rollup@4.21.3)(tslib@2.8.1)(typescript@5.9.3) + '@types/bn.js': + specifier: ^5.1.5 + version: 5.2.0 + '@types/node': + specifier: ^22.5.5 + version: 22.16.5 + '@typescript-eslint/eslint-plugin': + specifier: ^8.44.0 + version: 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.44.0 + version: 8.44.0(eslint@9.36.0)(typescript@5.9.3) + eslint: + specifier: ^9.36.0 + version: 9.36.0 + prettier: + specifier: ^3.3.3 + version: 3.6.2 + rimraf: + specifier: ^6.0.1 + version: 6.0.1 + rollup: + specifier: ^4.21.3 + version: 4.21.3 + rollup-plugin-dts: + specifier: ^6.1.1 + version: 6.1.1(rollup@4.21.3)(typescript@5.9.3) + tslib: + specifier: ^2.7.0 + version: 2.8.1 + tsx: + specifier: ^4.19.0 + version: 4.20.5 + typescript: + specifier: ^5.6.2 + version: 5.9.3 + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': @@ -746,6 +837,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + '@coral-xyz/anchor-errors@0.31.1': resolution: {integrity: sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==} engines: {node: '>=10'} @@ -774,6 +869,64 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@cucumber/ci-environment@10.0.1': + resolution: {integrity: sha512-/+ooDMPtKSmvcPMDYnMZt4LuoipfFfHaYspStI4shqw8FyKcfQAmekz6G+QKWjQQrvM+7Hkljwx58MEwPCwwzg==} + + '@cucumber/cucumber-expressions@18.0.1': + resolution: {integrity: sha512-NSid6bI+7UlgMywl5octojY5NXnxR9uq+JisjOrO52VbFsQM6gTWuQFE8syI10KnIBEdPzuEUSVEeZ0VFzRnZA==} + + '@cucumber/cucumber@11.3.0': + resolution: {integrity: sha512-1YGsoAzRfDyVOnRMTSZP/EcFsOBElOKa2r+5nin0DJAeK+Mp0mzjcmSllMgApGtck7Ji87wwy3kFONfHUHMn4g==} + engines: {node: 18 || 20 || 22 || >=23} + hasBin: true + + '@cucumber/gherkin-streams@5.0.1': + resolution: {integrity: sha512-/7VkIE/ASxIP/jd4Crlp4JHXqdNFxPGQokqWqsaCCiqBiu5qHoKMxcWNlp9njVL/n9yN4S08OmY3ZR8uC5x74Q==} + hasBin: true + peerDependencies: + '@cucumber/gherkin': '>=22.0.0' + '@cucumber/message-streams': '>=4.0.0' + '@cucumber/messages': '>=17.1.1' + + '@cucumber/gherkin-utils@9.2.0': + resolution: {integrity: sha512-3nmRbG1bUAZP3fAaUBNmqWO0z0OSkykZZotfLjyhc8KWwDSOrOmMJlBTd474lpA8EWh4JFLAX3iXgynBqBvKzw==} + hasBin: true + + '@cucumber/gherkin@30.0.4': + resolution: {integrity: sha512-pb7lmAJqweZRADTTsgnC3F5zbTh3nwOB1M83Q9ZPbUKMb3P76PzK6cTcPTJBHWy3l7isbigIv+BkDjaca6C8/g==} + + '@cucumber/gherkin@31.0.0': + resolution: {integrity: sha512-wlZfdPif7JpBWJdqvHk1Mkr21L5vl4EfxVUOS4JinWGf3FLRV6IKUekBv5bb5VX79fkDcfDvESzcQ8WQc07Wgw==} + + '@cucumber/html-formatter@21.10.1': + resolution: {integrity: sha512-isaaNMNnBYThsvaHy7i+9kkk9V3+rhgdkt0pd6TCY6zY1CSRZQ7tG6ST9pYyRaECyfbCeF7UGH0KpNEnh6UNvQ==} + peerDependencies: + '@cucumber/messages': '>=18' + + '@cucumber/junit-xml-formatter@0.7.1': + resolution: {integrity: sha512-AzhX+xFE/3zfoYeqkT7DNq68wAQfBcx4Dk9qS/ocXM2v5tBv6eFQ+w8zaSfsktCjYzu4oYRH/jh4USD1CYHfaQ==} + peerDependencies: + '@cucumber/messages': '*' + + '@cucumber/message-streams@4.0.1': + resolution: {integrity: sha512-Kxap9uP5jD8tHUZVjTWgzxemi/0uOsbGjd4LBOSxcJoOCRbESFwemUzilJuzNTB8pcTQUh8D5oudUyxfkJOKmA==} + peerDependencies: + '@cucumber/messages': '>=17.1.1' + + '@cucumber/messages@26.0.1': + resolution: {integrity: sha512-DIxSg+ZGariumO+Lq6bn4kOUIUET83A4umrnWmidjGFl8XxkBieUZtsmNbLYgH/gnsmP07EfxxdTr0hOchV1Sg==} + + '@cucumber/messages@27.2.0': + resolution: {integrity: sha512-f2o/HqKHgsqzFLdq6fAhfG1FNOQPdBdyMGpKwhb7hZqg0yZtx9BVqkTyuoNk83Fcvk3wjMVfouFXXHNEk4nddA==} + + '@cucumber/query@13.6.0': + resolution: {integrity: sha512-tiDneuD5MoWsJ9VKPBmQok31mSX9Ybl+U4wqDoXeZgsXHDURqzM3rnpWVV3bC34y9W6vuFxrlwF/m7HdOxwqRw==} + peerDependencies: + '@cucumber/messages': '*' + + '@cucumber/tag-expressions@6.1.2': + resolution: {integrity: sha512-xa3pER+ntZhGCxRXSguDTKEHTZpUUsp+RzTRNnit+vi5cqnk6abLdSLg5i3HZXU3c74nQ8afQC6IT507EN74oQ==} + '@esbuild-plugins/node-globals-polyfill@0.2.3': resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==} peerDependencies: @@ -1865,6 +2018,33 @@ packages: resolution: {integrity: sha512-PJBmyayrlfxM7nbqjomF4YcT1sApQwZio0NHSsT0EzhJqljRmvhzqZua43TyEs80nJk2Cn2FGPg/N8phH6KeCQ==} engines: {node: '>=18.0.0'} + '@solana/accounts@6.5.0': + resolution: {integrity: sha512-h3zQFjwZjmy+YxgTGOEna6g74Tsn4hTBaBCslwPT4QjqWhywe2JrM2Ab0ANfJcj7g/xrHF5QJ/FnUIcyUTeVfQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/addresses@6.5.0': + resolution: {integrity: sha512-iD4/u3CWchQcPofbwzteaE9RnFJSoi654Rnhru5fOu6U2XOte3+7t50d6OxdxQ109ho2LqZyVtyCo2Wb7u1aJQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/assertions@6.5.0': + resolution: {integrity: sha512-rEAf40TtC9r6EtJFLe39WID4xnTNT6hdOVRfD1xDzmIQdVOyGgIbJGt2FAuB/uQDKLWneWMnvGDBim+K61Bljw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/buffer-layout-utils@0.2.0': resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} engines: {node: '>= 10'} @@ -1892,6 +2072,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-core@6.5.0': + resolution: {integrity: sha512-Wb+YUj7vUKz5CxqZkrkugtQjxOP2fkMKnffySRlAmVAkpRnQvBY/2eP3VJAKTgDD4ru9xHSIQSpDu09hC/cQZg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-data-structures@2.0.0-experimental.8618508': resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} @@ -1905,6 +2094,15 @@ packages: peerDependencies: typescript: '>=5' + '@solana/codecs-data-structures@6.5.0': + resolution: {integrity: sha512-Rxi5zVJ1YA+E6FoSQ7RHP+3DF4U7ski0mJ3H5CsYQP24QLRlBqWB3X6m2n9GHT5O3s49UR0sqeF4oyq0lF8bKw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-numbers@2.0.0-experimental.8618508': resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} @@ -1924,6 +2122,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-numbers@6.5.0': + resolution: {integrity: sha512-gU/7eYqD+zl2Kwzo7ctt7YHaxF+c3RX164F+iU4X02dwq8DGVcypp+kmEF1QaO6OiShtdryTxhL+JJmEBjhdfA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/codecs-strings@2.0.0-experimental.8618508': resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} peerDependencies: @@ -1941,6 +2148,18 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5' + '@solana/codecs-strings@6.5.0': + resolution: {integrity: sha512-9TuQQxumA9gWJeJzbv1GUg0+o0nZp204EijX3efR+lgBOKbkU7W0UWp33ygAZ+RvWE+kTs48ePoYoJ7UHpyxkQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: ^5.0.0 + peerDependenciesMeta: + fastestsmallesttextencoderdecoder: + optional: true + typescript: + optional: true + '@solana/codecs@2.0.0-preview.4': resolution: {integrity: sha512-gLMupqI4i+G4uPi2SGF/Tc1aXcviZF2ybC81x7Q/fARamNSgNOCUUoSCg9nWu1Gid6+UhA7LH80sWI8XjKaRog==} peerDependencies: @@ -1951,6 +2170,24 @@ packages: peerDependencies: typescript: '>=5' + '@solana/codecs@6.5.0': + resolution: {integrity: sha512-WfqMqUXk4jcCJQ9nfKqjDcCJN2Pt8/AKe/E78z8OcblFGVJnTzcu2yZpE2gsqM+DJyCVKdQmOY+NS8Uckk5e5w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/compat@6.5.0': + resolution: {integrity: sha512-Z5kfLg9AoxEhZOAx5hu4KnTDdGQbggTVPdBIJtpmgGPH321JVDV2DPUPHfLezx8upN+ftMdweNLS/4DMucNddg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/errors@2.0.0-preview.4': resolution: {integrity: sha512-kadtlbRv2LCWr8A9V22On15Us7Nn8BvqNaOB4hXsTB3O0fU40D1ru2l+cReqLcRPij4znqlRzW9Xi0m6J5DIhA==} hasBin: true @@ -1970,6 +2207,88 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/errors@6.5.0': + resolution: {integrity: sha512-XPc0I8Ck6vgx8Uu+LVLewx/1RWDkXkY3lU+1aN1kmbrPAQWbX4Txk7GPmuIIFpyys8o5aKocYfNxJOPKvfaQhg==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/fast-stable-stringify@6.5.0': + resolution: {integrity: sha512-5ATQDwBVZMoenX5KS23uFswtaAGoaZB9TthzUXle3tkU3tOfgQTuEWEoqEBYc7ct0sK6LtyE1XXT/NP5YvAkkQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/functional@6.5.0': + resolution: {integrity: sha512-/KYgY7ZpBJfkN8+qlIvxuBpxv32U9jHXIOOJh3U5xk8Ncsa9Ex5VwbU9NkOf43MJjoIamsP0vARCHjcqJwe5JQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/instruction-plans@6.5.0': + resolution: {integrity: sha512-zp2asevpyMwvhajHYM1aruYpO+xf3LSwHEI2FK6E2hddYZaEhuBy+bz+NZ1ixCyfx3iXcq7MamlFQc2ySHDyUQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/instructions@6.5.0': + resolution: {integrity: sha512-2mQP/1qqr5PCfaVMzs9KofBjpyS7J1sBV6PidGoX9Dg5/4UgwJJ+7yfCVQPn37l1nKCShm4I+pQAy5vbmrxJmA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/keys@6.5.0': + resolution: {integrity: sha512-CN5jmodX9j5CZKrWLM5XGaRlrLl/Ebl4vgqDXrnwC2NiSfUslLsthuORMuVUTDqkzBX/jd/tgVXFRH2NYNzREQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/kit@6.5.0': + resolution: {integrity: sha512-4ysrtqMRd7CTYRv179gQq4kbw9zMsJCLhWjiyOmLZ4co4ld3L654D8ykW7yqWE5PJwF0hzEfheE7oBscO37nvw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/nominal-types@6.5.0': + resolution: {integrity: sha512-HngIM2nlaDPXk0EDX0PklFqpjGDKuOFnlEKS0bfr2F9CorFwiNhNjhb9lPH+FdgsogD1wJ8wgLMMk1LZWn5kgQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/offchain-messages@6.5.0': + resolution: {integrity: sha512-IYuidJCwfXg5xlh3rkflkA1fbTKWTsip8MdI+znvXm87grfqOYCTd6t/SKiV4BhLl/65Tn0wB/zvZ1cmzJqa1w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + '@solana/options@2.0.0-experimental.8618508': resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} @@ -1983,44 +2302,260 @@ packages: peerDependencies: typescript: '>=5' - '@solana/spl-token-group@0.0.5': - resolution: {integrity: sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ==} - engines: {node: '>=16'} + '@solana/options@6.5.0': + resolution: {integrity: sha512-jdZjSKGCQpsMFK+3CiUEI7W9iGsndi46R4Abk66ULNLDoMsjvfqNy8kqktm0TN0++EX8dKEecpFwxFaA4VlY5g==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.94.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token-metadata@0.1.2': - resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} - engines: {node: '>=16'} + '@solana/plugin-core@6.5.0': + resolution: {integrity: sha512-L6N69oNQOAqljH4GnLTaxpwJB0nibW9DrybHZxpGWshyv6b/EvwvkDVRKj5bNqtCG+HRZUHnEhLi1UgZVNkjpQ==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.87.6 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token-metadata@0.1.5': - resolution: {integrity: sha512-DSBlo7vjuLe/xvNn75OKKndDBkFxlqjLdWlq6rf40StnrhRn7TDntHGLZpry1cf3uzQFShqeLROGNPAJwvkPnA==} - engines: {node: '>=16'} + '@solana/plugin-interfaces@6.5.0': + resolution: {integrity: sha512-/ZlybbMaR7P4ySersOe1huioMADWze0AzsHbzgkpt5dJUv2tz5cpaKdu7TEVQkUZAFhLdqXQULNGqAU5neOgzg==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.95.3 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token@0.3.11': - resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} - engines: {node: '>=16'} + '@solana/program-client-core@6.5.0': + resolution: {integrity: sha512-eUz1xSeDKySGIjToAryPmlESdj8KX0Np7R+Pjt+kSFGw5Jgmn/Inh4o8luoeEnf5XwbvSPVb4aHpIsDyoUVbIg==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.88.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-token@0.4.8': - resolution: {integrity: sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA==} - engines: {node: '>=16'} + '@solana/programs@6.5.0': + resolution: {integrity: sha512-srn3nEROBxCnBpVz/bvLkVln1BZtk3bS3nuReu3yaeOLkKl8b0h1Zp0YmXVyXHzdMcYahsTvKKLR1ZtLZEyEPA==} + engines: {node: '>=20.18.0'} peerDependencies: - '@solana/web3.js': ^1.94.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/spl-type-length-value@0.1.0': - resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} - engines: {node: '>=16'} + '@solana/promises@6.5.0': + resolution: {integrity: sha512-n5rsA3YwOO2nUst6ghuVw6RSnuZQYqevqBKqVYbw11Z4XezsoQ6hb78opW3J9YNYapw9wLWy6tEfUsJjY+xtGw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@solana/web3.js@1.98.4': - resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + '@solana/rpc-api@6.5.0': + resolution: {integrity: sha512-b+kftroO8vZFzLHj7Nk/uATS3HOlBUsUqdGg3eTQrW1pFgkyq5yIoEYHeFF7ApUN/SJLTK86U8ofCaXabd2SXA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true - '@standard-schema/spec@1.0.0': + '@solana/rpc-parsed-types@6.5.0': + resolution: {integrity: sha512-129c8meL6CxRg56/HfhkFOpwYteQH9Rt0wyXOXZQx3a3FNpcJLd4JdPvxDsLBE3EupEkXLGVku/1bGKz+F2J+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-spec-types@6.5.0': + resolution: {integrity: sha512-XasJp+sOW6PLfNoalzoLnm+j3LEZF8XOQmSrOqv9AGrGxQckkuOf6iXZucWTqeNKdstsOpU28BN2B6qOavfRzQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-spec@6.5.0': + resolution: {integrity: sha512-k4O7Kg0QfVyjUqQovL+WZJ1iuPzq0jiUDcWYgvzFjYVxQDVOIZmAol7yTvLEL4maVmf0tNFDsrDaB6t75MKRZA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-subscriptions-api@6.5.0': + resolution: {integrity: sha512-smqNjT2C5Vf9nWGIwiYOLOP744gRWKi2i2g0i3ZVdsfoouvB0d/WTQ2bbWq47MrdV8FSuGnjAOM3dRIwYmYOWw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-subscriptions-channel-websocket@6.5.0': + resolution: {integrity: sha512-xRKH3ZwIoV9Zua9Gp0RR0eL8lXNgx+iNIkE3F0ROlOzI48lt4lRJ7jLrHQCN3raVtkatFVuEyZ7e9eLHK9zhAw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-subscriptions-spec@6.5.0': + resolution: {integrity: sha512-Mi8g9rNS2lG7lyNkDhOVfQVfDC7hXKgH+BlI5qKGk+8cfyU7VDq6tVjDysu6kBWGOPHZxyCvcL6+xW/EkdVoAg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-subscriptions@6.5.0': + resolution: {integrity: sha512-EenogPQw9Iy8VUj8anu7xoBnPk7gu1J6sAi4MTVlNVz02sNjdUBJoSS0PRJZuhSM1ktPTtHrNwqlXP8TxPR7jg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-transformers@6.5.0': + resolution: {integrity: sha512-kS0d+LuuSLfsod2cm2xp0mNj65PL1aomwu6VKtubmsdESwPXHIaI9XrpkPCBuhNSz1SwVp4OkfK5O/VOOHYHSw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-transport-http@6.5.0': + resolution: {integrity: sha512-A3qgDGiUIHdtAfc2OyazlQa7IvRh+xyl0dmzaZlz4rY7Oc7Xk8jmXtaKGkgXihLyAK3oVSqSz5gn9yEfx55eXA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc-types@6.5.0': + resolution: {integrity: sha512-hxts27+Z2VNv4IjXGcXkqbj/MgrN9Xtw/4iE1qZk68T2OAb5vA4b8LHchsOHmHvrzZfo8XDvB9mModCdM3JPsQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/rpc@6.5.0': + resolution: {integrity: sha512-lGj7ZMVOR3Rf16aByXD6ghrMqw3G8rAMuWCHU4uMKES5M5VLqNv6o71bSyoTxVMGrmYdbALOvCbFMFINAxtoBg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/signers@6.5.0': + resolution: {integrity: sha512-AL75/DyDUhc+QQ+VGZT7aRwJNzIUTWvmLNXQRlCVhLRuyroXzZEL2WJBs8xOwbZXjY8weacfYT7UNM8qK6ucDg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/spl-token-group@0.0.5': + resolution: {integrity: sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.94.0 + + '@solana/spl-token-metadata@0.1.2': + resolution: {integrity: sha512-hJYnAJNkDrtkE2Q41YZhCpeOGU/0JgRFXbtrtOuGGeKc3pkEUHB9DDoxZAxx+XRno13GozUleyBi0qypz4c3bw==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.87.6 + + '@solana/spl-token-metadata@0.1.5': + resolution: {integrity: sha512-DSBlo7vjuLe/xvNn75OKKndDBkFxlqjLdWlq6rf40StnrhRn7TDntHGLZpry1cf3uzQFShqeLROGNPAJwvkPnA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.95.3 + + '@solana/spl-token@0.3.11': + resolution: {integrity: sha512-bvohO3rIMSVL24Pb+I4EYTJ6cL82eFpInEXD/I8K8upOGjpqHsKUoAempR/RnUlI1qSFNyFlWJfu6MNUgfbCQQ==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.88.0 + + '@solana/spl-token@0.4.8': + resolution: {integrity: sha512-RO0JD9vPRi4LsAbMUdNbDJ5/cv2z11MGhtAvFeRzT4+hAGE/FUzRi0tkkWtuCfSIU3twC6CtmAihRp/+XXjWsA==} + engines: {node: '>=16'} + peerDependencies: + '@solana/web3.js': ^1.94.0 + + '@solana/spl-type-length-value@0.1.0': + resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} + engines: {node: '>=16'} + + '@solana/subscribable@6.5.0': + resolution: {integrity: sha512-Jmy2NYmQN68FsQzKJ5CY3qrxXBJdb5qtJKp8B4byPPO5liKNIsC59HpT0Tq8MCNSfBMmOkWF2rrVot2/g1iB1A==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/sysvars@6.5.0': + resolution: {integrity: sha512-iLSS5qj0MWNiGH1LN1E4jhGsXH9D3tWSjwaB6zK9LjhLdVYcPfkosBkj7s0EHHrH03QlwiuFdU0Y2kH8Jcp8kw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/transaction-confirmation@6.5.0': + resolution: {integrity: sha512-hfdRBq4toZj7DRMgBN3F0VtJpmTAEtcVTTDZoiszoSpSVa2cAvFth6KypIqASVFZyi9t4FKolLP8ASd3/39UQg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/transaction-messages@6.5.0': + resolution: {integrity: sha512-ueXkm5xaRlqYBFAlABhaCKK/DuzIYSot0FybwSDeOQCDy2hvU9Zda16Iwa1n56M0fG+XUvFJz2woG3u9DhQh1g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/transactions@6.5.0': + resolution: {integrity: sha512-b3eJrrGmwpk64VLHjOrmXKAahPpba42WX/FqSUn4WRXPoQjga7Mb57yp+EaRVeQfjszKCkF+13yu+ni6iv2NFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/web3.js@1.98.4': + resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} '@swc/helpers@0.5.13': @@ -2030,6 +2565,10 @@ packages: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} engines: {node: '>=14.16'} + '@teppeis/multimaps@3.0.0': + resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} + engines: {node: '>=14'} + '@tsconfig/node10@1.0.9': resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} @@ -2102,6 +2641,9 @@ packages: '@types/node@22.16.5': resolution: {integrity: sha512-bJFoMATwIGaxxx8VJPeM8TonI8t579oRvgAuT8zFugJsJZgzqv0Fu8Mhp68iecjzG7cnN3mO2dJQ5uUM2EFrgQ==} + '@types/normalize-package-data@2.4.4': + resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/prop-types@15.7.15': resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} @@ -2117,6 +2659,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/uuid@8.3.4': resolution: {integrity: sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==} @@ -2299,6 +2844,10 @@ packages: resolution: {integrity: sha512-YdhtCd19sKRKfAAUsrcC1wzm4JuzJoiX4pOJqIoW2qmKj5WzG/dL8uUJ0361zaXtHqK7gEhOwtAtz7t3Yq3X5g==} engines: {node: '>=18'} + ansi-regex@4.1.1: + resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} + engines: {node: '>=6'} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2327,6 +2876,9 @@ packages: resolution: {integrity: sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==} engines: {node: '>=14'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -2383,6 +2935,9 @@ packages: asn1.js@4.10.1: resolution: {integrity: sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==} + assertion-error-formatter@3.0.0: + resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -2636,6 +3191,9 @@ packages: cipher-base@1.0.4: resolution: {integrity: sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==} + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + clean-stack@3.0.1: resolution: {integrity: sha512-lR9wNiMRcVQjSB3a7xXGLuz4cr4wJuuXlaAEbRutGowQTmlp7R72/DOgN21e8jdwblMWl9UOJMJXarX94pzKdg==} engines: {node: '>=10'} @@ -2660,6 +3218,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + cli-truncate@4.0.0: resolution: {integrity: sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==} engines: {node: '>=18'} @@ -2696,6 +3258,10 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@12.1.0: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} @@ -2704,8 +3270,8 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - commander@14.0.1: - resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} engines: {node: '>=20'} commander@2.20.3: @@ -2715,6 +3281,10 @@ packages: resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} engines: {node: '>= 6'} + commander@9.1.0: + resolution: {integrity: sha512-i0/MaqBtdbnJ4XQs4Pmyb+oFQl+q0lsAmokVUH92SlSw4fkeAcG3bVon+Qt7hmtF+u3Het6o4VgrcY3qAoEB6w==} + engines: {node: ^12.20.0 || >=14} + commondir@1.0.1: resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} @@ -2990,6 +3560,9 @@ packages: error-ex@1.3.2: resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + es-abstract@1.22.2: resolution: {integrity: sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==} engines: {node: '>= 0.4'} @@ -3275,6 +3848,10 @@ packages: ffjavascript@0.3.1: resolution: {integrity: sha512-4PbK1WYodQtuF47D4pRI5KUg3Q392vuP5WjE1THSnceHdXwU3ijaoS0OqxTzLknCtz4Z2TtABzkBdBdMn3B/Aw==} + figures@3.2.0: + resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} + engines: {node: '>=8'} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3293,6 +3870,10 @@ packages: resolution: {integrity: sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg==} hasBin: true + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -3467,20 +4048,27 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@11.0.0: resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} engines: {node: 20 || >=22} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-dirs@3.0.1: + resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} + engines: {node: '>=10'} globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} @@ -3530,6 +4118,10 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + has-ansi@4.0.1: + resolution: {integrity: sha512-Qr4RtTm30xvEdqUXbSBVWDu+PrTokJOwe/FU+VdfJPk+MXAPoeOzKpRyrDTnZIJwAkQ4oBLTU53nu0HrkF/Z2A==} + engines: {node: '>=8'} + has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -3687,6 +4279,10 @@ packages: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} engines: {node: '>=12'} + index-to-position@1.2.0: + resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} + engines: {node: '>=18'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -3697,6 +4293,10 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + ini@2.0.0: + resolution: {integrity: sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==} + engines: {node: '>=10'} + ink@5.0.1: resolution: {integrity: sha512-ae4AW/t8jlkj/6Ou21H2av0wxTk8vrGzXv+v2v7j4in+bl1M5XRMVbfNghzhBokV++FjF8RBDJvYo+ttR9YVRg==} engines: {node: '>=18'} @@ -3832,6 +4432,10 @@ packages: engines: {node: '>=18'} hasBin: true + is-installed-globally@0.4.0: + resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} + engines: {node: '>=10'} + is-interactive@2.0.0: resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} engines: {node: '>=12'} @@ -4066,6 +4670,9 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} + knuth-shuffle-seeded@1.0.6: + resolution: {integrity: sha512-9pFH0SplrfyKyojCLxZfMcvkhf5hH0d+UwR9nTVJ/DDQJGuzcXjTwB7TP7sDfehSudlGGaOLblmEWqv04ERVWg==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -4087,6 +4694,12 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.sortby@4.7.0: + resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -4125,6 +4738,10 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + luxon@3.6.1: + resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} + engines: {node: '>=12'} + magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} @@ -4182,6 +4799,11 @@ packages: engines: {node: '>=4'} hasBin: true + mime@3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -4238,6 +4860,11 @@ packages: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true + mkdirp@2.1.6: + resolution: {integrity: sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==} + engines: {node: '>=10'} + hasBin: true + mkdirp@3.0.1: resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} engines: {node: '>=10'} @@ -4262,6 +4889,9 @@ packages: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} engines: {node: ^18.17.0 || >=20.5.0} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -4396,6 +5026,10 @@ packages: - which - write-file-atomic + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + object-hash@3.0.0: resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} engines: {node: '>= 6'} @@ -4497,6 +5131,10 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pad-right@0.2.2: + resolution: {integrity: sha512-4cy8M95ioIGolCoMmm2cMntGR1lPLEbOMzOKu8bzjuJP6JpzEMQcDHmh7hHLYGgob+nKe1YHFMaG4V59HQa89g==} + engines: {node: '>=0.10.0'} + pako@2.1.0: resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==} @@ -4519,6 +5157,10 @@ packages: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} + parse-json@8.3.0: + resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==} + engines: {node: '>=18'} + pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} @@ -4647,10 +5289,17 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} + property-expr@2.0.6: + resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -4702,6 +5351,14 @@ packages: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} + read-package-up@11.0.0: + resolution: {integrity: sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==} + engines: {node: '>=18'} + + read-pkg@9.0.1: + resolution: {integrity: sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==} + engines: {node: '>=18'} + read-yaml-file@2.1.0: resolution: {integrity: sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==} engines: {node: '>=10.13'} @@ -4721,6 +5378,9 @@ packages: resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} engines: {node: '>= 0.10'} + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -4728,6 +5388,13 @@ packages: regenerator-runtime@0.14.0: resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==} + regexp-match-indices@1.0.2: + resolution: {integrity: sha512-DwZuAkt8NF5mKwGGER1EGh2PRqyvhRhhLviH+R8y8dIuaQROlUfXjt4s9ZTXstIsSkptf06BSvwcEmmfheJJWQ==} + + regexp-tree@0.1.27: + resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} + hasBin: true + regexp.prototype.flags@1.5.1: resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} engines: {node: '>= 0.4'} @@ -4744,6 +5411,10 @@ packages: resolution: {integrity: sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==} engines: {node: '>=14'} + repeat-string@1.6.1: + resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==} + engines: {node: '>=0.10'} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} @@ -4877,6 +5548,9 @@ packages: secure-compare@3.0.1: resolution: {integrity: sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==} + seed-random@2.2.0: + resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -4886,6 +5560,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.1: + resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==} + engines: {node: '>=10'} + hasBin: true + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -5035,6 +5714,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} @@ -5042,6 +5724,10 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + string-argv@0.3.1: + resolution: {integrity: sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg==} + engines: {node: '>=0.6.19'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -5159,6 +5845,13 @@ packages: text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -5166,6 +5859,9 @@ packages: resolution: {integrity: sha512-Kw36UHxJEELq2VUqdaSGR2/8cAsPgMtvX8uGVU6Jk26O66PhXec0A5ZnRYs47btbtwPDpXXF66+Fo3vimCM9aQ==} engines: {node: '>=16'} + tiny-case@1.0.3: + resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==} + tiny-jsonc@1.0.2: resolution: {integrity: sha512-f5QDAfLq6zIVSyCZQZhhyl0QS6MvAyTxgz4X4x3+EoCktNWEYJ6PeoEA97fyb98njpBNNi88ybpD7m+BDFXaCw==} @@ -5202,6 +5898,9 @@ packages: toml@3.0.0: resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + toposort@2.0.2: + resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -5274,10 +5973,18 @@ packages: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + type-fest@4.35.0: resolution: {integrity: sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==} engines: {node: '>=16'} + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + typed-array-buffer@1.0.0: resolution: {integrity: sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==} engines: {node: '>= 0.4'} @@ -5360,6 +6067,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.24.6: + resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} + + unicorn-magic@0.1.0: + resolution: {integrity: sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==} + engines: {node: '>=18'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5394,9 +6108,20 @@ packages: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} + util-arity@1.1.0: + resolution: {integrity: sha512-kkyIsXKwemfSy8ZEoaIz06ApApnWsk5hQO0vLjZS6UkBiGiW++Jsyb8vSBoc0WKlffGoGs5yYy/j5pp8zckrFA==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + hasBin: true + + uuid@11.0.5: + resolution: {integrity: sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==} + hasBin: true + uuid@8.3.2: resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} hasBin: true @@ -5501,6 +6226,7 @@ packages: whatwg-encoding@2.0.0: resolution: {integrity: sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==} engines: {node: '>=12'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5613,6 +6339,22 @@ packages: utf-8-validate: optional: true + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5665,6 +6407,9 @@ packages: yoga-wasm-web@0.3.3: resolution: {integrity: sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==} + yup@1.6.1: + resolution: {integrity: sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==} + snapshots: '@alcalzone/ansi-tokenize@0.1.3': @@ -6331,6 +7076,9 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@colors/colors@1.5.0': + optional: true + '@coral-xyz/anchor-errors@0.31.1': {} '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': @@ -6382,6 +7130,12 @@ snapshots: bn.js: 5.2.1 buffer-layout: 1.2.2 + '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10))': + dependencies: + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + bn.js: 5.2.1 + buffer-layout: 1.2.2 + '@coral-xyz/borsh@0.31.1(@solana/web3.js@1.98.4(typescript@4.9.5))': dependencies: '@solana/web3.js': 1.98.4(typescript@4.9.5) @@ -6392,6 +7146,116 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@cucumber/ci-environment@10.0.1': {} + + '@cucumber/cucumber-expressions@18.0.1': + dependencies: + regexp-match-indices: 1.0.2 + + '@cucumber/cucumber@11.3.0': + dependencies: + '@cucumber/ci-environment': 10.0.1 + '@cucumber/cucumber-expressions': 18.0.1 + '@cucumber/gherkin': 30.0.4 + '@cucumber/gherkin-streams': 5.0.1(@cucumber/gherkin@30.0.4)(@cucumber/message-streams@4.0.1(@cucumber/messages@27.2.0))(@cucumber/messages@27.2.0) + '@cucumber/gherkin-utils': 9.2.0 + '@cucumber/html-formatter': 21.10.1(@cucumber/messages@27.2.0) + '@cucumber/junit-xml-formatter': 0.7.1(@cucumber/messages@27.2.0) + '@cucumber/message-streams': 4.0.1(@cucumber/messages@27.2.0) + '@cucumber/messages': 27.2.0 + '@cucumber/tag-expressions': 6.1.2 + assertion-error-formatter: 3.0.0 + capital-case: 1.0.4 + chalk: 4.1.2 + cli-table3: 0.6.5 + commander: 10.0.1 + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 + figures: 3.2.0 + glob: 10.5.0 + has-ansi: 4.0.1 + indent-string: 4.0.0 + is-installed-globally: 0.4.0 + is-stream: 2.0.1 + knuth-shuffle-seeded: 1.0.6 + lodash.merge: 4.6.2 + lodash.mergewith: 4.6.2 + luxon: 3.6.1 + mime: 3.0.0 + mkdirp: 2.1.6 + mz: 2.7.0 + progress: 2.0.3 + read-package-up: 11.0.0 + semver: 7.7.1 + string-argv: 0.3.1 + supports-color: 8.1.1 + type-fest: 4.41.0 + util-arity: 1.1.0 + yaml: 2.8.1 + yup: 1.6.1 + + '@cucumber/gherkin-streams@5.0.1(@cucumber/gherkin@30.0.4)(@cucumber/message-streams@4.0.1(@cucumber/messages@27.2.0))(@cucumber/messages@27.2.0)': + dependencies: + '@cucumber/gherkin': 30.0.4 + '@cucumber/message-streams': 4.0.1(@cucumber/messages@27.2.0) + '@cucumber/messages': 27.2.0 + commander: 9.1.0 + source-map-support: 0.5.21 + + '@cucumber/gherkin-utils@9.2.0': + dependencies: + '@cucumber/gherkin': 31.0.0 + '@cucumber/messages': 27.2.0 + '@teppeis/multimaps': 3.0.0 + commander: 13.1.0 + source-map-support: 0.5.21 + + '@cucumber/gherkin@30.0.4': + dependencies: + '@cucumber/messages': 26.0.1 + + '@cucumber/gherkin@31.0.0': + dependencies: + '@cucumber/messages': 26.0.1 + + '@cucumber/html-formatter@21.10.1(@cucumber/messages@27.2.0)': + dependencies: + '@cucumber/messages': 27.2.0 + + '@cucumber/junit-xml-formatter@0.7.1(@cucumber/messages@27.2.0)': + dependencies: + '@cucumber/messages': 27.2.0 + '@cucumber/query': 13.6.0(@cucumber/messages@27.2.0) + '@teppeis/multimaps': 3.0.0 + luxon: 3.6.1 + xmlbuilder: 15.1.1 + + '@cucumber/message-streams@4.0.1(@cucumber/messages@27.2.0)': + dependencies: + '@cucumber/messages': 27.2.0 + + '@cucumber/messages@26.0.1': + dependencies: + '@types/uuid': 10.0.0 + class-transformer: 0.5.1 + reflect-metadata: 0.2.2 + uuid: 10.0.0 + + '@cucumber/messages@27.2.0': + dependencies: + '@types/uuid': 10.0.0 + class-transformer: 0.5.1 + reflect-metadata: 0.2.2 + uuid: 11.0.5 + + '@cucumber/query@13.6.0(@cucumber/messages@27.2.0)': + dependencies: + '@cucumber/messages': 27.2.0 + '@teppeis/multimaps': 3.0.0 + lodash.sortby: 4.7.0 + + '@cucumber/tag-expressions@6.1.2': {} + '@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.25.10)': dependencies: esbuild: 0.25.10 @@ -7062,6 +7926,15 @@ snapshots: rollup: 4.21.3 tslib: 2.7.0 + '@rollup/plugin-typescript@11.1.6(rollup@4.21.3)(tslib@2.8.1)(typescript@5.9.3)': + dependencies: + '@rollup/pluginutils': 5.1.0(rollup@4.21.3) + resolve: 1.22.8 + typescript: 5.9.3 + optionalDependencies: + rollup: 4.21.3 + tslib: 2.8.1 + '@rollup/pluginutils@5.1.0(rollup@4.21.3)': dependencies: '@types/estree': 1.0.7 @@ -7488,6 +8361,37 @@ snapshots: '@smithy/types': 4.5.0 tslib: 2.8.1 + '@solana/accounts@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 6.5.0(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -7500,6 +8404,18 @@ snapshots: - typescript - utf-8-validate + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + bigint-buffer: 1.1.5 + bignumber.js: 9.1.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@solana/buffer-layout@4.0.1': dependencies: buffer: 6.0.3 @@ -7526,6 +8442,17 @@ snapshots: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-core@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-core@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-data-structures@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7545,6 +8472,14 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-data-structures@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-numbers@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7573,6 +8508,19 @@ snapshots: '@solana/errors': 2.3.0(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-numbers@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/codecs-numbers@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7595,6 +8543,15 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.2 + '@solana/codecs-strings@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + '@solana/codecs@2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.0.0-preview.4(typescript@5.9.2) @@ -7617,6 +8574,31 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/codecs@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/compat@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/errors@2.0.0-preview.4(typescript@5.9.2)': dependencies: chalk: 5.6.2 @@ -7632,39 +8614,358 @@ snapshots: '@solana/errors@2.3.0(typescript@4.9.5)': dependencies: chalk: 5.6.2 - commander: 14.0.1 + commander: 14.0.3 typescript: 4.9.5 - '@solana/errors@2.3.0(typescript@5.9.2)': + '@solana/errors@2.3.0(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.2 + + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 5.9.3 + + '@solana/errors@6.5.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + optionalDependencies: + typescript: 5.9.3 + + '@solana/fast-stable-stringify@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/functional@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/instruction-plans@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 6.5.0(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/instructions@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@solana/keys@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 6.5.0(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/accounts': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/instruction-plans': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/offchain-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/plugin-core': 6.5.0(typescript@5.9.3) + '@solana/plugin-interfaces': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/program-client-core': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-api': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + + '@solana/nominal-types@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/offchain-messages@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@2.0.0-experimental.8618508': + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + + '@solana/options@2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-preview.4(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-preview.4(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-preview.4(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.0.0-preview.4(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + dependencies: + '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) + '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/options@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/plugin-core@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/plugin-interfaces@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instruction-plans': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/program-client-core@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/accounts': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/instruction-plans': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/plugin-interfaces': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-api': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-api@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-transformers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-spec-types@6.5.0(typescript@5.9.3)': + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-spec@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-subscriptions-api@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-transformers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@6.5.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 6.5.0(typescript@5.9.3) + '@solana/subscribable': 6.5.0(typescript@5.9.3) + ws: 8.20.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@solana/rpc-subscriptions-spec@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/promises': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + '@solana/subscribable': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@solana/rpc-subscriptions@6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/promises': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 6.5.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-subscriptions-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-transformers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + + '@solana/rpc-transformers@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@6.5.0(typescript@5.9.3)': dependencies: - chalk: 5.6.2 - commander: 14.0.1 - typescript: 5.9.2 + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + undici-types: 7.24.6 + optionalDependencies: + typescript: 5.9.3 - '@solana/options@2.0.0-experimental.8618508': + '@solana/rpc-types@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - '@solana/codecs-core': 2.0.0-experimental.8618508 - '@solana/codecs-numbers': 2.0.0-experimental.8618508 + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder - '@solana/options@2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.0.0-preview.4(typescript@5.9.2) - '@solana/codecs-data-structures': 2.0.0-preview.4(typescript@5.9.2) - '@solana/codecs-numbers': 2.0.0-preview.4(typescript@5.9.2) - '@solana/codecs-strings': 2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.0.0-preview.4(typescript@5.9.2) - typescript: 5.9.2 + '@solana/rpc@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/rpc-api': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-spec': 6.5.0(typescript@5.9.3) + '@solana/rpc-spec-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-transformers': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-transport-http': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/options@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.0.0-rc.1(typescript@5.9.2) - '@solana/codecs-data-structures': 2.0.0-rc.1(typescript@5.9.2) - '@solana/codecs-numbers': 2.0.0-rc.1(typescript@5.9.2) - '@solana/codecs-strings': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) - typescript: 5.9.2 + '@solana/signers@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + '@solana/offchain-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 transitivePeerDependencies: - fastestsmallesttextencoderdecoder @@ -7689,6 +8990,18 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': + dependencies: + '@solana/codecs-core': 2.0.0-experimental.8618508 + '@solana/codecs-data-structures': 2.0.0-experimental.8618508 + '@solana/codecs-numbers': 2.0.0-experimental.8618508 + '@solana/codecs-strings': 2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/options': 2.0.0-experimental.8618508 + '@solana/spl-type-length-value': 0.1.0 + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/spl-token-metadata@0.1.5(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7712,6 +9025,20 @@ snapshots: - typescript - utf-8-validate + '@solana/spl-token@0.3.11(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/buffer-layout': 4.0.1 + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10) + buffer: 6.0.3 + transitivePeerDependencies: + - bufferutil + - encoding + - fastestsmallesttextencoderdecoder + - typescript + - utf-8-validate + '@solana/spl-token@0.4.8(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 @@ -7731,6 +9058,79 @@ snapshots: dependencies: buffer: 6.0.3 + '@solana/subscribable@6.5.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 6.5.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@solana/sysvars@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/accounts': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 6.5.0(typescript@5.9.3) + '@solana/rpc': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 6.5.0(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@5.0.10) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - fastestsmallesttextencoderdecoder + - utf-8-validate + + '@solana/transaction-messages@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 6.5.0(typescript@5.9.3) + '@solana/codecs-data-structures': 6.5.0(typescript@5.9.3) + '@solana/codecs-numbers': 6.5.0(typescript@5.9.3) + '@solana/codecs-strings': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 6.5.0(typescript@5.9.3) + '@solana/functional': 6.5.0(typescript@5.9.3) + '@solana/instructions': 6.5.0(typescript@5.9.3) + '@solana/keys': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 6.5.0(typescript@5.9.3) + '@solana/rpc-types': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 @@ -7754,6 +9154,29 @@ snapshots: - typescript - utf-8-validate + '@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': + dependencies: + '@babel/runtime': 7.25.6 + '@noble/curves': 1.4.2 + '@noble/hashes': 1.5.0 + '@solana/buffer-layout': 4.0.1 + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + agentkeepalive: 4.5.0 + bn.js: 5.2.1 + borsh: 0.7.0 + bs58: 4.0.1 + buffer: 6.0.3 + fast-stable-stringify: 1.0.0 + jayson: 4.1.2(bufferutil@4.0.8)(utf-8-validate@5.0.10) + node-fetch: 2.7.0 + rpc-websockets: 9.0.2 + superstruct: 2.0.2 + transitivePeerDependencies: + - bufferutil + - encoding + - typescript + - utf-8-validate + '@solana/web3.js@1.98.4(typescript@4.9.5)': dependencies: '@babel/runtime': 7.25.6 @@ -7787,6 +9210,8 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@teppeis/multimaps@3.0.0': {} + '@tsconfig/node10@1.0.9': {} '@tsconfig/node12@1.0.11': {} @@ -7854,6 +9279,8 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/normalize-package-data@2.4.4': {} + '@types/prop-types@15.7.15': {} '@types/react@18.3.24': @@ -7870,6 +9297,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@types/uuid@8.3.4': {} '@types/uuid@9.0.8': {} @@ -7907,6 +9336,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.44.0(eslint@9.36.0)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/type-utils': 8.44.0(eslint@9.36.0)(typescript@5.9.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.36.0)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.44.0 + eslint: 9.36.0 + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@typescript-eslint/scope-manager': 8.44.0 @@ -7919,6 +9365,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.44.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.44.0(typescript@5.9.2)': dependencies: '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.2) @@ -7928,6 +9386,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.44.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.3) + '@typescript-eslint/types': 8.44.0 + debug: 4.4.3(supports-color@8.1.1) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/scope-manager@7.13.1': dependencies: '@typescript-eslint/types': 7.13.1 @@ -7942,6 +9409,10 @@ snapshots: dependencies: typescript: 5.9.2 + '@typescript-eslint/tsconfig-utils@8.44.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@typescript-eslint/type-utils@8.44.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.44.0 @@ -7954,6 +9425,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.44.0(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.36.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@7.13.1': {} '@typescript-eslint/types@8.44.0': {} @@ -7989,6 +9472,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.44.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.3) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 + debug: 4.4.3(supports-color@8.1.1) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.13.1(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.36.0) @@ -8011,6 +9510,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.44.0(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) + '@typescript-eslint/scope-manager': 8.44.0 + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) + eslint: 9.36.0 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@7.13.1': dependencies: '@typescript-eslint/types': 7.13.1 @@ -8099,6 +9609,8 @@ snapshots: dependencies: environment: 1.1.0 + ansi-regex@4.1.1: {} + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} @@ -8118,6 +9630,8 @@ snapshots: ansis@3.17.0: {} + any-promise@1.3.0: {} + arg@4.1.3: {} argparse@2.0.1: {} @@ -8210,6 +9724,12 @@ snapshots: inherits: 2.0.4 minimalistic-assert: 1.0.1 + assertion-error-formatter@3.0.0: + dependencies: + diff: 4.0.2 + pad-right: 0.2.2 + repeat-string: 1.6.1 + assertion-error@2.0.1: {} async-function@1.0.0: {} @@ -8505,6 +10025,8 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + class-transformer@0.5.1: {} + clean-stack@3.0.1: dependencies: escape-string-regexp: 4.0.0 @@ -8525,6 +10047,12 @@ snapshots: cli-spinners@2.9.2: {} + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + cli-truncate@4.0.0: dependencies: slice-ansi: 5.0.0 @@ -8562,16 +10090,20 @@ snapshots: dependencies: delayed-stream: 1.0.0 + commander@10.0.1: {} + commander@12.1.0: {} commander@13.1.0: {} - commander@14.0.1: {} + commander@14.0.3: {} commander@2.20.3: {} commander@5.1.0: {} + commander@9.1.0: {} + commondir@1.0.1: {} concat-map@0.0.1: {} @@ -8855,6 +10387,10 @@ snapshots: dependencies: is-arrayish: 0.2.1 + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + es-abstract@1.22.2: dependencies: array-buffer-byte-length: 1.0.0 @@ -9117,8 +10653,7 @@ snapshots: escalade@3.2.0: {} - escape-string-regexp@1.0.5: - optional: true + escape-string-regexp@1.0.5: {} escape-string-regexp@2.0.0: {} @@ -9356,6 +10891,10 @@ snapshots: wasmcurves: 0.2.2 web-worker: 1.2.0 + figures@3.2.0: + dependencies: + escape-string-regexp: 1.0.5 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -9378,6 +10917,8 @@ snapshots: transitivePeerDependencies: - supports-color + find-up-simple@1.0.1: {} + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -9588,6 +11129,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + global-dirs@3.0.1: + dependencies: + ini: 2.0.0 + globals@14.0.0: {} globals@15.9.0: {} @@ -9656,6 +11201,10 @@ snapshots: graphemer@1.4.0: {} + has-ansi@4.0.1: + dependencies: + ansi-regex: 4.1.1 + has-bigints@1.0.2: {} has-bigints@1.1.0: {} @@ -9820,6 +11369,8 @@ snapshots: indent-string@5.0.0: {} + index-to-position@1.2.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -9829,13 +11380,15 @@ snapshots: ini@1.3.8: {} + ini@2.0.0: {} + ink@5.0.1(@types/react@18.3.24)(bufferutil@4.0.8)(react@18.3.1)(utf-8-validate@5.0.10): dependencies: '@alcalzone/ansi-tokenize': 0.1.3 ansi-escapes: 7.1.0 ansi-styles: 6.2.1 auto-bind: 5.0.1 - chalk: 5.4.1 + chalk: 5.6.2 cli-boxes: 3.0.0 cli-cursor: 4.0.0 cli-truncate: 4.0.0 @@ -9993,6 +11546,11 @@ snapshots: is-in-ci@0.1.0: {} + is-installed-globally@0.4.0: + dependencies: + global-dirs: 3.0.1 + is-path-inside: 3.0.3 + is-interactive@2.0.0: {} is-map@2.0.3: {} @@ -10209,6 +11767,10 @@ snapshots: kleur@3.0.3: {} + knuth-shuffle-seeded@1.0.6: + dependencies: + seed-random: 2.2.0 + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -10228,6 +11790,10 @@ snapshots: lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.sortby@4.7.0: {} + lodash@4.17.21: {} log-symbols@4.1.0: @@ -10262,6 +11828,8 @@ snapshots: lunr@2.3.9: {} + luxon@3.6.1: {} + magic-string@0.30.11: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -10318,6 +11886,8 @@ snapshots: mime@1.6.0: {} + mime@3.0.0: {} + mimic-fn@2.1.0: {} mimic-function@5.0.1: {} @@ -10360,6 +11930,8 @@ snapshots: dependencies: minimist: 1.2.8 + mkdirp@2.1.6: {} + mkdirp@3.0.1: {} mocha@11.7.5: @@ -10394,6 +11966,12 @@ snapshots: mute-stream@2.0.0: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.8: {} natural-compare@1.4.0: {} @@ -10450,6 +12028,8 @@ snapshots: npm@10.9.3: {} + object-assign@4.1.1: {} + object-hash@3.0.0: {} object-inspect@1.12.3: {} @@ -10596,6 +12176,10 @@ snapshots: package-json-from-dist@1.0.1: {} + pad-right@0.2.2: + dependencies: + repeat-string: 1.6.1 + pako@2.1.0: {} param-case@3.0.4: @@ -10628,6 +12212,12 @@ snapshots: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + parse-json@8.3.0: + dependencies: + '@babel/code-frame': 7.27.1 + index-to-position: 1.2.0 + type-fest: 4.41.0 + pascal-case@3.1.2: dependencies: no-case: 3.0.4 @@ -10728,11 +12318,15 @@ snapshots: process-nextick-args@2.0.1: {} + progress@2.0.3: {} + prompts@2.4.2: dependencies: kleur: 3.0.3 sisteransi: 1.0.5 + property-expr@2.0.6: {} + proto-list@1.2.4: {} proxy-from-env@1.1.0: {} @@ -10781,6 +12375,20 @@ snapshots: dependencies: loose-envify: 1.4.0 + read-package-up@11.0.0: + dependencies: + find-up-simple: 1.0.1 + read-pkg: 9.0.1 + type-fest: 4.41.0 + + read-pkg@9.0.1: + dependencies: + '@types/normalize-package-data': 2.4.4 + normalize-package-data: 6.0.2 + parse-json: 8.3.0 + type-fest: 4.41.0 + unicorn-magic: 0.1.0 + read-yaml-file@2.1.0: dependencies: js-yaml: 4.1.1 @@ -10808,6 +12416,8 @@ snapshots: dependencies: resolve: 1.22.8 + reflect-metadata@0.2.2: {} + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -10821,6 +12431,12 @@ snapshots: regenerator-runtime@0.14.0: {} + regexp-match-indices@1.0.2: + dependencies: + regexp-tree: 0.1.27 + + regexp-tree@0.1.27: {} + regexp.prototype.flags@1.5.1: dependencies: call-bind: 1.0.8 @@ -10847,6 +12463,8 @@ snapshots: dependencies: '@pnpm/npm-conf': 2.3.1 + repeat-string@1.6.1: {} + require-directory@2.1.1: {} requires-port@1.0.0: {} @@ -10919,6 +12537,14 @@ snapshots: optionalDependencies: '@babel/code-frame': 7.24.2 + rollup-plugin-dts@6.1.1(rollup@4.21.3)(typescript@5.9.3): + dependencies: + magic-string: 0.30.11 + rollup: 4.21.3 + typescript: 5.9.3 + optionalDependencies: + '@babel/code-frame': 7.24.2 + rollup-plugin-polyfill-node@0.13.0(rollup@4.21.3): dependencies: '@rollup/plugin-inject': 5.0.5(rollup@4.21.3) @@ -11021,10 +12647,14 @@ snapshots: secure-compare@3.0.1: {} + seed-random@2.2.0: {} + semver@6.3.1: {} semver@7.6.3: {} + semver@7.7.1: {} + semver@7.7.2: {} semver@7.7.3: {} @@ -11192,10 +12822,14 @@ snapshots: stackback@0.0.2: {} + stackframe@1.3.4: {} + std-env@3.7.0: {} stdin-discarder@0.2.2: {} + string-argv@0.3.1: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -11353,10 +12987,20 @@ snapshots: text-encoding-utf-8@1.0.2: {} + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + through@2.3.8: {} tightrope@0.2.0: {} + tiny-case@1.0.3: {} + tiny-jsonc@1.0.2: {} tinybench@2.9.0: {} @@ -11382,6 +13026,8 @@ snapshots: toml@3.0.0: {} + toposort@2.0.2: {} + tr46@0.0.3: {} ts-api-utils@1.3.0(typescript@5.9.2): @@ -11392,6 +13038,10 @@ snapshots: dependencies: typescript: 5.9.2 + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-mocha@10.1.0(mocha@11.7.5): dependencies: mocha: 11.7.5 @@ -11460,8 +13110,12 @@ snapshots: type-fest@0.21.3: {} + type-fest@2.19.0: {} + type-fest@4.35.0: {} + type-fest@4.41.0: {} + typed-array-buffer@1.0.0: dependencies: call-bind: 1.0.8 @@ -11587,6 +13241,10 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.24.6: {} + + unicorn-magic@0.1.0: {} + unicorn-magic@0.3.0: {} union@0.5.0: @@ -11620,8 +13278,14 @@ snapshots: node-gyp-build: 4.6.1 optional: true + util-arity@1.1.0: {} + util-deprecate@1.0.2: {} + uuid@10.0.0: {} + + uuid@11.0.5: {} + uuid@8.3.2: {} uuid@9.0.1: {} @@ -11859,6 +13523,13 @@ snapshots: bufferutil: 4.0.8 utf-8-validate: 5.0.10 + ws@8.20.0(bufferutil@4.0.8)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.8 + utf-8-validate: 5.0.10 + + xmlbuilder@15.1.1: {} + y18n@5.0.8: {} yallist@3.1.1: {} @@ -11897,3 +13568,10 @@ snapshots: yoctocolors-cjs@2.1.3: {} yoga-wasm-web@0.3.3: {} + + yup@1.6.1: + dependencies: + property-expr: 2.0.6 + tiny-case: 1.0.3 + toposort: 2.0.2 + type-fest: 2.19.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a1786cae1..941d03514b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,5 @@ packages: - "sdk-tests/sdk-anchor-test/**" - "js/stateless.js/**" - "js/compressed-token/**" + - "js/token-interface/**" - "examples/**" diff --git a/scripts/format.sh b/scripts/format.sh index 0eb7d645ca..5a4449c473 100755 --- a/scripts/format.sh +++ b/scripts/format.sh @@ -5,6 +5,7 @@ set -e # JS formatting cd js/stateless.js && pnpm format && cd ../.. cd js/compressed-token && pnpm format && cd ../.. +cd js/token-interface && pnpm format && cd ../.. # Rust formatting cargo +nightly fmt --all diff --git a/scripts/lint.sh b/scripts/lint.sh index b3fba61500..8c988572ef 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -8,6 +8,7 @@ RUSTFMT_NIGHTLY_TOOLCHAIN="${RUSTFMT_NIGHTLY_TOOLCHAIN:-nightly-2025-10-26}" # JS linting (use subshells to avoid directory issues) (cd js/stateless.js && pnpm prettier --write . && pnpm lint) (cd js/compressed-token && pnpm prettier --write . && pnpm lint) +(cd js/token-interface && pnpm prettier --write . && pnpm lint) # Rust linting cargo +"$RUSTFMT_NIGHTLY_TOOLCHAIN" fmt --all -- --check