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/token-interface/README.md b/js/token-interface/README.md new file mode 100644 index 0000000000..dc76536c49 --- /dev/null +++ b/js/token-interface/README.md @@ -0,0 +1,109 @@ +# `@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 { buildTransferInstructions } from '@lightprotocol/token-interface/kit'; +``` + +## Canonical for web3.js users + +Use `buildTransferInstructions` from the root export. + +```ts +import { buildTransferInstructions } from '@lightprotocol/token-interface'; + +const instructions = await buildTransferInstructions({ + 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 +``` + +Backwards-compatible alias: + +```ts +import { createTransferInstructions } from '@lightprotocol/token-interface'; +``` + +## 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 { buildTransferInstructionsNowrap } 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 (`buildTransferInstructions`, `createLoadInstructions`, `createApproveInstructions`, `createRevokeInstructions`). +- If sender SPL/T22 balances were wrapped by the flow, source SPL/T22 ATAs are closed afterward. +- Recipient ATA is derived from `(recipient, mint, tokenProgram)`; default is light token program. +- Recipient-side load is still intentionally disabled. \ No newline at end of file diff --git a/js/token-interface/eslint.config.cjs b/js/token-interface/eslint.config.cjs new file mode 100644 index 0000000000..54e0f6819f --- /dev/null +++ b/js/token-interface/eslint.config.cjs @@ -0,0 +1,113 @@ +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', + '**/*.test.ts', + '**/*.spec.ts', + 'vitest.config.ts', + ], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + describe: 'readonly', + it: 'readonly', + expect: 'readonly', + beforeEach: 'readonly', + afterEach: 'readonly', + beforeAll: 'readonly', + afterAll: 'readonly', + jest: 'readonly', + test: '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/package.json b/js/token-interface/package.json new file mode 100644 index 0000000000..41ce8ad06b --- /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", + "eslint-plugin-vitest": "^0.5.4", + "prettier": "^3.3.3", + "rimraf": "^6.0.1", + "rollup": "^4.21.3", + "rollup-plugin-dts": "^6.1.1", + "tslib": "^2.7.0", + "typescript": "^5.6.2", + "vitest": "^2.1.1" + }, + "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 EXCLUDE_E2E=true vitest run tests/unit --reporter=verbose", + "test:e2e:all": "pnpm build:deps:v2 && pnpm test-validator && LIGHT_PROTOCOL_VERSION=V2 vitest run tests/e2e --reporter=verbose --bail=1", + "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..beac18c327 --- /dev/null +++ b/js/token-interface/rollup.config.js @@ -0,0 +1,74 @@ +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..ec2decd3be --- /dev/null +++ b/js/token-interface/src/account.ts @@ -0,0 +1,252 @@ +import { getAssociatedTokenAddress } from './read/associated-token-address'; +import { + parseLightTokenCold, + parseLightTokenHot, +} from './read/get-account'; +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 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, hotInfo as any).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.`, + ); + } +} + +export function createSingleCompressedAccountRpc( + rpc: Rpc, + owner: PublicKey, + mint: PublicKey, + selected: ParsedTokenAccount, +): Rpc { + const filteredRpc = Object.create(rpc) as Rpc; + + filteredRpc.getCompressedTokenAccountsByOwner = async ( + queryOwner, + options, + ) => { + const result = await rpc.getCompressedTokenAccountsByOwner( + queryOwner, + options, + ); + + if (queryOwner.equals(owner) && options?.mint?.equals(mint)) { + return { + ...result, + items: [selected], + }; + } + + return result; + }; + + return filteredRpc; +} diff --git a/js/token-interface/src/constants.ts b/js/token-interface/src/constants.ts new file mode 100644 index 0000000000..d96ec34244 --- /dev/null +++ b/js/token-interface/src/constants.ts @@ -0,0 +1,42 @@ +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 deriveSplPoolPdaWithIndex( + 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..6936c9b74d --- /dev/null +++ b/js/token-interface/src/helpers.ts @@ -0,0 +1,52 @@ +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), + ); +} 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..362ca17e08 --- /dev/null +++ b/js/token-interface/src/instructions/_plan.ts @@ -0,0 +1,22 @@ +import { fromLegacyTransactionInstruction } from '@solana/compat'; +import { + sequentialInstructionPlan, + type InstructionPlan, +} from '@solana/instruction-plans'; +import type { TransactionInstruction } from '@solana/web3.js'; + +export type KitInstruction = ReturnType; + +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..e507bc8160 --- /dev/null +++ b/js/token-interface/src/instructions/approve.ts @@ -0,0 +1,126 @@ +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { assertAccountNotFrozen, getAta } from '../account'; +import type { + CreateApproveInstructionsInput, + CreateRawApproveInstructionInput, +} from '../types'; +import { buildLoadInstructionList } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_APPROVE_DISCRIMINATOR = 4; + +function toBigIntAmount(amount: number | bigint): bigint { + return BigInt(amount.toString()); +} + +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 account = await getAta({ + rpc, + owner, + mint, + }); + + assertAccountNotFrozen(account, 'approve'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: true, + })), + createApproveInstruction({ + tokenAccount: account.address, + delegate, + owner, + amount: toBigIntAmount(amount), + payer, + }), + ]; +} + +export async function createApproveInstructionsNowrap({ + rpc, + payer, + owner, + mint, + delegate, + amount, +}: CreateApproveInstructionsInput): Promise { + const account = await getAta({ + rpc, + owner, + mint, + }); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: false, + })), + createApproveInstruction({ + tokenAccount: account.address, + 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..982433f55a --- /dev/null +++ b/js/token-interface/src/instructions/ata.ts @@ -0,0 +1,416 @@ +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 buffer = Buffer.alloc(2000); + const len = CreateAssociatedTokenAccountInstructionDataLayout.encode( + { + compressibleConfig: params.compressibleConfig || null, + }, + buffer, + ); + + const discriminator = idempotent + ? CREATE_ASSOCIATED_TOKEN_ACCOUNT_IDEMPOTENT_DISCRIMINATOR + : CREATE_ASSOCIATED_TOKEN_ACCOUNT_DISCRIMINATOR; + + return Buffer.concat([discriminator, buffer.subarray(0, len)]); +} + +export interface CreateAssociatedLightTokenAccountInstructionParams { + feePayer: PublicKey; + owner: PublicKey; + mint: PublicKey; + compressibleConfig?: CompressibleConfig; + configAccount?: PublicKey; + rentPayerPda?: PublicKey; +} + +/** + * Create instruction for creating an associated light-token account. + * Uses the default rent sponsor PDA by default. + * + * @param feePayer Fee payer public key. + * @param owner Owner of the associated token account. + * @param mint Mint address. + * @param compressibleConfig Compressible configuration (defaults to rent sponsor config). + * @param configAccount Config account (defaults to LIGHT_TOKEN_CONFIG). + * @param rentPayerPda Rent payer PDA (defaults to LIGHT_TOKEN_RENT_SPONSOR). + */ +export function createAssociatedLightTokenAccountInstruction( + feePayer: PublicKey, + owner: PublicKey, + mint: PublicKey, + compressibleConfig: CompressibleConfig | null = DEFAULT_COMPRESSIBLE_CONFIG, + configAccount: PublicKey = LIGHT_TOKEN_CONFIG, + rentPayerPda: PublicKey = LIGHT_TOKEN_RENT_SPONSOR, +): TransactionInstruction { + 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: feePayer, 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 feePayer Fee payer public key. + * @param owner Owner of the associated token account. + * @param mint Mint address. + * @param compressibleConfig Compressible configuration (defaults to rent sponsor config). + * @param configAccount Config account (defaults to LIGHT_TOKEN_CONFIG). + * @param rentPayerPda Rent payer PDA (defaults to LIGHT_TOKEN_RENT_SPONSOR). + */ +export function createAssociatedLightTokenAccountIdempotentInstruction( + feePayer: PublicKey, + owner: PublicKey, + mint: PublicKey, + compressibleConfig: CompressibleConfig | null = DEFAULT_COMPRESSIBLE_CONFIG, + configAccount: PublicKey = LIGHT_TOKEN_CONFIG, + rentPayerPda: PublicKey = LIGHT_TOKEN_RENT_SPONSOR, +): TransactionInstruction { + 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: feePayer, 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; +} + +/** + * Create instruction for creating an associated token account (SPL, Token-2022, + * or light-token). Follows SPL Token API signature with optional light-token config at the + * end. + * + * @param payer Fee payer public key. + * @param associatedToken Associated token account address. + * @param owner Owner of the associated token account. + * @param mint Mint address. + * @param programId Token program ID (default: TOKEN_PROGRAM_ID). + * @param associatedTokenProgramId Associated token program ID. + * @param lightTokenConfig Optional light-token-specific configuration. + */ +function createAssociatedTokenAccountInstruction( + payer: PublicKey, + associatedToken: PublicKey, + owner: PublicKey, + mint: PublicKey, + programId: PublicKey = TOKEN_PROGRAM_ID, + associatedTokenProgramId?: PublicKey, + lightTokenConfig?: LightTokenConfig, +): TransactionInstruction { + const effectiveAssociatedTokenProgramId = + associatedTokenProgramId ?? getAtaProgramId(programId); + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return createAssociatedLightTokenAccountInstruction( + payer, + owner, + mint, + lightTokenConfig?.compressibleConfig, + lightTokenConfig?.configAccount, + lightTokenConfig?.rentPayerPda, + ); + } else { + return createSplAssociatedTokenAccountInstruction( + payer, + associatedToken, + owner, + mint, + programId, + effectiveAssociatedTokenProgramId, + ); + } +} + +/** + * Create idempotent instruction for creating an associated token account (SPL, + * Token-2022, or light-token). Follows SPL Token API signature with optional light-token + * config at the end. + * + * @param payer Fee payer public key. + * @param associatedToken Associated token account address. + * @param owner Owner of the associated token account. + * @param mint Mint address. + * @param programId Token program ID (default: TOKEN_PROGRAM_ID). + * @param associatedTokenProgramId Associated token program ID. + * @param lightTokenConfig Optional light-token-specific configuration. + */ +function createAssociatedTokenAccountIdempotentInstruction( + payer: PublicKey, + associatedToken: PublicKey, + owner: PublicKey, + mint: PublicKey, + programId: PublicKey = TOKEN_PROGRAM_ID, + associatedTokenProgramId?: PublicKey, + lightTokenConfig?: LightTokenConfig, +): TransactionInstruction { + const effectiveAssociatedTokenProgramId = + associatedTokenProgramId ?? getAtaProgramId(programId); + + if (programId.equals(LIGHT_TOKEN_PROGRAM_ID)) { + return createAssociatedLightTokenAccountIdempotentInstruction( + payer, + owner, + mint, + lightTokenConfig?.compressibleConfig, + lightTokenConfig?.configAccount, + lightTokenConfig?.rentPayerPda, + ); + } else { + return createSplAssociatedTokenAccountIdempotentInstruction( + payer, + 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, + 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..7f038882b0 --- /dev/null +++ b/js/token-interface/src/instructions/burn.ts @@ -0,0 +1,184 @@ +import { Buffer } from 'buffer'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { assertAccountNotFrozen, getAta } from '../account'; +import type { + CreateBurnInstructionsInput, + CreateRawBurnCheckedInstructionInput, + CreateRawBurnInstructionInput, +} from '../types'; +import { buildLoadInstructionList } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_BURN_DISCRIMINATOR = 8; +const LIGHT_TOKEN_BURN_CHECKED_DISCRIMINATOR = 15; + +function toBigIntAmount(amount: number | bigint): bigint { + return BigInt(amount.toString()); +} + +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 account = await getAta({ rpc, owner, mint }); + + assertAccountNotFrozen(account, 'burn'); + + const amountBn = toBigIntAmount(amount); + const burnIx = + decimals !== undefined + ? createBurnCheckedInstruction({ + source: account.address, + mint, + authority, + amount: amountBn, + decimals, + payer, + }) + : createBurnInstruction({ + source: account.address, + mint, + authority, + amount: amountBn, + payer, + }); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: true, + })), + burnIx, + ]; +} + +export async function createBurnInstructionsNowrap({ + rpc, + payer, + owner, + mint, + authority, + amount, + decimals, +}: CreateBurnInstructionsInput): Promise { + const account = await getAta({ rpc, owner, mint }); + + assertAccountNotFrozen(account, 'burn'); + + const amountBn = toBigIntAmount(amount); + const burnIx = + decimals !== undefined + ? createBurnCheckedInstruction({ + source: account.address, + mint, + authority, + amount: amountBn, + decimals, + payer, + }) + : createBurnInstruction({ + source: account.address, + mint, + authority, + amount: amountBn, + payer, + }); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + 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..ef33ac6ae8 --- /dev/null +++ b/js/token-interface/src/instructions/freeze.ts @@ -0,0 +1,93 @@ +import { TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + assertAccountNotFrozen, + getAta, +} from '../account'; +import type { + CreateFreezeInstructionsInput, + CreateRawFreezeInstructionInput, +} from '../types'; +import { buildLoadInstructionList } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_FREEZE_ACCOUNT_DISCRIMINATOR = Buffer.from([10]); + +export function createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, +}: CreateRawFreezeInstructionInput): TransactionInstruction { + 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: LIGHT_TOKEN_FREEZE_ACCOUNT_DISCRIMINATOR, + }); +} + +export async function createFreezeInstructions({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateFreezeInstructionsInput): Promise { + const account = await getAta({ rpc, owner, mint }); + + assertAccountNotFrozen(account, 'freeze'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: true, + })), + createFreezeInstruction({ + tokenAccount: account.address, + mint, + freezeAuthority, + }), + ]; +} + +export async function createFreezeInstructionsNowrap({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateFreezeInstructionsInput): Promise { + const account = await getAta({ rpc, owner, mint }); + + assertAccountNotFrozen(account, 'freeze'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: false, + })), + createFreezeInstruction({ + tokenAccount: account.address, + 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..cdc63f66da --- /dev/null +++ b/js/token-interface/src/instructions/load.ts @@ -0,0 +1,1220 @@ +import { + Rpc, + LIGHT_TOKEN_PROGRAM_ID, + ParsedTokenAccount, + bn, + assertV2Enabled, + LightSystemProgram, + defaultStaticAccountsStruct, + ValidityProofWithContext, +} from '@lightprotocol/stateless.js'; +import { + ComputeBudgetProgram, + PublicKey, + TransactionInstruction, + SystemProgram, +} from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + TOKEN_2022_PROGRAM_ID, + getAssociatedTokenAddressSync, + createAssociatedTokenAccountIdempotentInstruction, + TokenAccountNotFoundError, +} from '@solana/spl-token'; +import { Buffer } from 'buffer'; +import { + AccountView, + checkNotFrozen, + COLD_SOURCE_TYPES, + getAtaView as _getAtaView, + TokenAccountSource, + isAuthorityForAccount, + filterAccountForAuthority, +} from '../read/get-account'; +import { getAssociatedTokenAddress } from '../read/associated-token-address'; +import { createAtaIdempotent } from './ata'; +import { createWrapInstruction } from './wrap'; +import { getSplPoolInfos, type SplPoolInfo } from '../spl-interface'; +import { getAtaProgramId, checkAtaAddress, AtaType } from '../read/ata-utils'; +import type { LoadOptions } from '../load-options'; +import { getMint } from '../read/get-mint'; +import { + COMPRESSED_TOKEN_PROGRAM_ID, + deriveCpiAuthorityPda, + MAX_TOP_UP, + TokenDataVersion, +} from '../constants'; +import { + encodeTransfer2InstructionData, + type Transfer2InstructionData, + type MultiInputTokenDataWithContext, + COMPRESSION_MODE_DECOMPRESS, + type Compression, + type Transfer2ExtensionData, +} from './layout/layout-transfer2'; +import { createSingleCompressedAccountRpc, getAtaOrNull } from '../account'; +import { normalizeInstructionBatches, toLoadOptions } from '../helpers'; +import { getAtaAddress } from '../read'; +import type { + CreateLoadInstructionsInput, + TokenInterfaceAccount, + CreateTransferInstructionsInput, +} from '../types'; +import { toInstructionPlan } from './_plan'; + +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 createLoadAtaInstructions instead. + * + * Supports decompressing to both light-token accounts and SPL token accounts: + * - For light-token destinations: No splPoolInfo needed + * - For SPL destinations: Provide splPoolInfo and decimals + * + * @param payer Fee payer public key + * @param inputCompressedTokenAccounts Input light-token accounts + * @param toAddress Destination token account address (light-token or SPL associated token account) + * @param amount Amount to decompress + * @param validityProof Validity proof (contains compressedProof and rootIndices) + * @param splPoolInfo Optional: SPL pool info for SPL destinations + * @param decimals Mint decimals (required for SPL destinations) + * @param maxTopUp Optional cap on rent top-up (units of 1k lamports; default no cap) + * @param authority Optional signer (owner or delegate). When omitted, owner is the signer. + * @returns TransactionInstruction + */ +export function createDecompressInstruction( + payer: PublicKey, + inputCompressedTokenAccounts: ParsedTokenAccount[], + toAddress: PublicKey, + amount: bigint, + validityProof: ValidityProofWithContext, + splPoolInfo: SplPoolInfo | undefined, + 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; + + // 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 (splPoolInfo) { + // Add SPL interface PDA (token pool) + poolAccountIndex = packedAccounts.length; + packedAccountIndices.set( + splPoolInfo.splPoolPda.toBase58(), + poolAccountIndex, + ); + packedAccounts.push(splPoolInfo.splPoolPda); + + // Add SPL token program + tokenProgramIndex = packedAccounts.length; + packedAccountIndices.set( + splPoolInfo.tokenProgram.toBase58(), + tokenProgramIndex, + ); + packedAccounts.push(splPoolInfo.tokenProgram); + + poolIndex = splPoolInfo.poolIndex; + poolBump = splPoolInfo.bump; + } + + // 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), + ); + 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: splPoolInfo ? poolAccountIndex : 0, + poolIndex: splPoolInfo ? poolIndex : 0, + bump: splPoolInfo ? 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 keys = [ + // 0: light_system_program (non-mutable) + { + pubkey: LightSystemProgram.programId, + isSigner: false, + isWritable: false, + }, + // 1: fee_payer (signer, mutable) + { pubkey: payer, 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 = + splPoolInfo !== undefined && pubkey.equals(splPoolInfo.splPoolPda); + return { + pubkey, + isSigner: i === signerIndex, + isWritable: isTreeOrQueue || isDestination || isPool, + }; + }), + ]; + + return new TransactionInstruction({ + programId: COMPRESSED_TOKEN_PROGRAM_ID, + keys, + data, + }); +} + +const MAX_INPUT_ACCOUNTS = 8; + +function chunkArray(array: T[], chunkSize: number): T[][] { + const chunks: T[][] = []; + for (let i = 0; i < array.length; i += chunkSize) { + chunks.push(array.slice(i, i + chunkSize)); + } + return chunks; +} + +function selectInputsForAmount( + accounts: ParsedTokenAccount[], + neededAmount: bigint, +): ParsedTokenAccount[] { + if (accounts.length === 0 || neededAmount <= BigInt(0)) return []; + + const sorted = [...accounts].sort((a, b) => { + const amtA = BigInt(a.parsed.amount.toString()); + const amtB = BigInt(b.parsed.amount.toString()); + if (amtB > amtA) return 1; + if (amtB < amtA) return -1; + return 0; + }); + + let accumulated = BigInt(0); + let countNeeded = 0; + for (const acc of sorted) { + countNeeded++; + accumulated += BigInt(acc.parsed.amount.toString()); + if (accumulated >= neededAmount) break; + } + + const selectCount = Math.min( + Math.max(countNeeded, MAX_INPUT_ACCOUNTS), + sorted.length, + ); + + return sorted.slice(0, selectCount); +} + +function assertUniqueInputHashes(chunks: ParsedTokenAccount[][]): void { + const seen = new Set(); + for (const chunk of chunks) { + for (const acc of chunk) { + const hashStr = acc.compressedAccount.hash.toString(); + if (seen.has(hashStr)) { + throw new Error( + `Duplicate compressed account hash across chunks: ${hashStr}. ` + + `Each compressed account must appear in exactly one chunk.`, + ); + } + seen.add(hashStr); + } + } +} + +function getCompressedTokenAccountsFromAtaSources( + sources: TokenAccountSource[], +): ParsedTokenAccount[] { + return sources + .filter((source) => source.loadContext !== undefined) + .filter((source) => COLD_SOURCE_TYPES.has(source.type)) + .map((source) => { + const fullData = source.accountInfo.data; + const discriminatorBytes = fullData.subarray( + 0, + Math.min(8, fullData.length), + ); + const accountDataBytes = + fullData.length > 8 ? fullData.subarray(8) : Buffer.alloc(0); + + const compressedAccount = { + treeInfo: source.loadContext!.treeInfo, + hash: source.loadContext!.hash, + leafIndex: source.loadContext!.leafIndex, + proveByIndex: source.loadContext!.proveByIndex, + owner: source.accountInfo.owner, + lamports: bn(source.accountInfo.lamports), + address: null, + data: + fullData.length === 0 + ? null + : { + discriminator: Array.from(discriminatorBytes), + data: Buffer.from(accountDataBytes), + dataHash: new Array(32).fill(0), + }, + readOnly: false, + }; + + const state = !source.parsed.isInitialized + ? 0 + : source.parsed.isFrozen + ? 2 + : 1; + + return { + compressedAccount: compressedAccount as any, + parsed: { + mint: source.parsed.mint, + owner: source.parsed.owner, + amount: bn(source.parsed.amount.toString()), + delegate: source.parsed.delegate, + state, + tlv: source.parsed.tlvData.length > 0 ? source.parsed.tlvData : null, + }, + } satisfies ParsedTokenAccount; + }); +} + +export async function createLoadAtaInstructionsInner( + rpc: Rpc, + ata: PublicKey, + owner: PublicKey, + mint: PublicKey, + decimals: number, + payer?: PublicKey, + loadOptions?: LoadOptions, +): Promise { + assertV2Enabled(); + payer ??= owner; + const wrap = loadOptions?.wrap ?? false; + + const effectiveOwner = owner; + const authorityPubkey = loadOptions?.delegatePubkey ?? owner; + + let accountView: AccountView; + try { + accountView = await _getAtaView( + rpc, + ata, + effectiveOwner, + mint, + undefined, + undefined, + wrap, + ); + } catch (e) { + if (e instanceof TokenAccountNotFoundError) { + return []; + } + throw e; + } + + const isDelegate = !effectiveOwner.equals(authorityPubkey); + if (isDelegate) { + if (!isAuthorityForAccount(accountView, authorityPubkey)) { + throw new Error("Signer is not the owner or a delegate of the account."); + } + accountView = filterAccountForAuthority(accountView, authorityPubkey); + } + + const internalBatches = await _buildLoadBatches( + rpc, + payer, + accountView, + loadOptions, + wrap, + ata, + undefined, + authorityPubkey, + decimals, + ); + + return internalBatches.map((batch) => [ + ComputeBudgetProgram.setComputeUnitLimit({ + units: calculateLoadBatchComputeUnits(batch), + }), + ...batch.instructions, + ]); +} + +interface InternalLoadBatch { + instructions: TransactionInstruction[]; + compressedAccounts: ParsedTokenAccount[]; + wrapCount: number; + hasAtaCreation: boolean; +} + +const CU_ATA_CREATION = 30_000; +const CU_WRAP = 50_000; +const CU_DECOMPRESS_BASE = 50_000; +const CU_FULL_PROOF = 100_000; +const CU_PER_ACCOUNT_PROVE_BY_INDEX = 10_000; +const CU_PER_ACCOUNT_FULL_PROOF = 30_000; +const CU_BUFFER_FACTOR = 1.3; +const CU_MIN = 50_000; +const CU_MAX = 1_400_000; + +function rawLoadBatchComputeUnits(batch: InternalLoadBatch): number { + let cu = 0; + if (batch.hasAtaCreation) cu += CU_ATA_CREATION; + cu += batch.wrapCount * CU_WRAP; + if (batch.compressedAccounts.length > 0) { + cu += CU_DECOMPRESS_BASE; + const needsFullProof = batch.compressedAccounts.some( + (acc) => !(acc.compressedAccount.proveByIndex ?? false), + ); + if (needsFullProof) cu += CU_FULL_PROOF; + for (const acc of batch.compressedAccounts) { + cu += + (acc.compressedAccount.proveByIndex ?? false) + ? CU_PER_ACCOUNT_PROVE_BY_INDEX + : CU_PER_ACCOUNT_FULL_PROOF; + } + } + return cu; +} + +function calculateLoadBatchComputeUnits(batch: InternalLoadBatch): number { + const cu = Math.ceil(rawLoadBatchComputeUnits(batch) * CU_BUFFER_FACTOR); + return Math.max(CU_MIN, Math.min(CU_MAX, cu)); +} + +async function _buildLoadBatches( + rpc: Rpc, + payer: PublicKey, + ata: AccountView, + options: LoadOptions | undefined, + wrap: boolean, + targetAta: PublicKey, + targetAmount: bigint | undefined, + authority: PublicKey | undefined, + decimals: number, +): Promise { + if (!ata._isAta || !ata._owner || !ata._mint) { + throw new Error( + "AccountView must be from getAtaView (requires _isAta, _owner, _mint)", + ); + } + + checkNotFrozen(ata, "load"); + + const owner = ata._owner; + const mint = ata._mint; + const sources = ata._sources ?? []; + + const allCompressedAccounts = + getCompressedTokenAccountsFromAtaSources(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 coldSources = sources.filter((s) => COLD_SOURCE_TYPES.has(s.type)); + + const splBalance = splSource?.amount ?? BigInt(0); + const t22Balance = t22Source?.amount ?? BigInt(0); + const coldBalance = coldSources.reduce((sum, s) => sum + s.amount, BigInt(0)); + + if ( + splBalance === BigInt(0) && + t22Balance === BigInt(0) && + coldBalance === BigInt(0) + ) { + return []; + } + + let splPoolInfo: SplPoolInfo | undefined; + const needsSplInfo = + wrap || + ataType === "spl" || + ataType === "token2022" || + splBalance > BigInt(0) || + t22Balance > BigInt(0); + if (needsSplInfo) { + try { + const splPoolInfos = + options?.splPoolInfos ?? (await getSplPoolInfos(rpc, mint)); + splPoolInfo = splPoolInfos.find( + (info: SplPoolInfo) => info.isInitialized, + ); + } catch (e) { + if (splBalance > BigInt(0) || t22Balance > BigInt(0)) { + throw e; + } + } + } + + const setupInstructions: TransactionInstruction[] = []; + let wrapCount = 0; + let needsAtaCreation = false; + + let decompressTarget: PublicKey = lightTokenAtaAddress; + let decompressSplInfo: SplPoolInfo | undefined; + let canDecompress = false; + + if (wrap) { + decompressTarget = lightTokenAtaAddress; + decompressSplInfo = undefined; + canDecompress = true; + + if (!lightTokenHotSource) { + needsAtaCreation = true; + setupInstructions.push( + createAtaIdempotent( + payer, + lightTokenAtaAddress, + owner, + mint, + LIGHT_TOKEN_PROGRAM_ID, + ), + ); + } + + if (splBalance > BigInt(0) && splPoolInfo) { + setupInstructions.push( + createWrapInstruction( + splAta, + lightTokenAtaAddress, + owner, + mint, + splBalance, + splPoolInfo, + decimals, + payer, + ), + ); + wrapCount++; + } + + if (t22Balance > BigInt(0) && splPoolInfo) { + setupInstructions.push( + createWrapInstruction( + t22Ata, + lightTokenAtaAddress, + owner, + mint, + t22Balance, + splPoolInfo, + decimals, + payer, + ), + ); + wrapCount++; + } + } else { + if (ataType === "light-token") { + decompressTarget = lightTokenAtaAddress; + decompressSplInfo = undefined; + canDecompress = true; + if (!lightTokenHotSource) { + needsAtaCreation = true; + setupInstructions.push( + createAtaIdempotent( + payer, + lightTokenAtaAddress, + owner, + mint, + LIGHT_TOKEN_PROGRAM_ID, + ), + ); + } + } else if (ataType === "spl" && splPoolInfo) { + decompressTarget = splAta; + decompressSplInfo = splPoolInfo; + canDecompress = true; + if (!splSource) { + needsAtaCreation = true; + setupInstructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + splAta, + owner, + mint, + TOKEN_PROGRAM_ID, + ), + ); + } + } else if (ataType === "token2022" && splPoolInfo) { + decompressTarget = t22Ata; + decompressSplInfo = splPoolInfo; + canDecompress = true; + if (!t22Source) { + needsAtaCreation = true; + setupInstructions.push( + createAssociatedTokenAccountIdempotentInstruction( + payer, + t22Ata, + owner, + mint, + TOKEN_2022_PROGRAM_ID, + ), + ); + } + } + } + + let accountsToLoad = allCompressedAccounts; + + if ( + targetAmount !== undefined && + canDecompress && + allCompressedAccounts.length > 0 + ) { + 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)) { + accountsToLoad = []; + } else { + accountsToLoad = selectInputsForAmount( + allCompressedAccounts, + neededFromCold, + ); + } + } + + if (!canDecompress || accountsToLoad.length === 0) { + if (setupInstructions.length === 0) return []; + return [ + { + instructions: setupInstructions, + compressedAccounts: [], + wrapCount, + hasAtaCreation: needsAtaCreation, + }, + ]; + } + + const chunks = chunkArray(accountsToLoad, MAX_INPUT_ACCOUNTS); + assertUniqueInputHashes(chunks); + + const proofs = await Promise.all( + chunks.map(async (chunk) => { + const proofInputs = chunk.map((acc) => ({ + hash: acc.compressedAccount.hash, + tree: acc.compressedAccount.treeInfo.tree, + queue: acc.compressedAccount.treeInfo.queue, + })); + return rpc.getValidityProofV0(proofInputs); + }), + ); + + const idempotentAtaIx = (() => { + if (wrap || ataType === "light-token") { + return createAtaIdempotent( + payer, + lightTokenAtaAddress, + owner, + mint, + LIGHT_TOKEN_PROGRAM_ID, + ); + } else if (ataType === "spl") { + return createAssociatedTokenAccountIdempotentInstruction( + payer, + splAta, + owner, + mint, + TOKEN_PROGRAM_ID, + ); + } else { + return createAssociatedTokenAccountIdempotentInstruction( + payer, + t22Ata, + owner, + mint, + TOKEN_2022_PROGRAM_ID, + ); + } + })(); + + const batches: InternalLoadBatch[] = []; + + for (let i = 0; i < chunks.length; i++) { + const chunk = chunks[i]; + const proof = proofs[i]; + const chunkAmount = chunk.reduce( + (sum, acc) => sum + BigInt(acc.parsed.amount.toString()), + BigInt(0), + ); + + const batchIxs: TransactionInstruction[] = []; + let batchWrapCount = 0; + let batchHasAtaCreation = false; + + if (i === 0) { + batchIxs.push(...setupInstructions); + batchWrapCount = wrapCount; + batchHasAtaCreation = needsAtaCreation; + } else { + batchIxs.push(idempotentAtaIx); + batchHasAtaCreation = true; + } + + const authorityForDecompress = authority ?? owner; + batchIxs.push( + createDecompressInstruction( + payer, + chunk, + decompressTarget, + chunkAmount, + proof, + decompressSplInfo, + decimals, + undefined, + authorityForDecompress, + ), + ); + + batches.push({ + instructions: batchIxs, + compressedAccounts: chunk, + wrapCount: batchWrapCount, + hasAtaCreation: batchHasAtaCreation, + }); + } + + return batches; +} + +export async function createLoadAtaInstructions( + rpc: Rpc, + ata: PublicKey, + owner: PublicKey, + mint: PublicKey, + payer?: PublicKey, + loadOptions?: LoadOptions, +): Promise { + const mintInfo = await getMint(rpc, mint); + return createLoadAtaInstructionsInner( + rpc, + ata, + owner, + mint, + mintInfo.mint.decimals, + payer, + loadOptions, + ); +} + +interface CreateLoadInstructionInternalInput extends CreateLoadInstructionsInput { + authority?: PublicKey; + account?: TokenInterfaceAccount | null; + wrap?: boolean; +} + +export async function createLoadInstructionInternal({ + rpc, + payer, + owner, + mint, + authority, + account, + wrap = false, +}: CreateLoadInstructionInternalInput): Promise<{ + instructions: TransactionInstruction[]; +} | null> { + const resolvedAccount = + account ?? + (await getAtaOrNull({ + rpc, + owner, + mint, + })); + const targetAta = getAtaAddress({ owner, mint }); + + const effectiveRpc = + resolvedAccount && resolvedAccount.compressedAccount + ? createSingleCompressedAccountRpc( + rpc, + owner, + mint, + resolvedAccount.compressedAccount, + ) + : rpc; + const instructions = normalizeInstructionBatches( + 'createLoadInstruction', + await createLoadAtaInstructions( + effectiveRpc, + targetAta, + owner, + mint, + payer, + toLoadOptions(owner, authority, wrap), + ), + ); + + if (instructions.length === 0) { + return null; + } + + return { + instructions, + }; +} + +export async function buildLoadInstructionList( + input: CreateLoadInstructionsInput & { + authority?: CreateTransferInstructionsInput['authority']; + account?: TokenInterfaceAccount | null; + wrap?: boolean; + }, +): Promise { + const load = await createLoadInstructionInternal(input); + + if (!load) { + return []; + } + + return load.instructions; +} + +export async function createLoadInstruction({ + rpc, + payer, + owner, + mint, +}: CreateLoadInstructionsInput): Promise { + const load = await createLoadInstructionInternal({ + rpc, + payer, + owner, + mint, + }); + + return load?.instructions[load.instructions.length - 1] ?? null; +} + +export async function createLoadInstructions({ + rpc, + payer, + owner, + mint, +}: CreateLoadInstructionsInput): Promise { + return buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + wrap: true, + }); +} + +export async function createLoadInstructionPlan( + input: CreateLoadInstructionsInput, +) { + return toInstructionPlan(await createLoadInstructions(input)); +} diff --git a/js/token-interface/src/instructions/revoke.ts b/js/token-interface/src/instructions/revoke.ts new file mode 100644 index 0000000000..198ba7f3ac --- /dev/null +++ b/js/token-interface/src/instructions/revoke.ts @@ -0,0 +1,107 @@ +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { assertAccountNotFrozen, getAta } from '../account'; +import type { + CreateRawRevokeInstructionInput, + CreateRevokeInstructionsInput, +} from '../types'; +import { buildLoadInstructionList } 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 account = await getAta({ + rpc, + owner, + mint, + }); + + assertAccountNotFrozen(account, 'revoke'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: true, + })), + createRevokeInstruction({ + tokenAccount: account.address, + owner, + payer, + }), + ]; +} + +export async function createRevokeInstructionsNowrap({ + rpc, + payer, + owner, + mint, +}: CreateRevokeInstructionsInput): Promise { + const account = await getAta({ + rpc, + owner, + mint, + }); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: false, + })), + createRevokeInstruction({ + tokenAccount: account.address, + 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..ae90ba367f --- /dev/null +++ b/js/token-interface/src/instructions/thaw.ts @@ -0,0 +1,93 @@ +import { TransactionInstruction } from '@solana/web3.js'; +import { Buffer } from 'buffer'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { + assertAccountFrozen, + getAta, +} from '../account'; +import type { + CreateRawThawInstructionInput, + CreateThawInstructionsInput, +} from '../types'; +import { buildLoadInstructionList } from './load'; +import { toInstructionPlan } from './_plan'; + +const LIGHT_TOKEN_THAW_ACCOUNT_DISCRIMINATOR = Buffer.from([11]); + +export function createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, +}: CreateRawThawInstructionInput): TransactionInstruction { + 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: LIGHT_TOKEN_THAW_ACCOUNT_DISCRIMINATOR, + }); +} + +export async function createThawInstructions({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateThawInstructionsInput): Promise { + const account = await getAta({ rpc, owner, mint }); + + assertAccountFrozen(account, 'thaw'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: true, + })), + createThawInstruction({ + tokenAccount: account.address, + mint, + freezeAuthority, + }), + ]; +} + +export async function createThawInstructionsNowrap({ + rpc, + payer, + owner, + mint, + freezeAuthority, +}: CreateThawInstructionsInput): Promise { + const account = await getAta({ rpc, owner, mint }); + + assertAccountFrozen(account, 'thaw'); + + return [ + ...(await buildLoadInstructionList({ + rpc, + payer, + owner, + mint, + account, + wrap: false, + })), + createThawInstruction({ + tokenAccount: account.address, + 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..74fa211004 --- /dev/null +++ b/js/token-interface/src/instructions/transfer.ts @@ -0,0 +1,277 @@ +import { Buffer } from 'buffer'; +import { SystemProgram, TransactionInstruction } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { getSplPoolInfos } from '../spl-interface'; +import { createUnwrapInstruction } from './unwrap'; +import { + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, + createCloseAccountInstruction, + unpackAccount, +} from '@solana/spl-token'; +import { getMintDecimals } from '../helpers'; +import { getAtaAddress } from '../read'; +import type { + CreateRawTransferInstructionInput, + CreateTransferInstructionsInput, +} from '../types'; +import { buildLoadInstructionList } from './load'; +import { toInstructionPlan } from './_plan'; +import { createAtaInstruction } from './ata'; + +const ZERO = BigInt(0); + +const LIGHT_TOKEN_TRANSFER_CHECKED_DISCRIMINATOR = 12; + +function toBigIntAmount(amount: number | bigint): bigint { + return BigInt(amount.toString()); +} + +async function getDerivedAtaBalance( + rpc: CreateTransferInstructionsInput['rpc'], + owner: CreateTransferInstructionsInput['sourceOwner'], + mint: CreateTransferInstructionsInput['mint'], + programId: typeof TOKEN_PROGRAM_ID | typeof TOKEN_2022_PROGRAM_ID, +): Promise { + const ata = getAtaAddress({ owner, mint, programId }); + const info = await rpc.getAccountInfo(ata); + if (!info || !info.owner.equals(programId)) { + return ZERO; + } + + return unpackAccount(ata, info, programId).amount; +} + +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); + + 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: payer.equals(authority) }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + { + pubkey: payer, + isSigner: !payer.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 buildTransferInstructions({ + rpc, + payer, + mint, + sourceOwner, + authority, + recipient, + tokenProgram, + amount, +}: CreateTransferInstructionsInput): Promise { + const amountBigInt = toBigIntAmount(amount); + const senderLoadInstructions = await buildLoadInstructionList({ + rpc, + payer, + owner: sourceOwner, + mint, + authority, + wrap: true, + }); + const recipientTokenProgramId = tokenProgram ?? LIGHT_TOKEN_PROGRAM_ID; + const recipientAta = getAtaAddress({ + owner: recipient, + mint, + programId: recipientTokenProgramId, + }); + const decimals = await getMintDecimals(rpc, mint); + const [senderSplBalance, senderT22Balance] = await Promise.all([ + getDerivedAtaBalance(rpc, sourceOwner, mint, TOKEN_PROGRAM_ID), + getDerivedAtaBalance(rpc, sourceOwner, mint, TOKEN_2022_PROGRAM_ID), + ]); + + const closeWrappedSourceInstructions: TransactionInstruction[] = []; + if (authority.equals(sourceOwner) && senderSplBalance > ZERO) { + closeWrappedSourceInstructions.push( + createCloseAccountInstruction( + getAtaAddress({ + owner: sourceOwner, + mint, + programId: TOKEN_PROGRAM_ID, + }), + sourceOwner, + sourceOwner, + [], + TOKEN_PROGRAM_ID, + ), + ); + } + if (authority.equals(sourceOwner) && senderT22Balance > ZERO) { + closeWrappedSourceInstructions.push( + createCloseAccountInstruction( + getAtaAddress({ + owner: sourceOwner, + mint, + programId: TOKEN_2022_PROGRAM_ID, + }), + sourceOwner, + sourceOwner, + [], + TOKEN_2022_PROGRAM_ID, + ), + ); + } + + 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, + amount: amountBigInt, + decimals, + }); + } else { + const splPoolInfos = await getSplPoolInfos(rpc, mint); + const splPoolInfo = splPoolInfos.find( + info => + info.isInitialized && + info.tokenProgram.equals(recipientTokenProgramId), + ); + if (!splPoolInfo) { + throw new Error( + `No initialized SPL pool found for tokenProgram ${recipientTokenProgramId.toBase58()}.`, + ); + } + transferInstruction = createUnwrapInstruction( + senderAta, + recipientAta, + authority, + mint, + amountBigInt, + splPoolInfo, + decimals, + payer, + ); + } + + return [ + ...senderLoadInstructions, + ...closeWrappedSourceInstructions, + createAtaInstruction({ + payer, + owner: recipient, + mint, + programId: recipientTokenProgramId, + }), + ...recipientLoadInstructions, + transferInstruction, + ]; +} + +/** + * No-wrap transfer flow builder (advanced). + */ +export async function buildTransferInstructionsNowrap({ + rpc, + payer, + mint, + sourceOwner, + authority, + recipient, + tokenProgram, + amount, +}: CreateTransferInstructionsInput): Promise { + const amountBigInt = toBigIntAmount(amount); + const senderLoadInstructions = await buildLoadInstructionList({ + rpc, + payer, + owner: sourceOwner, + mint, + authority, + wrap: false, + }); + + const recipientTokenProgramId = tokenProgram ?? LIGHT_TOKEN_PROGRAM_ID; + const recipientAta = getAtaAddress({ + owner: recipient, + mint, + programId: recipientTokenProgramId, + }); + const decimals = await getMintDecimals(rpc, mint); + 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, + amount: amountBigInt, + decimals, + }); + } else { + const splPoolInfos = await getSplPoolInfos(rpc, mint); + const splPoolInfo = splPoolInfos.find( + info => + info.isInitialized && + info.tokenProgram.equals(recipientTokenProgramId), + ); + if (!splPoolInfo) { + throw new Error( + `No initialized SPL pool found for tokenProgram ${recipientTokenProgramId.toBase58()}.`, + ); + } + transferInstruction = createUnwrapInstruction( + senderAta, + recipientAta, + authority, + mint, + amountBigInt, + splPoolInfo, + decimals, + payer, + ); + } + + return [...senderLoadInstructions, transferInstruction]; +} + +export async function createTransferInstructionPlan( + input: CreateTransferInstructionsInput, +) { + return toInstructionPlan(await buildTransferInstructions(input)); +} + +export { buildTransferInstructions as createTransferInstructions }; diff --git a/js/token-interface/src/instructions/unwrap.ts b/js/token-interface/src/instructions/unwrap.ts new file mode 100644 index 0000000000..407a962639 --- /dev/null +++ b/js/token-interface/src/instructions/unwrap.ts @@ -0,0 +1,120 @@ +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 { SplPoolInfo } from '../spl-interface'; +import { + encodeTransfer2InstructionData, + createCompressLightToken, + createDecompressSpl, + type Transfer2InstructionData, + type Compression, +} from './layout/layout-transfer2'; + +/** + * Create an unwrap instruction that moves tokens from a light-token account to an + * SPL/T22 account. + */ +export function createUnwrapInstruction( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + mint: PublicKey, + amount: bigint, + splPoolInfo: SplPoolInfo, + decimals: number, + payer: PublicKey = owner, + maxTopUp?: number, +): 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, + splPoolInfo.poolIndex, + splPoolInfo.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: splPoolInfo.splPoolPda, + isSigner: false, + isWritable: true, + }, + { + pubkey: splPoolInfo.tokenProgram, + 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..4a90404693 --- /dev/null +++ b/js/token-interface/src/instructions/wrap.ts @@ -0,0 +1,133 @@ +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 { SplPoolInfo } from '../spl-interface'; +import { + encodeTransfer2InstructionData, + createCompressSpl, + createDecompressLightToken, + type Transfer2InstructionData, + type Compression, +} from './layout/layout-transfer2'; + +/** + * Create a wrap instruction that moves tokens from an SPL/T22 account to a + * light-token account. + * + * @param source Source SPL/T22 token account + * @param destination Destination light-token account + * @param owner Owner of the source account (signer) + * @param mint Mint address + * @param amount Amount to wrap, + * @param splPoolInfo SPL pool info for the compression + * @param decimals Mint decimals (required for transfer_checked) + * @param payer Fee payer (defaults to owner) + * @param maxTopUp Optional cap on rent top-up (units of 1k lamports; default no cap) + * @returns Instruction to wrap tokens + */ +export function createWrapInstruction( + source: PublicKey, + destination: PublicKey, + owner: PublicKey, + mint: PublicKey, + amount: bigint, + splPoolInfo: SplPoolInfo, + decimals: number, + payer: PublicKey = owner, + maxTopUp?: number, +): 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, + splPoolInfo.poolIndex, + splPoolInfo.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: splPoolInfo.splPoolPda, + isSigner: false, + isWritable: true, + }, + { + pubkey: splPoolInfo.tokenProgram, + 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..17489c2db8 --- /dev/null +++ b/js/token-interface/src/kit/index.ts @@ -0,0 +1,145 @@ +import type { TransactionInstruction } from '@solana/web3.js'; +import { + buildTransferInstructions as buildTransferInstructionsTx, + buildTransferInstructionsNowrap as buildTransferInstructionsNowrapTx, + 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 buildTransferInstructions( + input: CreateTransferInstructionsInput, +): Promise { + return wrap(buildTransferInstructionsTx(input)); +} + +export async function buildTransferInstructionsNowrap( + input: CreateTransferInstructionsInput, +): Promise { + return wrap(buildTransferInstructionsNowrapTx(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..7a288d7e21 --- /dev/null +++ b/js/token-interface/src/load-options.ts @@ -0,0 +1,8 @@ +import type { PublicKey } from '@solana/web3.js'; +import type { SplPoolInfo } from './spl-interface'; + +export interface LoadOptions { + splPoolInfos?: SplPoolInfo[]; + 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..c71d8ac871 --- /dev/null +++ b/js/token-interface/src/read/get-account.ts @@ -0,0 +1,1095 @@ +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; +} + +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), + }; +} + +/** Convert compressed account to AccountInfo */ +function toAccountInfo( + compressedAccount: CompressedAccountWithMerkleContext, +): AccountInfo { + const dataDiscriminatorBuffer: Buffer = Buffer.from( + compressedAccount.data!.discriminator, + ); + const dataBuffer: Buffer = Buffer.from(compressedAccount.data!.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(compressedAccount.data!.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 { + // Canonical address for unified mode is always the light-token associated token account + const lightTokenAta = + address ?? + 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(address!); + + 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({ items: [] as any[] }), + ]); + + 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 + : ({ items: [] as any[] } as const); + 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). + canonicalDelegate = coldDelegatedSources[0].parsed.delegate!; + 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..e8089ac3f6 --- /dev/null +++ b/js/token-interface/src/read/get-mint.ts @@ -0,0 +1,235 @@ +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..aa604a3ff5 --- /dev/null +++ b/js/token-interface/src/read/index.ts @@ -0,0 +1,28 @@ +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..5c84dbb2f1 --- /dev/null +++ b/js/token-interface/src/spl-interface.ts @@ -0,0 +1,71 @@ +import { Commitment, PublicKey } from '@solana/web3.js'; +import { unpackAccount } from '@solana/spl-token'; +import { bn, Rpc } from '@lightprotocol/stateless.js'; +import BN from 'bn.js'; +import { deriveSplPoolPdaWithIndex } from './constants'; + +export type SplPoolInfo = { + mint: PublicKey; + splPoolPda: PublicKey; + tokenProgram: PublicKey; + activity?: { + txs: number; + amountAdded: BN; + amountRemoved: BN; + }; + isInitialized: boolean; + balance: BN; + poolIndex: number; + bump: number; +}; + +export async function getSplPoolInfos( + rpc: Rpc, + mint: PublicKey, + commitment?: Commitment, +): Promise { + const addressesAndBumps = Array.from({ length: 5 }, (_, i) => + deriveSplPoolPdaWithIndex(mint, i), + ); + + const accountInfos = await rpc.getMultipleAccountsInfo( + addressesAndBumps.map(([address]) => address), + commitment, + ); + + if (accountInfos[0] === null) { + throw new Error(`SPL pool not found for mint ${mint.toBase58()}.`); + } + + const parsedInfos = addressesAndBumps.map(([address], i) => + accountInfos[i] ? unpackAccount(address, accountInfos[i], accountInfos[i].owner) : null, + ); + + const tokenProgram = accountInfos[0].owner; + + return parsedInfos.map((parsedInfo, i) => { + if (!parsedInfo) { + return { + mint, + splPoolPda: addressesAndBumps[i][0], + tokenProgram, + activity: undefined, + balance: bn(0), + isInitialized: false, + poolIndex: i, + bump: addressesAndBumps[i][1], + }; + } + + return { + mint, + splPoolPda: parsedInfo.address, + tokenProgram, + activity: undefined, + balance: bn(parsedInfo.amount.toString()), + isInitialized: true, + poolIndex: 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..535b7b30f5 --- /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/approve-revoke.test.ts b/js/token-interface/tests/e2e/approve-revoke.test.ts new file mode 100644 index 0000000000..db20219d90 --- /dev/null +++ b/js/token-interface/tests/e2e/approve-revoke.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { ComputeBudgetProgram, Keypair } from '@solana/web3.js'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createApproveInstructions, + createRevokeInstructions, + getAtaAddress, +} from '../../src'; +import { + createMintFixture, + getHotDelegate, + mintCompressedToOwner, + sendInstructions, +} from './helpers'; + +describe('approve and revoke instructions', () => { + it('approves and revokes on the canonical ata', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const delegate = Keypair.generate(); + const tokenAccount = getAtaAddress({ + owner: owner.publicKey, + mint: fixture.mint, + }); + + await mintCompressedToOwner(fixture, owner.publicKey, 4_000n); + + const approveInstructions = await createApproveInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + delegate: delegate.publicKey, + amount: 1_500n, + }); + + expect( + approveInstructions.some(instruction => + instruction.programId.equals(ComputeBudgetProgram.programId), + ), + ).toBe(false); + + await sendInstructions(fixture.rpc, fixture.payer, approveInstructions, [ + owner, + ]); + + const delegated = await getHotDelegate(fixture.rpc, tokenAccount); + expect(delegated.delegate?.toBase58()).toBe(delegate.publicKey.toBase58()); + expect(delegated.delegatedAmount).toBe(1_500n); + + const revokeInstructions = await createRevokeInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + }); + + expect( + revokeInstructions.some(instruction => + instruction.programId.equals(ComputeBudgetProgram.programId), + ), + ).toBe(false); + + await sendInstructions(fixture.rpc, fixture.payer, revokeInstructions, [ + owner, + ]); + + const revoked = await getHotDelegate(fixture.rpc, tokenAccount); + expect(revoked.delegate).toBeNull(); + expect(revoked.delegatedAmount).toBe(0n); + }); +}); diff --git a/js/token-interface/tests/e2e/ata-read.test.ts b/js/token-interface/tests/e2e/ata-read.test.ts new file mode 100644 index 0000000000..7df37a66ed --- /dev/null +++ b/js/token-interface/tests/e2e/ata-read.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createAtaInstructions, + getAta, + getAtaAddress, +} from '../../src'; +import { createMintFixture, sendInstructions } from './helpers'; + +describe('ata creation and reads', () => { + it('creates the canonical ata and reads it back', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const ata = getAtaAddress({ + owner: owner.publicKey, + mint: fixture.mint, + }); + + const instructions = await createAtaInstructions({ + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + }); + + expect(instructions).toHaveLength(1); + + await sendInstructions(fixture.rpc, fixture.payer, instructions); + + const account = await getAta({ + rpc: fixture.rpc, + owner: owner.publicKey, + mint: fixture.mint, + }); + + expect(account.parsed.address.toBase58()).toBe(ata.toBase58()); + expect(account.parsed.owner.toBase58()).toBe(owner.publicKey.toBase58()); + expect(account.parsed.mint.toBase58()).toBe(fixture.mint.toBase58()); + expect(account.parsed.amount).toBe(0n); + }); +}); diff --git a/js/token-interface/tests/e2e/freeze-thaw.test.ts b/js/token-interface/tests/e2e/freeze-thaw.test.ts new file mode 100644 index 0000000000..ed18beedb7 --- /dev/null +++ b/js/token-interface/tests/e2e/freeze-thaw.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from 'vitest'; +import { AccountState } from '@solana/spl-token'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createAtaInstructions, + createFreezeInstructions, + createThawInstructions, + getAtaAddress, +} from '../../src'; +import { + createMintFixture, + getHotState, + mintCompressedToOwner, + sendInstructions, +} from './helpers'; + +describe('freeze and thaw instructions', () => { + it('freezes and thaws a loaded hot account', async () => { + const fixture = await createMintFixture({ withFreezeAuthority: true }); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const tokenAccount = getAtaAddress({ + owner: owner.publicKey, + mint: fixture.mint, + }); + + await sendInstructions( + fixture.rpc, + fixture.payer, + await createAtaInstructions({ + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + }), + ); + + await mintCompressedToOwner(fixture, owner.publicKey, 2_500n); + + await sendInstructions( + fixture.rpc, + fixture.payer, + await createFreezeInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + freezeAuthority: fixture.freezeAuthority!.publicKey, + }), + [fixture.freezeAuthority!], + ); + + expect(await getHotState(fixture.rpc, tokenAccount)).toBe( + AccountState.Frozen, + ); + + await sendInstructions( + fixture.rpc, + fixture.payer, + await createThawInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + freezeAuthority: fixture.freezeAuthority!.publicKey, + }), + [fixture.freezeAuthority!], + ); + + expect(await getHotState(fixture.rpc, tokenAccount)).toBe( + AccountState.Initialized, + ); + }); +}); diff --git a/js/token-interface/tests/e2e/helpers.ts b/js/token-interface/tests/e2e/helpers.ts new file mode 100644 index 0000000000..fc6545ffcf --- /dev/null +++ b/js/token-interface/tests/e2e/helpers.ts @@ -0,0 +1,188 @@ +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 { getSplPoolInfos } 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 getSplPoolInfos(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.'); + } + + await mintTo( + fixture.rpc, + fixture.payer, + fixture.mint, + owner, + fixture.mintAuthority, + bn(amount.toString()), + fixture.stateTreeInfo, + selectedSplInterfaceInfo, + ); +} + +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/e2e/load.test.ts b/js/token-interface/tests/e2e/load.test.ts new file mode 100644 index 0000000000..495623230f --- /dev/null +++ b/js/token-interface/tests/e2e/load.test.ts @@ -0,0 +1,98 @@ +import { describe, expect, it } from 'vitest'; +import { ComputeBudgetProgram } from '@solana/web3.js'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createLoadInstructions, + getAta, + getAtaAddress, +} from '../../src'; +import { + createMintFixture, + getCompressedAmounts, + getHotBalance, + mintCompressedToOwner, + sendInstructions, +} from './helpers'; + +describe('load instructions', () => { + it('getAta only exposes the biggest compressed balance and tracks the ignored ones', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + + await mintCompressedToOwner(fixture, owner.publicKey, 400n); + await mintCompressedToOwner(fixture, owner.publicKey, 300n); + await mintCompressedToOwner(fixture, owner.publicKey, 200n); + + const account = await getAta({ + rpc: fixture.rpc, + owner: owner.publicKey, + mint: fixture.mint, + }); + + expect(account.parsed.amount).toBe(400n); + expect(account.compressedAmount).toBe(400n); + expect(account.requiresLoad).toBe(true); + expect(account.ignoredCompressedAccounts).toHaveLength(2); + expect(account.ignoredCompressedAmount).toBe(500n); + }); + + it('loads one compressed balance per call and leaves the smaller ones untouched', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const tokenAccount = getAtaAddress({ + owner: owner.publicKey, + mint: fixture.mint, + }); + + await mintCompressedToOwner(fixture, owner.publicKey, 500n); + await mintCompressedToOwner(fixture, owner.publicKey, 300n); + await mintCompressedToOwner(fixture, owner.publicKey, 200n); + + const firstInstructions = await createLoadInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + }); + + expect(firstInstructions.length).toBeGreaterThan(0); + expect( + firstInstructions.some(instruction => + instruction.programId.equals(ComputeBudgetProgram.programId), + ), + ).toBe(false); + + await sendInstructions(fixture.rpc, fixture.payer, firstInstructions, [ + owner, + ]); + + expect(await getHotBalance(fixture.rpc, tokenAccount)).toBe(500n); + expect( + await getCompressedAmounts( + fixture.rpc, + owner.publicKey, + fixture.mint, + ), + ).toEqual([300n, 200n]); + + const secondInstructions = await createLoadInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + }); + + await sendInstructions(fixture.rpc, fixture.payer, secondInstructions, [ + owner, + ]); + + expect(await getHotBalance(fixture.rpc, tokenAccount)).toBe(800n); + expect( + await getCompressedAmounts( + fixture.rpc, + owner.publicKey, + fixture.mint, + ), + ).toEqual([200n]); + }); +}); diff --git a/js/token-interface/tests/e2e/transfer.test.ts b/js/token-interface/tests/e2e/transfer.test.ts new file mode 100644 index 0000000000..b9f001a43c --- /dev/null +++ b/js/token-interface/tests/e2e/transfer.test.ts @@ -0,0 +1,245 @@ +import { describe, expect, it } from 'vitest'; +import { ComputeBudgetProgram, Keypair } from '@solana/web3.js'; +import { + TOKEN_PROGRAM_ID, + getAssociatedTokenAddressSync, + unpackAccount, +} from '@solana/spl-token'; +import { newAccountWithLamports } from '@lightprotocol/stateless.js'; +import { + createApproveInstructions, + createAtaInstructions, + createTransferInstructions, + getAta, + getAtaAddress, +} from '../../src'; +import { + createMintFixture, + getCompressedAmounts, + getHotBalance, + mintCompressedToOwner, + sendInstructions, +} from './helpers'; + +describe('transfer instructions', () => { + it('builds a single-transaction transfer flow without compute budget instructions', async () => { + const fixture = await createMintFixture(); + const sender = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = Keypair.generate(); + + await mintCompressedToOwner(fixture, sender.publicKey, 5_000n); + + const instructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: recipient.publicKey, + amount: 2_000n, + }); + + expect(instructions.length).toBeGreaterThan(0); + expect( + instructions.some(instruction => + instruction.programId.equals(ComputeBudgetProgram.programId), + ), + ).toBe(false); + + await sendInstructions(fixture.rpc, fixture.payer, instructions, [ + sender, + ]); + + const recipientAta = await getAta({ + rpc: fixture.rpc, + owner: recipient.publicKey, + mint: fixture.mint, + }); + const senderAta = getAtaAddress({ + owner: sender.publicKey, + mint: fixture.mint, + }); + + expect(recipientAta.parsed.amount).toBe(2_000n); + expect(await getHotBalance(fixture.rpc, senderAta)).toBe(3_000n); + }); + + it('supports non-light destination path with SPL ATA recipient', async () => { + const fixture = await createMintFixture(); + const sender = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = await newAccountWithLamports(fixture.rpc, 1e9); + const recipientSplAta = getAssociatedTokenAddressSync( + fixture.mint, + recipient.publicKey, + false, + TOKEN_PROGRAM_ID, + ); + + await mintCompressedToOwner(fixture, sender.publicKey, 3_000n); + + const instructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: recipient.publicKey, + tokenProgram: TOKEN_PROGRAM_ID, + amount: 1_250n, + }); + + await sendInstructions(fixture.rpc, fixture.payer, instructions, [sender]); + + const recipientSplInfo = await fixture.rpc.getAccountInfo(recipientSplAta); + expect(recipientSplInfo).not.toBeNull(); + const recipientSpl = unpackAccount( + recipientSplAta, + recipientSplInfo!, + TOKEN_PROGRAM_ID, + ); + expect(recipientSpl.amount).toBe(1_250n); + }); + + it('passes through on-chain insufficient-funds error for transfer', async () => { + const fixture = await createMintFixture(); + const sender = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = Keypair.generate(); + + await mintCompressedToOwner(fixture, sender.publicKey, 500n); + await mintCompressedToOwner(fixture, sender.publicKey, 300n); + await mintCompressedToOwner(fixture, sender.publicKey, 200n); + + const instructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: recipient.publicKey, + amount: 600n, + }); + + await expect( + sendInstructions(fixture.rpc, fixture.payer, instructions, [sender]), + ).rejects.toThrow('custom program error'); + }); + + it('does not pre-reject zero amount (on-chain behavior decides)', async () => { + const fixture = await createMintFixture(); + const sender = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = Keypair.generate(); + const senderAta = getAtaAddress({ + owner: sender.publicKey, + mint: fixture.mint, + }); + + await mintCompressedToOwner(fixture, sender.publicKey, 500n); + + const instructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: recipient.publicKey, + amount: 0n, + }); + + await sendInstructions(fixture.rpc, fixture.payer, instructions, [sender]); + expect(await getHotBalance(fixture.rpc, senderAta)).toBe(500n); + }); + + it('does not load the recipient compressed balance yet', async () => { + const fixture = await createMintFixture(); + const sender = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = await newAccountWithLamports(fixture.rpc, 1e9); + const recipientAtaAddress = getAtaAddress({ + owner: recipient.publicKey, + mint: fixture.mint, + }); + + await mintCompressedToOwner(fixture, sender.publicKey, 400n); + await mintCompressedToOwner(fixture, recipient.publicKey, 300n); + + const instructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: sender.publicKey, + authority: sender.publicKey, + recipient: recipient.publicKey, + amount: 200n, + }); + + await sendInstructions(fixture.rpc, fixture.payer, instructions, [ + sender, + ]); + + expect(await getHotBalance(fixture.rpc, recipientAtaAddress)).toBe(200n); + expect( + await getCompressedAmounts( + fixture.rpc, + recipient.publicKey, + fixture.mint, + ), + ).toEqual([300n]); + + const recipientAta = await getAta({ + rpc: fixture.rpc, + owner: recipient.publicKey, + mint: fixture.mint, + }); + + expect(recipientAta.parsed.amount).toBe(500n); + expect(recipientAta.compressedAmount).toBe(300n); + }); + + it('supports delegated payments after approval', async () => { + const fixture = await createMintFixture(); + const owner = await newAccountWithLamports(fixture.rpc, 1e9); + const delegate = await newAccountWithLamports(fixture.rpc, 1e9); + const recipient = Keypair.generate(); + const ownerAta = getAtaAddress({ + owner: owner.publicKey, + mint: fixture.mint, + }); + + await mintCompressedToOwner(fixture, owner.publicKey, 500n); + + const approveInstructions = await createApproveInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + owner: owner.publicKey, + mint: fixture.mint, + delegate: delegate.publicKey, + amount: 300n, + }); + + await sendInstructions(fixture.rpc, fixture.payer, approveInstructions, [ + owner, + ]); + + const transferInstructions = await createTransferInstructions({ + rpc: fixture.rpc, + payer: fixture.payer.publicKey, + mint: fixture.mint, + sourceOwner: owner.publicKey, + recipient: recipient.publicKey, + amount: 250n, + authority: delegate.publicKey, + }); + + await sendInstructions(fixture.rpc, fixture.payer, transferInstructions, [ + delegate, + ]); + + const recipientAta = await getAta({ + rpc: fixture.rpc, + owner: recipient.publicKey, + mint: fixture.mint, + }); + + expect(recipientAta.parsed.amount).toBe(250n); + expect(await getHotBalance(fixture.rpc, ownerAta)).toBe(250n); + }); +}); diff --git a/js/token-interface/tests/unit/instruction-builders.test.ts b/js/token-interface/tests/unit/instruction-builders.test.ts new file mode 100644 index 0000000000..e1545d9c5b --- /dev/null +++ b/js/token-interface/tests/unit/instruction-builders.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from 'vitest'; +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'; + +describe('instruction builders', () => { + it('creates a canonical light-token ata instruction', () => { + const payer = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + + const instruction = createAtaInstruction({ + payer, + owner, + mint, + }); + + expect(instruction.programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); + expect(instruction.keys[0].pubkey.equals(owner)).toBe(true); + expect(instruction.keys[1].pubkey.equals(mint)).toBe(true); + expect(instruction.keys[2].pubkey.equals(payer)).toBe(true); + }); + + it('creates a checked transfer instruction', () => { + const source = Keypair.generate().publicKey; + const destination = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + const authority = Keypair.generate().publicKey; + const payer = Keypair.generate().publicKey; + + const instruction = createTransferCheckedInstruction({ + source, + destination, + mint, + authority, + payer, + amount: 42n, + decimals: 9, + }); + + expect(instruction.programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); + expect(instruction.data[0]).toBe(12); + expect(instruction.keys[0].pubkey.equals(source)).toBe(true); + expect(instruction.keys[2].pubkey.equals(destination)).toBe(true); + }); + + it('creates approve, revoke, freeze, and thaw instructions', () => { + const tokenAccount = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const delegate = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + const freezeAuthority = Keypair.generate().publicKey; + + const approve = createApproveInstruction({ + tokenAccount, + delegate, + owner, + amount: 10n, + }); + const revoke = createRevokeInstruction({ + tokenAccount, + owner, + }); + const freeze = createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, + }); + const thaw = createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, + }); + + expect(approve.programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); + expect(revoke.programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); + expect(freeze.data[0]).toBe(10); + expect(thaw.data[0]).toBe(11); + }); + +}); diff --git a/js/token-interface/tests/unit/kit.test.ts b/js/token-interface/tests/unit/kit.test.ts new file mode 100644 index 0000000000..28af3b619b --- /dev/null +++ b/js/token-interface/tests/unit/kit.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { Keypair } from '@solana/web3.js'; +import { createAtaInstruction } from '../../src/instructions'; +import { + buildTransferInstructions, + createAtaInstructions, + createTransferInstructionPlan, + toKitInstructions, +} from '../../src/kit'; + +describe('kit adapter', () => { + it('converts legacy instructions to kit instructions', () => { + const instruction = createAtaInstruction({ + payer: Keypair.generate().publicKey, + owner: Keypair.generate().publicKey, + mint: Keypair.generate().publicKey, + }); + + const converted = toKitInstructions([instruction]); + + expect(converted).toHaveLength(1); + expect(converted[0]).toBeDefined(); + expect(typeof converted[0]).toBe('object'); + }); + + it('wraps canonical builders for kit consumers', async () => { + const instructions = await createAtaInstructions({ + payer: Keypair.generate().publicKey, + owner: Keypair.generate().publicKey, + mint: Keypair.generate().publicKey, + }); + + expect(instructions).toHaveLength(1); + expect(instructions[0]).toBeDefined(); + }); + + it('exports transfer builder and plan builder', () => { + expect(typeof buildTransferInstructions).toBe('function'); + expect(typeof createTransferInstructionPlan).toBe('function'); + }); +}); diff --git a/js/token-interface/tests/unit/public-api.test.ts b/js/token-interface/tests/unit/public-api.test.ts new file mode 100644 index 0000000000..f25408939d --- /dev/null +++ b/js/token-interface/tests/unit/public-api.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +import { Keypair } from '@solana/web3.js'; +import { LIGHT_TOKEN_PROGRAM_ID } from '@lightprotocol/stateless.js'; +import { getAssociatedTokenAddress } from '../../src/read'; +import { + buildTransferInstructions, + MultiTransactionNotSupportedError, + createAtaInstructions, + createFreezeInstruction, + createThawInstruction, + getAtaAddress, +} from '../../src'; + +describe('public api', () => { + it('derives the canonical light-token ata address', () => { + const owner = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + + expect(getAtaAddress({ owner, mint }).equals( + getAssociatedTokenAddress(mint, owner), + )).toBe(true); + }); + + it('builds one canonical ata instruction', async () => { + const payer = Keypair.generate().publicKey; + const owner = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + + const instructions = await createAtaInstructions({ + payer, + owner, + mint, + }); + + expect(instructions).toHaveLength(1); + expect(instructions[0].programId.equals(LIGHT_TOKEN_PROGRAM_ID)).toBe( + true, + ); + }); + + it('raw freeze and thaw instructions use light-token discriminators', () => { + const tokenAccount = Keypair.generate().publicKey; + const mint = Keypair.generate().publicKey; + const freezeAuthority = Keypair.generate().publicKey; + + const freeze = createFreezeInstruction({ + tokenAccount, + mint, + freezeAuthority, + }); + const thaw = createThawInstruction({ + tokenAccount, + mint, + freezeAuthority, + }); + + expect(freeze.data[0]).toBe(10); + expect(thaw.data[0]).toBe(11); + }); + + it('exposes a clear single-transaction error', () => { + const error = new MultiTransactionNotSupportedError( + 'createLoadInstructions', + 2, + ); + + expect(error.name).toBe('MultiTransactionNotSupportedError'); + expect(error.message).toContain('single-transaction'); + expect(error.message).toContain('createLoadInstructions'); + }); + + it('exports canonical transfer builder', () => { + expect(typeof buildTransferInstructions).toBe('function'); + }); +}); 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..e836181c0e --- /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", "vitest.config.ts"] +} diff --git a/js/token-interface/vitest.config.ts b/js/token-interface/vitest.config.ts new file mode 100644 index 0000000000..93411502d2 --- /dev/null +++ b/js/token-interface/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config'; +import { resolve } from 'path'; + +export default defineConfig({ + logLevel: 'info', + test: { + include: process.env.EXCLUDE_E2E + ? ['tests/unit/**/*.test.ts'] + : ['tests/**/*.test.ts'], + includeSource: ['src/**/*.{js,ts}'], + fileParallelism: false, + testTimeout: 350000, + hookTimeout: 100000, + reporters: ['verbose'], + }, + define: { + 'import.meta.vitest': false, + }, + build: { + lib: { + formats: ['es', 'cjs'], + entry: resolve(__dirname, 'src/index.ts'), + fileName: 'index', + }, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 158713b83f..3c3a32e0cd 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: + '@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 + eslint-plugin-vitest: + specifier: ^0.5.4 + version: 0.5.4(@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))(eslint@9.36.0)(typescript@5.9.3)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)) + 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 + typescript: + specifier: ^5.6.2 + version: 5.9.3 + vitest: + specifier: ^2.1.1 + version: 2.1.1(@types/node@22.16.5)(terser@5.43.1) + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': @@ -1865,6 +1956,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 +2010,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 +2032,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 +2060,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 +2086,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 +2108,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 +2145,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,6 +2240,177 @@ packages: peerDependencies: typescript: '>=5' + '@solana/options@6.5.0': + resolution: {integrity: sha512-jdZjSKGCQpsMFK+3CiUEI7W9iGsndi46R4Abk66ULNLDoMsjvfqNy8kqktm0TN0++EX8dKEecpFwxFaA4VlY5g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/plugin-core@6.5.0': + resolution: {integrity: sha512-L6N69oNQOAqljH4GnLTaxpwJB0nibW9DrybHZxpGWshyv6b/EvwvkDVRKj5bNqtCG+HRZUHnEhLi1UgZVNkjpQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/plugin-interfaces@6.5.0': + resolution: {integrity: sha512-/ZlybbMaR7P4ySersOe1huioMADWze0AzsHbzgkpt5dJUv2tz5cpaKdu7TEVQkUZAFhLdqXQULNGqAU5neOgzg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/program-client-core@6.5.0': + resolution: {integrity: sha512-eUz1xSeDKySGIjToAryPmlESdj8KX0Np7R+Pjt+kSFGw5Jgmn/Inh4o8luoeEnf5XwbvSPVb4aHpIsDyoUVbIg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/programs@6.5.0': + resolution: {integrity: sha512-srn3nEROBxCnBpVz/bvLkVln1BZtk3bS3nuReu3yaeOLkKl8b0h1Zp0YmXVyXHzdMcYahsTvKKLR1ZtLZEyEPA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@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/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 + + '@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'} @@ -2013,9 +2441,54 @@ packages: peerDependencies: '@solana/web3.js': ^1.94.0 - '@solana/spl-type-length-value@0.1.0': - resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} - engines: {node: '>=16'} + '@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==} @@ -2704,8 +3177,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: @@ -5360,6 +5833,9 @@ 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.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5613,6 +6089,18 @@ 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 + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -6382,6 +6870,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) @@ -7062,6 +7556,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 +7991,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 +8034,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 +8072,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 +8102,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 +8138,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,76 +8173,429 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.2 - '@solana/codecs@2.0.0-preview.4(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) + '@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/options': 2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/codecs@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/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + typescript: 5.9.2 + 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 + commander: 12.1.0 + typescript: 5.9.2 + + '@solana/errors@2.0.0-rc.1(typescript@5.9.2)': + dependencies: + chalk: 5.6.2 + commander: 12.1.0 + typescript: 5.9.2 + + '@solana/errors@2.3.0(typescript@4.9.5)': + dependencies: + chalk: 5.6.2 + commander: 14.0.3 + typescript: 4.9.5 + + '@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/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/options': 2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 + '@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/codecs@2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + '@solana/rpc-subscriptions-channel-websocket@6.5.0(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@5.0.10)': 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/options': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 + '@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: - - fastestsmallesttextencoderdecoder + - bufferutil + - utf-8-validate - '@solana/errors@2.0.0-preview.4(typescript@5.9.2)': + '@solana/rpc-subscriptions-spec@6.5.0(typescript@5.9.3)': dependencies: - chalk: 5.6.2 - commander: 12.1.0 - typescript: 5.9.2 + '@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/errors@2.0.0-rc.1(typescript@5.9.2)': - dependencies: - chalk: 5.6.2 - commander: 12.1.0 - typescript: 5.9.2 + '@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/errors@2.3.0(typescript@4.9.5)': + '@solana/rpc-transformers@6.5.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': dependencies: - chalk: 5.6.2 - commander: 14.0.1 - typescript: 4.9.5 + '@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/errors@2.3.0(typescript@5.9.2)': + '@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 +8620,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 +8655,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 +8688,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 +8784,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 @@ -7907,6 +8960,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 +8989,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 +9010,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 +9033,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 +9049,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': {} @@ -7973,6 +9080,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@7.13.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/visitor-keys': 7.13.1 + debug: 4.4.3(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.3 + ts-api-utils: 1.3.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.2)': dependencies: '@typescript-eslint/project-service': 8.44.0(typescript@5.9.2) @@ -7989,6 +9111,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) @@ -8000,6 +9138,17 @@ snapshots: - supports-color - typescript + '@typescript-eslint/utils@7.13.1(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.36.0) + '@typescript-eslint/scope-manager': 7.13.1 + '@typescript-eslint/types': 7.13.1 + '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.9.3) + eslint: 9.36.0 + transitivePeerDependencies: + - supports-color + - typescript + '@typescript-eslint/utils@8.44.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) @@ -8011,6 +9160,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 @@ -8566,7 +9726,7 @@ snapshots: commander@13.1.0: {} - commander@14.0.1: {} + commander@14.0.3: {} commander@2.20.3: {} @@ -9213,6 +10373,17 @@ snapshots: - supports-color - typescript + eslint-plugin-vitest@0.5.4(@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))(eslint@9.36.0)(typescript@5.9.3)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)): + dependencies: + '@typescript-eslint/utils': 7.13.1(eslint@9.36.0)(typescript@5.9.3) + eslint: 9.36.0 + optionalDependencies: + '@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) + vitest: 2.1.1(@types/node@22.16.5)(terser@5.43.1) + transitivePeerDependencies: + - supports-color + - typescript + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -9835,7 +11006,7 @@ snapshots: 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 @@ -10919,6 +12090,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) @@ -11388,10 +12567,18 @@ snapshots: dependencies: typescript: 5.9.2 + ts-api-utils@1.3.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-api-utils@2.1.0(typescript@5.9.2): 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 @@ -11587,6 +12774,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.24.6: {} + unicorn-magic@0.3.0: {} union@0.5.0: @@ -11859,6 +13048,11 @@ 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 + y18n@5.0.8: {} yallist@3.1.1: {} 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/**"