From efe3b69a31525033ad0cb9025458af57c3fab504 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 23 Jan 2026 22:03:34 +0000 Subject: [PATCH 1/2] feat: ts token sdk + idl --- js/token-idl/.prettierignore | 4 + js/token-idl/.prettierrc | 10 + js/token-idl/eslint.config.cjs | 66 + js/token-idl/package.json | 27 + js/token-idl/scripts/generate-clients.ts | 57 + js/token-idl/src/idl.ts | 1198 +++++++++++++++++ js/token-idl/tsconfig.json | 16 + js/token-sdk/.prettierignore | 6 + js/token-sdk/.prettierrc | 10 + js/token-sdk/eslint.config.cjs | 113 ++ js/token-sdk/package.json | 73 + js/token-sdk/src/codecs/compressible.ts | 170 +++ js/token-sdk/src/codecs/index.ts | 49 + js/token-sdk/src/codecs/transfer2.ts | 396 ++++++ js/token-sdk/src/codecs/types.ts | 304 +++++ js/token-sdk/src/constants.ts | 180 +++ js/token-sdk/src/generated/.gitkeep | 2 + js/token-sdk/src/index.ts | 189 +++ js/token-sdk/src/instructions/approve.ts | 91 ++ js/token-sdk/src/instructions/burn.ts | 96 ++ js/token-sdk/src/instructions/close.ts | 51 + js/token-sdk/src/instructions/create-ata.ts | 115 ++ js/token-sdk/src/instructions/freeze-thaw.ts | 88 ++ js/token-sdk/src/instructions/index.ts | 12 + js/token-sdk/src/instructions/mint-to.ts | 96 ++ .../src/instructions/transfer-interface.ts | 123 ++ js/token-sdk/src/instructions/transfer.ts | 155 +++ js/token-sdk/src/rpc/client.ts | 103 ++ js/token-sdk/src/rpc/index.ts | 11 + js/token-sdk/src/utils/derivation.ts | 149 ++ js/token-sdk/src/utils/index.ts | 19 + js/token-sdk/src/utils/validation.ts | 99 ++ js/token-sdk/tsconfig.json | 24 + pnpm-lock.yaml | 1120 ++++++++++++++- pnpm-workspace.yaml | 2 + scripts/lint.sh | 2 + 36 files changed, 5215 insertions(+), 11 deletions(-) create mode 100644 js/token-idl/.prettierignore create mode 100644 js/token-idl/.prettierrc create mode 100644 js/token-idl/eslint.config.cjs create mode 100644 js/token-idl/package.json create mode 100644 js/token-idl/scripts/generate-clients.ts create mode 100644 js/token-idl/src/idl.ts create mode 100644 js/token-idl/tsconfig.json create mode 100644 js/token-sdk/.prettierignore create mode 100644 js/token-sdk/.prettierrc create mode 100644 js/token-sdk/eslint.config.cjs create mode 100644 js/token-sdk/package.json create mode 100644 js/token-sdk/src/codecs/compressible.ts create mode 100644 js/token-sdk/src/codecs/index.ts create mode 100644 js/token-sdk/src/codecs/transfer2.ts create mode 100644 js/token-sdk/src/codecs/types.ts create mode 100644 js/token-sdk/src/constants.ts create mode 100644 js/token-sdk/src/generated/.gitkeep create mode 100644 js/token-sdk/src/index.ts create mode 100644 js/token-sdk/src/instructions/approve.ts create mode 100644 js/token-sdk/src/instructions/burn.ts create mode 100644 js/token-sdk/src/instructions/close.ts create mode 100644 js/token-sdk/src/instructions/create-ata.ts create mode 100644 js/token-sdk/src/instructions/freeze-thaw.ts create mode 100644 js/token-sdk/src/instructions/index.ts create mode 100644 js/token-sdk/src/instructions/mint-to.ts create mode 100644 js/token-sdk/src/instructions/transfer-interface.ts create mode 100644 js/token-sdk/src/instructions/transfer.ts create mode 100644 js/token-sdk/src/rpc/client.ts create mode 100644 js/token-sdk/src/rpc/index.ts create mode 100644 js/token-sdk/src/utils/derivation.ts create mode 100644 js/token-sdk/src/utils/index.ts create mode 100644 js/token-sdk/src/utils/validation.ts create mode 100644 js/token-sdk/tsconfig.json diff --git a/js/token-idl/.prettierignore b/js/token-idl/.prettierignore new file mode 100644 index 0000000000..00b694961e --- /dev/null +++ b/js/token-idl/.prettierignore @@ -0,0 +1,4 @@ +node_modules +dist +build +coverage diff --git a/js/token-idl/.prettierrc b/js/token-idl/.prettierrc new file mode 100644 index 0000000000..59be93e26f --- /dev/null +++ b/js/token-idl/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "useTabs": false, + "tabWidth": 4, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/js/token-idl/eslint.config.cjs b/js/token-idl/eslint.config.cjs new file mode 100644 index 0000000000..66152ccf0a --- /dev/null +++ b/js/token-idl/eslint.config.cjs @@ -0,0 +1,66 @@ +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/**', + '*.config.js', + 'eslint.config.js', + 'eslint.config.cjs', + ], + }, + 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: ['src/**/*.ts', 'scripts/**/*.ts'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, + globals: { + process: 'readonly', + console: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + Buffer: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + }, + rules: { + ...tseslint.configs.recommended.rules, + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-explicit-any': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/no-unused-vars': 0, + '@typescript-eslint/no-require-imports': 0, + 'no-prototype-builtins': 0, + 'no-undef': 0, + 'no-unused-vars': 0, + 'no-redeclare': 0, + }, + }, +]; diff --git a/js/token-idl/package.json b/js/token-idl/package.json new file mode 100644 index 0000000000..63adc03804 --- /dev/null +++ b/js/token-idl/package.json @@ -0,0 +1,27 @@ +{ + "name": "@lightprotocol/token-idl", + "version": "0.1.0", + "description": "Light Protocol Token IDL and Codama client generation", + "type": "module", + "private": true, + "scripts": { + "generate": "tsx scripts/generate-clients.ts", + "build": "pnpm run generate", + "lint": "eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@codama/nodes": "^1.4.1", + "@codama/renderers-js": "^1.2.8", + "@codama/visitors": "^1.4.1", + "@codama/visitors-core": "^1.4.1", + "@eslint/js": "9.36.0", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "codama": "^1.4.1", + "eslint": "^9.36.0", + "prettier": "^3.3.3", + "tsx": "^4.19.2", + "typescript": "^5.7.3" + } +} diff --git a/js/token-idl/scripts/generate-clients.ts b/js/token-idl/scripts/generate-clients.ts new file mode 100644 index 0000000000..dbc0240c96 --- /dev/null +++ b/js/token-idl/scripts/generate-clients.ts @@ -0,0 +1,57 @@ +/** + * Generate TypeScript clients from the Light Token IDL using Codama. + */ + +import { createFromRoot } from 'codama'; +import { renderJavaScriptVisitor } from '@codama/renderers-js'; +import { setInstructionAccountDefaultValuesVisitor } from '@codama/visitors'; +import { publicKeyValueNode } from 'codama'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +import { + lightTokenIdl, + LIGHT_TOKEN_PROGRAM_ID, + SYSTEM_PROGRAM, +} from '../src/idl.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +// Output directory for generated TypeScript +const typescriptOutputDir = path.resolve( + __dirname, + '../../token-sdk/src/generated', +); + +console.log('Creating Codama instance from Light Token IDL...'); +const codama = createFromRoot(lightTokenIdl); + +// Apply default account values for common accounts +console.log('Applying default account values...'); +codama.update( + setInstructionAccountDefaultValuesVisitor([ + { + account: 'systemProgram', + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }, + { + account: 'selfProgram', + defaultValue: publicKeyValueNode(LIGHT_TOKEN_PROGRAM_ID), + }, + ]), +); + +// Generate TypeScript client +console.log(`Generating TypeScript client to ${typescriptOutputDir}...`); +codama.accept( + renderJavaScriptVisitor(typescriptOutputDir, { + formatCode: true, + dependencyMap: { + // Map codama codecs to @solana/codecs + generatedPackage: '@lightprotocol/token-sdk', + }, + }), +); + +console.log('Generation complete!'); diff --git a/js/token-idl/src/idl.ts b/js/token-idl/src/idl.ts new file mode 100644 index 0000000000..2ddf03698d --- /dev/null +++ b/js/token-idl/src/idl.ts @@ -0,0 +1,1198 @@ +/** + * Light Protocol Token IDL + * + * Programmatic IDL definition for the Light Token program using Codama. + * The program uses single-byte SPL-compatible discriminators (3-18) and + * custom discriminators (100+) with Pinocchio-based instruction dispatch. + */ + +import { + rootNode, + programNode, + instructionNode, + instructionAccountNode, + instructionArgumentNode, + pdaNode, + pdaValueNode, + pdaLinkNode, + constantDiscriminatorNode, + constantValueNode, + constantPdaSeedNodeFromString, + variablePdaSeedNode, + numberTypeNode, + numberValueNode, + publicKeyTypeNode, + publicKeyValueNode, + booleanTypeNode, + optionTypeNode, + bytesTypeNode, + structTypeNode, + structFieldTypeNode, + arrayTypeNode, + fixedSizeTypeNode, +} from 'codama'; + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +export const LIGHT_TOKEN_PROGRAM_ID = + 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'; +export const CPI_AUTHORITY = 'GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy'; +export const MINT_ADDRESS_TREE = 'amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'; +export const SYSTEM_PROGRAM = '11111111111111111111111111111111'; + +// ============================================================================ +// INSTRUCTION DISCRIMINATORS +// ============================================================================ + +/** SPL-compatible discriminators */ +export const DISCRIMINATOR = { + TRANSFER: 3, + APPROVE: 4, + REVOKE: 5, + MINT_TO: 7, + BURN: 8, + CLOSE: 9, + FREEZE: 10, + THAW: 11, + TRANSFER_CHECKED: 12, + MINT_TO_CHECKED: 14, + BURN_CHECKED: 15, + CREATE_TOKEN_ACCOUNT: 18, + CREATE_ATA: 100, + TRANSFER2: 101, + CREATE_ATA_IDEMPOTENT: 102, + MINT_ACTION: 103, +} as const; + +// ============================================================================ +// TYPE DEFINITIONS +// ============================================================================ + +/** Compression mode enum for Transfer2 */ +const compressionModeType = numberTypeNode('u8'); + +/** Compression struct for Transfer2 */ +const compressionStructType = structTypeNode([ + structFieldTypeNode({ name: 'mode', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'mint', type: numberTypeNode('u8') }), + structFieldTypeNode({ + name: 'sourceOrRecipient', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ name: 'authority', type: numberTypeNode('u8') }), + structFieldTypeNode({ + name: 'poolAccountIndex', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ name: 'poolIndex', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'bump', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'decimals', type: numberTypeNode('u8') }), +]); + +/** Packed merkle context */ +const packedMerkleContextType = structTypeNode([ + structFieldTypeNode({ + name: 'merkleTreePubkeyIndex', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ + name: 'queuePubkeyIndex', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ name: 'leafIndex', type: numberTypeNode('u32') }), + structFieldTypeNode({ name: 'proveByIndex', type: booleanTypeNode() }), +]); + +/** Input token data with context */ +const multiInputTokenDataType = structTypeNode([ + structFieldTypeNode({ name: 'owner', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'hasDelegate', type: booleanTypeNode() }), + structFieldTypeNode({ name: 'delegate', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'mint', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'version', type: numberTypeNode('u8') }), + structFieldTypeNode({ + name: 'merkleContext', + type: packedMerkleContextType, + }), + structFieldTypeNode({ name: 'rootIndex', type: numberTypeNode('u16') }), +]); + +/** Output token data */ +const multiTokenOutputDataType = structTypeNode([ + structFieldTypeNode({ name: 'owner', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + structFieldTypeNode({ name: 'hasDelegate', type: booleanTypeNode() }), + structFieldTypeNode({ name: 'delegate', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'mint', type: numberTypeNode('u8') }), + structFieldTypeNode({ name: 'version', type: numberTypeNode('u8') }), +]); + +/** CPI context */ +const cpiContextType = structTypeNode([ + structFieldTypeNode({ name: 'setContext', type: booleanTypeNode() }), + structFieldTypeNode({ name: 'firstSetContext', type: booleanTypeNode() }), + structFieldTypeNode({ + name: 'cpiContextAccountIndex', + type: numberTypeNode('u8'), + }), +]); + +/** Compressible extension instruction data */ +const compressibleExtensionDataType = structTypeNode([ + structFieldTypeNode({ + name: 'tokenAccountVersion', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ name: 'rentPayment', type: numberTypeNode('u8') }), + structFieldTypeNode({ + name: 'compressionOnly', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ name: 'writeTopUp', type: numberTypeNode('u32') }), + structFieldTypeNode({ + name: 'compressToPubkey', + type: optionTypeNode( + structTypeNode([ + structFieldTypeNode({ + name: 'bump', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ + name: 'programId', + type: fixedSizeTypeNode(bytesTypeNode(), 32), + }), + structFieldTypeNode({ + name: 'seeds', + type: arrayTypeNode(bytesTypeNode()), + }), + ]), + ), + }), +]); + +// ============================================================================ +// IDL ROOT +// ============================================================================ + +export const lightTokenIdl = rootNode( + programNode({ + name: 'lightToken', + publicKey: LIGHT_TOKEN_PROGRAM_ID, + version: '1.0.0', + docs: ['Light Protocol compressed token program'], + + // ======================================================================== + // PDAs + // ======================================================================== + pdas: [ + pdaNode({ + name: 'associatedTokenAccount', + seeds: [ + variablePdaSeedNode('owner', publicKeyTypeNode()), + constantPdaSeedNodeFromString( + 'utf8', + LIGHT_TOKEN_PROGRAM_ID, + ), + variablePdaSeedNode('mint', publicKeyTypeNode()), + ], + docs: [ + 'Associated token account PDA: [owner, LIGHT_TOKEN_PROGRAM_ID, mint]', + ], + }), + pdaNode({ + name: 'lightMint', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'compressed_mint'), + variablePdaSeedNode('mintSigner', publicKeyTypeNode()), + ], + docs: ['Light mint PDA: ["compressed_mint", mintSigner]'], + }), + pdaNode({ + name: 'splInterfacePool', + seeds: [ + constantPdaSeedNodeFromString('utf8', 'pool'), + variablePdaSeedNode('mint', publicKeyTypeNode()), + ], + docs: ['SPL interface pool PDA: ["pool", mint]'], + }), + ], + + // ======================================================================== + // ACCOUNTS (for generated types) + // ======================================================================== + accounts: [], + + // ======================================================================== + // INSTRUCTIONS + // ======================================================================== + instructions: [ + // ---------------------------------------------------------------------- + // CToken Transfer (discriminator: 3) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenTransfer', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.TRANSFER), + ), + ), + ], + docs: ['Transfer CToken between decompressed accounts'], + accounts: [ + instructionAccountNode({ + name: 'source', + isSigner: false, + isWritable: true, + docs: ['Source CToken account'], + }), + instructionAccountNode({ + name: 'destination', + isSigner: false, + isWritable: true, + docs: ['Destination CToken account'], + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + docs: ['Authority (owner or delegate)'], + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + docs: ['System program for rent top-up'], + }), + instructionAccountNode({ + name: 'feePayer', + isSigner: true, + isWritable: true, + isOptional: true, + docs: ['Optional fee payer for rent top-ups'], + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.TRANSFER), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + docs: ['Amount to transfer'], + }), + instructionArgumentNode({ + name: 'maxTopUp', + type: optionTypeNode(numberTypeNode('u16')), + docs: [ + 'Maximum lamports for rent top-up (0 = no limit)', + ], + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken TransferChecked (discriminator: 12) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenTransferChecked', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.TRANSFER_CHECKED), + ), + ), + ], + docs: ['Transfer CToken with decimals validation'], + accounts: [ + instructionAccountNode({ + name: 'source', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'destination', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + docs: ['Authority (owner or delegate)'], + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + docs: ['System program for rent top-up'], + }), + instructionAccountNode({ + name: 'feePayer', + isSigner: true, + isWritable: true, + isOptional: true, + docs: ['Optional fee payer for rent top-ups'], + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.TRANSFER_CHECKED, + ), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + instructionArgumentNode({ + name: 'decimals', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'maxTopUp', + type: optionTypeNode(numberTypeNode('u16')), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Approve (discriminator: 4) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenApprove', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.APPROVE), + ), + ), + ], + docs: ['Approve delegate on decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'delegate', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'owner', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.APPROVE), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Revoke (discriminator: 5) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenRevoke', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.REVOKE), + ), + ), + ], + docs: ['Revoke delegate on decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'owner', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.REVOKE), + defaultValueStrategy: 'omitted', + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken MintTo (discriminator: 7) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenMintTo', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.MINT_TO), + ), + ), + ], + docs: ['Mint tokens to decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mintAuthority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.MINT_TO), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken MintToChecked (discriminator: 14) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenMintToChecked', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.MINT_TO_CHECKED), + ), + ), + ], + docs: ['Mint tokens with decimals validation'], + accounts: [ + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mintAuthority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.MINT_TO_CHECKED, + ), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + instructionArgumentNode({ + name: 'decimals', + type: numberTypeNode('u8'), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Burn (discriminator: 8) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenBurn', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.BURN), + ), + ), + ], + docs: ['Burn tokens from decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.BURN), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken BurnChecked (discriminator: 15) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenBurnChecked', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.BURN_CHECKED), + ), + ), + ], + docs: ['Burn tokens with decimals validation'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.BURN_CHECKED, + ), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + instructionArgumentNode({ + name: 'decimals', + type: numberTypeNode('u8'), + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Close (discriminator: 9) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenClose', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.CLOSE), + ), + ), + ], + docs: ['Close decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'destination', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'owner', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.CLOSE), + defaultValueStrategy: 'omitted', + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Freeze (discriminator: 10) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenFreeze', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.FREEZE), + ), + ), + ], + docs: ['Freeze decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'freezeAuthority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.FREEZE), + defaultValueStrategy: 'omitted', + }), + ], + }), + + // ---------------------------------------------------------------------- + // CToken Thaw (discriminator: 11) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'ctokenThaw', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.THAW), + ), + ), + ], + docs: ['Thaw frozen decompressed CToken account'], + accounts: [ + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'freezeAuthority', + isSigner: true, + isWritable: false, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.THAW), + defaultValueStrategy: 'omitted', + }), + ], + }), + + // ---------------------------------------------------------------------- + // Create Token Account (discriminator: 18) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'createTokenAccount', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.CREATE_TOKEN_ACCOUNT), + ), + ), + ], + docs: [ + 'Create CToken account (equivalent to SPL InitializeAccount3)', + ], + accounts: [ + instructionAccountNode({ + name: 'owner', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'payer', + isSigner: true, + isWritable: true, + }), + instructionAccountNode({ + name: 'tokenAccount', + isSigner: false, + isWritable: true, + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }), + instructionAccountNode({ + name: 'compressibleConfig', + isSigner: false, + isWritable: false, + isOptional: true, + }), + instructionAccountNode({ + name: 'rentSponsor', + isSigner: false, + isWritable: true, + isOptional: true, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.CREATE_TOKEN_ACCOUNT, + ), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'compressibleConfig', + type: optionTypeNode(compressibleExtensionDataType), + }), + ], + }), + + // ---------------------------------------------------------------------- + // Create Associated Token Account (discriminator: 100) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'createAssociatedTokenAccount', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.CREATE_ATA), + ), + ), + ], + docs: ['Create associated CToken account'], + accounts: [ + instructionAccountNode({ + name: 'owner', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'payer', + isSigner: true, + isWritable: true, + }), + instructionAccountNode({ + name: 'associatedTokenAccount', + isSigner: false, + isWritable: true, + defaultValue: pdaValueNode( + pdaLinkNode('associatedTokenAccount'), + ), + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }), + instructionAccountNode({ + name: 'compressibleConfig', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'rentSponsor', + isSigner: false, + isWritable: true, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.CREATE_ATA), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'bump', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'compressibleConfig', + type: optionTypeNode(compressibleExtensionDataType), + }), + ], + }), + + // ---------------------------------------------------------------------- + // Create Associated Token Account Idempotent (discriminator: 102) + // ---------------------------------------------------------------------- + instructionNode({ + name: 'createAssociatedTokenAccountIdempotent', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode( + DISCRIMINATOR.CREATE_ATA_IDEMPOTENT, + ), + ), + ), + ], + docs: [ + 'Create associated CToken account (idempotent - no-op if exists)', + ], + accounts: [ + instructionAccountNode({ + name: 'owner', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'mint', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'payer', + isSigner: true, + isWritable: true, + }), + instructionAccountNode({ + name: 'associatedTokenAccount', + isSigner: false, + isWritable: true, + defaultValue: pdaValueNode( + pdaLinkNode('associatedTokenAccount'), + ), + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }), + instructionAccountNode({ + name: 'compressibleConfig', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'rentSponsor', + isSigner: false, + isWritable: true, + }), + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.CREATE_ATA_IDEMPOTENT, + ), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'bump', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'compressibleConfig', + type: optionTypeNode(compressibleExtensionDataType), + }), + ], + }), + + // ---------------------------------------------------------------------- + // Transfer2 (discriminator: 101) - Batch transfer instruction + // ---------------------------------------------------------------------- + instructionNode({ + name: 'transfer2', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.TRANSFER2), + ), + ), + ], + docs: [ + 'Batch transfer instruction for compressed/decompressed operations.', + 'Supports: transfer, compress, decompress, compress-and-close.', + ], + accounts: [ + instructionAccountNode({ + name: 'feePayer', + isSigner: true, + isWritable: true, + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + }), + instructionAccountNode({ + name: 'lightSystemProgram', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'registeredProgramPda', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'accountCompressionAuthority', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'accountCompressionProgram', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'selfProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode( + LIGHT_TOKEN_PROGRAM_ID, + ), + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }), + // Remaining accounts are dynamic based on the transfer + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode(DISCRIMINATOR.TRANSFER2), + defaultValueStrategy: 'omitted', + }), + instructionArgumentNode({ + name: 'withTransactionHash', + type: booleanTypeNode(), + }), + instructionArgumentNode({ + name: 'withLamportsChangeAccountMerkleTreeIndex', + type: booleanTypeNode(), + }), + instructionArgumentNode({ + name: 'lamportsChangeAccountMerkleTreeIndex', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'lamportsChangeAccountOwnerIndex', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'outputQueue', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'maxTopUp', + type: numberTypeNode('u16'), + }), + instructionArgumentNode({ + name: 'cpiContext', + type: optionTypeNode(cpiContextType), + }), + instructionArgumentNode({ + name: 'compressions', + type: optionTypeNode( + arrayTypeNode(compressionStructType), + ), + }), + // Note: proof, inTokenData, outTokenData, inLamports, outLamports, inTlv, outTlv + // are complex nested structures that will be handled by manual codecs + ], + }), + + // ---------------------------------------------------------------------- + // MintAction (discriminator: 103) - Batch mint operations + // ---------------------------------------------------------------------- + instructionNode({ + name: 'mintAction', + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + numberTypeNode('u8'), + numberValueNode(DISCRIMINATOR.MINT_ACTION), + ), + ), + ], + docs: [ + 'Batch instruction for compressed mint management.', + 'Supports: CreateMint, MintTo, UpdateAuthorities, DecompressMint, etc.', + ], + accounts: [ + instructionAccountNode({ + name: 'feePayer', + isSigner: true, + isWritable: true, + }), + instructionAccountNode({ + name: 'authority', + isSigner: true, + isWritable: false, + }), + instructionAccountNode({ + name: 'lightSystemProgram', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'registeredProgramPda', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'accountCompressionAuthority', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'accountCompressionProgram', + isSigner: false, + isWritable: false, + }), + instructionAccountNode({ + name: 'selfProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode( + LIGHT_TOKEN_PROGRAM_ID, + ), + }), + instructionAccountNode({ + name: 'systemProgram', + isSigner: false, + isWritable: false, + defaultValue: publicKeyValueNode(SYSTEM_PROGRAM), + }), + // Remaining accounts are dynamic based on the mint action + ], + arguments: [ + instructionArgumentNode({ + name: 'discriminator', + type: numberTypeNode('u8'), + defaultValue: numberValueNode( + DISCRIMINATOR.MINT_ACTION, + ), + defaultValueStrategy: 'omitted', + }), + // MintAction has complex nested data handled by manual codecs + ], + }), + ], + + // ======================================================================== + // DEFINED TYPES + // ======================================================================== + definedTypes: [], + + // ======================================================================== + // ERRORS + // ======================================================================== + errors: [], + }), +); + +export default lightTokenIdl; diff --git a/js/token-idl/tsconfig.json b/js/token-idl/tsconfig.json new file mode 100644 index 0000000000..6f5308104c --- /dev/null +++ b/js/token-idl/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*", "scripts/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/js/token-sdk/.prettierignore b/js/token-sdk/.prettierignore new file mode 100644 index 0000000000..b709f5b6b2 --- /dev/null +++ b/js/token-sdk/.prettierignore @@ -0,0 +1,6 @@ +node_modules +dist +build +coverage +*.d.ts.map +*.js.map diff --git a/js/token-sdk/.prettierrc b/js/token-sdk/.prettierrc new file mode 100644 index 0000000000..59be93e26f --- /dev/null +++ b/js/token-sdk/.prettierrc @@ -0,0 +1,10 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 80, + "useTabs": false, + "tabWidth": 4, + "bracketSpacing": true, + "arrowParens": "avoid" +} diff --git a/js/token-sdk/eslint.config.cjs b/js/token-sdk/eslint.config.cjs new file mode 100644 index 0000000000..06f1a6f6b0 --- /dev/null +++ b/js/token-sdk/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', + 'eslint.config.cjs', + ], + }, + 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: ['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, + 'no-redeclare': 0, + }, + }, + { + 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', + 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, + 'no-redeclare': 0, + }, + }, +]; diff --git a/js/token-sdk/package.json b/js/token-sdk/package.json new file mode 100644 index 0000000000..8c27baa265 --- /dev/null +++ b/js/token-sdk/package.json @@ -0,0 +1,73 @@ +{ + "name": "@lightprotocol/token-sdk", + "version": "0.1.0", + "description": "Light Protocol Token SDK for Solana Kit (web3.js v2)", + "type": "module", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./codecs": { + "import": "./dist/codecs/index.js", + "types": "./dist/codecs/index.d.ts" + }, + "./instructions": { + "import": "./dist/instructions/index.js", + "types": "./dist/instructions/index.d.ts" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint .", + "format": "prettier --write .", + "prepublishOnly": "pnpm run build" + }, + "peerDependencies": { + "@solana/kit": "^2.1.0" + }, + "dependencies": { + "@solana/addresses": "^2.1.0", + "@solana/codecs": "^2.1.0", + "@solana/instructions": "^2.1.0", + "@solana/signers": "^2.1.0", + "@solana/transaction-messages": "^2.1.0", + "@solana/keys": "^2.1.0" + }, + "devDependencies": { + "@solana/kit": "^2.1.0", + "@eslint/js": "9.36.0", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "eslint": "^9.36.0", + "prettier": "^3.3.3", + "typescript": "^5.7.3", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18" + }, + "keywords": [ + "solana", + "light-protocol", + "compressed-token", + "zk-compression", + "web3" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/Lightprotocol/light-protocol.git", + "directory": "js/token-sdk" + } +} diff --git a/js/token-sdk/src/codecs/compressible.ts b/js/token-sdk/src/codecs/compressible.ts new file mode 100644 index 0000000000..fd8e0ac21a --- /dev/null +++ b/js/token-sdk/src/codecs/compressible.ts @@ -0,0 +1,170 @@ +/** + * Compressible extension codecs using Solana Kit patterns. + */ + +import { + type Codec, + type Decoder, + type Encoder, + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + getU32Decoder, + getU32Encoder, + getBytesDecoder, + getBytesEncoder, + getArrayDecoder, + getArrayEncoder, + addDecoderSizePrefix, + addEncoderSizePrefix, + getOptionEncoder, + getOptionDecoder, + fixEncoderSize, + fixDecoderSize, +} from '@solana/codecs'; + +import type { + CompressToPubkey, + CompressibleExtensionInstructionData, + CreateAtaInstructionData, +} from './types.js'; + +import { DISCRIMINATOR } from '../constants.js'; + +// ============================================================================ +// VEC CODEC (Borsh-style: u32 length prefix) +// ============================================================================ + +function getVecEncoder(itemEncoder: Encoder): Encoder { + return addEncoderSizePrefix( + getArrayEncoder(itemEncoder), + getU32Encoder(), + ) as Encoder; +} + +function getVecDecoder(itemDecoder: Decoder): Decoder { + return addDecoderSizePrefix(getArrayDecoder(itemDecoder), getU32Decoder()); +} + +// ============================================================================ +// COMPRESS TO PUBKEY CODEC +// ============================================================================ + +export const getCompressToPubkeyEncoder = (): Encoder => + getStructEncoder([ + ['bump', getU8Encoder()], + ['programId', fixEncoderSize(getBytesEncoder(), 32)], + ['seeds', getVecEncoder(getVecEncoder(getU8Encoder()))], + ]) as unknown as Encoder; + +export const getCompressToPubkeyDecoder = (): Decoder => + getStructDecoder([ + ['bump', getU8Decoder()], + ['programId', fixDecoderSize(getBytesDecoder(), 32)], + ['seeds', getVecDecoder(getVecDecoder(getU8Decoder()))], + ]) as unknown as Decoder; + +export const getCompressToPubkeyCodec = (): Codec => + combineCodec(getCompressToPubkeyEncoder(), getCompressToPubkeyDecoder()); + +// ============================================================================ +// COMPRESSIBLE EXTENSION INSTRUCTION DATA CODEC +// ============================================================================ + +export const getCompressibleExtensionDataEncoder = + (): Encoder => + getStructEncoder([ + ['tokenAccountVersion', getU8Encoder()], + ['rentPayment', getU8Encoder()], + ['compressionOnly', getU8Encoder()], + ['writeTopUp', getU32Encoder()], + [ + 'compressToPubkey', + getOptionEncoder(getCompressToPubkeyEncoder()), + ], + ]); + +export const getCompressibleExtensionDataDecoder = + (): Decoder => + getStructDecoder([ + ['tokenAccountVersion', getU8Decoder()], + ['rentPayment', getU8Decoder()], + ['compressionOnly', getU8Decoder()], + ['writeTopUp', getU32Decoder()], + [ + 'compressToPubkey', + getOptionDecoder(getCompressToPubkeyDecoder()), + ], + ]) as unknown as Decoder; + +export const getCompressibleExtensionDataCodec = + (): Codec => + combineCodec( + getCompressibleExtensionDataEncoder(), + getCompressibleExtensionDataDecoder(), + ); + +// ============================================================================ +// CREATE ATA INSTRUCTION DATA CODEC +// ============================================================================ + +export const getCreateAtaDataEncoder = (): Encoder => + getStructEncoder([ + ['bump', getU8Encoder()], + [ + 'compressibleConfig', + getOptionEncoder(getCompressibleExtensionDataEncoder()), + ], + ]); + +export const getCreateAtaDataDecoder = (): Decoder => + getStructDecoder([ + ['bump', getU8Decoder()], + [ + 'compressibleConfig', + getOptionDecoder(getCompressibleExtensionDataDecoder()), + ], + ]) as unknown as Decoder; + +export const getCreateAtaDataCodec = (): Codec => + combineCodec(getCreateAtaDataEncoder(), getCreateAtaDataDecoder()); + +// ============================================================================ +// FULL INSTRUCTION ENCODERS +// ============================================================================ + +/** + * Encodes the CreateAssociatedTokenAccount instruction data. + */ +export function encodeCreateAtaInstructionData( + data: CreateAtaInstructionData, + idempotent = false, +): Uint8Array { + const discriminator = idempotent + ? DISCRIMINATOR.CREATE_ATA_IDEMPOTENT + : DISCRIMINATOR.CREATE_ATA; + + const dataEncoder = getCreateAtaDataEncoder(); + const dataBytes = dataEncoder.encode(data); + + const result = new Uint8Array(1 + dataBytes.length); + result[0] = discriminator; + result.set(new Uint8Array(dataBytes), 1); + + return result; +} + +/** + * Default compressible extension params for rent-free ATAs. + */ +export function defaultCompressibleParams(): CompressibleExtensionInstructionData { + return { + tokenAccountVersion: 0, + rentPayment: 0, + compressionOnly: 0, + writeTopUp: 0, + compressToPubkey: null, + }; +} diff --git a/js/token-sdk/src/codecs/index.ts b/js/token-sdk/src/codecs/index.ts new file mode 100644 index 0000000000..532c2139e8 --- /dev/null +++ b/js/token-sdk/src/codecs/index.ts @@ -0,0 +1,49 @@ +/** + * Light Token SDK Codecs + * + * Serialization codecs for Light Token instruction data using Solana Kit patterns. + */ + +// Types +export * from './types.js'; + +// Transfer2 codecs +export { + getCompressionEncoder, + getCompressionDecoder, + getCompressionCodec, + getPackedMerkleContextEncoder, + getPackedMerkleContextDecoder, + getPackedMerkleContextCodec, + getMultiInputTokenDataEncoder, + getMultiInputTokenDataDecoder, + getMultiInputTokenDataCodec, + getMultiTokenOutputDataEncoder, + getMultiTokenOutputDataDecoder, + getMultiTokenOutputDataCodec, + getCpiContextEncoder, + getCpiContextDecoder, + getCpiContextCodec, + getCompressedProofEncoder, + getCompressedProofDecoder, + getCompressedProofCodec, + getTransfer2BaseEncoder, + getTransfer2BaseDecoder, + encodeTransfer2InstructionData, + type Transfer2BaseInstructionData, +} from './transfer2.js'; + +// Compressible codecs +export { + getCompressToPubkeyEncoder, + getCompressToPubkeyDecoder, + getCompressToPubkeyCodec, + getCompressibleExtensionDataEncoder, + getCompressibleExtensionDataDecoder, + getCompressibleExtensionDataCodec, + getCreateAtaDataEncoder, + getCreateAtaDataDecoder, + getCreateAtaDataCodec, + encodeCreateAtaInstructionData, + defaultCompressibleParams, +} from './compressible.js'; diff --git a/js/token-sdk/src/codecs/transfer2.ts b/js/token-sdk/src/codecs/transfer2.ts new file mode 100644 index 0000000000..5eac0adaeb --- /dev/null +++ b/js/token-sdk/src/codecs/transfer2.ts @@ -0,0 +1,396 @@ +/** + * Transfer2 instruction codecs using Solana Kit patterns. + */ + +import { + type Codec, + type Decoder, + type Encoder, + combineCodec, + getStructDecoder, + getStructEncoder, + getU8Decoder, + getU8Encoder, + getU16Decoder, + getU16Encoder, + getU32Decoder, + getU32Encoder, + getU64Decoder, + getU64Encoder, + getBooleanDecoder, + getBooleanEncoder, + getArrayDecoder, + getArrayEncoder, + getBytesDecoder, + getBytesEncoder, + addDecoderSizePrefix, + addEncoderSizePrefix, + getOptionEncoder, + getOptionDecoder, + fixEncoderSize, + fixDecoderSize, +} from '@solana/codecs'; + +import type { + Compression, + PackedMerkleContext, + MultiInputTokenDataWithContext, + MultiTokenTransferOutputData, + CompressedCpiContext, + CompressedProof, + Transfer2InstructionData, +} from './types.js'; + +import { DISCRIMINATOR } from '../constants.js'; + +// ============================================================================ +// COMPRESSION CODEC +// ============================================================================ + +export const getCompressionEncoder = (): Encoder => + getStructEncoder([ + ['mode', getU8Encoder()], + ['amount', getU64Encoder()], + ['mint', getU8Encoder()], + ['sourceOrRecipient', getU8Encoder()], + ['authority', getU8Encoder()], + ['poolAccountIndex', getU8Encoder()], + ['poolIndex', getU8Encoder()], + ['bump', getU8Encoder()], + ['decimals', getU8Encoder()], + ]); + +export const getCompressionDecoder = (): Decoder => + getStructDecoder([ + ['mode', getU8Decoder()], + ['amount', getU64Decoder()], + ['mint', getU8Decoder()], + ['sourceOrRecipient', getU8Decoder()], + ['authority', getU8Decoder()], + ['poolAccountIndex', getU8Decoder()], + ['poolIndex', getU8Decoder()], + ['bump', getU8Decoder()], + ['decimals', getU8Decoder()], + ]); + +export const getCompressionCodec = (): Codec => + combineCodec(getCompressionEncoder(), getCompressionDecoder()); + +// ============================================================================ +// PACKED MERKLE CONTEXT CODEC +// ============================================================================ + +export const getPackedMerkleContextEncoder = (): Encoder => + getStructEncoder([ + ['merkleTreePubkeyIndex', getU8Encoder()], + ['queuePubkeyIndex', getU8Encoder()], + ['leafIndex', getU32Encoder()], + ['proveByIndex', getBooleanEncoder()], + ]); + +export const getPackedMerkleContextDecoder = (): Decoder => + getStructDecoder([ + ['merkleTreePubkeyIndex', getU8Decoder()], + ['queuePubkeyIndex', getU8Decoder()], + ['leafIndex', getU32Decoder()], + ['proveByIndex', getBooleanDecoder()], + ]); + +export const getPackedMerkleContextCodec = (): Codec => + combineCodec( + getPackedMerkleContextEncoder(), + getPackedMerkleContextDecoder(), + ); + +// ============================================================================ +// INPUT TOKEN DATA CODEC +// ============================================================================ + +export const getMultiInputTokenDataEncoder = + (): Encoder => + getStructEncoder([ + ['owner', getU8Encoder()], + ['amount', getU64Encoder()], + ['hasDelegate', getBooleanEncoder()], + ['delegate', getU8Encoder()], + ['mint', getU8Encoder()], + ['version', getU8Encoder()], + ['merkleContext', getPackedMerkleContextEncoder()], + ['rootIndex', getU16Encoder()], + ]); + +export const getMultiInputTokenDataDecoder = + (): Decoder => + getStructDecoder([ + ['owner', getU8Decoder()], + ['amount', getU64Decoder()], + ['hasDelegate', getBooleanDecoder()], + ['delegate', getU8Decoder()], + ['mint', getU8Decoder()], + ['version', getU8Decoder()], + ['merkleContext', getPackedMerkleContextDecoder()], + ['rootIndex', getU16Decoder()], + ]); + +export const getMultiInputTokenDataCodec = + (): Codec => + combineCodec( + getMultiInputTokenDataEncoder(), + getMultiInputTokenDataDecoder(), + ); + +// ============================================================================ +// OUTPUT TOKEN DATA CODEC +// ============================================================================ + +export const getMultiTokenOutputDataEncoder = + (): Encoder => + getStructEncoder([ + ['owner', getU8Encoder()], + ['amount', getU64Encoder()], + ['hasDelegate', getBooleanEncoder()], + ['delegate', getU8Encoder()], + ['mint', getU8Encoder()], + ['version', getU8Encoder()], + ]); + +export const getMultiTokenOutputDataDecoder = + (): Decoder => + getStructDecoder([ + ['owner', getU8Decoder()], + ['amount', getU64Decoder()], + ['hasDelegate', getBooleanDecoder()], + ['delegate', getU8Decoder()], + ['mint', getU8Decoder()], + ['version', getU8Decoder()], + ]); + +export const getMultiTokenOutputDataCodec = + (): Codec => + combineCodec( + getMultiTokenOutputDataEncoder(), + getMultiTokenOutputDataDecoder(), + ); + +// ============================================================================ +// CPI CONTEXT CODEC +// ============================================================================ + +export const getCpiContextEncoder = (): Encoder => + getStructEncoder([ + ['setContext', getBooleanEncoder()], + ['firstSetContext', getBooleanEncoder()], + ['cpiContextAccountIndex', getU8Encoder()], + ]); + +export const getCpiContextDecoder = (): Decoder => + getStructDecoder([ + ['setContext', getBooleanDecoder()], + ['firstSetContext', getBooleanDecoder()], + ['cpiContextAccountIndex', getU8Decoder()], + ]); + +export const getCpiContextCodec = (): Codec => + combineCodec(getCpiContextEncoder(), getCpiContextDecoder()); + +// ============================================================================ +// PROOF CODEC +// ============================================================================ + +export const getCompressedProofEncoder = (): Encoder => + getStructEncoder([ + ['a', fixEncoderSize(getBytesEncoder(), 32)], + ['b', fixEncoderSize(getBytesEncoder(), 64)], + ['c', fixEncoderSize(getBytesEncoder(), 32)], + ]) as unknown as Encoder; + +export const getCompressedProofDecoder = (): Decoder => + getStructDecoder([ + ['a', fixDecoderSize(getBytesDecoder(), 32)], + ['b', fixDecoderSize(getBytesDecoder(), 64)], + ['c', fixDecoderSize(getBytesDecoder(), 32)], + ]) as unknown as Decoder; + +export const getCompressedProofCodec = (): Codec => + combineCodec(getCompressedProofEncoder(), getCompressedProofDecoder()); + +// ============================================================================ +// VECTOR CODECS (with u32 length prefix for Borsh compatibility) +// ============================================================================ + +/** + * Creates an encoder for a Vec type (Borsh style: u32 length prefix). + */ +function getVecEncoder(itemEncoder: Encoder): Encoder { + return addEncoderSizePrefix( + getArrayEncoder(itemEncoder), + getU32Encoder(), + ) as Encoder; +} + +/** + * Creates a decoder for a Vec type (Borsh style: u32 length prefix). + */ +function getVecDecoder(itemDecoder: Decoder): Decoder { + return addDecoderSizePrefix(getArrayDecoder(itemDecoder), getU32Decoder()); +} + +// ============================================================================ +// TRANSFER2 INSTRUCTION DATA CODEC (Base fields only) +// Note: TLV fields require manual serialization due to complex nested structures +// ============================================================================ + +/** + * Base Transfer2 instruction data (without TLV fields). + */ +export interface Transfer2BaseInstructionData { + withTransactionHash: boolean; + withLamportsChangeAccountMerkleTreeIndex: boolean; + lamportsChangeAccountMerkleTreeIndex: number; + lamportsChangeAccountOwnerIndex: number; + outputQueue: number; + maxTopUp: number; + cpiContext: CompressedCpiContext | null; + compressions: readonly Compression[] | null; + proof: CompressedProof | null; + inTokenData: readonly MultiInputTokenDataWithContext[]; + outTokenData: readonly MultiTokenTransferOutputData[]; + inLamports: readonly bigint[] | null; + outLamports: readonly bigint[] | null; +} + +export const getTransfer2BaseEncoder = + (): Encoder => + getStructEncoder([ + ['withTransactionHash', getBooleanEncoder()], + ['withLamportsChangeAccountMerkleTreeIndex', getBooleanEncoder()], + ['lamportsChangeAccountMerkleTreeIndex', getU8Encoder()], + ['lamportsChangeAccountOwnerIndex', getU8Encoder()], + ['outputQueue', getU8Encoder()], + ['maxTopUp', getU16Encoder()], + ['cpiContext', getOptionEncoder(getCpiContextEncoder())], + [ + 'compressions', + getOptionEncoder(getVecEncoder(getCompressionEncoder())), + ], + ['proof', getOptionEncoder(getCompressedProofEncoder())], + ['inTokenData', getVecEncoder(getMultiInputTokenDataEncoder())], + ['outTokenData', getVecEncoder(getMultiTokenOutputDataEncoder())], + ['inLamports', getOptionEncoder(getVecEncoder(getU64Encoder()))], + ['outLamports', getOptionEncoder(getVecEncoder(getU64Encoder()))], + ]) as unknown as Encoder; + +export const getTransfer2BaseDecoder = + (): Decoder => + getStructDecoder([ + ['withTransactionHash', getBooleanDecoder()], + ['withLamportsChangeAccountMerkleTreeIndex', getBooleanDecoder()], + ['lamportsChangeAccountMerkleTreeIndex', getU8Decoder()], + ['lamportsChangeAccountOwnerIndex', getU8Decoder()], + ['outputQueue', getU8Decoder()], + ['maxTopUp', getU16Decoder()], + ['cpiContext', getOptionDecoder(getCpiContextDecoder())], + [ + 'compressions', + getOptionDecoder(getVecDecoder(getCompressionDecoder())), + ], + ['proof', getOptionDecoder(getCompressedProofDecoder())], + ['inTokenData', getVecDecoder(getMultiInputTokenDataDecoder())], + ['outTokenData', getVecDecoder(getMultiTokenOutputDataDecoder())], + ['inLamports', getOptionDecoder(getVecDecoder(getU64Decoder()))], + ['outLamports', getOptionDecoder(getVecDecoder(getU64Decoder()))], + ]) as unknown as Decoder; + +// ============================================================================ +// TRANSFER2 FULL ENCODER (with discriminator and TLV fields) +// ============================================================================ + +/** + * Encodes the full Transfer2 instruction data including discriminator and TLV. + */ +export function encodeTransfer2InstructionData( + data: Transfer2InstructionData, +): Uint8Array { + const baseEncoder = getTransfer2BaseEncoder(); + + // Encode base data + const baseData: Transfer2BaseInstructionData = { + withTransactionHash: data.withTransactionHash, + withLamportsChangeAccountMerkleTreeIndex: + data.withLamportsChangeAccountMerkleTreeIndex, + lamportsChangeAccountMerkleTreeIndex: + data.lamportsChangeAccountMerkleTreeIndex, + lamportsChangeAccountOwnerIndex: data.lamportsChangeAccountOwnerIndex, + outputQueue: data.outputQueue, + maxTopUp: data.maxTopUp, + cpiContext: data.cpiContext, + compressions: data.compressions, + proof: data.proof, + inTokenData: data.inTokenData, + outTokenData: data.outTokenData, + inLamports: data.inLamports, + outLamports: data.outLamports, + }; + + const baseBytes = baseEncoder.encode(baseData); + + // Encode TLV fields (Option>>) + const inTlvBytes = encodeTlv(data.inTlv); + const outTlvBytes = encodeTlv(data.outTlv); + + // Combine: discriminator + base + inTlv + outTlv + const result = new Uint8Array( + 1 + baseBytes.length + inTlvBytes.length + outTlvBytes.length, + ); + result[0] = DISCRIMINATOR.TRANSFER2; + result.set(baseBytes, 1); + result.set(inTlvBytes, 1 + baseBytes.length); + result.set(outTlvBytes, 1 + baseBytes.length + inTlvBytes.length); + + return result; +} + +/** + * Encodes TLV data as Option>>. + * For now, we support null (None) or empty arrays. + * Full extension serialization would require additional codec implementations. + */ +function encodeTlv(tlv: unknown[][] | null): Uint8Array { + if (tlv === null) { + // Option::None + return new Uint8Array([0]); + } + + // Option::Some + Vec> + const chunks: Uint8Array[] = []; + + // Option::Some + chunks.push(new Uint8Array([1])); + + // Outer vec length (u32) + const outerLen = new Uint8Array(4); + new DataView(outerLen.buffer).setUint32(0, tlv.length, true); + chunks.push(outerLen); + + // For each inner vec + for (const innerVec of tlv) { + // Inner vec length (u32) + const innerLen = new Uint8Array(4); + new DataView(innerLen.buffer).setUint32(0, innerVec.length, true); + chunks.push(innerLen); + + // Note: Extension serialization would go here + // For now, we only support empty inner vectors + } + + // Concatenate all chunks + const totalLen = chunks.reduce((sum, chunk) => sum + chunk.length, 0); + const result = new Uint8Array(totalLen); + let offset = 0; + for (const chunk of chunks) { + result.set(chunk, offset); + offset += chunk.length; + } + + return result; +} diff --git a/js/token-sdk/src/codecs/types.ts b/js/token-sdk/src/codecs/types.ts new file mode 100644 index 0000000000..cdd3f959dc --- /dev/null +++ b/js/token-sdk/src/codecs/types.ts @@ -0,0 +1,304 @@ +/** + * Type definitions for Light Token codecs + */ + +import type { Address } from '@solana/addresses'; + +// ============================================================================ +// COMPRESSION TYPES +// ============================================================================ + +/** + * Compression operation for Transfer2 instruction. + * Describes how to compress/decompress tokens. + */ +export interface Compression { + /** Compression mode: 0=compress, 1=decompress, 2=compress_and_close */ + mode: number; + /** Amount to compress/decompress */ + amount: bigint; + /** Index of mint in packed accounts */ + mint: number; + /** Index of source (compress) or recipient (decompress) in packed accounts */ + sourceOrRecipient: number; + /** Index of authority in packed accounts */ + authority: number; + /** Index of pool account in packed accounts */ + poolAccountIndex: number; + /** Pool index (for multi-pool mints) */ + poolIndex: number; + /** PDA bump for pool derivation */ + bump: number; + /** Token decimals (or rent_sponsor_is_signer flag for CompressAndClose) */ + decimals: number; +} + +// ============================================================================ +// MERKLE CONTEXT TYPES +// ============================================================================ + +/** + * Packed merkle context for compressed accounts. + */ +export interface PackedMerkleContext { + /** Index of merkle tree pubkey in packed accounts */ + merkleTreePubkeyIndex: number; + /** Index of queue pubkey in packed accounts */ + queuePubkeyIndex: number; + /** Leaf index in the merkle tree */ + leafIndex: number; + /** Whether to prove by index (vs by hash) */ + proveByIndex: boolean; +} + +// ============================================================================ +// TOKEN DATA TYPES +// ============================================================================ + +/** + * Input token data with merkle context for Transfer2. + */ +export interface MultiInputTokenDataWithContext { + /** Index of owner in packed accounts */ + owner: number; + /** Token amount */ + amount: bigint; + /** Whether token has a delegate */ + hasDelegate: boolean; + /** Index of delegate in packed accounts (if hasDelegate) */ + delegate: number; + /** Index of mint in packed accounts */ + mint: number; + /** Token account version */ + version: number; + /** Merkle context for the compressed account */ + merkleContext: PackedMerkleContext; + /** Root index for validity proof */ + rootIndex: number; +} + +/** + * Output token data for Transfer2. + */ +export interface MultiTokenTransferOutputData { + /** Index of owner in packed accounts */ + owner: number; + /** Token amount */ + amount: bigint; + /** Whether token has a delegate */ + hasDelegate: boolean; + /** Index of delegate in packed accounts (if hasDelegate) */ + delegate: number; + /** Index of mint in packed accounts */ + mint: number; + /** Token account version */ + version: number; +} + +// ============================================================================ +// CPI CONTEXT +// ============================================================================ + +/** + * CPI context for compressed account operations. + */ +export interface CompressedCpiContext { + /** Whether to set the CPI context */ + setContext: boolean; + /** Whether this is the first set context call */ + firstSetContext: boolean; + /** Index of CPI context account in packed accounts */ + cpiContextAccountIndex: number; +} + +// ============================================================================ +// PROOF TYPES +// ============================================================================ + +/** + * Groth16 proof for compressed account validity. + */ +export interface CompressedProof { + /** Proof element A (32 bytes) */ + a: Uint8Array; + /** Proof element B (64 bytes) */ + b: Uint8Array; + /** Proof element C (32 bytes) */ + c: Uint8Array; +} + +// ============================================================================ +// EXTENSION TYPES +// ============================================================================ + +/** + * Token metadata extension data. + */ +export interface TokenMetadataExtension { + /** Update authority (optional) */ + updateAuthority: Address | null; + /** Token name */ + name: Uint8Array; + /** Token symbol */ + symbol: Uint8Array; + /** Token URI */ + uri: Uint8Array; + /** Additional metadata key-value pairs */ + additionalMetadata: Array<{ key: Uint8Array; value: Uint8Array }> | null; +} + +/** + * CompressedOnly extension data. + */ +export interface CompressedOnlyExtension { + /** Delegated amount */ + delegatedAmount: bigint; + /** Withheld transfer fee */ + withheldTransferFee: bigint; + /** Whether account is frozen */ + isFrozen: boolean; + /** Compression index */ + compressionIndex: number; + /** Whether this is an ATA */ + isAta: boolean; + /** PDA bump */ + bump: number; + /** Owner index in packed accounts */ + ownerIndex: number; +} + +/** + * Rent configuration for compressible accounts. + */ +export interface RentConfig { + /** Base rent in lamports */ + baseRent: number; + /** Compression cost in lamports */ + compressionCost: number; + /** Lamports per byte per epoch */ + lamportsPerBytePerEpoch: number; + /** Maximum funded epochs */ + maxFundedEpochs: number; + /** Maximum top-up amount */ + maxTopUp: number; +} + +/** + * Compression info for compressible accounts. + */ +export interface CompressionInfo { + /** Config account version */ + configAccountVersion: number; + /** Compress-to pubkey type: 0=none, 1=owner, 2=custom */ + compressToPubkey: number; + /** Account version */ + accountVersion: number; + /** Lamports per write operation */ + lamportsPerWrite: number; + /** Compression authority */ + compressionAuthority: Address; + /** Rent sponsor */ + rentSponsor: Address; + /** Last claimed slot */ + lastClaimedSlot: bigint; + /** Rent exemption paid */ + rentExemptionPaid: number; + /** Reserved bytes */ + reserved: number; + /** Rent configuration */ + rentConfig: RentConfig; +} + +/** + * Extension instruction data (union type). + */ +export type ExtensionInstructionData = + | { type: 'TokenMetadata'; data: TokenMetadataExtension } + | { type: 'CompressedOnly'; data: CompressedOnlyExtension } + | { type: 'Compressible'; data: CompressionInfo }; + +// ============================================================================ +// TRANSFER2 INSTRUCTION DATA +// ============================================================================ + +/** + * Full Transfer2 instruction data. + */ +export interface Transfer2InstructionData { + /** Whether to include transaction hash in hashing */ + withTransactionHash: boolean; + /** Whether to include lamports change account merkle tree index */ + withLamportsChangeAccountMerkleTreeIndex: boolean; + /** Merkle tree index for lamports change account */ + lamportsChangeAccountMerkleTreeIndex: number; + /** Owner index for lamports change account */ + lamportsChangeAccountOwnerIndex: number; + /** Output queue index */ + outputQueue: number; + /** Maximum top-up for rent */ + maxTopUp: number; + /** CPI context (optional) */ + cpiContext: CompressedCpiContext | null; + /** Compression operations (optional) */ + compressions: Compression[] | null; + /** Validity proof (optional) */ + proof: CompressedProof | null; + /** Input token data */ + inTokenData: MultiInputTokenDataWithContext[]; + /** Output token data */ + outTokenData: MultiTokenTransferOutputData[]; + /** Input lamports (optional) */ + inLamports: bigint[] | null; + /** Output lamports (optional) */ + outLamports: bigint[] | null; + /** Input TLV extensions (optional) */ + inTlv: ExtensionInstructionData[][] | null; + /** Output TLV extensions (optional) */ + outTlv: ExtensionInstructionData[][] | null; +} + +// ============================================================================ +// COMPRESSIBLE CONFIG TYPES +// ============================================================================ + +/** + * Compress-to pubkey configuration. + */ +export interface CompressToPubkey { + /** PDA bump */ + bump: number; + /** Program ID for the PDA */ + programId: Uint8Array; + /** Seeds for the PDA */ + seeds: Uint8Array[]; +} + +/** + * Compressible extension instruction data for create instructions. + */ +export interface CompressibleExtensionInstructionData { + /** Token account version */ + tokenAccountVersion: number; + /** Number of epochs to pre-pay rent */ + rentPayment: number; + /** Compression only mode: 0=false, 1=true */ + compressionOnly: number; + /** Lamports per write for top-up */ + writeTopUp: number; + /** Compress-to pubkey configuration (optional) */ + compressToPubkey: CompressToPubkey | null; +} + +// ============================================================================ +// CREATE ATA TYPES +// ============================================================================ + +/** + * Create Associated Token Account instruction data. + */ +export interface CreateAtaInstructionData { + /** PDA bump */ + bump: number; + /** Compressible config (optional) */ + compressibleConfig: CompressibleExtensionInstructionData | null; +} diff --git a/js/token-sdk/src/constants.ts b/js/token-sdk/src/constants.ts new file mode 100644 index 0000000000..2ce54f7e17 --- /dev/null +++ b/js/token-sdk/src/constants.ts @@ -0,0 +1,180 @@ +/** + * Light Protocol Token SDK Constants + */ + +import { address, type Address } from '@solana/addresses'; + +// ============================================================================ +// PROGRAM IDS +// ============================================================================ + +/** Light Token Program ID */ +export const LIGHT_TOKEN_PROGRAM_ID: Address = address( + 'cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m', +); + +/** Light System Program ID */ +export const LIGHT_SYSTEM_PROGRAM_ID: Address = address( + 'SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7', +); + +/** Account Compression Program ID */ +export const ACCOUNT_COMPRESSION_PROGRAM_ID: Address = address( + 'compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq', +); + +/** SPL Token Program ID */ +export const SPL_TOKEN_PROGRAM_ID: Address = address( + 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', +); + +/** SPL Token 2022 Program ID */ +export const SPL_TOKEN_2022_PROGRAM_ID: Address = address( + 'TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb', +); + +/** System Program ID */ +export const SYSTEM_PROGRAM_ID: Address = address( + '11111111111111111111111111111111', +); + +// ============================================================================ +// KNOWN ACCOUNTS +// ============================================================================ + +/** CPI Authority - used for cross-program invocations */ +export const CPI_AUTHORITY: Address = address( + 'GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy', +); + +/** Mint Address Tree - default tree for compressed mint addresses */ +export const MINT_ADDRESS_TREE: Address = address( + 'amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx', +); + +/** Native Mint (wrapped SOL) */ +export const NATIVE_MINT: Address = address( + 'So11111111111111111111111111111111111111112', +); + +// ============================================================================ +// INSTRUCTION DISCRIMINATORS +// ============================================================================ + +/** + * Instruction discriminators for the Light Token program. + * Uses SPL-compatible values (3-18) plus custom values (100+). + */ +export const DISCRIMINATOR = { + /** CToken transfer between decompressed accounts */ + TRANSFER: 3, + /** Approve delegate on CToken account */ + APPROVE: 4, + /** Revoke delegate on CToken account */ + REVOKE: 5, + /** Mint tokens to CToken account */ + MINT_TO: 7, + /** Burn tokens from CToken account */ + BURN: 8, + /** Close CToken account */ + CLOSE: 9, + /** Freeze CToken account */ + FREEZE: 10, + /** Thaw frozen CToken account */ + THAW: 11, + /** Transfer with decimals validation */ + TRANSFER_CHECKED: 12, + /** Mint with decimals validation */ + MINT_TO_CHECKED: 14, + /** Burn with decimals validation */ + BURN_CHECKED: 15, + /** Create CToken account */ + CREATE_TOKEN_ACCOUNT: 18, + /** Create associated CToken account */ + CREATE_ATA: 100, + /** Batch transfer instruction (compressed/decompressed) */ + TRANSFER2: 101, + /** Create associated CToken account (idempotent) */ + CREATE_ATA_IDEMPOTENT: 102, + /** Batch mint action instruction */ + MINT_ACTION: 103, + /** Claim rent from compressible accounts */ + CLAIM: 104, + /** Withdraw from funding pool */ + WITHDRAW_FUNDING_POOL: 105, +} as const; + +export type Discriminator = (typeof DISCRIMINATOR)[keyof typeof DISCRIMINATOR]; + +// ============================================================================ +// COMPRESSION MODES +// ============================================================================ + +/** + * Compression mode for Transfer2 instruction. + */ +export const COMPRESSION_MODE = { + /** Compress: SPL/CToken -> compressed token */ + COMPRESS: 0, + /** Decompress: compressed token -> SPL/CToken */ + DECOMPRESS: 1, + /** Compress and close the source account */ + COMPRESS_AND_CLOSE: 2, +} as const; + +export type CompressionMode = + (typeof COMPRESSION_MODE)[keyof typeof COMPRESSION_MODE]; + +// ============================================================================ +// EXTENSION DISCRIMINANTS +// ============================================================================ + +/** + * Extension discriminant values for TLV data. + */ +export const EXTENSION_DISCRIMINANT = { + /** Token metadata extension */ + TOKEN_METADATA: 19, + /** CompressedOnly extension */ + COMPRESSED_ONLY: 31, + /** Compressible extension */ + COMPRESSIBLE: 32, +} as const; + +export type ExtensionDiscriminant = + (typeof EXTENSION_DISCRIMINANT)[keyof typeof EXTENSION_DISCRIMINANT]; + +// ============================================================================ +// SEEDS +// ============================================================================ + +/** Compressed mint PDA seed */ +export const COMPRESSED_MINT_SEED = 'compressed_mint'; + +/** Pool PDA seed for SPL interface */ +export const POOL_SEED = 'pool'; + +/** Restricted pool PDA seed */ +export const RESTRICTED_POOL_SEED = 'restricted'; + +// ============================================================================ +// ACCOUNT SIZES +// ============================================================================ + +/** Size of a compressed mint account */ +export const MINT_ACCOUNT_SIZE = 82n; + +/** Base size of a CToken account (without extensions) */ +export const BASE_TOKEN_ACCOUNT_SIZE = 266n; + +/** Extension metadata overhead (Vec length) */ +export const EXTENSION_METADATA_SIZE = 4n; + +/** CompressedOnly extension size */ +export const COMPRESSED_ONLY_EXTENSION_SIZE = 16n; + +/** Transfer fee account extension size */ +export const TRANSFER_FEE_ACCOUNT_EXTENSION_SIZE = 9n; + +/** Transfer hook account extension size */ +export const TRANSFER_HOOK_ACCOUNT_EXTENSION_SIZE = 2n; diff --git a/js/token-sdk/src/generated/.gitkeep b/js/token-sdk/src/generated/.gitkeep new file mode 100644 index 0000000000..32bd937620 --- /dev/null +++ b/js/token-sdk/src/generated/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder for Codama-generated code +# Run `pnpm run generate` in js/token-idl to populate this directory diff --git a/js/token-sdk/src/index.ts b/js/token-sdk/src/index.ts new file mode 100644 index 0000000000..1689e81de6 --- /dev/null +++ b/js/token-sdk/src/index.ts @@ -0,0 +1,189 @@ +/** + * Light Protocol Token SDK + * + * TypeScript SDK for Light Protocol compressed tokens using Solana Kit (web3.js v2). + * + * @example + * ```typescript + * import { + * createTransferInstruction, + * createAssociatedTokenAccountInstruction, + * deriveAssociatedTokenAddress, + * LIGHT_TOKEN_PROGRAM_ID, + * } from '@lightprotocol/token-sdk'; + * + * // Derive ATA address + * const { address: ata, bump } = await deriveAssociatedTokenAddress(owner, mint); + * + * // Create transfer instruction + * const transferIx = createTransferInstruction({ + * source: sourceAta, + * destination: destAta, + * amount: 1000n, + * authority: owner, + * }); + * ``` + * + * @packageDocumentation + */ + +// ============================================================================ +// CONSTANTS +// ============================================================================ + +export { + // Program IDs + LIGHT_TOKEN_PROGRAM_ID, + LIGHT_SYSTEM_PROGRAM_ID, + ACCOUNT_COMPRESSION_PROGRAM_ID, + SPL_TOKEN_PROGRAM_ID, + SPL_TOKEN_2022_PROGRAM_ID, + SYSTEM_PROGRAM_ID, + + // Known accounts + CPI_AUTHORITY, + MINT_ADDRESS_TREE, + NATIVE_MINT, + + // Instruction discriminators + DISCRIMINATOR, + type Discriminator, + + // Compression modes + COMPRESSION_MODE, + type CompressionMode, + + // Extension discriminants + EXTENSION_DISCRIMINANT, + type ExtensionDiscriminant, + + // Seeds + COMPRESSED_MINT_SEED, + POOL_SEED, + RESTRICTED_POOL_SEED, + + // Account sizes + MINT_ACCOUNT_SIZE, + BASE_TOKEN_ACCOUNT_SIZE, + EXTENSION_METADATA_SIZE, + COMPRESSED_ONLY_EXTENSION_SIZE, + TRANSFER_FEE_ACCOUNT_EXTENSION_SIZE, + TRANSFER_HOOK_ACCOUNT_EXTENSION_SIZE, +} from './constants.js'; + +// ============================================================================ +// UTILITIES +// ============================================================================ + +export { + // PDA derivation + deriveAssociatedTokenAddress, + getAssociatedTokenAddressWithBump, + deriveMintAddress, + derivePoolAddress, + deriveCpiAuthority, + + // Validation + isLightTokenAccount, + determineTransferType, + validateAtaDerivation, + validatePositiveAmount, + validateDecimals, +} from './utils/index.js'; + +// ============================================================================ +// CODECS +// ============================================================================ + +export { + // Types + type Compression, + type PackedMerkleContext, + type MultiInputTokenDataWithContext, + type MultiTokenTransferOutputData, + type CompressedCpiContext, + type CompressedProof, + type TokenMetadataExtension, + type CompressedOnlyExtension, + type RentConfig, + type CompressionInfo, + type ExtensionInstructionData, + type Transfer2InstructionData, + type CompressToPubkey, + type CompressibleExtensionInstructionData, + type CreateAtaInstructionData, + + // Transfer2 codecs + getCompressionCodec, + getPackedMerkleContextCodec, + getMultiInputTokenDataCodec, + getMultiTokenOutputDataCodec, + getCpiContextCodec, + getCompressedProofCodec, + encodeTransfer2InstructionData, + type Transfer2BaseInstructionData, + + // Compressible codecs + getCompressToPubkeyCodec, + getCompressibleExtensionDataCodec, + getCreateAtaDataCodec, + encodeCreateAtaInstructionData, + defaultCompressibleParams, +} from './codecs/index.js'; + +// ============================================================================ +// INSTRUCTIONS +// ============================================================================ + +export { + // Transfer + createTransferInstruction, + createTransferCheckedInstruction, + createTransferInterfaceInstruction, + requiresCompression, + type TransferParams, + type TransferCheckedParams, + type TransferType, + type TransferInterfaceParams, + type TransferInterfaceResult, + + // Account + createAssociatedTokenAccountInstruction, + createAssociatedTokenAccountIdempotentInstruction, + createCloseAccountInstruction, + type CreateAtaParams, + type CreateAtaResult, + type CloseAccountParams, + + // Token operations + createApproveInstruction, + createRevokeInstruction, + createBurnInstruction, + createBurnCheckedInstruction, + createFreezeInstruction, + createThawInstruction, + type ApproveParams, + type RevokeParams, + type BurnParams, + type BurnCheckedParams, + type FreezeParams, + type ThawParams, + + // Mint + createMintToInstruction, + createMintToCheckedInstruction, + type MintToParams, + type MintToCheckedParams, +} from './instructions/index.js'; + +// ============================================================================ +// RPC CLIENT (Placeholder) +// ============================================================================ + +export { + createLightRpcClient, + isLightRpcAvailable, + type LightRpcClient, + type ParsedTokenAccount, + type ValidityProof, +} from './rpc/index.js'; diff --git a/js/token-sdk/src/instructions/approve.ts b/js/token-sdk/src/instructions/approve.ts new file mode 100644 index 0000000000..e21a060b8e --- /dev/null +++ b/js/token-sdk/src/instructions/approve.ts @@ -0,0 +1,91 @@ +/** + * Approve and revoke delegate instructions. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; +import { getU64Encoder } from '@solana/codecs'; + +import { DISCRIMINATOR, LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; + +/** + * Parameters for approving a delegate. + */ +export interface ApproveParams { + /** Token account to approve delegate on */ + tokenAccount: Address; + /** Delegate to approve */ + delegate: Address; + /** Owner of the token account - must be signer */ + owner: Address; + /** Amount to delegate */ + amount: bigint; +} + +/** + * Creates an approve instruction (discriminator: 4). + * + * Approves a delegate to transfer up to the specified amount. + * + * @param params - Approve parameters + * @returns The approve instruction + */ +export function createApproveInstruction(params: ApproveParams): IInstruction { + const { tokenAccount, delegate, owner, amount } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: delegate, role: 0 }, // readonly + { address: owner, role: 2 }, // readonly+signer + ]; + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const data = new Uint8Array(1 + amountBytes.length); + data[0] = DISCRIMINATOR.APPROVE; + data.set(new Uint8Array(amountBytes), 1); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} + +/** + * Parameters for revoking a delegate. + */ +export interface RevokeParams { + /** Token account to revoke delegate from */ + tokenAccount: Address; + /** Owner of the token account - must be signer */ + owner: Address; +} + +/** + * Creates a revoke instruction (discriminator: 5). + * + * Revokes the delegate authority from the token account. + * + * @param params - Revoke parameters + * @returns The revoke instruction + */ +export function createRevokeInstruction(params: RevokeParams): IInstruction { + const { tokenAccount, owner } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: owner, role: 2 }, // readonly+signer + ]; + + // Build instruction data (just discriminator) + const data = new Uint8Array([DISCRIMINATOR.REVOKE]); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/instructions/burn.ts b/js/token-sdk/src/instructions/burn.ts new file mode 100644 index 0000000000..3f11534cf1 --- /dev/null +++ b/js/token-sdk/src/instructions/burn.ts @@ -0,0 +1,96 @@ +/** + * Burn token instructions. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; +import { getU64Encoder } from '@solana/codecs'; + +import { DISCRIMINATOR, LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; + +/** + * Parameters for burning tokens. + */ +export interface BurnParams { + /** Token account to burn from */ + tokenAccount: Address; + /** Mint address */ + mint: Address; + /** Authority (owner or delegate) - must be signer */ + authority: Address; + /** Amount to burn */ + amount: bigint; +} + +/** + * Creates a burn instruction (discriminator: 8). + * + * Burns tokens from the token account and updates mint supply. + * + * @param params - Burn parameters + * @returns The burn instruction + */ +export function createBurnInstruction(params: BurnParams): IInstruction { + const { tokenAccount, mint, authority, amount } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: mint, role: 1 }, // writable + { address: authority, role: 2 }, // readonly+signer + ]; + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const data = new Uint8Array(1 + amountBytes.length); + data[0] = DISCRIMINATOR.BURN; + data.set(new Uint8Array(amountBytes), 1); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} + +/** + * Parameters for burn checked. + */ +export interface BurnCheckedParams extends BurnParams { + /** Expected decimals */ + decimals: number; +} + +/** + * Creates a burn checked instruction (discriminator: 15). + * + * Burns tokens with decimals validation. + * + * @param params - Burn checked parameters + * @returns The burn checked instruction + */ +export function createBurnCheckedInstruction( + params: BurnCheckedParams, +): IInstruction { + const { tokenAccount, mint, authority, amount, decimals } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: mint, role: 1 }, // writable + { address: authority, role: 2 }, // readonly+signer + ]; + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const data = new Uint8Array(1 + amountBytes.length + 1); + data[0] = DISCRIMINATOR.BURN_CHECKED; + data.set(new Uint8Array(amountBytes), 1); + data[1 + amountBytes.length] = decimals; + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/instructions/close.ts b/js/token-sdk/src/instructions/close.ts new file mode 100644 index 0000000000..e6d36675f5 --- /dev/null +++ b/js/token-sdk/src/instructions/close.ts @@ -0,0 +1,51 @@ +/** + * Close token account instruction. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; + +import { DISCRIMINATOR, LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; + +/** + * Parameters for closing a token account. + */ +export interface CloseAccountParams { + /** Token account to close */ + tokenAccount: Address; + /** Destination for remaining lamports */ + destination: Address; + /** Owner of the token account - must be signer */ + owner: Address; +} + +/** + * Creates a close token account instruction (discriminator: 9). + * + * Closes a decompressed CToken account and returns rent to the destination. + * For compressible accounts, rent goes to the rent sponsor. + * + * @param params - Close account parameters + * @returns The close instruction + */ +export function createCloseAccountInstruction( + params: CloseAccountParams, +): IInstruction { + const { tokenAccount, destination, owner } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: destination, role: 1 }, // writable + { address: owner, role: 2 }, // readonly+signer + ]; + + // Build instruction data (just discriminator) + const data = new Uint8Array([DISCRIMINATOR.CLOSE]); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/instructions/create-ata.ts b/js/token-sdk/src/instructions/create-ata.ts new file mode 100644 index 0000000000..83060b7719 --- /dev/null +++ b/js/token-sdk/src/instructions/create-ata.ts @@ -0,0 +1,115 @@ +/** + * Create Associated Token Account instruction. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; + +import { LIGHT_TOKEN_PROGRAM_ID, SYSTEM_PROGRAM_ID } from '../constants.js'; +import { deriveAssociatedTokenAddress } from '../utils/derivation.js'; +import { + encodeCreateAtaInstructionData, + defaultCompressibleParams, +} from '../codecs/compressible.js'; +import type { CompressibleExtensionInstructionData } from '../codecs/types.js'; + +/** + * Parameters for creating an associated token account. + */ +export interface CreateAtaParams { + /** Payer for the account creation */ + payer: Address; + /** Owner of the token account */ + owner: Address; + /** Mint address */ + mint: Address; + /** Compressible config account (for rent-free accounts) */ + compressibleConfig: Address; + /** Rent sponsor (for rent-free accounts) */ + rentSponsor: Address; + /** Compressible extension params (optional, uses defaults) */ + compressibleParams?: CompressibleExtensionInstructionData; + /** Whether to use idempotent variant (no-op if exists) */ + idempotent?: boolean; +} + +/** + * Result of ATA creation. + */ +export interface CreateAtaResult { + /** The derived ATA address */ + address: Address; + /** The PDA bump */ + bump: number; + /** The instruction to create the ATA */ + instruction: IInstruction; +} + +/** + * Creates an associated token account instruction. + * + * @param params - ATA creation parameters + * @returns The ATA address, bump, and instruction + */ +export async function createAssociatedTokenAccountInstruction( + params: CreateAtaParams, +): Promise { + const { + payer, + owner, + mint, + compressibleConfig, + rentSponsor, + compressibleParams = defaultCompressibleParams(), + idempotent = false, + } = params; + + // Derive the ATA address + const { address: ata, bump } = await deriveAssociatedTokenAddress( + owner, + mint, + ); + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: owner, role: 0 }, // readonly + { address: mint, role: 0 }, // readonly + { address: payer, role: 3 }, // writable+signer + { address: ata, role: 1 }, // writable + { address: SYSTEM_PROGRAM_ID, role: 0 }, // readonly + { address: compressibleConfig, role: 0 }, // readonly + { address: rentSponsor, role: 1 }, // writable + ]; + + // Build instruction data + const data = encodeCreateAtaInstructionData( + { + bump, + compressibleConfig: compressibleParams, + }, + idempotent, + ); + + const instruction: IInstruction = { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; + + return { address: ata, bump, instruction }; +} + +/** + * Creates an idempotent ATA instruction (no-op if account exists). + * + * @param params - ATA creation parameters (idempotent flag ignored) + * @returns The ATA address, bump, and instruction + */ +export async function createAssociatedTokenAccountIdempotentInstruction( + params: Omit, +): Promise { + return createAssociatedTokenAccountInstruction({ + ...params, + idempotent: true, + }); +} diff --git a/js/token-sdk/src/instructions/freeze-thaw.ts b/js/token-sdk/src/instructions/freeze-thaw.ts new file mode 100644 index 0000000000..34b9c05445 --- /dev/null +++ b/js/token-sdk/src/instructions/freeze-thaw.ts @@ -0,0 +1,88 @@ +/** + * Freeze and thaw token account instructions. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; + +import { DISCRIMINATOR, LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; + +/** + * Parameters for freezing a token account. + */ +export interface FreezeParams { + /** Token account to freeze */ + tokenAccount: Address; + /** Mint address */ + mint: Address; + /** Freeze authority - must be signer */ + freezeAuthority: Address; +} + +/** + * Creates a freeze instruction (discriminator: 10). + * + * Freezes a token account, preventing transfers. + * + * @param params - Freeze parameters + * @returns The freeze instruction + */ +export function createFreezeInstruction(params: FreezeParams): IInstruction { + const { tokenAccount, mint, freezeAuthority } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: mint, role: 0 }, // readonly + { address: freezeAuthority, role: 2 }, // readonly+signer + ]; + + // Build instruction data (just discriminator) + const data = new Uint8Array([DISCRIMINATOR.FREEZE]); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} + +/** + * Parameters for thawing a token account. + */ +export interface ThawParams { + /** Token account to thaw */ + tokenAccount: Address; + /** Mint address */ + mint: Address; + /** Freeze authority - must be signer */ + freezeAuthority: Address; +} + +/** + * Creates a thaw instruction (discriminator: 11). + * + * Thaws a frozen token account, allowing transfers again. + * + * @param params - Thaw parameters + * @returns The thaw instruction + */ +export function createThawInstruction(params: ThawParams): IInstruction { + const { tokenAccount, mint, freezeAuthority } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: tokenAccount, role: 1 }, // writable + { address: mint, role: 0 }, // readonly + { address: freezeAuthority, role: 2 }, // readonly+signer + ]; + + // Build instruction data (just discriminator) + const data = new Uint8Array([DISCRIMINATOR.THAW]); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/instructions/index.ts b/js/token-sdk/src/instructions/index.ts new file mode 100644 index 0000000000..92df4251ce --- /dev/null +++ b/js/token-sdk/src/instructions/index.ts @@ -0,0 +1,12 @@ +/** + * Light Token instruction builders. + */ + +export * from './create-ata.js'; +export * from './close.js'; +export * from './mint-to.js'; +export * from './approve.js'; +export * from './burn.js'; +export * from './freeze-thaw.js'; +export * from './transfer.js'; +export * from './transfer-interface.js'; diff --git a/js/token-sdk/src/instructions/mint-to.ts b/js/token-sdk/src/instructions/mint-to.ts new file mode 100644 index 0000000000..33900af230 --- /dev/null +++ b/js/token-sdk/src/instructions/mint-to.ts @@ -0,0 +1,96 @@ +/** + * Mint-to token instructions. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; +import { getU64Encoder } from '@solana/codecs'; + +import { DISCRIMINATOR, LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; + +/** + * Parameters for minting tokens. + */ +export interface MintToParams { + /** Mint address */ + mint: Address; + /** Token account to mint to */ + tokenAccount: Address; + /** Mint authority - must be signer */ + mintAuthority: Address; + /** Amount to mint */ + amount: bigint; +} + +/** + * Creates a mint-to instruction (discriminator: 7). + * + * Mints tokens to a decompressed CToken account. + * + * @param params - Mint-to parameters + * @returns The mint-to instruction + */ +export function createMintToInstruction(params: MintToParams): IInstruction { + const { mint, tokenAccount, mintAuthority, amount } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: mint, role: 1 }, // writable + { address: tokenAccount, role: 1 }, // writable + { address: mintAuthority, role: 2 }, // readonly+signer + ]; + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const data = new Uint8Array(1 + amountBytes.length); + data[0] = DISCRIMINATOR.MINT_TO; + data.set(new Uint8Array(amountBytes), 1); + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} + +/** + * Parameters for mint-to checked. + */ +export interface MintToCheckedParams extends MintToParams { + /** Expected decimals */ + decimals: number; +} + +/** + * Creates a mint-to checked instruction (discriminator: 14). + * + * Mints tokens with decimals validation. + * + * @param params - Mint-to checked parameters + * @returns The mint-to checked instruction + */ +export function createMintToCheckedInstruction( + params: MintToCheckedParams, +): IInstruction { + const { mint, tokenAccount, mintAuthority, amount, decimals } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: mint, role: 1 }, // writable + { address: tokenAccount, role: 1 }, // writable + { address: mintAuthority, role: 2 }, // readonly+signer + ]; + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const data = new Uint8Array(1 + amountBytes.length + 1); + data[0] = DISCRIMINATOR.MINT_TO_CHECKED; + data.set(new Uint8Array(amountBytes), 1); + data[1 + amountBytes.length] = decimals; + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/instructions/transfer-interface.ts b/js/token-sdk/src/instructions/transfer-interface.ts new file mode 100644 index 0000000000..6a90e5bb41 --- /dev/null +++ b/js/token-sdk/src/instructions/transfer-interface.ts @@ -0,0 +1,123 @@ +/** + * Transfer interface - auto-routing between light-to-light, light-to-SPL, and SPL-to-light. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction } from '@solana/instructions'; + +import { determineTransferType } from '../utils/validation.js'; +import { createTransferInstruction } from './transfer.js'; + +/** + * Transfer type for routing. + */ +export type TransferType = + | 'light-to-light' + | 'light-to-spl' + | 'spl-to-light' + | 'spl-to-spl'; + +/** + * Parameters for transfer interface. + */ +export interface TransferInterfaceParams { + /** Source account owner (to determine if Light or SPL) */ + sourceOwner: Address; + /** Destination account owner (to determine if Light or SPL) */ + destOwner: Address; + /** Source token account */ + source: Address; + /** Destination token account */ + destination: Address; + /** Amount to transfer */ + amount: bigint; + /** Authority for the transfer */ + authority: Address; + /** Mint address (for routing and pools) */ + mint: Address; + /** Maximum top-up for rent (optional) */ + maxTopUp?: number; +} + +/** + * Result of transfer interface routing. + */ +export interface TransferInterfaceResult { + /** The determined transfer type */ + transferType: TransferType; + /** The instruction(s) to execute */ + instructions: IInstruction[]; +} + +/** + * Creates transfer instruction(s) with automatic routing. + * + * Routes transfers based on account ownership: + * - Light-to-Light: Direct CToken transfer + * - Light-to-SPL: Decompress to SPL (requires Transfer2) + * - SPL-to-Light: Compress from SPL (requires Transfer2) + * - SPL-to-SPL: Falls through to SPL Token program + * + * @param params - Transfer interface parameters + * @returns The transfer type and instruction(s) + */ +export function createTransferInterfaceInstruction( + params: TransferInterfaceParams, +): TransferInterfaceResult { + const transferType = determineTransferType( + params.sourceOwner, + params.destOwner, + ); + + switch (transferType) { + case 'light-to-light': + return { + transferType, + instructions: [ + createTransferInstruction({ + source: params.source, + destination: params.destination, + amount: params.amount, + authority: params.authority, + maxTopUp: params.maxTopUp, + }), + ], + }; + + case 'light-to-spl': + // Light -> SPL requires Transfer2 with decompress mode + // This is a placeholder - full implementation requires validity proofs + throw new Error( + 'Light-to-SPL transfer requires Transfer2 with decompress mode. ' + + 'Use createDecompressInstruction() with a validity proof.', + ); + + case 'spl-to-light': + // SPL -> Light requires Transfer2 with compress mode + // This is a placeholder - full implementation requires validity proofs + throw new Error( + 'SPL-to-Light transfer requires Transfer2 with compress mode. ' + + 'Use createCompressInstruction() with a validity proof.', + ); + + case 'spl-to-spl': + throw new Error( + 'SPL-to-SPL transfers should use the SPL Token program directly.', + ); + } +} + +/** + * Helper to determine if a transfer requires compression operations. + * + * @param sourceOwner - Source account owner + * @param destOwner - Destination account owner + * @returns True if the transfer crosses the Light/SPL boundary + */ +export function requiresCompression( + sourceOwner: Address, + destOwner: Address, +): boolean { + const transferType = determineTransferType(sourceOwner, destOwner); + return transferType === 'light-to-spl' || transferType === 'spl-to-light'; +} diff --git a/js/token-sdk/src/instructions/transfer.ts b/js/token-sdk/src/instructions/transfer.ts new file mode 100644 index 0000000000..b88c2de8ef --- /dev/null +++ b/js/token-sdk/src/instructions/transfer.ts @@ -0,0 +1,155 @@ +/** + * CToken transfer instructions. + */ + +import type { Address } from '@solana/addresses'; +import type { IInstruction, IAccountMeta } from '@solana/instructions'; +import { getU64Encoder, getU16Encoder } from '@solana/codecs'; + +import { + DISCRIMINATOR, + LIGHT_TOKEN_PROGRAM_ID, + SYSTEM_PROGRAM_ID, +} from '../constants.js'; + +/** + * Parameters for CToken transfer. + */ +export interface TransferParams { + /** Source CToken account */ + source: Address; + /** Destination CToken account */ + destination: Address; + /** Amount to transfer */ + amount: bigint; + /** Authority (owner or delegate) - must be signer */ + authority: Address; + /** Maximum lamports for rent top-up (optional, 0 = no limit) */ + maxTopUp?: number; + /** Fee payer for rent top-ups (optional, defaults to authority) */ + feePayer?: Address; +} + +/** + * Creates a CToken transfer instruction (discriminator: 3). + * + * Transfers tokens between decompressed CToken accounts. + * + * @param params - Transfer parameters + * @returns The transfer instruction + */ +export function createTransferInstruction( + params: TransferParams, +): IInstruction { + const { source, destination, amount, authority, maxTopUp, feePayer } = + params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: source, role: 1 }, // writable + { address: destination, role: 1 }, // writable + { + address: authority, + role: maxTopUp !== undefined && !feePayer ? 3 : 2, // writable+signer if paying, else readonly+signer + }, + { address: SYSTEM_PROGRAM_ID, role: 0 }, // readonly + ]; + + // Add fee payer if provided + if (feePayer) { + accounts.push({ address: feePayer, role: 3 }); // writable+signer + } + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const maxTopUpBytes = + maxTopUp !== undefined + ? getU16Encoder().encode(maxTopUp) + : new Uint8Array(0); + + const data = new Uint8Array(1 + amountBytes.length + maxTopUpBytes.length); + data[0] = DISCRIMINATOR.TRANSFER; + data.set(new Uint8Array(amountBytes), 1); + if (maxTopUpBytes.length > 0) { + data.set(new Uint8Array(maxTopUpBytes), 1 + amountBytes.length); + } + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} + +/** + * Parameters for CToken transfer checked. + */ +export interface TransferCheckedParams extends TransferParams { + /** Mint address for validation */ + mint: Address; + /** Expected decimals */ + decimals: number; +} + +/** + * Creates a CToken transfer checked instruction (discriminator: 12). + * + * Transfers tokens with decimals validation. + * + * @param params - Transfer checked parameters + * @returns The transfer checked instruction + */ +export function createTransferCheckedInstruction( + params: TransferCheckedParams, +): IInstruction { + const { + source, + mint, + destination, + amount, + authority, + decimals, + maxTopUp, + feePayer, + } = params; + + // Build accounts + const accounts: IAccountMeta[] = [ + { address: source, role: 1 }, // writable + { address: mint, role: 0 }, // readonly + { address: destination, role: 1 }, // writable + { + address: authority, + role: maxTopUp !== undefined && !feePayer ? 3 : 2, // writable+signer if paying, else readonly+signer + }, + { address: SYSTEM_PROGRAM_ID, role: 0 }, // readonly + ]; + + // Add fee payer if provided + if (feePayer) { + accounts.push({ address: feePayer, role: 3 }); // writable+signer + } + + // Build instruction data + const amountBytes = getU64Encoder().encode(amount); + const maxTopUpBytes = + maxTopUp !== undefined + ? getU16Encoder().encode(maxTopUp) + : new Uint8Array(0); + + const data = new Uint8Array( + 1 + amountBytes.length + 1 + maxTopUpBytes.length, + ); + data[0] = DISCRIMINATOR.TRANSFER_CHECKED; + data.set(new Uint8Array(amountBytes), 1); + data[1 + amountBytes.length] = decimals; + if (maxTopUpBytes.length > 0) { + data.set(new Uint8Array(maxTopUpBytes), 1 + amountBytes.length + 1); + } + + return { + programAddress: LIGHT_TOKEN_PROGRAM_ID, + accounts, + data, + }; +} diff --git a/js/token-sdk/src/rpc/client.ts b/js/token-sdk/src/rpc/client.ts new file mode 100644 index 0000000000..4a64e20a31 --- /dev/null +++ b/js/token-sdk/src/rpc/client.ts @@ -0,0 +1,103 @@ +/** + * Light RPC Client - Placeholder + * + * This module will provide RPC client functionality for querying compressed + * accounts from Photon indexer and requesting validity proofs from the prover. + * + * NOT YET IMPLEMENTED - requires prover integration. + */ + +import type { Address } from '@solana/addresses'; + +// ============================================================================ +// TYPES (Placeholder interfaces for SDK use) +// ============================================================================ + +/** + * Parsed compressed token account. + */ +export interface ParsedTokenAccount { + /** Account hash */ + hash: Uint8Array; + /** Token mint */ + mint: Address; + /** Token owner */ + owner: Address; + /** Token amount */ + amount: bigint; + /** Delegate (if any) */ + delegate: Address | null; + /** Account state */ + state: number; + /** Merkle tree address */ + merkleTree: Address; + /** Leaf index */ + leafIndex: number; +} + +/** + * Validity proof for compressed account operations. + */ +export interface ValidityProof { + /** Groth16 proof element A */ + a: Uint8Array; + /** Groth16 proof element B */ + b: Uint8Array; + /** Groth16 proof element C */ + c: Uint8Array; + /** Root indices */ + rootIndices: number[]; +} + +/** + * Light RPC client interface. + */ +export interface LightRpcClient { + /** Get compressed token accounts by owner */ + getTokenAccountsByOwner( + owner: Address, + mint?: Address, + ): Promise; + /** Get validity proof for account hashes */ + getValidityProof(hashes: Uint8Array[]): Promise; +} + +// ============================================================================ +// FACTORY FUNCTION (Placeholder) +// ============================================================================ + +/** + * Creates a Light RPC client. + * + * @param _endpoint - RPC endpoint (unused in placeholder) + * @returns Never - throws an error + * @throws Error indicating the client is not yet implemented + * + * @example + * ```typescript + * // Future usage: + * const client = createLightRpcClient('https://photon.helius.dev'); + * const accounts = await client.getTokenAccountsByOwner(owner); + * const proof = await client.getValidityProof(accounts.map(a => a.hash)); + * ``` + */ +export function createLightRpcClient(_endpoint: string): LightRpcClient { + throw new Error( + 'Light RPC client is not yet implemented. ' + + 'This feature requires Photon indexer and prover server integration.', + ); +} + +/** + * Checks if Light RPC services are available. + * + * @param _photonUrl - Photon indexer URL (unused in placeholder) + * @param _proverUrl - Prover server URL (unused in placeholder) + * @returns Always false in placeholder + */ +export async function isLightRpcAvailable( + _photonUrl?: string, + _proverUrl?: string, +): Promise { + return false; +} diff --git a/js/token-sdk/src/rpc/index.ts b/js/token-sdk/src/rpc/index.ts new file mode 100644 index 0000000000..2cd06b0f4c --- /dev/null +++ b/js/token-sdk/src/rpc/index.ts @@ -0,0 +1,11 @@ +/** + * Light RPC Client exports. + */ + +export { + createLightRpcClient, + isLightRpcAvailable, + type LightRpcClient, + type ParsedTokenAccount, + type ValidityProof, +} from './client.js'; diff --git a/js/token-sdk/src/utils/derivation.ts b/js/token-sdk/src/utils/derivation.ts new file mode 100644 index 0000000000..88fef27e98 --- /dev/null +++ b/js/token-sdk/src/utils/derivation.ts @@ -0,0 +1,149 @@ +/** + * PDA derivation utilities for Light Token accounts. + */ + +import { + type Address, + address, + getAddressCodec, + getProgramDerivedAddress, +} from '@solana/addresses'; + +import { + LIGHT_TOKEN_PROGRAM_ID, + COMPRESSED_MINT_SEED, + POOL_SEED, +} from '../constants.js'; + +// ============================================================================ +// ASSOCIATED TOKEN ACCOUNT +// ============================================================================ + +/** + * Derives the associated token account address for a given owner and mint. + * + * Seeds: [owner, LIGHT_TOKEN_PROGRAM_ID, mint] + * + * @param owner - The token account owner + * @param mint - The token mint address + * @returns The derived ATA address and bump + */ +export async function deriveAssociatedTokenAddress( + owner: Address, + mint: Address, +): Promise<{ address: Address; bump: number }> { + const programIdBytes = getAddressCodec().encode(LIGHT_TOKEN_PROGRAM_ID); + + const [derivedAddress, bump] = await getProgramDerivedAddress({ + programAddress: LIGHT_TOKEN_PROGRAM_ID, + seeds: [ + getAddressCodec().encode(owner), + programIdBytes, + getAddressCodec().encode(mint), + ], + }); + + return { address: derivedAddress, bump }; +} + +/** + * Derives the ATA address and verifies the provided bump matches. + * + * @param owner - The token account owner + * @param mint - The token mint address + * @param bump - The expected PDA bump seed + * @returns The derived ATA address + * @throws Error if the provided bump does not match the derived bump + */ +export async function getAssociatedTokenAddressWithBump( + owner: Address, + mint: Address, + bump: number, +): Promise
{ + const { address: derivedAddress, bump: derivedBump } = + await deriveAssociatedTokenAddress(owner, mint); + + if (derivedBump !== bump) { + throw new Error(`Bump mismatch: expected ${bump}, got ${derivedBump}`); + } + + return derivedAddress; +} + +// ============================================================================ +// LIGHT MINT +// ============================================================================ + +/** + * Derives the Light mint PDA address from a mint signer. + * + * Seeds: ["compressed_mint", mintSigner] + * + * @param mintSigner - The mint signer/authority pubkey + * @returns The derived mint address and bump + */ +export async function deriveMintAddress( + mintSigner: Address, +): Promise<{ address: Address; bump: number }> { + const [derivedAddress, bump] = await getProgramDerivedAddress({ + programAddress: LIGHT_TOKEN_PROGRAM_ID, + seeds: [ + new TextEncoder().encode(COMPRESSED_MINT_SEED), + getAddressCodec().encode(mintSigner), + ], + }); + + return { address: derivedAddress, bump }; +} + +// ============================================================================ +// SPL INTERFACE POOL +// ============================================================================ + +/** + * Derives the SPL interface pool PDA address. + * + * Seeds: ["pool", mint] or ["pool", mint, index] + * + * @param mint - The token mint address + * @param index - Optional pool index (for multi-pool mints) + * @returns The derived pool address and bump + */ +export async function derivePoolAddress( + mint: Address, + index?: number, +): Promise<{ address: Address; bump: number }> { + const mintBytes = getAddressCodec().encode(mint); + const seeds: Uint8Array[] = [ + new TextEncoder().encode(POOL_SEED), + new Uint8Array(mintBytes), + ]; + + if (index !== undefined) { + // Add index as u16 little-endian + const indexBytes = new Uint8Array(2); + new DataView(indexBytes.buffer).setUint16(0, index, true); + seeds.push(indexBytes); + } + + const [derivedAddress, bump] = await getProgramDerivedAddress({ + programAddress: LIGHT_TOKEN_PROGRAM_ID, + seeds, + }); + + return { address: derivedAddress, bump }; +} + +// ============================================================================ +// CPI AUTHORITY +// ============================================================================ + +/** + * Derives the CPI authority PDA. + * + * @returns The CPI authority address + */ +export async function deriveCpiAuthority(): Promise
{ + // CPI authority is a known constant, but we can derive it for verification + return address('GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy'); +} diff --git a/js/token-sdk/src/utils/index.ts b/js/token-sdk/src/utils/index.ts new file mode 100644 index 0000000000..28f781ae7e --- /dev/null +++ b/js/token-sdk/src/utils/index.ts @@ -0,0 +1,19 @@ +/** + * Light Token SDK Utilities + */ + +export { + deriveAssociatedTokenAddress, + getAssociatedTokenAddressWithBump, + deriveMintAddress, + derivePoolAddress, + deriveCpiAuthority, +} from './derivation.js'; + +export { + isLightTokenAccount, + determineTransferType, + validateAtaDerivation, + validatePositiveAmount, + validateDecimals, +} from './validation.js'; diff --git a/js/token-sdk/src/utils/validation.ts b/js/token-sdk/src/utils/validation.ts new file mode 100644 index 0000000000..50eb1874fe --- /dev/null +++ b/js/token-sdk/src/utils/validation.ts @@ -0,0 +1,99 @@ +/** + * Validation utilities for Light Token accounts. + */ + +import type { Address } from '@solana/addresses'; +import { LIGHT_TOKEN_PROGRAM_ID } from '../constants.js'; +import { deriveAssociatedTokenAddress } from './derivation.js'; + +// ============================================================================ +// ACCOUNT TYPE DETECTION +// ============================================================================ + +/** + * Checks if an account owner indicates a Light Token account. + * + * @param owner - The account owner address + * @returns True if the owner is the Light Token program + */ +export function isLightTokenAccount(owner: Address): boolean { + return owner === LIGHT_TOKEN_PROGRAM_ID; +} + +/** + * Determines the transfer type based on source and destination owners. + * + * @param sourceOwner - Owner of the source account + * @param destOwner - Owner of the destination account + * @returns The transfer type + */ +export function determineTransferType( + sourceOwner: Address, + destOwner: Address, +): 'light-to-light' | 'light-to-spl' | 'spl-to-light' | 'spl-to-spl' { + const sourceIsLight = isLightTokenAccount(sourceOwner); + const destIsLight = isLightTokenAccount(destOwner); + + if (sourceIsLight && destIsLight) { + return 'light-to-light'; + } + if (sourceIsLight && !destIsLight) { + return 'light-to-spl'; + } + if (!sourceIsLight && destIsLight) { + return 'spl-to-light'; + } + return 'spl-to-spl'; +} + +// ============================================================================ +// ATA VALIDATION +// ============================================================================ + +/** + * Validates that an ATA address matches the expected derivation. + * + * @param ata - The ATA address to validate + * @param owner - The expected owner + * @param mint - The expected mint + * @returns True if the ATA matches the derivation + */ +export async function validateAtaDerivation( + ata: Address, + owner: Address, + mint: Address, +): Promise { + const { address: derivedAta } = await deriveAssociatedTokenAddress( + owner, + mint, + ); + return ata === derivedAta; +} + +// ============================================================================ +// AMOUNT VALIDATION +// ============================================================================ + +/** + * Validates that a transfer amount is positive. + * + * @param amount - The amount to validate + * @throws Error if amount is not positive + */ +export function validatePositiveAmount(amount: bigint): void { + if (amount <= 0n) { + throw new Error('Amount must be positive'); + } +} + +/** + * Validates decimal places for checked operations. + * + * @param decimals - The decimals value (0-255) + * @throws Error if decimals is out of range + */ +export function validateDecimals(decimals: number): void { + if (decimals < 0 || decimals > 255 || !Number.isInteger(decimals)) { + throw new Error('Decimals must be an integer between 0 and 255'); + } +} diff --git a/js/token-sdk/tsconfig.json b/js/token-sdk/tsconfig.json new file mode 100644 index 0000000000..780b37e2d1 --- /dev/null +++ b/js/token-sdk/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 158713b83f..a5d7e23c49 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,6 +435,91 @@ importers: specifier: ^2.1.1 version: 2.1.1(@types/node@22.16.5)(terser@5.43.1) + js/token-idl: + devDependencies: + '@codama/nodes': + specifier: ^1.4.1 + version: 1.5.0 + '@codama/renderers-js': + specifier: ^1.2.8 + version: 1.5.5(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@codama/visitors': + specifier: ^1.4.1 + version: 1.5.0 + '@codama/visitors-core': + specifier: ^1.4.1 + version: 1.5.0 + '@eslint/js': + specifier: 9.36.0 + version: 9.36.0 + '@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) + codama: + specifier: ^1.4.1 + version: 1.5.0 + eslint: + specifier: ^9.36.0 + version: 9.36.0 + prettier: + specifier: ^3.3.3 + version: 3.6.2 + tsx: + specifier: ^4.19.2 + version: 4.20.5 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + + js/token-sdk: + dependencies: + '@solana/addresses': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/instructions': + specifier: ^2.1.0 + version: 2.3.0(typescript@5.9.3) + '@solana/keys': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + devDependencies: + '@eslint/js': + specifier: 9.36.0 + version: 9.36.0 + '@solana/kit': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@typescript-eslint/eslint-plugin': + specifier: ^8.44.0 + version: 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.3))(eslint@9.36.0)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.44.0 + version: 8.44.0(eslint@9.36.0)(typescript@5.9.3) + eslint: + specifier: ^9.36.0 + version: 9.36.0 + prettier: + specifier: ^3.3.3 + version: 3.6.2 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.16.5)(terser@5.43.1) + sdk-tests/sdk-anchor-test: dependencies: '@coral-xyz/anchor': @@ -746,6 +831,36 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@codama/cli@1.4.4': + resolution: {integrity: sha512-0uLecW/RZC2c1wx3j/eiRAYvilvNY+2DoyEYu/hV0OfM1/uIgIyuy5U+wolV+LY4wLFYdApjYdy+5D32lngCHg==} + hasBin: true + + '@codama/errors@1.5.0': + resolution: {integrity: sha512-i4cS+S7JaZXhofQHFY3cwzt8rqxUVPNaeJND5VOyKUbtcOi933YXJXk52gDG4mc+CpGqHJijsJjfSpr1lJGxzg==} + hasBin: true + + '@codama/node-types@1.5.0': + resolution: {integrity: sha512-Ebz2vOUukmNaFXWdkni1ZihXkAIUnPYtqIMXYxKXOxjMP+TGz2q0lGtRo7sqw1pc2ksFBIkfBp5pZsl5p6gwXA==} + + '@codama/nodes@1.5.0': + resolution: {integrity: sha512-yg+xmorWiMNjS3n19CGIt/FZ/ZCuDIu+HEY45bq6gHu1MN3RtJZY+Q3v0ErnBPA60D8mNWkvkKoeSZXfzcAvfw==} + + '@codama/renderers-core@1.3.5': + resolution: {integrity: sha512-MuZLU+3LZPQb1HuZffwZl+v5JHQDe5LYHGhA1wTMNlwRedYIysSxBjogHNciNIHsKP3JjmqyYmLO5LCEp3hjaQ==} + + '@codama/renderers-js@1.5.5': + resolution: {integrity: sha512-zYVw8KGRHFzrpPKAv8PJI1pMy28qc/iEMspMC6Iw915Vsg0od75FUmUhDAvrTwgc28oyCmlrsWv6BNON4AKmqQ==} + engines: {node: '>=20.18.0'} + + '@codama/validators@1.5.0': + resolution: {integrity: sha512-p3ufDxnCH1jiuHGzcBv4/d+ctzUcKD2K3gX/W8169tC41o9DggjlEpNy1Z6YAAhVb3wHnmXVGA2qmp32rWSfWw==} + + '@codama/visitors-core@1.5.0': + resolution: {integrity: sha512-3PIAlBX0a06hIxzyPtQMfQcqWGFBgfbwysSwcXBbvHUYbemwhD6xwlBKJuqTwm9DyFj3faStp5fpvcp03Rjxtw==} + + '@codama/visitors@1.5.0': + resolution: {integrity: sha512-SwtQaleXxAaFz6uHygxki621q4nPUDQlnwEhsg+QKOjHpKWXjLYdJof+R8gUiTV/n7/IeNnjvxJTTNfUsvETPQ==} + '@coral-xyz/anchor-errors@0.31.1': resolution: {integrity: sha512-NhNEku4F3zzUSBtrYz84FzYWm48+9OvmT1Hhnwr6GnPQry2dsEqH/ti/7ASjjpoFTWRnPXrjAIT1qM6Isop+LQ==} engines: {node: '>=10'} @@ -1865,6 +1980,24 @@ packages: resolution: {integrity: sha512-PJBmyayrlfxM7nbqjomF4YcT1sApQwZio0NHSsT0EzhJqljRmvhzqZua43TyEs80nJk2Cn2FGPg/N8phH6KeCQ==} engines: {node: '>=18.0.0'} + '@solana/accounts@2.3.0': + resolution: {integrity: sha512-QgQTj404Z6PXNOyzaOpSzjgMOuGwG8vC66jSDB+3zHaRcEPRVRd2sVSrd1U6sHtnV3aiaS6YyDuPQMheg4K2jw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/addresses@2.3.0': + resolution: {integrity: sha512-ypTNkY2ZaRFpHLnHAgaW8a83N0/WoqdFvCqf4CQmnMdFsZSdC7qOwcbd7YzdaQn9dy+P2hybewzB+KP7LutxGA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/assertions@2.3.0': + resolution: {integrity: sha512-Ekoet3khNg3XFLN7MIz8W31wPQISpKUGDGTylLptI+JjCDWx3PIa88xjEMqFo02WJ8sBj2NLV64Xg1sBcsHjZQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/buffer-layout-utils@0.2.0': resolution: {integrity: sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==} engines: {node: '>= 10'} @@ -1892,6 +2025,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-core@5.4.0': + resolution: {integrity: sha512-rQ5jXgiDe2vIU+mYCHDjgwMd9WdzZfh4sc5H6JgYleAUjeTUX6mx8hTV2+pcXvvn27LPrgrt9jfxswbDb8O8ww==} + 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 +2047,12 @@ packages: peerDependencies: typescript: '>=5' + '@solana/codecs-data-structures@2.3.0': + resolution: {integrity: sha512-qvU5LE5DqEdYMYgELRHv+HMOx73sSoV1ZZkwIrclwUmwTbTaH8QAJURBj0RhQ/zCne7VuLLOZFFGv6jGigWhSw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/codecs-numbers@2.0.0-experimental.8618508': resolution: {integrity: sha512-EXQKfzFr3CkKKNzKSZPOOOzchXsFe90TVONWsSnVkonO9z+nGKALE0/L9uBmIFGgdzhhU9QQVFvxBMclIDJo2Q==} @@ -1924,6 +2072,15 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/codecs-numbers@5.4.0': + resolution: {integrity: sha512-z6LMkY+kXWx1alrvIDSAxexY5QLhsso638CjM7XI1u6dB7drTLWKhifyjnm1vOQc1VPVFmbYxTgKKpds8TY8tg==} + 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 +2098,25 @@ packages: fastestsmallesttextencoderdecoder: ^1.0.22 typescript: '>=5' + '@solana/codecs-strings@2.3.0': + resolution: {integrity: sha512-y5pSBYwzVziXu521hh+VxqUtp0hYGTl1eWGoc1W+8mdvBdC1kTqm/X7aYQw33J42hw03JjryvYOvmGgk3Qz/Ug==} + engines: {node: '>=20.18.0'} + peerDependencies: + fastestsmallesttextencoderdecoder: ^1.0.22 + typescript: '>=5.3.3' + + '@solana/codecs-strings@5.4.0': + resolution: {integrity: sha512-w0trrjfQDhkCVz7O1GTmHBk9m+MkljKx2uNBbQAD3/yW2Qn9dYiTrZ1/jDVq0/+lPPAUkbT3s3Yo7HUZ2QFmHw==} + 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 +2127,12 @@ packages: peerDependencies: typescript: '>=5' + '@solana/codecs@2.3.0': + resolution: {integrity: sha512-JVqGPkzoeyU262hJGdH64kNLH0M+Oew2CIPOa/9tR3++q2pEd4jU2Rxdfye9sd0Ce3XJrR5AIa8ZfbyQXzjh+g==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/errors@2.0.0-preview.4': resolution: {integrity: sha512-kadtlbRv2LCWr8A9V22On15Us7Nn8BvqNaOB4hXsTB3O0fU40D1ru2l+cReqLcRPij4znqlRzW9Xi0m6J5DIhA==} hasBin: true @@ -1970,6 +2152,52 @@ packages: peerDependencies: typescript: '>=5.3.3' + '@solana/errors@5.4.0': + resolution: {integrity: sha512-hNoAOmlZAszaVBrAy1Jf7amHJ8wnUnTU0BqhNQXknbSvirvsYr81yEud2iq18YiCqhyJ9SuQ5kWrSAT0x7S0oA==} + engines: {node: '>=20.18.0'} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + + '@solana/fast-stable-stringify@2.3.0': + resolution: {integrity: sha512-KfJPrMEieUg6D3hfQACoPy0ukrAV8Kio883llt/8chPEG3FVTX9z/Zuf4O01a15xZmBbmQ7toil2Dp0sxMJSxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/functional@2.3.0': + resolution: {integrity: sha512-AgsPh3W3tE+nK3eEw/W9qiSfTGwLYEvl0rWaxHht/lRcuDVwfKRzeSa5G79eioWFFqr+pTtoCr3D3OLkwKz02Q==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/instructions@2.3.0': + resolution: {integrity: sha512-PLMsmaIKu7hEAzyElrk2T7JJx4D+9eRwebhFZpy2PXziNSmFF929eRHKUsKqBFM3cYR1Yy3m6roBZfA+bGE/oQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/keys@2.3.0': + resolution: {integrity: sha512-ZVVdga79pNH+2pVcm6fr2sWz9HTwfopDVhYb0Lh3dh+WBmJjwkabXEIHey2rUES7NjFa/G7sV8lrUn/v8LDCCQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/kit@2.3.0': + resolution: {integrity: sha512-sb6PgwoW2LjE5oTFu4lhlS/cGt/NB3YrShEyx7JgWFWysfgLdJnhwWThgwy/4HjNsmtMrQGWVls0yVBHcMvlMQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/nominal-types@2.3.0': + resolution: {integrity: sha512-uKlMnlP4PWW5UTXlhKM8lcgIaNj8dvd8xO4Y9l+FVvh9RvW2TO0GwUO6JCo7JBzCB0PSqRJdWWaQ8pu1Ti/OkA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/options@2.0.0-experimental.8618508': resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} @@ -1983,6 +2211,103 @@ packages: peerDependencies: typescript: '>=5' + '@solana/options@2.3.0': + resolution: {integrity: sha512-PPnnZBRCWWoZQ11exPxf//DRzN2C6AoFsDI/u2AsQfYih434/7Kp4XLpfOMT/XESi+gdBMFNNfbES5zg3wAIkw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/programs@2.3.0': + resolution: {integrity: sha512-UXKujV71VCI5uPs+cFdwxybtHZAIZyQkqDiDnmK+DawtOO9mBn4Nimdb/6RjR2CXT78mzO9ZCZ3qfyX+ydcB7w==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/promises@2.3.0': + resolution: {integrity: sha512-GjVgutZKXVuojd9rWy1PuLnfcRfqsaCm7InCiZc8bqmJpoghlyluweNc7ml9Y5yQn1P2IOyzh9+p/77vIyNybQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-api@2.3.0': + resolution: {integrity: sha512-UUdiRfWoyYhJL9PPvFeJr4aJ554ob2jXcpn4vKmRVn9ire0sCbpQKYx6K8eEKHZWXKrDW8IDspgTl0gT/aJWVg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-parsed-types@2.3.0': + resolution: {integrity: sha512-B5pHzyEIbBJf9KHej+zdr5ZNAdSvu7WLU2lOUPh81KHdHQs6dEb310LGxcpCc7HVE8IEdO20AbckewDiAN6OCg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec-types@2.3.0': + resolution: {integrity: sha512-xQsb65lahjr8Wc9dMtP7xa0ZmDS8dOE2ncYjlvfyw/h4mpdXTUdrSMi6RtFwX33/rGuztQ7Hwaid5xLNSLvsFQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-spec@2.3.0': + resolution: {integrity: sha512-fA2LMX4BMixCrNB2n6T83AvjZ3oUQTu7qyPLyt8gHQaoEAXs8k6GZmu6iYcr+FboQCjUmRPgMaABbcr9j2J9Sw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-api@2.3.0': + resolution: {integrity: sha512-9mCjVbum2Hg9KGX3LKsrI5Xs0KX390lS+Z8qB80bxhar6MJPugqIPH8uRgLhCW9GN3JprAfjRNl7our8CPvsPQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions-channel-websocket@2.3.0': + resolution: {integrity: sha512-2oL6ceFwejIgeWzbNiUHI2tZZnaOxNTSerszcin7wYQwijxtpVgUHiuItM/Y70DQmH9sKhmikQp+dqeGalaJxw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + ws: ^8.18.0 + + '@solana/rpc-subscriptions-spec@2.3.0': + resolution: {integrity: sha512-rdmVcl4PvNKQeA2l8DorIeALCgJEMSu7U8AXJS1PICeb2lQuMeaR+6cs/iowjvIB0lMVjYN2sFf6Q3dJPu6wWg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-subscriptions@2.3.0': + resolution: {integrity: sha512-Uyr10nZKGVzvCOqwCZgwYrzuoDyUdwtgQRefh13pXIrdo4wYjVmoLykH49Omt6abwStB0a4UL5gX9V4mFdDJZg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transformers@2.3.0': + resolution: {integrity: sha512-UuHYK3XEpo9nMXdjyGKkPCOr7WsZsxs7zLYDO1A5ELH3P3JoehvrDegYRAGzBS2VKsfApZ86ZpJToP0K3PhmMA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-transport-http@2.3.0': + resolution: {integrity: sha512-HFKydmxGw8nAF5N+S0NLnPBDCe5oMDtI2RAmW8DMqP4U3Zxt2XWhvV1SNkAldT5tF0U1vP+is6fHxyhk4xqEvg==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc-types@2.3.0': + resolution: {integrity: sha512-O09YX2hED2QUyGxrMOxQ9GzH1LlEwwZWu69QbL4oYmIf6P5dzEEHcqRY6L1LsDVqc/dzAdEs/E1FaPrcIaIIPw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/rpc@2.3.0': + resolution: {integrity: sha512-ZWN76iNQAOCpYC7yKfb3UNLIMZf603JckLKOOLTHuy9MZnTN8XV6uwvDFhf42XvhglgUjGCEnbUqWtxQ9pa/pQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/signers@2.3.0': + resolution: {integrity: sha512-OSv6fGr/MFRx6J+ZChQMRqKNPGGmdjkqarKkRzkwmv7v8quWsIRnJT5EV8tBy3LI4DLO/A8vKiNSPzvm1TdaiQ==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/spl-token-group@0.0.5': resolution: {integrity: sha512-CLJnWEcdoUBpQJfx9WEbX3h6nTdNiUzswfFdkABUik7HVwSNA98u5AYvBVK2H93d9PGMOHAak2lHW9xr+zAJGQ==} engines: {node: '>=16'} @@ -2017,6 +2342,36 @@ packages: resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} engines: {node: '>=16'} + '@solana/subscribable@2.3.0': + resolution: {integrity: sha512-DkgohEDbMkdTWiKAoatY02Njr56WXx9e/dKKfmne8/Ad6/2llUIrax78nCdlvZW9quXMaXPTxZvdQqo9N669Og==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/sysvars@2.3.0': + resolution: {integrity: sha512-LvjADZrpZ+CnhlHqfI5cmsRzX9Rpyb1Ox2dMHnbsRNzeKAMhu9w4ZBIaeTdO322zsTr509G1B+k2ABD3whvUBA==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-confirmation@2.3.0': + resolution: {integrity: sha512-UiEuiHCfAAZEKdfne/XljFNJbsKAe701UQHKXEInYzIgBjRbvaeYZlBmkkqtxwcasgBTOmEaEKT44J14N9VZDw==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transaction-messages@2.3.0': + resolution: {integrity: sha512-bgqvWuy3MqKS5JdNLH649q+ngiyOu5rGS3DizSnWwYUd76RxZl1kN6CoqHSrrMzFMvis6sck/yPGG3wqrMlAww==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + + '@solana/transactions@2.3.0': + resolution: {integrity: sha512-LnTvdi8QnrQtuEZor5Msje61sDpPstTVwKg4y81tNxDhiyomjuvnSNLAq6QsB9gIxUqbNzPZgOG9IU4I4/Uaug==} + engines: {node: '>=20.18.0'} + peerDependencies: + typescript: '>=5.3.3' + '@solana/web3.js@1.98.4': resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} @@ -2227,6 +2582,9 @@ packages: '@vitest/expect@2.1.1': resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/mocker@2.1.1': resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} peerDependencies: @@ -2239,21 +2597,47 @@ packages: vite: optional: true + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.1': resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/runner@2.1.1': resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/snapshot@2.1.1': resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/spy@2.1.1': resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/utils@2.1.1': resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + JSONStream@1.3.5: resolution: {integrity: sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==} hasBin: true @@ -2672,6 +3056,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + codama@1.5.0: + resolution: {integrity: sha512-hhfSzrOiDX3bV7QmJneEBsBk3ln4gIcMJs6P8BlEJ3EFI+P0QZaTT5W61o8Tq0/79hTZeyj0gP65HZ/LYJil+w==} + hasBin: true + code-excerpt@4.0.0: resolution: {integrity: sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -2708,6 +3096,10 @@ packages: resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} engines: {node: '>=20'} + commander@14.0.2: + resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==} + engines: {node: '>=20'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3014,6 +3406,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -3214,6 +3609,10 @@ packages: evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + eyes@0.1.8: resolution: {integrity: sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==} engines: {node: '> 0.1.90'} @@ -4037,6 +4436,10 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stable-stringify@1.3.0: + resolution: {integrity: sha512-qtYiSSFlwot9XHtF9bD9c7rwKjr+RecWT//ZnPvSmEjpV5mmPOCN4j8UjY5hbjNkOwZ/jQv3J6R1/pL7RwgMsg==} + engines: {node: '>= 0.4'} + json-stringify-safe@5.0.1: resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} @@ -4055,6 +4458,9 @@ packages: jsonfile@4.0.0: resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonify@0.0.1: + resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==} + jsonparse@1.3.1: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} @@ -4128,6 +4534,9 @@ packages: magic-string@0.30.11: resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magic-string@0.30.5: resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} @@ -5035,6 +5444,9 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@3.7.0: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} @@ -5175,6 +5587,9 @@ packages: tinyexec@0.3.0: resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -5360,6 +5775,9 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.19.0: + resolution: {integrity: sha512-Rjk2OWDZf2eiXVQjY2HyE3XPjqW/wXnSZq0QkOsPKZEnaetNNBObTp91LYfGdB8hRbRZk4HFcM/cONw452B0AQ==} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -5424,6 +5842,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite@5.0.4: resolution: {integrity: sha512-RzAr8LSvM8lmhB4tQ5OPcBhpjOZRZjuxv9zO5UcxeoY2bd3kP3Ticd40Qma9/BqZ8JS96Ll/jeBX9u+LJZrhVg==} engines: {node: ^18.0.0 || >=20.0.0} @@ -5477,18 +5900,43 @@ packages: jsdom: optional: true - wait-on@7.2.0: - resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} - engines: {node: '>=12.0.0'} + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} hasBin: true - - wasmbuilder@0.0.16: - resolution: {integrity: sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA==} - - wasmcurves@0.2.2: - resolution: {integrity: sha512-JRY908NkmKjFl4ytnTu5ED6AwPD+8VJ9oc94kdq7h5bIwbj0L4TDJ69mG+2aLs2SoCmGfqIesMWTEJjtYsoQXQ==} - - web-streams-polyfill@3.2.1: + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + wait-on@7.2.0: + resolution: {integrity: sha512-wCQcHkRazgjG5XoAq9jbTMLpNIjoSlZslrJ2+N9MxDsGEv1HnFoVjOCexL0ESva7Y9cu350j+DWADdk54s4AFQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + wasmbuilder@0.0.16: + resolution: {integrity: sha512-Qx3lEFqaVvp1cEYW7Bfi+ebRJrOiwz2Ieu7ZG2l7YyeSJIok/reEQCQCuicj/Y32ITIJuGIM9xZQppGx5LrQdA==} + + wasmcurves@0.2.2: + resolution: {integrity: sha512-JRY908NkmKjFl4ytnTu5ED6AwPD+8VJ9oc94kdq7h5bIwbj0L4TDJ69mG+2aLs2SoCmGfqIesMWTEJjtYsoQXQ==} + + web-streams-polyfill@3.2.1: resolution: {integrity: sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==} engines: {node: '>= 8'} @@ -6331,6 +6779,65 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@codama/cli@1.4.4': + dependencies: + '@codama/nodes': 1.5.0 + '@codama/visitors': 1.5.0 + '@codama/visitors-core': 1.5.0 + commander: 14.0.2 + picocolors: 1.1.1 + prompts: 2.4.2 + + '@codama/errors@1.5.0': + dependencies: + '@codama/node-types': 1.5.0 + commander: 14.0.2 + picocolors: 1.1.1 + + '@codama/node-types@1.5.0': {} + + '@codama/nodes@1.5.0': + dependencies: + '@codama/errors': 1.5.0 + '@codama/node-types': 1.5.0 + + '@codama/renderers-core@1.3.5': + dependencies: + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + '@codama/visitors-core': 1.5.0 + + '@codama/renderers-js@1.5.5(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + '@codama/renderers-core': 1.3.5 + '@codama/visitors-core': 1.5.0 + '@solana/codecs-strings': 5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + prettier: 3.6.2 + semver: 7.7.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - typescript + + '@codama/validators@1.5.0': + dependencies: + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + '@codama/visitors-core': 1.5.0 + + '@codama/visitors-core@1.5.0': + dependencies: + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + json-stable-stringify: 1.3.0 + + '@codama/visitors@1.5.0': + dependencies: + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + '@codama/visitors-core': 1.5.0 + '@coral-xyz/anchor-errors@0.31.1': {} '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': @@ -7488,6 +7995,34 @@ snapshots: '@smithy/types': 4.5.0 tslib: 2.8.1 + '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/addresses@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/assertions@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + 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 @@ -7526,6 +8061,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@5.4.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 5.4.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 +8091,13 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) typescript: 5.9.2 + '@solana/codecs-data-structures@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + '@solana/codecs-numbers@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7573,6 +8126,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@5.4.0(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.4.0(typescript@5.9.3) + '@solana/errors': 5.4.0(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + '@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7595,6 +8161,23 @@ snapshots: fastestsmallesttextencoderdecoder: 1.0.22 typescript: 5.9.2 + '@solana/codecs-strings@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + + '@solana/codecs-strings@5.4.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 5.4.0(typescript@5.9.3) + '@solana/codecs-numbers': 5.4.0(typescript@5.9.3) + '@solana/errors': 5.4.0(typescript@5.9.3) + optionalDependencies: + fastestsmallesttextencoderdecoder: 1.0.22 + typescript: 5.9.3 + '@solana/codecs@2.0.0-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs-core': 2.0.0-preview.4(typescript@5.9.2) @@ -7617,6 +8200,17 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/codecs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/options': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/errors@2.0.0-preview.4(typescript@5.9.2)': dependencies: chalk: 5.6.2 @@ -7641,6 +8235,73 @@ snapshots: commander: 14.0.1 typescript: 5.9.2 + '@solana/errors@2.3.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.1 + typescript: 5.9.3 + + '@solana/errors@5.4.0(typescript@5.9.3)': + dependencies: + chalk: 5.6.2 + commander: 14.0.2 + optionalDependencies: + typescript: 5.9.3 + + '@solana/fast-stable-stringify@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/functional@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/instructions@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/keys@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/assertions': 2.3.0(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/nominal-types@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + '@solana/options@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7668,6 +8329,168 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder + '@solana/options@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/programs@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/promises@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-parsed-types@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-spec-types@2.3.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@solana/rpc-spec@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/rpc-subscriptions-api@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + ws: 8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10) + + '@solana/rpc-subscriptions-spec@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/subscribable': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/rpc-transformers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc-transport-http@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + undici-types: 7.19.0 + + '@solana/rpc-types@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/rpc@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/rpc-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-spec': 2.3.0(typescript@5.9.3) + '@solana/rpc-spec-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-transport-http': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/signers@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + '@solana/spl-token-group@0.0.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-preview.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -7731,6 +8554,71 @@ snapshots: dependencies: buffer: 6.0.3 + '@solana/subscribable@2.3.0(typescript@5.9.3)': + dependencies: + '@solana/errors': 2.3.0(typescript@5.9.3) + typescript: 5.9.3 + + '@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10))': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/promises': 2.3.0(typescript@5.9.3) + '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + - ws + + '@solana/transaction-messages@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - fastestsmallesttextencoderdecoder + + '@solana/transactions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)': + dependencies: + '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs-core': 2.3.0(typescript@5.9.3) + '@solana/codecs-data-structures': 2.3.0(typescript@5.9.3) + '@solana/codecs-numbers': 2.3.0(typescript@5.9.3) + '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/errors': 2.3.0(typescript@5.9.3) + '@solana/functional': 2.3.0(typescript@5.9.3) + '@solana/instructions': 2.3.0(typescript@5.9.3) + '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/nominal-types': 2.3.0(typescript@5.9.3) + '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + 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 @@ -7907,6 +8795,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 +8824,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 +8845,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 +8868,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 +8884,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.44.0(eslint@9.36.0)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/typescript-estree': 8.44.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.44.0(eslint@9.36.0)(typescript@5.9.3) + debug: 4.4.3(supports-color@8.1.1) + eslint: 9.36.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@7.13.1': {} '@typescript-eslint/types@8.44.0': {} @@ -7989,6 +8931,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.44.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.44.0(typescript@5.9.3) + '@typescript-eslint/types': 8.44.0 + '@typescript-eslint/visitor-keys': 8.44.0 + debug: 4.4.3(supports-color@8.1.1) + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@7.13.1(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.36.0) @@ -8011,6 +8969,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 @@ -8028,6 +8997,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.0.4(@types/node@22.16.5)(terser@5.43.1))': dependencies: '@vitest/spy': 2.1.1 @@ -8036,31 +9012,64 @@ snapshots: optionalDependencies: vite: 5.0.4(@types/node@22.16.5)(terser@5.43.1) + '@vitest/mocker@2.1.9(vite@5.0.4(@types/node@22.16.5)(terser@5.43.1))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.0.4(@types/node@22.16.5)(terser@5.43.1) + '@vitest/pretty-format@2.1.1': dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + '@vitest/runner@2.1.1': dependencies: '@vitest/utils': 2.1.1 pathe: 1.1.2 + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + '@vitest/snapshot@2.1.1': dependencies: '@vitest/pretty-format': 2.1.1 magic-string: 0.30.11 pathe: 1.1.2 + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + '@vitest/spy@2.1.1': dependencies: tinyspy: 3.0.2 + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + '@vitest/utils@2.1.1': dependencies: '@vitest/pretty-format': 2.1.1 loupe: 3.2.0 tinyrainbow: 1.2.0 + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.0 + tinyrainbow: 1.2.0 + JSONStream@1.3.5: dependencies: jsonparse: 1.3.1 @@ -8538,6 +9547,14 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + codama@1.5.0: + dependencies: + '@codama/cli': 1.4.4 + '@codama/errors': 1.5.0 + '@codama/nodes': 1.5.0 + '@codama/validators': 1.5.0 + '@codama/visitors': 1.5.0 + code-excerpt@4.0.0: dependencies: convert-to-spaces: 2.0.1 @@ -8568,6 +9585,8 @@ snapshots: commander@14.0.1: {} + commander@14.0.2: {} + commander@2.20.3: {} commander@5.1.0: {} @@ -9008,6 +10027,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -9295,6 +10316,8 @@ snapshots: md5.js: 1.3.5 safe-buffer: 5.2.1 + expect-type@1.3.0: {} + eyes@0.1.8: {} fast-check@3.23.2: @@ -10187,6 +11210,14 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stable-stringify@1.3.0: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + isarray: 2.0.5 + jsonify: 0.0.1 + object-keys: 1.1.1 + json-stringify-safe@5.0.1: {} json5@1.0.2: @@ -10201,6 +11232,8 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonify@0.0.1: {} + jsonparse@1.3.1: {} keyv@4.5.4: @@ -10266,6 +11299,10 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + magic-string@0.30.5: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -11192,6 +12229,8 @@ snapshots: stackback@0.0.2: {} + std-env@3.10.0: {} + std-env@3.7.0: {} stdin-discarder@0.2.2: {} @@ -11363,6 +12402,8 @@ snapshots: tinyexec@0.3.0: {} + tinyexec@0.3.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -11392,6 +12433,10 @@ snapshots: dependencies: typescript: 5.9.2 + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + ts-mocha@10.1.0(mocha@11.7.5): dependencies: mocha: 11.7.5 @@ -11587,6 +12632,8 @@ snapshots: undici-types@6.21.0: {} + undici-types@7.19.0: {} + unicorn-magic@0.3.0: {} union@0.5.0: @@ -11653,6 +12700,23 @@ snapshots: - supports-color - terser + vite-node@2.1.9(@types/node@22.16.5)(terser@5.43.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3(supports-color@8.1.1) + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.0.4(@types/node@22.16.5)(terser@5.43.1) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + vite@5.0.4(@types/node@22.16.5)(terser@5.43.1): dependencies: esbuild: 0.19.5 @@ -11696,6 +12760,40 @@ snapshots: - supports-color - terser + vitest@2.1.9(@types/node@22.16.5)(terser@5.43.1): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.0.4(@types/node@22.16.5)(terser@5.43.1)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3(supports-color@8.1.1) + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.0.1 + tinyrainbow: 1.2.0 + vite: 5.0.4(@types/node@22.16.5)(terser@5.43.1) + vite-node: 2.1.9(@types/node@22.16.5)(terser@5.43.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.16.5 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - stylus + - sugarss + - supports-color + - terser + wait-on@7.2.0: dependencies: axios: 1.12.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3a1786cae1..3fec7b2697 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,4 +5,6 @@ packages: - "sdk-tests/sdk-anchor-test/**" - "js/stateless.js/**" - "js/compressed-token/**" + - "js/token-idl/**" + - "js/token-sdk/**" - "examples/**" diff --git a/scripts/lint.sh b/scripts/lint.sh index a12aaf558e..0808acaedc 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -5,6 +5,8 @@ set -e # JS linting cd js/stateless.js && pnpm prettier --check . && pnpm lint && cd ../.. cd js/compressed-token && pnpm prettier --check . && pnpm lint && cd ../.. +cd js/token-idl && pnpm prettier --check . && pnpm lint && cd ../.. +cd js/token-sdk && pnpm prettier --check . && pnpm lint && cd ../.. # Rust linting cargo +nightly fmt --all -- --check From 3097317c5dfda3a9e93c3eea404b790ca91b7cca Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 24 Jan 2026 19:39:40 +0000 Subject: [PATCH 2/2] stash separated token client --- js/token-client/package.json | 52 ++ js/token-client/src/index.ts | 56 ++ js/token-client/src/indexer.ts | 607 ++++++++++++++++++++ js/token-client/src/load.ts | 346 +++++++++++ js/token-client/tests/unit/client.test.ts | 337 +++++++++++ js/token-client/tsconfig.json | 24 + js/token-sdk/package.json | 4 +- js/token-sdk/src/client/index.ts | 41 ++ js/token-sdk/src/client/types.ts | 296 ++++++++++ js/token-sdk/src/index.ts | 30 +- js/token-sdk/src/rpc/client.ts | 103 ---- js/token-sdk/src/rpc/index.ts | 11 - js/token-sdk/tests/e2e/instructions.test.ts | 512 +++++++++++++++++ js/token-sdk/tests/unit/utils.test.ts | 287 +++++++++ pnpm-lock.yaml | 22 + pnpm-workspace.yaml | 1 + 16 files changed, 2608 insertions(+), 121 deletions(-) create mode 100644 js/token-client/package.json create mode 100644 js/token-client/src/index.ts create mode 100644 js/token-client/src/indexer.ts create mode 100644 js/token-client/src/load.ts create mode 100644 js/token-client/tests/unit/client.test.ts create mode 100644 js/token-client/tsconfig.json create mode 100644 js/token-sdk/src/client/index.ts create mode 100644 js/token-sdk/src/client/types.ts delete mode 100644 js/token-sdk/src/rpc/client.ts delete mode 100644 js/token-sdk/src/rpc/index.ts create mode 100644 js/token-sdk/tests/e2e/instructions.test.ts create mode 100644 js/token-sdk/tests/unit/utils.test.ts diff --git a/js/token-client/package.json b/js/token-client/package.json new file mode 100644 index 0000000000..776de6cad9 --- /dev/null +++ b/js/token-client/package.json @@ -0,0 +1,52 @@ +{ + "name": "@lightprotocol/token-client", + "version": "0.1.0", + "description": "Light Protocol indexer client for compressed tokens", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "tsc", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest" + }, + "peerDependencies": { + "@solana/kit": "^2.1.0" + }, + "dependencies": { + "@lightprotocol/token-sdk": "workspace:*", + "@solana/addresses": "^2.1.0", + "@solana/codecs": "^2.1.0" + }, + "devDependencies": { + "typescript": "^5.7.3", + "vitest": "^2.1.8" + }, + "engines": { + "node": ">=18" + }, + "keywords": [ + "solana", + "light-protocol", + "compressed-token", + "indexer", + "zk-compression" + ], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/Lightprotocol/light-protocol.git", + "directory": "js/token-client" + } +} diff --git a/js/token-client/src/index.ts b/js/token-client/src/index.ts new file mode 100644 index 0000000000..ce9aba9b9d --- /dev/null +++ b/js/token-client/src/index.ts @@ -0,0 +1,56 @@ +/** + * Light Protocol Token Client + * + * Indexer client and account loading functions for compressed tokens. + * + * @example + * ```typescript + * import { + * createLightIndexer, + * loadTokenAccountsForTransfer, + * selectAccountsForAmount, + * } from '@lightprotocol/token-client'; + * + * // Types from token-sdk: + * import { TreeType, CompressedTokenAccount } from '@lightprotocol/token-sdk'; + * + * const indexer = createLightIndexer('https://photon.helius.dev'); + * const loaded = await loadTokenAccountsForTransfer(indexer, owner, 1000n); + * ``` + * + * @packageDocumentation + */ + +// Indexer +export { + type LightIndexer, + PhotonIndexer, + createLightIndexer, + isLightIndexerAvailable, +} from './indexer.js'; + +// Load functions +export { + // Types + type InputTokenAccount, + type MerkleContext, + type LoadedTokenAccounts, + type LoadTokenAccountsOptions, + type SelectedAccounts, + + // Load functions + loadTokenAccountsForTransfer, + loadTokenAccount, + loadAllTokenAccounts, + loadCompressedAccount, + loadCompressedAccountByHash, + + // Account selection + selectAccountsForAmount, + + // Proof helpers + getValidityProofForAccounts, + needsValidityProof, + getTreeInfo, + getOutputTreeInfo, +} from './load.js'; diff --git a/js/token-client/src/indexer.ts b/js/token-client/src/indexer.ts new file mode 100644 index 0000000000..b9b64813f8 --- /dev/null +++ b/js/token-client/src/indexer.ts @@ -0,0 +1,607 @@ +/** + * Light Token Client Indexer + * + * Minimal indexer client for fetching compressed accounts and validity proofs. + * Implements the core methods needed for the AccountInterface pattern. + */ + +import { address as createAddress, type Address } from '@solana/addresses'; +import { getBase58Decoder, getBase58Encoder } from '@solana/codecs'; + +import { + type CompressedAccount, + type CompressedTokenAccount, + type ValidityProofWithContext, + type GetCompressedTokenAccountsOptions, + type IndexerResponse, + type ItemsWithCursor, + type AddressWithTree, + type TreeInfo, + type TokenData, + type CompressedAccountData, + type AccountProofInputs, + type AddressProofInputs, + type RootIndex, + TreeType, + AccountState, + IndexerError, + IndexerErrorCode, + assertV2Tree, +} from '@lightprotocol/token-sdk'; + +// ============================================================================ +// INTERFACES +// ============================================================================ + +/** + * Light indexer interface. + * + * Provides the minimum methods required for fetching compressed accounts + * and validity proofs needed for token operations. + */ +export interface LightIndexer { + /** + * Fetch a compressed account by its address. + * + * @param address - 32-byte compressed account address + * @returns The compressed account or null if not found + */ + getCompressedAccount( + address: Uint8Array, + ): Promise>; + + /** + * Fetch a compressed account by its hash. + * + * @param hash - 32-byte account hash + * @returns The compressed account or null if not found + */ + getCompressedAccountByHash( + hash: Uint8Array, + ): Promise>; + + /** + * Fetch compressed token accounts by owner. + * + * @param owner - Owner address + * @param options - Optional filters and pagination + * @returns Paginated list of token accounts + */ + getCompressedTokenAccountsByOwner( + owner: Address, + options?: GetCompressedTokenAccountsOptions, + ): Promise>>; + + /** + * Fetch multiple compressed accounts by their addresses. + * + * @param addresses - Array of 32-byte addresses + * @returns Array of compressed accounts (null for not found) + */ + getMultipleCompressedAccounts( + addresses: Uint8Array[], + ): Promise>; + + /** + * Fetch a validity proof for the given account hashes and new addresses. + * + * @param hashes - Account hashes to prove existence + * @param newAddresses - New addresses to prove uniqueness (optional) + * @returns Validity proof with context + */ + getValidityProof( + hashes: Uint8Array[], + newAddresses?: AddressWithTree[], + ): Promise>; +} + +// ============================================================================ +// PHOTON INDEXER IMPLEMENTATION +// ============================================================================ + +/** + * JSON-RPC request structure. + */ +interface JsonRpcRequest { + jsonrpc: '2.0'; + id: string; + method: string; + params: unknown; +} + +/** + * JSON-RPC response structure. + */ +interface JsonRpcResponse { + jsonrpc: '2.0'; + id: string; + result?: { + context: { slot: number }; + value: T; + }; + error?: { + code: number; + message: string; + data?: unknown; + }; +} + +/** + * Photon indexer client. + * + * Implements the LightIndexer interface using the Photon API. + */ +export class PhotonIndexer implements LightIndexer { + private requestId = 0; + // base58Encoder: string -> Uint8Array (for decoding base58 strings FROM API) + private readonly base58Encoder = getBase58Encoder(); + // base58Decoder: Uint8Array -> string (for encoding bytes TO base58 for API) + private readonly base58Decoder = getBase58Decoder(); + + /** + * Create a new PhotonIndexer. + * + * @param endpoint - Photon API endpoint URL + */ + constructor(private readonly endpoint: string) {} + + async getCompressedAccount( + address: Uint8Array, + ): Promise> { + const addressB58 = this.bytesToBase58(address); + const response = await this.rpcCall( + 'getCompressedAccountV2', + { address: addressB58 }, + ); + + return { + context: { slot: BigInt(response.context.slot) }, + value: response.value + ? this.parseAccountV2(response.value) + : null, + }; + } + + async getCompressedAccountByHash( + hash: Uint8Array, + ): Promise> { + const hashB58 = this.bytesToBase58(hash); + const response = await this.rpcCall( + 'getCompressedAccountByHashV2', + { hash: hashB58 }, + ); + + return { + context: { slot: BigInt(response.context.slot) }, + value: response.value + ? this.parseAccountV2(response.value) + : null, + }; + } + + async getCompressedTokenAccountsByOwner( + owner: Address, + options?: GetCompressedTokenAccountsOptions, + ): Promise>> { + const params: Record = { owner: owner.toString() }; + if (options?.mint) { + params.mint = options.mint.toString(); + } + if (options?.cursor) { + params.cursor = options.cursor; + } + if (options?.limit !== undefined) { + params.limit = options.limit; + } + + const response = await this.rpcCall( + 'getCompressedTokenAccountsByOwnerV2', + params, + ); + + return { + context: { slot: BigInt(response.context.slot) }, + value: { + items: response.value.items.map((item) => + this.parseTokenAccountV2(item), + ), + cursor: response.value.cursor, + }, + }; + } + + async getMultipleCompressedAccounts( + addresses: Uint8Array[], + ): Promise> { + const addressesB58 = addresses.map((a) => this.bytesToBase58(a)); + const response = await this.rpcCall( + 'getMultipleCompressedAccountsV2', + { addresses: addressesB58 }, + ); + + return { + context: { slot: BigInt(response.context.slot) }, + value: response.value.items.map((item) => + item ? this.parseAccountV2(item) : null, + ), + }; + } + + async getValidityProof( + hashes: Uint8Array[], + newAddresses?: AddressWithTree[], + ): Promise> { + const hashesB58 = hashes.map((h) => this.bytesToBase58(h)); + const addressesParam = newAddresses?.map((a) => ({ + address: this.bytesToBase58(a.address), + tree: a.tree.toString(), + })); + + const response = await this.rpcCall( + 'getValidityProofV2', + { + hashes: hashesB58, + newAddresses: addressesParam ?? [], + }, + ); + + return { + context: { slot: BigInt(response.context.slot) }, + value: this.parseValidityProofV2(response.value), + }; + } + + // ======================================================================== + // PRIVATE HELPERS + // ======================================================================== + + private async rpcCall( + method: string, + params: unknown, + ): Promise<{ context: { slot: number }; value: T }> { + const request: JsonRpcRequest = { + jsonrpc: '2.0', + id: String(++this.requestId), + method, + params, + }; + + let response: Response; + try { + response = await fetch(this.endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(request), + }); + } catch (e) { + throw new IndexerError( + IndexerErrorCode.NetworkError, + `Failed to fetch from ${this.endpoint}: ${e}`, + e, + ); + } + + if (!response.ok) { + throw new IndexerError( + IndexerErrorCode.NetworkError, + `HTTP error ${response.status}: ${response.statusText}`, + ); + } + + let json: JsonRpcResponse; + try { + json = (await response.json()) as JsonRpcResponse; + } catch (e) { + throw new IndexerError( + IndexerErrorCode.InvalidResponse, + `Invalid JSON response: ${e}`, + e, + ); + } + + if (json.error) { + throw new IndexerError( + IndexerErrorCode.RpcError, + `RPC error ${json.error.code}: ${json.error.message}`, + json.error, + ); + } + + if (!json.result) { + throw new IndexerError( + IndexerErrorCode.InvalidResponse, + 'Missing result in response', + ); + } + + return json.result; + } + + private parseTreeInfo(ctx: PhotonMerkleContextV2): TreeInfo { + // Validate V2-only tree types + assertV2Tree(ctx.treeType as TreeType); + + const info: TreeInfo = { + tree: createAddress(ctx.tree), + queue: createAddress(ctx.queue), + treeType: ctx.treeType as TreeType, + }; + if (ctx.cpiContext) { + info.cpiContext = createAddress(ctx.cpiContext); + } + if (ctx.nextTreeContext) { + info.nextTreeInfo = this.parseTreeInfo(ctx.nextTreeContext); + } + return info; + } + + private parseAccountData( + data: PhotonAccountData, + ): CompressedAccountData { + return { + discriminator: this.bigintToBytes8(BigInt(data.discriminator)), + data: this.base64Decode(data.data), + dataHash: this.base58ToBytes(data.dataHash), + }; + } + + private parseAccountV2(account: PhotonAccountV2): CompressedAccount { + return { + hash: this.base58ToBytes(account.hash), + address: account.address + ? this.base58ToBytes(account.address) + : null, + owner: createAddress(account.owner), + lamports: BigInt(account.lamports), + data: account.data ? this.parseAccountData(account.data) : null, + leafIndex: account.leafIndex, + treeInfo: this.parseTreeInfo(account.merkleContext), + proveByIndex: Boolean(account.proveByIndex), + seq: account.seq !== null ? BigInt(account.seq) : null, + slotCreated: BigInt(account.slotCreated), + }; + } + + private parseTokenData(data: PhotonTokenData): TokenData { + return { + mint: createAddress(data.mint), + owner: createAddress(data.owner), + amount: BigInt(data.amount), + delegate: data.delegate ? createAddress(data.delegate) : null, + state: + data.state === 'frozen' + ? AccountState.Frozen + : AccountState.Initialized, + tlv: data.tlv ? this.base64Decode(data.tlv) : null, + }; + } + + private parseTokenAccountV2( + tokenAccount: PhotonTokenAccountV2, + ): CompressedTokenAccount { + return { + token: this.parseTokenData(tokenAccount.tokenData), + account: this.parseAccountV2(tokenAccount.account), + }; + } + + private parseRootIndex(ri: PhotonRootIndex): RootIndex { + return { + rootIndex: ri.rootIndex, + proveByIndex: Boolean(ri.proveByIndex), + }; + } + + private parseAccountProofInputs( + input: PhotonAccountProofInputs, + ): AccountProofInputs { + return { + hash: this.base58ToBytes(input.hash), + root: this.base58ToBytes(input.root), + rootIndex: this.parseRootIndex(input.rootIndex), + leafIndex: input.leafIndex, + treeInfo: this.parseTreeInfo(input.merkleContext), + }; + } + + private parseAddressProofInputs( + input: PhotonAddressProofInputs, + ): AddressProofInputs { + return { + address: this.base58ToBytes(input.address), + root: this.base58ToBytes(input.root), + rootIndex: input.rootIndex, + treeInfo: this.parseTreeInfo(input.merkleContext), + }; + } + + private parseValidityProofV2( + proof: PhotonValidityProofV2, + ): ValidityProofWithContext { + return { + proof: proof.compressedProof + ? { + a: Uint8Array.from(proof.compressedProof.a), + b: Uint8Array.from(proof.compressedProof.b), + c: Uint8Array.from(proof.compressedProof.c), + } + : null, + accounts: proof.accounts.map((a) => this.parseAccountProofInputs(a)), + addresses: proof.addresses.map((a) => + this.parseAddressProofInputs(a), + ), + }; + } + + /** + * Convert bytes to base58 string. + * Uses the decoder because it decodes bytes FROM internal format TO base58 string. + */ + private bytesToBase58(bytes: Uint8Array): string { + return this.base58Decoder.decode(bytes); + } + + /** + * Convert base58 string to bytes. + * Uses the encoder because it encodes base58 string TO internal byte format. + */ + private base58ToBytes(str: string): Uint8Array { + // The encoder returns ReadonlyUint8Array, so we need to copy to mutable Uint8Array + return Uint8Array.from(this.base58Encoder.encode(str)); + } + + private base64Decode(str: string): Uint8Array { + // Use atob for browser/node compatibility + const binary = atob(str); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + return bytes; + } + + private bigintToBytes8(value: bigint): Uint8Array { + const bytes = new Uint8Array(8); + let remaining = value; + for (let i = 0; i < 8; i++) { + bytes[i] = Number(remaining & 0xffn); + remaining >>= 8n; + } + return bytes; + } +} + +// ============================================================================ +// PHOTON API RESPONSE TYPES (Internal) +// ============================================================================ + +interface PhotonMerkleContextV2 { + tree: string; + queue: string; + treeType: number; + cpiContext?: string | null; + nextTreeContext?: PhotonMerkleContextV2 | null; +} + +interface PhotonAccountData { + discriminator: string | number; + data: string; + dataHash: string; +} + +interface PhotonAccountV2 { + address: string | null; + hash: string; + data: PhotonAccountData | null; + lamports: string | number; + owner: string; + leafIndex: number; + seq: number | null; + slotCreated: string | number; + merkleContext: PhotonMerkleContextV2; + proveByIndex: boolean | number; +} + +interface PhotonTokenData { + mint: string; + owner: string; + amount: string | number; + delegate: string | null; + state: string; + tlv: string | null; +} + +interface PhotonTokenAccountV2 { + tokenData: PhotonTokenData; + account: PhotonAccountV2; +} + +interface PhotonTokenAccountListV2 { + items: PhotonTokenAccountV2[]; + cursor: string | null; +} + +interface PhotonMultipleAccountsV2 { + items: (PhotonAccountV2 | null)[]; +} + +interface PhotonRootIndex { + rootIndex: number; + proveByIndex: boolean | number; +} + +interface PhotonAccountProofInputs { + hash: string; + root: string; + rootIndex: PhotonRootIndex; + merkleContext: PhotonMerkleContextV2; + leafIndex: number; +} + +interface PhotonAddressProofInputs { + address: string; + root: string; + rootIndex: number; + merkleContext: PhotonMerkleContextV2; +} + +interface PhotonCompressedProof { + a: number[]; + b: number[]; + c: number[]; +} + +interface PhotonValidityProofV2 { + compressedProof: PhotonCompressedProof | null; + accounts: PhotonAccountProofInputs[]; + addresses: PhotonAddressProofInputs[]; +} + +// ============================================================================ +// FACTORY FUNCTION +// ============================================================================ + +/** + * Create a Light indexer client. + * + * @param endpoint - Photon API endpoint URL + * @returns LightIndexer instance + * + * @example + * ```typescript + * const indexer = createLightIndexer('https://photon.helius.dev'); + * const accounts = await indexer.getCompressedTokenAccountsByOwner(owner); + * const proof = await indexer.getValidityProof(hashes); + * ``` + */ +export function createLightIndexer(endpoint: string): LightIndexer { + return new PhotonIndexer(endpoint); +} + +/** + * Check if Light indexer services are available. + * + * @param endpoint - Photon API endpoint URL + * @returns True if the indexer is healthy + */ +export async function isLightIndexerAvailable( + endpoint: string, +): Promise { + try { + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: '1', + method: 'getIndexerHealth', + params: {}, + }), + }); + if (!response.ok) return false; + const json = await response.json(); + return !json.error; + } catch { + return false; + } +} diff --git a/js/token-client/src/load.ts b/js/token-client/src/load.ts new file mode 100644 index 0000000000..0c642a97d9 --- /dev/null +++ b/js/token-client/src/load.ts @@ -0,0 +1,346 @@ +/** + * Light Token Client Load Functions + * + * Functions for loading compressed account data for use in transactions. + * Implements the AccountInterface pattern from sdk-libs/client. + */ + +import type { Address } from '@solana/addresses'; + +import type { LightIndexer } from './indexer.js'; +import type { + CompressedAccount, + CompressedTokenAccount, + ValidityProofWithContext, + GetCompressedTokenAccountsOptions, + TreeInfo, +} from '@lightprotocol/token-sdk'; + +// ============================================================================ +// ACCOUNT INTERFACE TYPES +// ============================================================================ + +/** + * Input account for building transfer instructions. + * + * Contains the token account data and proof context needed for the transaction. + */ +export interface InputTokenAccount { + /** The compressed token account */ + tokenAccount: CompressedTokenAccount; + /** Merkle context for the account */ + merkleContext: MerkleContext; +} + +/** + * Merkle context for a compressed account. + */ +export interface MerkleContext { + /** Merkle tree pubkey */ + tree: Address; + /** Queue pubkey */ + queue: Address; + /** Leaf index in the tree */ + leafIndex: number; + /** Whether to prove by index */ + proveByIndex: boolean; +} + +/** + * Loaded token accounts with validity proof. + * + * This is the result of loading token accounts for a transaction. + * Contains all the data needed to build transfer instructions. + */ +export interface LoadedTokenAccounts { + /** Input token accounts with their merkle contexts */ + inputs: InputTokenAccount[]; + /** Validity proof for all inputs */ + proof: ValidityProofWithContext; + /** Total amount available across all inputs */ + totalAmount: bigint; +} + +/** + * Options for loading token accounts. + */ +export interface LoadTokenAccountsOptions { + /** Filter by mint */ + mint?: Address; + /** Maximum number of accounts to load */ + limit?: number; + /** Minimum amount required (will load accounts until this is met) */ + minAmount?: bigint; +} + +// ============================================================================ +// LOAD FUNCTIONS +// ============================================================================ + +/** + * Load token accounts for a transfer. + * + * Fetches token accounts for the given owner, selects enough accounts + * to meet the required amount, and fetches a validity proof. + * + * @param indexer - Light indexer client + * @param owner - Token account owner + * @param amount - Amount to transfer + * @param options - Optional filters + * @returns Loaded token accounts with proof + * @throws Error if insufficient balance + * + * @example + * ```typescript + * const indexer = createLightIndexer('https://photon.helius.dev'); + * const loaded = await loadTokenAccountsForTransfer( + * indexer, + * owner, + * 1000n, + * { mint: tokenMint } + * ); + * // Use loaded.inputs and loaded.proof to build transfer instruction + * ``` + */ +export async function loadTokenAccountsForTransfer( + indexer: LightIndexer, + owner: Address, + amount: bigint, + options?: LoadTokenAccountsOptions, +): Promise { + // Fetch token accounts + const fetchOptions: GetCompressedTokenAccountsOptions = {}; + if (options?.mint) { + fetchOptions.mint = options.mint; + } + if (options?.limit) { + fetchOptions.limit = options.limit; + } + + const response = await indexer.getCompressedTokenAccountsByOwner( + owner, + fetchOptions, + ); + + const tokenAccounts = response.value.items; + + if (tokenAccounts.length === 0) { + throw new Error(`No token accounts found for owner ${owner}`); + } + + // Select accounts to meet the required amount + const selectedAccounts = selectAccountsForAmount(tokenAccounts, amount); + + if (selectedAccounts.totalAmount < amount) { + throw new Error( + `Insufficient balance: have ${selectedAccounts.totalAmount}, need ${amount}`, + ); + } + + // Get validity proof for selected accounts + const hashes = selectedAccounts.accounts.map((a) => a.account.hash); + const proofResponse = await indexer.getValidityProof(hashes); + + // Build input accounts with merkle contexts + const inputs: InputTokenAccount[] = selectedAccounts.accounts.map( + (tokenAccount) => ({ + tokenAccount, + merkleContext: { + tree: tokenAccount.account.treeInfo.tree, + queue: tokenAccount.account.treeInfo.queue, + leafIndex: tokenAccount.account.leafIndex, + proveByIndex: tokenAccount.account.proveByIndex, + }, + }), + ); + + return { + inputs, + proof: proofResponse.value, + totalAmount: selectedAccounts.totalAmount, + }; +} + +/** + * Load a single token account by owner and mint (ATA pattern). + * + * @param indexer - Light indexer client + * @param owner - Token account owner + * @param mint - Token mint + * @returns The token account or null if not found + */ +export async function loadTokenAccount( + indexer: LightIndexer, + owner: Address, + mint: Address, +): Promise { + const response = await indexer.getCompressedTokenAccountsByOwner(owner, { + mint, + limit: 1, + }); + + return response.value.items[0] ?? null; +} + +/** + * Load all token accounts for an owner. + * + * @param indexer - Light indexer client + * @param owner - Token account owner + * @param options - Optional filters + * @returns Array of token accounts + */ +export async function loadAllTokenAccounts( + indexer: LightIndexer, + owner: Address, + options?: GetCompressedTokenAccountsOptions, +): Promise { + const allAccounts: CompressedTokenAccount[] = []; + let cursor: string | undefined = options?.cursor; + + do { + const response = await indexer.getCompressedTokenAccountsByOwner( + owner, + { ...options, cursor }, + ); + + allAccounts.push(...response.value.items); + cursor = response.value.cursor ?? undefined; + } while (cursor); + + return allAccounts; +} + +/** + * Load a compressed account by address. + * + * @param indexer - Light indexer client + * @param address - 32-byte account address + * @returns The compressed account or null if not found + */ +export async function loadCompressedAccount( + indexer: LightIndexer, + address: Uint8Array, +): Promise { + const response = await indexer.getCompressedAccount(address); + return response.value; +} + +/** + * Load a compressed account by hash. + * + * @param indexer - Light indexer client + * @param hash - 32-byte account hash + * @returns The compressed account or null if not found + */ +export async function loadCompressedAccountByHash( + indexer: LightIndexer, + hash: Uint8Array, +): Promise { + const response = await indexer.getCompressedAccountByHash(hash); + return response.value; +} + +// ============================================================================ +// ACCOUNT SELECTION +// ============================================================================ + +/** + * Result of account selection. + */ +export interface SelectedAccounts { + /** Selected accounts */ + accounts: CompressedTokenAccount[]; + /** Total amount across selected accounts */ + totalAmount: bigint; +} + +/** + * Select token accounts to meet the required amount. + * + * Uses a greedy algorithm that prefers larger accounts first + * to minimize the number of inputs. + * + * @param accounts - Available token accounts + * @param requiredAmount - Amount needed + * @returns Selected accounts and their total amount + */ +export function selectAccountsForAmount( + accounts: CompressedTokenAccount[], + requiredAmount: bigint, +): SelectedAccounts { + // Sort by amount descending (prefer larger accounts) + const sorted = [...accounts].sort((a, b) => { + const diff = b.token.amount - a.token.amount; + return diff > 0n ? 1 : diff < 0n ? -1 : 0; + }); + + const selected: CompressedTokenAccount[] = []; + let total = 0n; + + for (const account of sorted) { + if (total >= requiredAmount) { + break; + } + selected.push(account); + total += account.token.amount; + } + + return { + accounts: selected, + totalAmount: total, + }; +} + +// ============================================================================ +// PROOF HELPERS +// ============================================================================ + +/** + * Get a validity proof for multiple token accounts. + * + * @param indexer - Light indexer client + * @param accounts - Token accounts to prove + * @returns Validity proof with context + */ +export async function getValidityProofForAccounts( + indexer: LightIndexer, + accounts: CompressedTokenAccount[], +): Promise { + const hashes = accounts.map((a) => a.account.hash); + const response = await indexer.getValidityProof(hashes); + return response.value; +} + +/** + * Check if an account needs a validity proof or can prove by index. + * + * @param account - The compressed account + * @returns True if validity proof is needed + */ +export function needsValidityProof(account: CompressedAccount): boolean { + return !account.proveByIndex; +} + +/** + * Extract tree info from a compressed account. + * + * @param account - The compressed account + * @returns Tree info + */ +export function getTreeInfo(account: CompressedAccount): TreeInfo { + return account.treeInfo; +} + +/** + * Get the output tree for new state. + * + * If the tree has a next tree (tree is full), use that. + * Otherwise use the current tree. + * + * @param treeInfo - Current tree info + * @returns Tree info for output state + */ +export function getOutputTreeInfo(treeInfo: TreeInfo): TreeInfo { + return treeInfo.nextTreeInfo ?? treeInfo; +} diff --git a/js/token-client/tests/unit/client.test.ts b/js/token-client/tests/unit/client.test.ts new file mode 100644 index 0000000000..0d6df3fbf0 --- /dev/null +++ b/js/token-client/tests/unit/client.test.ts @@ -0,0 +1,337 @@ +/** + * Unit tests for Light Token Client + * + * Tests for: + * - Account selection algorithm (selectAccountsForAmount) + * - Tree info helpers (getOutputTreeInfo) + * - Proof helpers (needsValidityProof) + * - IndexerError + * - V2-only tree validation (assertV2Tree) + */ + +import { describe, it, expect } from 'vitest'; +import { address, type Address } from '@solana/addresses'; + +import { + selectAccountsForAmount, + getOutputTreeInfo, + needsValidityProof, +} from '../../src/index.js'; + +import { + assertV2Tree, + TreeType, + IndexerError, + IndexerErrorCode, + type CompressedTokenAccount, + type CompressedAccount, + type TreeInfo, +} from '@lightprotocol/token-sdk'; + +// ============================================================================ +// TEST HELPERS +// ============================================================================ + +// Valid test addresses (32-44 chars base58) +const MOCK_TREE = address('amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'); +const MOCK_QUEUE = address('SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7'); +const MOCK_OWNER = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); +const MOCK_MINT = address('So11111111111111111111111111111111111111112'); +const MOCK_PROGRAM = address('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'); + +/** + * Create a minimal mock CompressedTokenAccount for testing. + */ +function createMockTokenAccount( + amount: bigint, + owner: Address = MOCK_OWNER, +): CompressedTokenAccount { + const mockTreeInfo: TreeInfo = { + tree: MOCK_TREE, + queue: MOCK_QUEUE, + treeType: TreeType.StateV2, + }; + + const mockAccount: CompressedAccount = { + hash: new Uint8Array(32), + address: null, + owner: MOCK_PROGRAM, + lamports: 0n, + data: null, + leafIndex: 0, + treeInfo: mockTreeInfo, + proveByIndex: false, + seq: null, + slotCreated: 0n, + }; + + return { + token: { + mint: MOCK_MINT, + owner, + amount, + delegate: null, + state: 0, + tlv: null, + }, + account: mockAccount, + }; +} + +/** + * Create a mock TreeInfo for testing. + */ +function createMockTreeInfo(treeType: TreeType, nextTree?: TreeInfo): TreeInfo { + return { + tree: MOCK_TREE, + queue: MOCK_QUEUE, + treeType, + nextTreeInfo: nextTree, + }; +} + +// ============================================================================ +// TEST: Account Selection Algorithm (selectAccountsForAmount) +// ============================================================================ + +describe('selectAccountsForAmount', () => { + it('1.1 selects single account when it has enough balance', () => { + const accounts = [ + createMockTokenAccount(1000n), + createMockTokenAccount(500n), + createMockTokenAccount(200n), + ]; + + const result = selectAccountsForAmount(accounts, 800n); + + expect(result.accounts.length).toBe(1); + expect(result.accounts[0].token.amount).toBe(1000n); + expect(result.totalAmount).toBe(1000n); + }); + + it('1.2 selects multiple accounts to meet required amount (greedy, largest first)', () => { + const accounts = [ + createMockTokenAccount(300n), + createMockTokenAccount(500n), + createMockTokenAccount(200n), + ]; + + const result = selectAccountsForAmount(accounts, 700n); + + // Should select 500 first (largest), then 300 + expect(result.accounts.length).toBe(2); + expect(result.accounts[0].token.amount).toBe(500n); + expect(result.accounts[1].token.amount).toBe(300n); + expect(result.totalAmount).toBe(800n); + }); + + it('1.3 returns all accounts when total balance is insufficient', () => { + const accounts = [ + createMockTokenAccount(100n), + createMockTokenAccount(200n), + createMockTokenAccount(50n), + ]; + + const result = selectAccountsForAmount(accounts, 1000n); + + expect(result.accounts.length).toBe(3); + expect(result.totalAmount).toBe(350n); + }); + + it('1.4 handles empty accounts array', () => { + const result = selectAccountsForAmount([], 100n); + + expect(result.accounts.length).toBe(0); + expect(result.totalAmount).toBe(0n); + }); + + it('1.5 handles zero required amount', () => { + const accounts = [ + createMockTokenAccount(100n), + createMockTokenAccount(200n), + ]; + + const result = selectAccountsForAmount(accounts, 0n); + + expect(result.accounts.length).toBe(0); + expect(result.totalAmount).toBe(0n); + }); + + it('1.6 handles exact match', () => { + const accounts = [ + createMockTokenAccount(100n), + createMockTokenAccount(200n), + createMockTokenAccount(300n), + ]; + + const result = selectAccountsForAmount(accounts, 300n); + + // Should select only the 300 account + expect(result.accounts.length).toBe(1); + expect(result.accounts[0].token.amount).toBe(300n); + expect(result.totalAmount).toBe(300n); + }); +}); + +// ============================================================================ +// TEST: Tree Info Helpers (getOutputTreeInfo) +// ============================================================================ + +describe('getOutputTreeInfo', () => { + it('2.1 returns nextTreeInfo when present', () => { + const nextTree = createMockTreeInfo(TreeType.StateV2); + // Use a different valid address for the next tree + nextTree.tree = address('GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy'); + + const currentTree = createMockTreeInfo(TreeType.StateV2, nextTree); + + const result = getOutputTreeInfo(currentTree); + + expect(result.tree).toBe(nextTree.tree); + expect(result).toBe(nextTree); + }); + + it('2.2 returns current tree when no next tree', () => { + const currentTree = createMockTreeInfo(TreeType.StateV2); + + const result = getOutputTreeInfo(currentTree); + + expect(result).toBe(currentTree); + expect(result.tree).toBe(currentTree.tree); + }); + + it('2.3 handles null nextTreeInfo', () => { + const currentTree = createMockTreeInfo(TreeType.StateV2); + currentTree.nextTreeInfo = null; + + const result = getOutputTreeInfo(currentTree); + + expect(result).toBe(currentTree); + }); +}); + +// ============================================================================ +// TEST: Proof Helpers (needsValidityProof) +// ============================================================================ + +describe('needsValidityProof', () => { + it('3.1 returns true when proveByIndex is false', () => { + const account: CompressedAccount = { + hash: new Uint8Array(32), + address: null, + owner: address('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), + lamports: 0n, + data: null, + leafIndex: 0, + treeInfo: createMockTreeInfo(TreeType.StateV2), + proveByIndex: false, + seq: null, + slotCreated: 0n, + }; + + expect(needsValidityProof(account)).toBe(true); + }); + + it('3.2 returns false when proveByIndex is true', () => { + const account: CompressedAccount = { + hash: new Uint8Array(32), + address: null, + owner: address('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'), + lamports: 0n, + data: null, + leafIndex: 0, + treeInfo: createMockTreeInfo(TreeType.StateV2), + proveByIndex: true, + seq: null, + slotCreated: 0n, + }; + + expect(needsValidityProof(account)).toBe(false); + }); +}); + +// ============================================================================ +// TEST: IndexerError +// ============================================================================ + +describe('IndexerError', () => { + it('4.1 constructs error with correct code, message, and cause', () => { + const cause = new Error('Original error'); + const error = new IndexerError( + IndexerErrorCode.NetworkError, + 'Connection failed', + cause, + ); + + expect(error.code).toBe(IndexerErrorCode.NetworkError); + expect(error.message).toBe('Connection failed'); + expect(error.cause).toBe(cause); + expect(error.name).toBe('IndexerError'); + expect(error instanceof Error).toBe(true); + }); + + it('4.2 works without cause', () => { + const error = new IndexerError( + IndexerErrorCode.InvalidResponse, + 'Bad response', + ); + + expect(error.code).toBe(IndexerErrorCode.InvalidResponse); + expect(error.message).toBe('Bad response'); + expect(error.cause).toBeUndefined(); + }); + + it('4.3 supports all error codes', () => { + const codes = [ + IndexerErrorCode.NetworkError, + IndexerErrorCode.InvalidResponse, + IndexerErrorCode.RpcError, + IndexerErrorCode.NotFound, + ]; + + for (const code of codes) { + const error = new IndexerError(code, `Error: ${code}`); + expect(error.code).toBe(code); + } + }); +}); + +// ============================================================================ +// TEST: V2-Only Tree Validation (assertV2Tree) +// ============================================================================ + +describe('assertV2Tree', () => { + it('5.1 throws for StateV1 tree type', () => { + expect(() => assertV2Tree(TreeType.StateV1)).toThrow(IndexerError); + expect(() => assertV2Tree(TreeType.StateV1)).toThrow( + 'V1 tree types are not supported', + ); + }); + + it('5.2 throws for AddressV1 tree type', () => { + expect(() => assertV2Tree(TreeType.AddressV1)).toThrow(IndexerError); + expect(() => assertV2Tree(TreeType.AddressV1)).toThrow( + 'V1 tree types are not supported', + ); + }); + + it('5.3 passes for StateV2 tree type', () => { + expect(() => assertV2Tree(TreeType.StateV2)).not.toThrow(); + }); + + it('5.4 passes for AddressV2 tree type', () => { + expect(() => assertV2Tree(TreeType.AddressV2)).not.toThrow(); + }); + + it('5.5 thrown error has correct error code', () => { + try { + assertV2Tree(TreeType.StateV1); + expect.fail('Should have thrown'); + } catch (e) { + expect(e).toBeInstanceOf(IndexerError); + expect((e as IndexerError).code).toBe( + IndexerErrorCode.InvalidResponse, + ); + } + }); +}); diff --git a/js/token-client/tsconfig.json b/js/token-client/tsconfig.json new file mode 100644 index 0000000000..780b37e2d1 --- /dev/null +++ b/js/token-client/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "tests"] +} diff --git a/js/token-sdk/package.json b/js/token-sdk/package.json index 8c27baa265..9b676b3d4c 100644 --- a/js/token-sdk/package.json +++ b/js/token-sdk/package.json @@ -27,7 +27,9 @@ "scripts": { "build": "tsc", "clean": "rm -rf dist", - "test": "vitest run", + "test": "vitest run tests/unit/", + "test:unit": "vitest run tests/unit/", + "test:e2e": "vitest run tests/e2e/", "test:watch": "vitest", "lint": "eslint .", "format": "prettier --write .", diff --git a/js/token-sdk/src/client/index.ts b/js/token-sdk/src/client/index.ts new file mode 100644 index 0000000000..f7b5cfa030 --- /dev/null +++ b/js/token-sdk/src/client/index.ts @@ -0,0 +1,41 @@ +/** + * Light Token SDK Client Types + * + * Types for interacting with the Light Protocol indexer (Photon). + * Implementation moved to @lightprotocol/token-client package. + */ + +// Types only - implementation in @lightprotocol/token-client +export { + // Tree types + TreeType, + type TreeInfo, + + // Account types + AccountState, + type CompressedAccountData, + type CompressedAccount, + type TokenData, + type CompressedTokenAccount, + + // Proof types + type ValidityProof, + type RootIndex, + type AccountProofInputs, + type AddressProofInputs, + type ValidityProofWithContext, + + // Request/response types + type AddressWithTree, + type GetCompressedTokenAccountsOptions, + type ResponseContext, + type IndexerResponse, + type ItemsWithCursor, + + // Error types + IndexerErrorCode, + IndexerError, + + // Validation + assertV2Tree, +} from './types.js'; diff --git a/js/token-sdk/src/client/types.ts b/js/token-sdk/src/client/types.ts new file mode 100644 index 0000000000..3230230663 --- /dev/null +++ b/js/token-sdk/src/client/types.ts @@ -0,0 +1,296 @@ +/** + * Light Token SDK Client Types + * + * Core types for interacting with the Light Protocol indexer (Photon). + * These types align with the Rust sdk-libs/client types. + */ + +import type { Address } from '@solana/addresses'; + +// ============================================================================ +// TREE TYPES +// ============================================================================ + +/** + * Tree type enum matching Rust TreeType. + */ +export enum TreeType { + /** V1 state merkle tree */ + StateV1 = 1, + /** V1 address merkle tree */ + AddressV1 = 2, + /** V2 state merkle tree */ + StateV2 = 3, + /** V2 address merkle tree */ + AddressV2 = 4, +} + +/** + * Tree info for a merkle tree context. + */ +export interface TreeInfo { + /** Merkle tree pubkey */ + tree: Address; + /** Queue pubkey */ + queue: Address; + /** Tree type */ + treeType: TreeType; + /** CPI context (optional) */ + cpiContext?: Address; + /** Next tree info (when current tree is full) */ + nextTreeInfo?: TreeInfo | null; +} + +// ============================================================================ +// ACCOUNT TYPES +// ============================================================================ + +/** + * Account state for token accounts. + */ +export enum AccountState { + Initialized = 0, + Frozen = 1, +} + +/** + * Compressed account data. + */ +export interface CompressedAccountData { + /** 8-byte discriminator */ + discriminator: Uint8Array; + /** Account data bytes */ + data: Uint8Array; + /** 32-byte data hash */ + dataHash: Uint8Array; +} + +/** + * Compressed account matching Rust CompressedAccount. + */ +export interface CompressedAccount { + /** 32-byte account hash */ + hash: Uint8Array; + /** 32-byte address (optional) */ + address: Uint8Array | null; + /** Owner program pubkey */ + owner: Address; + /** Lamports */ + lamports: bigint; + /** Account data (optional) */ + data: CompressedAccountData | null; + /** Leaf index in the merkle tree */ + leafIndex: number; + /** Tree info */ + treeInfo: TreeInfo; + /** Whether to prove by index */ + proveByIndex: boolean; + /** Sequence number (optional) */ + seq: bigint | null; + /** Slot when account was created */ + slotCreated: bigint; +} + +/** + * Token-specific data. + */ +export interface TokenData { + /** Token mint */ + mint: Address; + /** Token owner */ + owner: Address; + /** Token amount */ + amount: bigint; + /** Delegate (optional) */ + delegate: Address | null; + /** Account state */ + state: AccountState; + /** TLV extension data (optional) */ + tlv: Uint8Array | null; +} + +/** + * Compressed token account combining account and token data. + */ +export interface CompressedTokenAccount { + /** Token-specific data */ + token: TokenData; + /** General account information */ + account: CompressedAccount; +} + +// ============================================================================ +// PROOF TYPES +// ============================================================================ + +/** + * Groth16 validity proof. + */ +export interface ValidityProof { + /** 32 bytes - G1 point */ + a: Uint8Array; + /** 64 bytes - G2 point */ + b: Uint8Array; + /** 32 bytes - G1 point */ + c: Uint8Array; +} + +/** + * Root index for proof context. + */ +export interface RootIndex { + /** The root index value */ + rootIndex: number; + /** Whether to prove by index rather than validity proof */ + proveByIndex: boolean; +} + +/** + * Account proof inputs for validity proof context. + */ +export interface AccountProofInputs { + /** 32-byte account hash */ + hash: Uint8Array; + /** 32-byte merkle root */ + root: Uint8Array; + /** Root index info */ + rootIndex: RootIndex; + /** Leaf index */ + leafIndex: number; + /** Tree info */ + treeInfo: TreeInfo; +} + +/** + * Address proof inputs for validity proof context. + */ +export interface AddressProofInputs { + /** 32-byte address */ + address: Uint8Array; + /** 32-byte merkle root */ + root: Uint8Array; + /** Root index */ + rootIndex: number; + /** Tree info */ + treeInfo: TreeInfo; +} + +/** + * Validity proof with full context. + */ +export interface ValidityProofWithContext { + /** The validity proof (null if proving by index) */ + proof: ValidityProof | null; + /** Account proof inputs */ + accounts: AccountProofInputs[]; + /** Address proof inputs */ + addresses: AddressProofInputs[]; +} + +// ============================================================================ +// REQUEST/RESPONSE TYPES +// ============================================================================ + +/** + * Address with tree for new address proofs. + */ +export interface AddressWithTree { + /** 32-byte address */ + address: Uint8Array; + /** Address tree pubkey */ + tree: Address; +} + +/** + * Options for fetching compressed token accounts. + */ +export interface GetCompressedTokenAccountsOptions { + /** Filter by mint */ + mint?: Address; + /** Pagination cursor */ + cursor?: string; + /** Maximum results to return */ + limit?: number; +} + +/** + * Response context with slot. + */ +export interface ResponseContext { + /** Slot of the response */ + slot: bigint; +} + +/** + * Response wrapper with context. + */ +export interface IndexerResponse { + /** Response context */ + context: ResponseContext; + /** Response value */ + value: T; +} + +/** + * Paginated items with cursor. + */ +export interface ItemsWithCursor { + /** Items in this page */ + items: T[]; + /** Cursor for next page (null if no more pages) */ + cursor: string | null; +} + +// ============================================================================ +// ERROR TYPES +// ============================================================================ + +/** + * Indexer error codes. + */ +export enum IndexerErrorCode { + /** Network/fetch error */ + NetworkError = 'NETWORK_ERROR', + /** Invalid response format */ + InvalidResponse = 'INVALID_RESPONSE', + /** RPC error response */ + RpcError = 'RPC_ERROR', + /** Account not found */ + NotFound = 'NOT_FOUND', +} + +/** + * Error from indexer operations. + */ +export class IndexerError extends Error { + constructor( + public readonly code: IndexerErrorCode, + message: string, + public readonly cause?: unknown, + ) { + super(message); + this.name = 'IndexerError'; + } +} + +// ============================================================================ +// VALIDATION +// ============================================================================ + +/** + * Assert that tree is V2. Throws if V1. + * + * The SDK only supports V2 trees. V1 trees from the indexer response + * must be rejected to ensure proper protocol compatibility. + * + * @param treeType - The tree type to validate + * @throws IndexerError if tree type is V1 + */ +export function assertV2Tree(treeType: TreeType): void { + if (treeType === TreeType.StateV1 || treeType === TreeType.AddressV1) { + throw new IndexerError( + IndexerErrorCode.InvalidResponse, + `V1 tree types are not supported. Got: ${TreeType[treeType]}`, + ); + } +} diff --git a/js/token-sdk/src/index.ts b/js/token-sdk/src/index.ts index 1689e81de6..6a4eb15b56 100644 --- a/js/token-sdk/src/index.ts +++ b/js/token-sdk/src/index.ts @@ -177,13 +177,31 @@ export { } from './instructions/index.js'; // ============================================================================ -// RPC CLIENT (Placeholder) +// CLIENT TYPES (Indexer & load functions in @lightprotocol/token-client) // ============================================================================ export { - createLightRpcClient, - isLightRpcAvailable, - type LightRpcClient, - type ParsedTokenAccount, + // Validation + assertV2Tree, + + // Types + TreeType, + AccountState, + IndexerErrorCode, + IndexerError, + type TreeInfo, + type CompressedAccountData, + type CompressedAccount, + type TokenData, + type CompressedTokenAccount, type ValidityProof, -} from './rpc/index.js'; + type RootIndex, + type AccountProofInputs, + type AddressProofInputs, + type ValidityProofWithContext, + type AddressWithTree, + type GetCompressedTokenAccountsOptions, + type ResponseContext, + type IndexerResponse, + type ItemsWithCursor, +} from './client/index.js'; diff --git a/js/token-sdk/src/rpc/client.ts b/js/token-sdk/src/rpc/client.ts deleted file mode 100644 index 4a64e20a31..0000000000 --- a/js/token-sdk/src/rpc/client.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Light RPC Client - Placeholder - * - * This module will provide RPC client functionality for querying compressed - * accounts from Photon indexer and requesting validity proofs from the prover. - * - * NOT YET IMPLEMENTED - requires prover integration. - */ - -import type { Address } from '@solana/addresses'; - -// ============================================================================ -// TYPES (Placeholder interfaces for SDK use) -// ============================================================================ - -/** - * Parsed compressed token account. - */ -export interface ParsedTokenAccount { - /** Account hash */ - hash: Uint8Array; - /** Token mint */ - mint: Address; - /** Token owner */ - owner: Address; - /** Token amount */ - amount: bigint; - /** Delegate (if any) */ - delegate: Address | null; - /** Account state */ - state: number; - /** Merkle tree address */ - merkleTree: Address; - /** Leaf index */ - leafIndex: number; -} - -/** - * Validity proof for compressed account operations. - */ -export interface ValidityProof { - /** Groth16 proof element A */ - a: Uint8Array; - /** Groth16 proof element B */ - b: Uint8Array; - /** Groth16 proof element C */ - c: Uint8Array; - /** Root indices */ - rootIndices: number[]; -} - -/** - * Light RPC client interface. - */ -export interface LightRpcClient { - /** Get compressed token accounts by owner */ - getTokenAccountsByOwner( - owner: Address, - mint?: Address, - ): Promise; - /** Get validity proof for account hashes */ - getValidityProof(hashes: Uint8Array[]): Promise; -} - -// ============================================================================ -// FACTORY FUNCTION (Placeholder) -// ============================================================================ - -/** - * Creates a Light RPC client. - * - * @param _endpoint - RPC endpoint (unused in placeholder) - * @returns Never - throws an error - * @throws Error indicating the client is not yet implemented - * - * @example - * ```typescript - * // Future usage: - * const client = createLightRpcClient('https://photon.helius.dev'); - * const accounts = await client.getTokenAccountsByOwner(owner); - * const proof = await client.getValidityProof(accounts.map(a => a.hash)); - * ``` - */ -export function createLightRpcClient(_endpoint: string): LightRpcClient { - throw new Error( - 'Light RPC client is not yet implemented. ' + - 'This feature requires Photon indexer and prover server integration.', - ); -} - -/** - * Checks if Light RPC services are available. - * - * @param _photonUrl - Photon indexer URL (unused in placeholder) - * @param _proverUrl - Prover server URL (unused in placeholder) - * @returns Always false in placeholder - */ -export async function isLightRpcAvailable( - _photonUrl?: string, - _proverUrl?: string, -): Promise { - return false; -} diff --git a/js/token-sdk/src/rpc/index.ts b/js/token-sdk/src/rpc/index.ts deleted file mode 100644 index 2cd06b0f4c..0000000000 --- a/js/token-sdk/src/rpc/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Light RPC Client exports. - */ - -export { - createLightRpcClient, - isLightRpcAvailable, - type LightRpcClient, - type ParsedTokenAccount, - type ValidityProof, -} from './client.js'; diff --git a/js/token-sdk/tests/e2e/instructions.test.ts b/js/token-sdk/tests/e2e/instructions.test.ts new file mode 100644 index 0000000000..152f85a202 --- /dev/null +++ b/js/token-sdk/tests/e2e/instructions.test.ts @@ -0,0 +1,512 @@ +/** + * E2E Tests for Light Token SDK Instructions + * + * These tests verify instruction building and serialization. + * + * Tests that require a running validator are marked with `.skip` + * and can be run separately with `pnpm test:e2e:live`. + * + * To run with a local validator: + * 1. Start the test validator: `./../../cli/test_bin/run test-validator` + * 2. Run tests: `pnpm test:e2e` + * + * Endpoints: + * - Solana: http://127.0.0.1:8899 + * - Compression API: http://127.0.0.1:8784 + * - Prover: http://127.0.0.1:3001 + */ + +import { describe, it, expect } from 'vitest'; +import { address } from '@solana/addresses'; + +import { + // Instruction builders + createAssociatedTokenAccountInstruction, + createAssociatedTokenAccountIdempotentInstruction, + createTransferInstruction, + createTransferCheckedInstruction, + createTransferInterfaceInstruction, + createCloseAccountInstruction, + createMintToInstruction, + createMintToCheckedInstruction, + createBurnInstruction, + createBurnCheckedInstruction, + createFreezeInstruction, + createThawInstruction, + createApproveInstruction, + createRevokeInstruction, + + // Constants + LIGHT_TOKEN_PROGRAM_ID, + DISCRIMINATOR, +} from '../../src/index.js'; + +// ============================================================================ +// TEST HELPERS +// ============================================================================ + +// Use known valid Solana addresses for testing +const TEST_PAYER = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); +const TEST_OWNER = address('11111111111111111111111111111111'); +const TEST_MINT = address('So11111111111111111111111111111111111111112'); +const TEST_SOURCE = address('amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx'); +const TEST_DEST = address('GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy'); +const TEST_DELEGATE = address('SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7'); +const TEST_AUTHORITY = address('compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq'); +const TEST_FREEZE_AUTH = address('cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m'); +const TEST_CONFIG = address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb'); +const TEST_SPONSOR = address('BPFLoaderUpgradeab1e11111111111111111111111'); + +// ============================================================================ +// TEST: Create Associated Token Account Instructions +// ============================================================================ + +describe('createAssociatedTokenAccountInstruction', () => { + it('8.1 creates ATA instruction with correct accounts and data', async () => { + const result = await createAssociatedTokenAccountInstruction({ + payer: TEST_PAYER, + owner: TEST_OWNER, + mint: TEST_MINT, + compressibleConfig: TEST_CONFIG, + rentSponsor: TEST_SPONSOR, + }); + + // Verify result structure + expect(result.address).toBeDefined(); + expect(typeof result.bump).toBe('number'); + expect(result.bump).toBeGreaterThanOrEqual(0); + expect(result.bump).toBeLessThanOrEqual(255); + + // Verify instruction + expect(result.instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(result.instruction.accounts).toHaveLength(7); + expect(result.instruction.data).toBeInstanceOf(Uint8Array); + + // First byte should be CREATE_ATA discriminator + expect(result.instruction.data[0]).toBe(DISCRIMINATOR.CREATE_ATA); + }); + + it('8.1.1 uses consistent PDA derivation', async () => { + const result1 = await createAssociatedTokenAccountInstruction({ + payer: TEST_PAYER, + owner: TEST_OWNER, + mint: TEST_MINT, + compressibleConfig: TEST_CONFIG, + rentSponsor: TEST_SPONSOR, + }); + + const result2 = await createAssociatedTokenAccountInstruction({ + payer: TEST_PAYER, + owner: TEST_OWNER, + mint: TEST_MINT, + compressibleConfig: TEST_CONFIG, + rentSponsor: TEST_SPONSOR, + }); + + expect(result1.address).toBe(result2.address); + expect(result1.bump).toBe(result2.bump); + }); +}); + +describe('createAssociatedTokenAccountIdempotentInstruction', () => { + it('8.2 creates idempotent ATA instruction', async () => { + const result = await createAssociatedTokenAccountIdempotentInstruction({ + payer: TEST_PAYER, + owner: TEST_OWNER, + mint: TEST_MINT, + compressibleConfig: TEST_CONFIG, + rentSponsor: TEST_SPONSOR, + }); + + expect(result.address).toBeDefined(); + expect(result.instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + + // First byte should be CREATE_ATA_IDEMPOTENT discriminator + expect(result.instruction.data[0]).toBe( + DISCRIMINATOR.CREATE_ATA_IDEMPOTENT, + ); + }); +}); + +// ============================================================================ +// TEST: Transfer Instructions +// ============================================================================ + +describe('createTransferInstruction', () => { + it('8.3 creates transfer instruction with correct structure', () => { + const instruction = createTransferInstruction({ + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(4); + expect(instruction.data[0]).toBe(DISCRIMINATOR.TRANSFER); + + // Verify amount encoding (little-endian u64) + const amountBytes = instruction.data.slice(1, 9); + const dataView = new DataView(amountBytes.buffer, amountBytes.byteOffset); + const amount = dataView.getBigUint64(0, true); + expect(amount).toBe(1000n); + }); + + it('8.3.1 includes maxTopUp when provided', () => { + const instruction = createTransferInstruction({ + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + maxTopUp: 5000, + }); + + // Data should be 1 (disc) + 8 (amount) + 2 (maxTopUp) + expect(instruction.data.length).toBe(11); + }); + + it('8.3.2 includes fee payer when provided', () => { + const feePayer = address('Vote111111111111111111111111111111111111111'); + const instruction = createTransferInstruction({ + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + maxTopUp: 5000, + feePayer, + }); + + expect(instruction.accounts).toHaveLength(5); + }); +}); + +describe('createTransferCheckedInstruction', () => { + it('8.4 creates transfer checked instruction', () => { + const instruction = createTransferCheckedInstruction({ + source: TEST_SOURCE, + destination: TEST_DEST, + mint: TEST_MINT, + amount: 1000n, + authority: TEST_AUTHORITY, + decimals: 9, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(5); + expect(instruction.data[0]).toBe(DISCRIMINATOR.TRANSFER_CHECKED); + + // Verify decimals is in the data + const decimals = instruction.data[9]; // After disc (1) + amount (8) + expect(decimals).toBe(9); + }); +}); + +describe('createTransferInterfaceInstruction', () => { + it('8.5 routes light-to-light transfer correctly', () => { + const result = createTransferInterfaceInstruction({ + sourceOwner: LIGHT_TOKEN_PROGRAM_ID, + destOwner: LIGHT_TOKEN_PROGRAM_ID, + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + mint: TEST_MINT, + }); + + expect(result.transferType).toBe('light-to-light'); + expect(result.instructions).toHaveLength(1); + expect(result.instructions[0].programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + }); + + it('8.5.1 throws for unsupported transfer types', () => { + const splProgram = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + // Light-to-SPL + expect(() => + createTransferInterfaceInstruction({ + sourceOwner: LIGHT_TOKEN_PROGRAM_ID, + destOwner: splProgram, + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + mint: TEST_MINT, + }), + ).toThrow('Light-to-SPL transfer requires Transfer2'); + + // SPL-to-Light + expect(() => + createTransferInterfaceInstruction({ + sourceOwner: splProgram, + destOwner: LIGHT_TOKEN_PROGRAM_ID, + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + mint: TEST_MINT, + }), + ).toThrow('SPL-to-Light transfer requires Transfer2'); + + // SPL-to-SPL + expect(() => + createTransferInterfaceInstruction({ + sourceOwner: splProgram, + destOwner: splProgram, + source: TEST_SOURCE, + destination: TEST_DEST, + amount: 1000n, + authority: TEST_AUTHORITY, + mint: TEST_MINT, + }), + ).toThrow('SPL-to-SPL transfers should use the SPL Token program'); + }); +}); + +// ============================================================================ +// TEST: Close Account Instruction +// ============================================================================ + +describe('createCloseAccountInstruction', () => { + it('8.6 creates close account instruction', () => { + const instruction = createCloseAccountInstruction({ + tokenAccount: TEST_SOURCE, + destination: TEST_DEST, + owner: TEST_OWNER, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.CLOSE); + expect(instruction.data.length).toBe(1); + }); +}); + +// ============================================================================ +// TEST: Mint Instructions +// ============================================================================ + +describe('createMintToInstruction', () => { + it('8.7 creates mint-to instruction', () => { + const instruction = createMintToInstruction({ + mint: TEST_MINT, + tokenAccount: TEST_DEST, + mintAuthority: TEST_AUTHORITY, + amount: 1_000_000n, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.MINT_TO); + + // Verify amount + const amountBytes = instruction.data.slice(1, 9); + const dataView = new DataView(amountBytes.buffer, amountBytes.byteOffset); + expect(dataView.getBigUint64(0, true)).toBe(1_000_000n); + }); +}); + +describe('createMintToCheckedInstruction', () => { + it('8.8 creates mint-to checked instruction', () => { + const instruction = createMintToCheckedInstruction({ + mint: TEST_MINT, + tokenAccount: TEST_DEST, + mintAuthority: TEST_AUTHORITY, + amount: 1_000_000n, + decimals: 6, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.data[0]).toBe(DISCRIMINATOR.MINT_TO_CHECKED); + expect(instruction.data[9]).toBe(6); // Decimals + }); +}); + +// ============================================================================ +// TEST: Burn Instructions +// ============================================================================ + +describe('createBurnInstruction', () => { + it('8.9 creates burn instruction', () => { + const instruction = createBurnInstruction({ + tokenAccount: TEST_SOURCE, + mint: TEST_MINT, + authority: TEST_AUTHORITY, + amount: 500n, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.BURN); + + // Verify amount + const amountBytes = instruction.data.slice(1, 9); + const dataView = new DataView(amountBytes.buffer, amountBytes.byteOffset); + expect(dataView.getBigUint64(0, true)).toBe(500n); + }); +}); + +describe('createBurnCheckedInstruction', () => { + it('8.10 creates burn checked instruction', () => { + const instruction = createBurnCheckedInstruction({ + tokenAccount: TEST_SOURCE, + mint: TEST_MINT, + authority: TEST_AUTHORITY, + amount: 500n, + decimals: 9, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.data[0]).toBe(DISCRIMINATOR.BURN_CHECKED); + expect(instruction.data[9]).toBe(9); // Decimals + }); +}); + +// ============================================================================ +// TEST: Freeze/Thaw Instructions +// ============================================================================ + +describe('createFreezeInstruction', () => { + it('8.11 creates freeze instruction', () => { + const instruction = createFreezeInstruction({ + tokenAccount: TEST_SOURCE, + mint: TEST_MINT, + freezeAuthority: TEST_FREEZE_AUTH, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.FREEZE); + expect(instruction.data.length).toBe(1); + }); +}); + +describe('createThawInstruction', () => { + it('8.12 creates thaw instruction', () => { + const instruction = createThawInstruction({ + tokenAccount: TEST_SOURCE, + mint: TEST_MINT, + freezeAuthority: TEST_FREEZE_AUTH, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.THAW); + expect(instruction.data.length).toBe(1); + }); +}); + +// ============================================================================ +// TEST: Approve/Revoke Instructions +// ============================================================================ + +describe('createApproveInstruction', () => { + it('8.13 creates approve instruction', () => { + const instruction = createApproveInstruction({ + tokenAccount: TEST_SOURCE, + delegate: TEST_DELEGATE, + owner: TEST_OWNER, + amount: 10_000n, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(3); + expect(instruction.data[0]).toBe(DISCRIMINATOR.APPROVE); + + // Verify amount + const amountBytes = instruction.data.slice(1, 9); + const dataView = new DataView(amountBytes.buffer, amountBytes.byteOffset); + expect(dataView.getBigUint64(0, true)).toBe(10_000n); + }); +}); + +describe('createRevokeInstruction', () => { + it('8.14 creates revoke instruction', () => { + const instruction = createRevokeInstruction({ + tokenAccount: TEST_SOURCE, + owner: TEST_OWNER, + }); + + expect(instruction.programAddress).toBe(LIGHT_TOKEN_PROGRAM_ID); + expect(instruction.accounts).toHaveLength(2); + expect(instruction.data[0]).toBe(DISCRIMINATOR.REVOKE); + expect(instruction.data.length).toBe(1); + }); +}); + +// ============================================================================ +// LIVE E2E TESTS (require running validator) +// ============================================================================ + +// These tests require a running test validator and are skipped by default. +// To run: Start validator, then run `LIVE_E2E=true pnpm test:e2e` + +describe.skip('Live E2E tests (require validator)', () => { + // const RPC_URL = 'http://127.0.0.1:8899'; + // const INDEXER_URL = 'http://127.0.0.1:8784'; + + it('should create and fund ATA', async () => { + // Implementation would: + // 1. Create keypair, airdrop SOL + // 2. Create mint + // 3. Create ATA with createAssociatedTokenAccountInstruction + // 4. Send transaction + // 5. Verify account exists via indexer + expect(true).toBe(true); + }); + + it('should transfer tokens between accounts', async () => { + // Implementation would: + // 1. Setup two accounts with tokens + // 2. Create transfer instruction + // 3. Send transaction + // 4. Verify balances changed + expect(true).toBe(true); + }); + + it('should mint tokens to account', async () => { + // Implementation would: + // 1. Create mint with authority + // 2. Create token account + // 3. Mint tokens + // 4. Verify balance increased + expect(true).toBe(true); + }); + + it('should burn tokens from account', async () => { + // Implementation would: + // 1. Setup account with tokens + // 2. Burn tokens + // 3. Verify balance decreased and supply decreased + expect(true).toBe(true); + }); + + it('should freeze and thaw account', async () => { + // Implementation would: + // 1. Create mint with freeze authority + // 2. Create token account + // 3. Freeze account + // 4. Verify account is frozen + // 5. Thaw account + // 6. Verify account is unfrozen + expect(true).toBe(true); + }); + + it('should approve and revoke delegate', async () => { + // Implementation would: + // 1. Create token account + // 2. Approve delegate + // 3. Verify delegate set + // 4. Revoke delegate + // 5. Verify delegate cleared + expect(true).toBe(true); + }); + + it('should close token account', async () => { + // Implementation would: + // 1. Create token account with zero balance + // 2. Close account + // 3. Verify account no longer exists + // 4. Verify rent returned + expect(true).toBe(true); + }); +}); diff --git a/js/token-sdk/tests/unit/utils.test.ts b/js/token-sdk/tests/unit/utils.test.ts new file mode 100644 index 0000000000..415f579acf --- /dev/null +++ b/js/token-sdk/tests/unit/utils.test.ts @@ -0,0 +1,287 @@ +/** + * Unit tests for Light Token SDK Utils + * + * Tests for: + * - PDA derivation functions + * - Validation functions + */ + +import { describe, it, expect } from 'vitest'; +import { address, type Address } from '@solana/addresses'; + +import { + deriveAssociatedTokenAddress, + getAssociatedTokenAddressWithBump, + deriveMintAddress, + derivePoolAddress, + deriveCpiAuthority, + validatePositiveAmount, + validateDecimals, + validateAtaDerivation, + isLightTokenAccount, + determineTransferType, + LIGHT_TOKEN_PROGRAM_ID, + CPI_AUTHORITY, +} from '../../src/index.js'; + +// ============================================================================ +// TEST: PDA Derivation Functions +// ============================================================================ + +describe('deriveAssociatedTokenAddress', () => { + it('6.1 derives correct ATA address', async () => { + const owner = address('11111111111111111111111111111111'); + const mint = address('So11111111111111111111111111111111111111112'); + + const result = await deriveAssociatedTokenAddress(owner, mint); + + expect(result.address).toBeDefined(); + expect(typeof result.bump).toBe('number'); + expect(result.bump).toBeGreaterThanOrEqual(0); + expect(result.bump).toBeLessThanOrEqual(255); + }); + + it('6.1.1 produces consistent results for same inputs', async () => { + const owner = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + const mint = address('So11111111111111111111111111111111111111112'); + + const result1 = await deriveAssociatedTokenAddress(owner, mint); + const result2 = await deriveAssociatedTokenAddress(owner, mint); + + expect(result1.address).toBe(result2.address); + expect(result1.bump).toBe(result2.bump); + }); + + it('6.1.2 produces different addresses for different owners', async () => { + const owner1 = address('11111111111111111111111111111111'); + const owner2 = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + const mint = address('So11111111111111111111111111111111111111112'); + + const result1 = await deriveAssociatedTokenAddress(owner1, mint); + const result2 = await deriveAssociatedTokenAddress(owner2, mint); + + expect(result1.address).not.toBe(result2.address); + }); +}); + +describe('getAssociatedTokenAddressWithBump', () => { + it('6.2 returns address when bump matches', async () => { + const owner = address('11111111111111111111111111111111'); + const mint = address('So11111111111111111111111111111111111111112'); + + // First derive to get the correct bump + const { address: expectedAddress, bump } = + await deriveAssociatedTokenAddress(owner, mint); + + // Then verify with bump + const result = await getAssociatedTokenAddressWithBump( + owner, + mint, + bump, + ); + + expect(result).toBe(expectedAddress); + }); + + it('6.2.1 throws when bump does not match', async () => { + const owner = address('11111111111111111111111111111111'); + const mint = address('So11111111111111111111111111111111111111112'); + + // Get the correct bump first + const { bump: correctBump } = await deriveAssociatedTokenAddress( + owner, + mint, + ); + + // Use wrong bump + const wrongBump = (correctBump + 1) % 256; + + await expect( + getAssociatedTokenAddressWithBump(owner, mint, wrongBump), + ).rejects.toThrow('Bump mismatch'); + }); +}); + +describe('deriveMintAddress', () => { + it('6.3 derives correct mint address', async () => { + const mintSigner = address('11111111111111111111111111111111'); + + const result = await deriveMintAddress(mintSigner); + + expect(result.address).toBeDefined(); + expect(typeof result.bump).toBe('number'); + expect(result.bump).toBeGreaterThanOrEqual(0); + expect(result.bump).toBeLessThanOrEqual(255); + }); + + it('6.3.1 produces consistent results', async () => { + const mintSigner = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + const result1 = await deriveMintAddress(mintSigner); + const result2 = await deriveMintAddress(mintSigner); + + expect(result1.address).toBe(result2.address); + expect(result1.bump).toBe(result2.bump); + }); +}); + +describe('derivePoolAddress', () => { + it('6.4 derives correct pool address without index', async () => { + const mint = address('So11111111111111111111111111111111111111112'); + + const result = await derivePoolAddress(mint); + + expect(result.address).toBeDefined(); + expect(typeof result.bump).toBe('number'); + }); + + it('6.4.1 derives correct pool address with index', async () => { + const mint = address('So11111111111111111111111111111111111111112'); + + const result = await derivePoolAddress(mint, 0); + + expect(result.address).toBeDefined(); + expect(typeof result.bump).toBe('number'); + }); + + it('6.4.2 different indices produce different addresses', async () => { + const mint = address('So11111111111111111111111111111111111111112'); + + const result0 = await derivePoolAddress(mint, 0); + const result1 = await derivePoolAddress(mint, 1); + + expect(result0.address).not.toBe(result1.address); + }); + + it('6.4.3 no index differs from index 0', async () => { + const mint = address('So11111111111111111111111111111111111111112'); + + const resultNoIndex = await derivePoolAddress(mint); + const resultIndex0 = await derivePoolAddress(mint, 0); + + expect(resultNoIndex.address).not.toBe(resultIndex0.address); + }); +}); + +describe('deriveCpiAuthority', () => { + it('6.5 derives correct CPI authority', async () => { + const result = await deriveCpiAuthority(); + + expect(result).toBe(CPI_AUTHORITY); + }); +}); + +// ============================================================================ +// TEST: Validation Functions +// ============================================================================ + +describe('validatePositiveAmount', () => { + it('7.1 passes for positive amount', () => { + expect(() => validatePositiveAmount(1n)).not.toThrow(); + expect(() => validatePositiveAmount(100n)).not.toThrow(); + expect(() => validatePositiveAmount(BigInt(Number.MAX_SAFE_INTEGER))).not.toThrow(); + }); + + it('7.1.1 throws for zero', () => { + expect(() => validatePositiveAmount(0n)).toThrow('Amount must be positive'); + }); + + it('7.1.2 throws for negative', () => { + expect(() => validatePositiveAmount(-1n)).toThrow('Amount must be positive'); + expect(() => validatePositiveAmount(-100n)).toThrow('Amount must be positive'); + }); +}); + +describe('validateDecimals', () => { + it('7.2 passes for valid decimals', () => { + expect(() => validateDecimals(0)).not.toThrow(); + expect(() => validateDecimals(6)).not.toThrow(); + expect(() => validateDecimals(9)).not.toThrow(); + expect(() => validateDecimals(255)).not.toThrow(); + }); + + it('7.2.1 throws for negative decimals', () => { + expect(() => validateDecimals(-1)).toThrow( + 'Decimals must be an integer between 0 and 255', + ); + }); + + it('7.2.2 throws for decimals > 255', () => { + expect(() => validateDecimals(256)).toThrow( + 'Decimals must be an integer between 0 and 255', + ); + }); + + it('7.2.3 throws for non-integer decimals', () => { + expect(() => validateDecimals(1.5)).toThrow( + 'Decimals must be an integer between 0 and 255', + ); + expect(() => validateDecimals(6.9)).toThrow( + 'Decimals must be an integer between 0 and 255', + ); + }); +}); + +describe('validateAtaDerivation', () => { + it('7.3 validates correct ATA derivation', async () => { + const owner = address('11111111111111111111111111111111'); + const mint = address('So11111111111111111111111111111111111111112'); + + const { address: ata } = await deriveAssociatedTokenAddress(owner, mint); + + const isValid = await validateAtaDerivation(ata, owner, mint); + + expect(isValid).toBe(true); + }); + + it('7.3.1 returns false for wrong ATA', async () => { + const owner = address('11111111111111111111111111111111'); + const mint = address('So11111111111111111111111111111111111111112'); + const wrongAta = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + const isValid = await validateAtaDerivation(wrongAta, owner, mint); + + expect(isValid).toBe(false); + }); +}); + +describe('isLightTokenAccount', () => { + it('7.4 correctly identifies Light token accounts', () => { + expect(isLightTokenAccount(LIGHT_TOKEN_PROGRAM_ID)).toBe(true); + }); + + it('7.4.1 returns false for non-Light accounts', () => { + const splToken = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + const systemProgram = address('11111111111111111111111111111111'); + + expect(isLightTokenAccount(splToken)).toBe(false); + expect(isLightTokenAccount(systemProgram)).toBe(false); + }); +}); + +describe('determineTransferType', () => { + const lightProgram = LIGHT_TOKEN_PROGRAM_ID; + const splProgram = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA'); + + it('7.5 returns light-to-light for both Light accounts', () => { + expect(determineTransferType(lightProgram, lightProgram)).toBe( + 'light-to-light', + ); + }); + + it('7.5.1 returns light-to-spl for Light source, SPL dest', () => { + expect(determineTransferType(lightProgram, splProgram)).toBe( + 'light-to-spl', + ); + }); + + it('7.5.2 returns spl-to-light for SPL source, Light dest', () => { + expect(determineTransferType(splProgram, lightProgram)).toBe( + 'spl-to-light', + ); + }); + + it('7.5.3 returns spl-to-spl for both SPL accounts', () => { + expect(determineTransferType(splProgram, splProgram)).toBe('spl-to-spl'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a5d7e23c49..c13efa4063 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -435,6 +435,28 @@ importers: specifier: ^2.1.1 version: 2.1.1(@types/node@22.16.5)(terser@5.43.1) + js/token-client: + dependencies: + '@lightprotocol/token-sdk': + specifier: workspace:* + version: link:../token-sdk + '@solana/addresses': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/codecs': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3) + '@solana/kit': + specifier: ^2.1.0 + version: 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(ws@8.18.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)) + devDependencies: + typescript: + specifier: ^5.7.3 + version: 5.9.3 + vitest: + specifier: ^2.1.8 + version: 2.1.9(@types/node@22.16.5)(terser@5.43.1) + js/token-idl: devDependencies: '@codama/nodes': diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 3fec7b2697..771fd57a33 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,4 +7,5 @@ packages: - "js/compressed-token/**" - "js/token-idl/**" - "js/token-sdk/**" + - "js/token-client/**" - "examples/**"