diff --git a/Cargo.lock b/Cargo.lock index cf681b64c6..58a4e01f5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 4 +version = 3 [[package]] name = "Inflector" @@ -3453,6 +3453,7 @@ dependencies = [ "solana-rpc-client", "solana-rpc-client-api", "solana-signature", + "solana-signer", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", diff --git a/cli/package.json b/cli/package.json index 12d48a5719..77ef3fda8f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -54,7 +54,7 @@ "@oclif/plugin-not-found": "^3.2.68", "@oclif/plugin-plugins": "^5.4.47", "@oclif/table": "^0.4.14", - "@solana/web3.js": "1.98.4", + "@solana/web3.js": "1.98.0", "axios": "1.12.2", "case-anything": "^2.1.13", "cli-progress": "^3.12.0", diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 664f916e91..f23e8fe96d 100644 --- a/js/compressed-token/package.json +++ b/js/compressed-token/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/compressed-token", - "version": "0.22.0", + "version": "0.22.1-alpha.0", "description": "JS client to interact with the compressed-token program", "sideEffects": false, "main": "dist/cjs/node/index.cjs", @@ -41,7 +41,6 @@ "devDependencies": { "@coral-xyz/anchor": "^0.29.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@eslint/js": "9.36.0", "@lightprotocol/hasher.rs": "0.2.1", "@lightprotocol/programs": "workspace:*", "@rollup/plugin-alias": "^5.1.0", @@ -53,14 +52,13 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@solana/spl-token": "0.4.8", - "@solana/web3.js": "1.98.4", "@types/bn.js": "^5.1.5", "@types/node": "^22.5.5", - "@typescript-eslint/eslint-plugin": "^8.44.0", - "@typescript-eslint/parser": "^8.44.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", "add": "^2.0.6", "crypto-browserify": "^3.12.0", - "eslint": "^9.36.0", + "eslint": "^8.56.0", "eslint-plugin-import": "^2.30.0", "eslint-plugin-n": "^17.10.2", "eslint-plugin-promise": "^7.1.0", diff --git a/js/compressed-token/rollup.config.js b/js/compressed-token/rollup.config.js index f19a4b3c29..12d2c4a462 100644 --- a/js/compressed-token/rollup.config.js +++ b/js/compressed-token/rollup.config.js @@ -20,7 +20,7 @@ const rolls = (fmt, env) => ({ external: [ '@solana/web3.js', '@solana/spl-token', - '@coral-xyz/borsh', + // '@coral-xyz/borsh', '@lightprotocol/stateless.js', ], plugins: [ diff --git a/js/stateless.js/package.json b/js/stateless.js/package.json index 3e2d985645..da07b4e513 100644 --- a/js/stateless.js/package.json +++ b/js/stateless.js/package.json @@ -1,6 +1,6 @@ { "name": "@lightprotocol/stateless.js", - "version": "0.22.0", + "version": "0.22.1-alpha.0", "description": "JavaScript API for Light & ZK Compression", "sideEffects": false, "main": "dist/cjs/node/index.cjs", @@ -35,12 +35,12 @@ ], "license": "Apache-2.0", "peerDependencies": { - "@solana/web3.js": ">=1.73.5" + "@solana/web3.js": ">=1.73.5", + "bn.js": "^5.1.2" }, "dependencies": { "@coral-xyz/borsh": "^0.29.0", "@noble/hashes": "1.5.0", - "bn.js": "^5.2.1", "bs58": "^6.0.0", "buffer": "6.0.3", "buffer-layout": "^1.2.2", @@ -49,10 +49,10 @@ "superstruct": "2.0.2" }, "devDependencies": { + "bn.js": "^5.1.2", "@coral-xyz/anchor": "0.29.0", "@coral-xyz/borsh": "^0.29.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", - "@eslint/js": "9.36.0", "@lightprotocol/hasher.rs": "0.2.1", "@lightprotocol/programs": "workspace:*", "@playwright/test": "^1.47.1", @@ -63,12 +63,11 @@ "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", - "@solana/web3.js": "1.98.4", "@types/bn.js": "^5.1.5", "@types/node": "^22.5.5", - "@typescript-eslint/eslint-plugin": "^8.44.0", - "@typescript-eslint/parser": "^8.44.0", - "eslint": "^9.36.0", + "@typescript-eslint/eslint-plugin": "^7.13.1", + "@typescript-eslint/parser": "^7.13.1", + "eslint": "^8.56.0", "eslint-plugin-n": "^17.10.2", "eslint-plugin-promise": "^7.1.0", "eslint-plugin-vitest": "^0.5.4", diff --git a/js/stateless.js/rollup.config.js b/js/stateless.js/rollup.config.js index c42b978ea9..955e18ff0e 100644 --- a/js/stateless.js/rollup.config.js +++ b/js/stateless.js/rollup.config.js @@ -16,7 +16,7 @@ const rolls = (fmt, env) => ({ entryFileNames: `[name].${fmt === 'cjs' ? 'cjs' : 'js'}`, sourcemap: true, }, - external: ['@solana/web3.js'], + external: ['@solana/web3.js', 'bn.js'], plugins: [ replace({ preventAssignment: true, diff --git a/js/stateless.js/src/programs/system/pack.ts b/js/stateless.js/src/programs/system/pack.ts index de88c30e33..c9bdb1aaf8 100644 --- a/js/stateless.js/src/programs/system/pack.ts +++ b/js/stateless.js/src/programs/system/pack.ts @@ -1,4 +1,5 @@ import { AccountMeta, PublicKey } from '@solana/web3.js'; +import BN from 'bn.js'; import { AccountProofInput, CompressedAccountLegacy, @@ -7,13 +8,16 @@ import { PackedCompressedAccountWithMerkleContext, TreeInfo, TreeType, + ValidityProof, } from '../../state'; +import { ValidityProofWithContext } from '../../rpc-interface'; import { CompressedAccountWithMerkleContextLegacy, PackedAddressTreeInfo, PackedStateTreeInfo, } from '../../state/compressed-account'; import { featureFlags } from '../../constants'; +import { PackedAccounts, PackedAccountsSmall } from '../../utils'; /** * @internal Finds the index of a PublicKey in an array, or adds it if not @@ -72,18 +76,10 @@ export function toAccountMetas(remainingAccounts: PublicKey[]): AccountMeta[] { ); } -export interface PackedStateTreeInfos { - packedTreeInfos: PackedStateTreeInfo[]; - outputTreeIndex: number; -} - -export interface PackedTreeInfos { - stateTrees?: PackedStateTreeInfos; - addressTrees: PackedAddressTreeInfo[]; -} - const INVALID_TREE_INDEX = -1; + /** + * @deprecated Use {@link packTreeInfos} instead. * Packs TreeInfos. Replaces PublicKey with index pointer to remaining accounts. * * Only use for MUT, CLOSE, NEW_ADDRESSES. For INIT, pass @@ -99,7 +95,7 @@ const INVALID_TREE_INDEX = -1; * @returns Remaining accounts, packed state and address tree infos, state tree * output index and address tree infos. */ -export function packTreeInfos( +export function packTreeInfosWithPubkeys( remainingAccounts: PublicKey[], accountProofInputs: AccountProofInput[], newAddressProofInputs: NewAddressProofInput[], @@ -113,7 +109,7 @@ export function packTreeInfos( // Early exit. if (accountProofInputs.length === 0 && newAddressProofInputs.length === 0) { return { - stateTrees: undefined, + stateTrees: null, addressTrees: addressTreeInfos, }; } @@ -181,7 +177,7 @@ export function packTreeInfos( packedTreeInfos: stateTreeInfos, outputTreeIndex, } - : undefined, + : null, addressTrees: addressTreeInfos, }; } @@ -307,3 +303,259 @@ export function packCompressedAccounts( remainingAccounts: _remainingAccounts, }; } + +/** + * Root index for state tree proofs. + */ +export type RootIndex = { + proofByIndex: boolean; + rootIndex: number; +}; + +/** + * Creates a RootIndex for proving by merkle proof. + */ +export function createRootIndex(rootIndex: number): RootIndex { + return { + proofByIndex: false, + rootIndex, + }; +} + +/** + * Creates a RootIndex for proving by leaf index. + */ +export function createRootIndexByIndex(): RootIndex { + return { + proofByIndex: true, + rootIndex: 0, + }; +} + +/** + * Account proof inputs for state tree accounts. + */ +export type AccountProofInputs = { + hash: Uint8Array; + root: Uint8Array; + rootIndex: RootIndex; + leafIndex: number; + treeInfo: TreeInfo; +}; + +/** + * Address proof inputs for address tree accounts. + */ +export type AddressProofInputs = { + address: Uint8Array; + root: Uint8Array; + rootIndex: number; + treeInfo: TreeInfo; +}; + +/** + * Validity proof with context structure that matches Rust implementation. + */ +export type ValidityProofWithContextV2 = { + proof: ValidityProof | null; + accounts: AccountProofInputs[]; + addresses: AddressProofInputs[]; +}; + +/** + * Packed state tree infos. + */ +export type PackedStateTreeInfos = { + packedTreeInfos: PackedStateTreeInfo[]; + outputTreeIndex: number; +}; + +/** + * Packed tree infos containing both state and address trees. + */ +export type PackedTreeInfos = { + stateTrees: PackedStateTreeInfos | null; + addressTrees: PackedAddressTreeInfo[]; +}; + +/** + * Packs the output tree index based on tree type. + * For StateV1, returns the index of the tree account. + * For StateV2, returns the index of the queue account. + */ +function packOutputTreeIndex( + treeInfo: TreeInfo, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): number { + switch (treeInfo.treeType) { + case TreeType.StateV1: + return packedAccounts.insertOrGet(treeInfo.tree); + case TreeType.StateV2: + return packedAccounts.insertOrGet(treeInfo.queue); + default: + throw new Error('Invalid tree type for packing output tree index'); + } +} + +/** + * Converts ValidityProofWithContext to ValidityProofWithContextV2 format. + * Infers the split between state and address accounts based on tree types. + */ +function convertValidityProofToV2( + validityProof: ValidityProofWithContext, +): ValidityProofWithContextV2 { + const accounts: AccountProofInputs[] = []; + const addresses: AddressProofInputs[] = []; + + for (let i = 0; i < validityProof.treeInfos.length; i++) { + const treeInfo = validityProof.treeInfos[i]; + + if ( + treeInfo.treeType === TreeType.StateV1 || + treeInfo.treeType === TreeType.StateV2 + ) { + // State tree account + accounts.push({ + hash: new Uint8Array(validityProof.leaves[i].toArray('le', 32)), + root: new Uint8Array(validityProof.roots[i].toArray('le', 32)), + rootIndex: { + proofByIndex: validityProof.proveByIndices[i], + rootIndex: validityProof.rootIndices[i], + }, + leafIndex: validityProof.leafIndices[i], + treeInfo, + }); + } else { + // Address tree account + addresses.push({ + address: new Uint8Array( + validityProof.leaves[i].toArray('le', 32), + ), + root: new Uint8Array(validityProof.roots[i].toArray('le', 32)), + rootIndex: validityProof.rootIndices[i], + treeInfo, + }); + } + } + + return { + proof: validityProof.compressedProof, + accounts, + addresses, + }; +} + +/** + * Packs tree infos from ValidityProofWithContext into packed format. This is a + * TypeScript equivalent of the Rust pack_tree_infos method. + * + * @param validityProof - The validity proof with context (flat format) + * @param packedAccounts - The packed accounts manager (supports both PackedAccounts and PackedAccountsSmall) + * @returns Packed tree infos + */ +export function packTreeInfos( + validityProof: ValidityProofWithContext, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos; + +/** + * Packs tree infos from ValidityProofWithContextV2 into packed format. This is + * a TypeScript equivalent of the Rust pack_tree_infos method. + * + * @param validityProof - The validity proof with context (structured format) + * @param packedAccounts - The packed accounts manager (supports both PackedAccounts and PackedAccountsSmall) + * @returns Packed tree infos + */ +export function packTreeInfos( + validityProof: ValidityProofWithContextV2, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos; + +export function packTreeInfos( + validityProof: ValidityProofWithContext | ValidityProofWithContextV2, + packedAccounts: PackedAccounts | PackedAccountsSmall, +): PackedTreeInfos { + // Convert flat format to structured format if needed + const structuredProof = + 'accounts' in validityProof + ? (validityProof as ValidityProofWithContextV2) + : convertValidityProofToV2( + validityProof as ValidityProofWithContext, + ); + const packedTreeInfos: PackedStateTreeInfo[] = []; + const addressTrees: PackedAddressTreeInfo[] = []; + let outputTreeIndex: number | null = null; + + // Process state tree accounts + for (const account of structuredProof.accounts) { + // Pack TreeInfo + const merkleTreePubkeyIndex = packedAccounts.insertOrGet( + account.treeInfo.tree, + ); + const queuePubkeyIndex = packedAccounts.insertOrGet( + account.treeInfo.queue, + ); + + const treeInfoPacked: PackedStateTreeInfo = { + rootIndex: account.rootIndex.rootIndex, + merkleTreePubkeyIndex, + queuePubkeyIndex, + leafIndex: account.leafIndex, + proveByIndex: account.rootIndex.proofByIndex, + }; + packedTreeInfos.push(treeInfoPacked); + + // Determine output tree index + // If a next Merkle tree exists, the Merkle tree is full -> use the next Merkle tree for new state. + // Else use the current Merkle tree for new state. + if (account.treeInfo.nextTreeInfo) { + // SAFETY: account will always have a state Merkle tree context. + // packOutputTreeIndex only throws on an invalid address Merkle tree context. + const index = packOutputTreeIndex( + account.treeInfo.nextTreeInfo, + packedAccounts, + ); + if (outputTreeIndex === null) { + outputTreeIndex = index; + } + } else { + // SAFETY: account will always have a state Merkle tree context. + // packOutputTreeIndex only throws on an invalid address Merkle tree context. + const index = packOutputTreeIndex(account.treeInfo, packedAccounts); + if (outputTreeIndex === null) { + outputTreeIndex = index; + } + } + } + + // Process address tree accounts + for (const address of structuredProof.addresses) { + // Pack AddressTreeInfo + const addressMerkleTreePubkeyIndex = packedAccounts.insertOrGet( + address.treeInfo.tree, + ); + const addressQueuePubkeyIndex = packedAccounts.insertOrGet( + address.treeInfo.queue, + ); + + addressTrees.push({ + addressMerkleTreePubkeyIndex, + addressQueuePubkeyIndex, + rootIndex: address.rootIndex, + }); + } + + // Create final packed tree infos + const stateTrees = + packedTreeInfos.length === 0 + ? null + : { + packedTreeInfos, + outputTreeIndex: outputTreeIndex!, + }; + + return { + stateTrees, + addressTrees, + }; +} diff --git a/js/stateless.js/src/state/bn.ts b/js/stateless.js/src/state/bn.ts index b8b69581a9..36cc6c85c3 100644 --- a/js/stateless.js/src/state/bn.ts +++ b/js/stateless.js/src/state/bn.ts @@ -1,5 +1,9 @@ import BN from 'bn.js'; import { Buffer } from 'buffer'; + +// Re-export BN class +export { default as BN } from 'bn.js'; + export const bn = ( number: string | number | BN | Buffer | Uint8Array | number[], base?: number | 'hex' | undefined, diff --git a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts index 5f0c9e96a6..748a1292fd 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/get-compressed-token-accounts.ts @@ -62,7 +62,7 @@ export function parseTokenLayoutWithIdl( if (data.length === 0) return null; - if (compressedAccount.owner.toBase58() !== programId.toBase58()) { + if (!compressedAccount.owner.equals(programId)) { throw new Error( `Invalid owner ${compressedAccount.owner.toBase58()} for token layout`, ); @@ -76,6 +76,25 @@ export function parseTokenLayoutWithIdl( } } +/** + * Manually parse the compressed token layout for a given compressed account. + * @param compressedAccount - The compressed account + * @returns The parsed token data + */ +export function parseTokenData(data: Buffer): TokenData | null { + if (data === null) return null; + if (data.length === 0) return null; + + try { + const decoded = TokenDataLayout.decode(Buffer.from(data)); + + return decoded; + } catch (error) { + console.error('Decoding error:', error); + throw error; + } +} + /** * parse compressed accounts of an event with token layout * @internal diff --git a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts index 3b206a7285..ef98013c9b 100644 --- a/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts +++ b/js/stateless.js/src/test-helpers/test-rpc/test-rpc.ts @@ -1,4 +1,9 @@ -import { Connection, ConnectionConfig, PublicKey } from '@solana/web3.js'; +import { + AccountInfo, + Connection, + ConnectionConfig, + PublicKey, +} from '@solana/web3.js'; import BN from 'bn.js'; import { getCompressedAccountByHashTest, @@ -13,6 +18,7 @@ import { import { MerkleTree } from '../merkle-tree/merkle-tree'; import { getParsedEvents } from './get-parsed-events'; import { + COMPRESSED_TOKEN_PROGRAM_ID, defaultTestStateTreeAccounts, localTestActiveStateTreeInfos, } from '../../constants'; @@ -39,8 +45,10 @@ import { import { BN254, CompressedAccountWithMerkleContext, + MerkleContext, MerkleContextWithMerkleProof, PublicTransactionEvent, + TokenData, TreeType, bn, } from '../../state'; @@ -148,7 +156,7 @@ export class TestRpc extends Connection implements CompressionApiInterface { connectionConfig?: ConnectionConfig, testRpcConfig?: TestRpcConfig, ) { - super(endpoint, connectionConfig || { commitment: 'confirmed' }); + super(endpoint, connectionConfig || 'confirmed'); this.compressionApiEndpoint = compressionApiEndpoint; this.proverEndpoint = proverEndpoint; diff --git a/js/stateless.js/src/utils/address.ts b/js/stateless.js/src/utils/address.ts index fd5811b58a..2432cad789 100644 --- a/js/stateless.js/src/utils/address.ts +++ b/js/stateless.js/src/utils/address.ts @@ -1,13 +1,49 @@ import { PublicKey } from '@solana/web3.js'; -import { - hashToBn254FieldSizeBe, - hashvToBn254FieldSizeBe, - hashvToBn254FieldSizeBeU8Array, -} from './conversion'; +import { hashToBn254FieldSizeBe, hashvToBn254FieldSizeBe } from './conversion'; import { defaultTestStateTreeAccounts } from '../constants'; import { getIndexOrAdd } from '../programs/system/pack'; import { keccak_256 } from '@noble/hashes/sha3'; +/** + * Derive an address for a compressed account from a seed and an address Merkle + * tree public key. + * + * @param seed 32 bytes seed to derive the address from + * @param addressMerkleTreePubkey Address Merkle tree public key as bytes. + * @param programIdBytes Program ID bytes. + * @returns Derived address as bytes + */ +export function deriveAddressV2( + seed: Uint8Array, + addressMerkleTreePubkey: Uint8Array, + programIdBytes: Uint8Array, +): Uint8Array { + const slices = [seed, addressMerkleTreePubkey, programIdBytes]; + + return hashVWithBumpSeed(slices); +} + +export function hashVWithBumpSeed(bytes: Uint8Array[]): Uint8Array { + const HASH_TO_FIELD_SIZE_SEED = 255; // u8::MAX + + const hasher = keccak_256.create(); + + // Hash all input bytes + for (const input of bytes) { + hasher.update(input); + } + + // Add the bump seed (just like Rust version) + hasher.update(new Uint8Array([HASH_TO_FIELD_SIZE_SEED])); + + const hash = hasher.digest(); + + // Truncate to BN254 field size (just like Rust version) + hash[0] = 0; + + return hash; +} + export function deriveAddressSeed( seeds: Uint8Array[], programId: PublicKey, @@ -17,7 +53,9 @@ export function deriveAddressSeed( return hash; } -/* +/** + * @deprecated Use {@link deriveAddressV2} instead, unless you're using v1. + * * Derive an address for a compressed account from a seed and an address Merkle * tree public key. * @@ -45,42 +83,6 @@ export function deriveAddress( return new PublicKey(buf); } -export function deriveAddressSeedV2(seeds: Uint8Array[]): Uint8Array { - const combinedSeeds: Uint8Array[] = seeds.map(seed => - Uint8Array.from(seed), - ); - const hash = hashvToBn254FieldSizeBeU8Array(combinedSeeds); - return hash; -} - -/** - * Derives an address from a seed using the v2 method (matching Rust's derive_address_from_seed) - * - * @param addressSeed The address seed (32 bytes) - * @param addressMerkleTreePubkey Merkle tree public key - * @param programId Program ID - * @returns Derived address - */ -export function deriveAddressV2( - addressSeed: Uint8Array, - addressMerkleTreePubkey: PublicKey, - programId: PublicKey, -): PublicKey { - if (addressSeed.length != 32) { - throw new Error('Address seed length is not 32 bytes.'); - } - const merkleTreeBytes = addressMerkleTreePubkey.toBytes(); - const programIdBytes = programId.toBytes(); - // Match Rust implementation: hash [seed, merkle_tree_pubkey, program_id] - const combined = [ - Uint8Array.from(addressSeed), - Uint8Array.from(merkleTreeBytes), - Uint8Array.from(programIdBytes), - ]; - const hash = hashvToBn254FieldSizeBeU8Array(combined); - return new PublicKey(hash); -} - export interface NewAddressParams { /** * Seed for the compressed account. Must be seed used to derive diff --git a/js/stateless.js/src/utils/conversion.ts b/js/stateless.js/src/utils/conversion.ts index 2343b545bd..86ebf6b880 100644 --- a/js/stateless.js/src/utils/conversion.ts +++ b/js/stateless.js/src/utils/conversion.ts @@ -78,20 +78,8 @@ export function hashToBn254FieldSizeBe(bytes: Buffer): [Buffer, number] | null { return null; } -export function hashvToBn254FieldSizeBeU8Array( - bytes: Uint8Array[], -): Uint8Array { - const hasher = keccak_256.create(); - for (const input of bytes) { - hasher.update(input); - } - hasher.update(Uint8Array.from([255])); - const hash = hasher.digest(); - hash[0] = 0; - return hash; -} - /** + * TODO: make consistent with latest rust. (use u8::max bumpseed) * Hash the provided `bytes` with Keccak256 and ensure that the result fits in * the BN254 prime field by truncating the resulting hash to 31 bytes. * diff --git a/js/stateless.js/src/utils/index.ts b/js/stateless.js/src/utils/index.ts index 1135d41f81..d079b7e786 100644 --- a/js/stateless.js/src/utils/index.ts +++ b/js/stateless.js/src/utils/index.ts @@ -10,3 +10,4 @@ export * from './sleep'; export * from './validation'; export * from './state-tree-lookup-table'; export * from './get-state-tree-infos'; +export * from './packed-accounts'; diff --git a/js/stateless.js/src/utils/packed-accounts.ts b/js/stateless.js/src/utils/packed-accounts.ts new file mode 100644 index 0000000000..13aee187cc --- /dev/null +++ b/js/stateless.js/src/utils/packed-accounts.ts @@ -0,0 +1,503 @@ +import { defaultStaticAccountsStruct } from '../constants'; +import { LightSystemProgram } from '../programs/system'; +import { AccountMeta, PublicKey, SystemProgram } from '@solana/web3.js'; + +/** + * This file provides two variants of packed accounts for Light Protocol: + * + * 1. PackedAccounts - Matches CpiAccounts (11 system accounts) + * - Includes: LightSystemProgram, Authority, RegisteredProgramPda, NoopProgram, + * AccountCompressionAuthority, AccountCompressionProgram, InvokingProgram, + * [Optional: SolPoolPda, DecompressionRecipient], SystemProgram, [Optional: CpiContext] + * + * 2. PackedAccountsSmall - Matches CpiAccountsSmall (9 system accounts max) + * - Includes: LightSystemProgram, Authority, RegisteredProgramPda, + * AccountCompressionAuthority, AccountCompressionProgram, SystemProgram, + * [Optional: SolPoolPda, DecompressionRecipient, CpiContext] + * - Excludes: NoopProgram and InvokingProgram for a more compact structure + */ + +/** + * Create a PackedAccounts instance to pack the light protocol system accounts + * for your custom program instruction. Typically, you will append them to the + * end of your instruction's accounts / remainingAccounts. + * + * This matches the full CpiAccounts structure with 11 system accounts including + * NoopProgram and InvokingProgram. For a more compact version, use PackedAccountsSmall. + * + * @example + * ```ts + * const packedAccounts = PackedAccounts.newWithSystemAccounts(config); + * + * const instruction = new TransactionInstruction({ + * keys: [...yourInstructionAccounts, ...packedAccounts.toAccountMetas()], + * programId: selfProgram, + * data: data, + * }); + * ``` + */ +export class PackedAccounts { + private preAccounts: AccountMeta[] = []; + private systemAccounts: AccountMeta[] = []; + private nextIndex: number = 0; + private map: Map = new Map(); + + static newWithSystemAccounts( + config: SystemAccountMetaConfig, + ): PackedAccounts { + const instance = new PackedAccounts(); + instance.addSystemAccounts(config); + return instance; + } + + addPreAccountsSigner(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: false }); + } + + addPreAccountsSignerMut(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: true }); + } + + addPreAccountsMeta(accountMeta: AccountMeta): void { + this.preAccounts.push(accountMeta); + } + + addSystemAccounts(config: SystemAccountMetaConfig): void { + this.systemAccounts.push(...getLightSystemAccountMetas(config)); + } + + insertOrGet(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, true); + } + + insertOrGetReadOnly(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, false); + } + + insertOrGetConfig( + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, + ): number { + const key = pubkey.toString(); + const entry = this.map.get(key); + if (entry) { + return entry[0]; + } + const index = this.nextIndex++; + const meta: AccountMeta = { pubkey, isSigner, isWritable }; + this.map.set(key, [index, meta]); + return index; + } + + private hashSetAccountsToMetas(): AccountMeta[] { + const entries = Array.from(this.map.entries()); + entries.sort((a, b) => a[1][0] - b[1][0]); + return entries.map(([, [, meta]]) => meta); + } + + private getOffsets(): [number, number] { + const systemStart = this.preAccounts.length; + const packedStart = systemStart + this.systemAccounts.length; + return [systemStart, packedStart]; + } + + toAccountMetas(): { + remainingAccounts: AccountMeta[]; + systemStart: number; + packedStart: number; + } { + const packed = this.hashSetAccountsToMetas(); + const [systemStart, packedStart] = this.getOffsets(); + return { + remainingAccounts: [ + ...this.preAccounts, + ...this.systemAccounts, + ...packed, + ], + systemStart, + packedStart, + }; + } +} + +/** + * Creates a PackedAccounts instance with system accounts for the specified + * program. This is a convenience wrapper around SystemAccountMetaConfig.new() + * and PackedAccounts.newWithSystemAccounts(). + * + * @param programId - The program ID that will be using these system accounts + * @returns A new PackedAccounts instance with system accounts configured + * + * @example + * ```ts + * const packedAccounts = createPackedAccounts(myProgram.programId); + * + * const instruction = new TransactionInstruction({ + * keys: [...yourInstructionAccounts, ...packedAccounts.toAccountMetas().remainingAccounts], + * programId: myProgram.programId, + * data: instructionData, + * }); + * ``` + */ +export function createPackedAccounts(programId: PublicKey): PackedAccounts { + const systemAccountConfig = SystemAccountMetaConfig.new(programId); + return PackedAccounts.newWithSystemAccounts(systemAccountConfig); +} + +/** + * Creates a PackedAccounts instance with system accounts and CPI context for the specified program. + * This is a convenience wrapper that includes CPI context configuration. + * + * @param programId - The program ID that will be using these system accounts + * @param cpiContext - The CPI context account public key + * @returns A new PackedAccounts instance with system accounts and CPI context configured + * + * @example + * ```ts + * const packedAccounts = createPackedAccountsWithCpiContext( + * myProgram.programId, + * cpiContextAccount + * ); + * ``` + */ +export function createPackedAccountsWithCpiContext( + programId: PublicKey, + cpiContext: PublicKey, +): PackedAccounts { + const systemAccountConfig = SystemAccountMetaConfig.newWithCpiContext( + programId, + cpiContext, + ); + return PackedAccounts.newWithSystemAccounts(systemAccountConfig); +} + +export class SystemAccountMetaConfig { + selfProgram: PublicKey; + cpiContext?: PublicKey; + solCompressionRecipient?: PublicKey; + solPoolPda?: PublicKey; + + private constructor( + selfProgram: PublicKey, + cpiContext?: PublicKey, + solCompressionRecipient?: PublicKey, + solPoolPda?: PublicKey, + ) { + this.selfProgram = selfProgram; + this.cpiContext = cpiContext; + this.solCompressionRecipient = solCompressionRecipient; + this.solPoolPda = solPoolPda; + } + + static new(selfProgram: PublicKey): SystemAccountMetaConfig { + return new SystemAccountMetaConfig(selfProgram); + } + + static newWithCpiContext( + selfProgram: PublicKey, + cpiContext: PublicKey, + ): SystemAccountMetaConfig { + return new SystemAccountMetaConfig(selfProgram, cpiContext); + } +} + +/** + * Get the light protocol system accounts for your custom program instruction. + * Use via `link PackedAccounts.addSystemAccounts(config)`. + */ +export function getLightSystemAccountMetas( + config: SystemAccountMetaConfig, +): AccountMeta[] { + const signerSeed = new TextEncoder().encode('cpi_authority'); + const cpiSigner = PublicKey.findProgramAddressSync( + [signerSeed], + config.selfProgram, + )[0]; + const defaults = SystemAccountPubkeys.default(); + const metas: AccountMeta[] = [ + { + pubkey: defaults.lightSystemProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: cpiSigner, isSigner: false, isWritable: false }, + { + pubkey: defaults.registeredProgramPda, + isSigner: false, + isWritable: false, + }, + { pubkey: defaults.noopProgram, isSigner: false, isWritable: false }, + { + pubkey: defaults.accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: config.selfProgram, isSigner: false, isWritable: false }, + ]; + if (config.solPoolPda) { + metas.push({ + pubkey: config.solPoolPda, + isSigner: false, + isWritable: true, + }); + } + if (config.solCompressionRecipient) { + metas.push({ + pubkey: config.solCompressionRecipient, + isSigner: false, + isWritable: true, + }); + } + metas.push({ + pubkey: defaults.systemProgram, + isSigner: false, + isWritable: false, + }); + if (config.cpiContext) { + metas.push({ + pubkey: config.cpiContext, + isSigner: false, + isWritable: true, + }); + } + return metas; +} + +/** + * PackedAccountsSmall matches the CpiAccountsSmall structure with simplified account ordering. + * This is a more compact version that excludes NoopProgram and InvokingProgram. + */ +export class PackedAccountsSmall { + private preAccounts: AccountMeta[] = []; + private systemAccounts: AccountMeta[] = []; + private nextIndex: number = 0; + private map: Map = new Map(); + + static newWithSystemAccounts( + config: SystemAccountMetaConfig, + ): PackedAccountsSmall { + const instance = new PackedAccountsSmall(); + instance.addSystemAccounts(config); + return instance; + } + + /** + * Returns the internal map of pubkey to [index, AccountMeta]. + * For debugging purposes only. + */ + getNamedMetas(): Map { + return this.map; + } + + addPreAccountsSigner(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: false }); + } + + addPreAccountsSignerMut(pubkey: PublicKey): void { + this.preAccounts.push({ pubkey, isSigner: true, isWritable: true }); + } + + addPreAccountsMeta(accountMeta: AccountMeta): void { + this.preAccounts.push(accountMeta); + } + + addSystemAccounts(config: SystemAccountMetaConfig): void { + this.systemAccounts.push(...getLightSystemAccountMetasSmall(config)); + } + + insertOrGet(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, true); + } + + insertOrGetReadOnly(pubkey: PublicKey): number { + return this.insertOrGetConfig(pubkey, false, false); + } + + insertOrGetConfig( + pubkey: PublicKey, + isSigner: boolean, + isWritable: boolean, + ): number { + const key = pubkey.toString(); + const entry = this.map.get(key); + if (entry) { + return entry[0]; + } + const index = this.nextIndex++; + const meta: AccountMeta = { pubkey, isSigner, isWritable }; + this.map.set(key, [index, meta]); + return index; + } + + private hashSetAccountsToMetas(): AccountMeta[] { + const entries = Array.from(this.map.entries()); + entries.sort((a, b) => a[1][0] - b[1][0]); + return entries.map(([, [, meta]]) => meta); + } + + private getOffsets(): [number, number] { + const systemStart = this.preAccounts.length; + const packedStart = systemStart + this.systemAccounts.length; + return [systemStart, packedStart]; + } + + toAccountMetas(): { + remainingAccounts: AccountMeta[]; + systemStart: number; + packedStart: number; + } { + const packed = this.hashSetAccountsToMetas(); + const [systemStart, packedStart] = this.getOffsets(); + return { + remainingAccounts: [ + ...this.preAccounts, + ...this.systemAccounts, + ...packed, + ], + systemStart, + packedStart, + }; + } +} + +/** + * Get the light protocol system accounts for the small variant. + * This matches CpiAccountsSmall ordering: removes NoopProgram and InvokingProgram. + */ +export function getLightSystemAccountMetasSmall( + config: SystemAccountMetaConfig, +): AccountMeta[] { + const signerSeed = new TextEncoder().encode('cpi_authority'); + const cpiSigner = PublicKey.findProgramAddressSync( + [signerSeed], + config.selfProgram, + )[0]; + const defaults = SystemAccountPubkeys.default(); + + // Small variant ordering: LightSystemProgram, Authority, RegisteredProgramPda, + // AccountCompressionAuthority, AccountCompressionProgram, SystemProgram, + // [Optional: SolPoolPda, DecompressionRecipient, CpiContext] + const metas: AccountMeta[] = [ + { + pubkey: defaults.lightSystemProgram, + isSigner: false, + isWritable: false, + }, + { pubkey: cpiSigner, isSigner: false, isWritable: false }, + { + pubkey: defaults.registeredProgramPda, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionAuthority, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.accountCompressionProgram, + isSigner: false, + isWritable: false, + }, + { + pubkey: defaults.systemProgram, + isSigner: false, + isWritable: false, + }, + ]; + + // Optional accounts in order + if (config.solPoolPda) { + metas.push({ + pubkey: config.solPoolPda, + isSigner: false, + isWritable: true, + }); + } + if (config.solCompressionRecipient) { + metas.push({ + pubkey: config.solCompressionRecipient, + isSigner: false, + isWritable: true, + }); + } + if (config.cpiContext) { + metas.push({ + pubkey: config.cpiContext, + isSigner: false, + isWritable: true, + }); + } + return metas; +} + +/** + * Creates a PackedAccountsSmall instance with system accounts for the specified program. + * This uses the simplified account ordering that matches CpiAccountsSmall. + */ +export function createPackedAccountsSmall( + programId: PublicKey, +): PackedAccountsSmall { + const systemAccountConfig = SystemAccountMetaConfig.new(programId); + return PackedAccountsSmall.newWithSystemAccounts(systemAccountConfig); +} + +/** + * Creates a PackedAccountsSmall instance with system accounts and CPI context. + */ +export function createPackedAccountsSmallWithCpiContext( + programId: PublicKey, + cpiContext: PublicKey, +): PackedAccountsSmall { + const systemAccountConfig = SystemAccountMetaConfig.newWithCpiContext( + programId, + cpiContext, + ); + return PackedAccountsSmall.newWithSystemAccounts(systemAccountConfig); +} + +export class SystemAccountPubkeys { + lightSystemProgram: PublicKey; + systemProgram: PublicKey; + accountCompressionProgram: PublicKey; + accountCompressionAuthority: PublicKey; + registeredProgramPda: PublicKey; + noopProgram: PublicKey; + solPoolPda: PublicKey; + + private constructor( + lightSystemProgram: PublicKey, + systemProgram: PublicKey, + accountCompressionProgram: PublicKey, + accountCompressionAuthority: PublicKey, + registeredProgramPda: PublicKey, + noopProgram: PublicKey, + solPoolPda: PublicKey, + ) { + this.lightSystemProgram = lightSystemProgram; + this.systemProgram = systemProgram; + this.accountCompressionProgram = accountCompressionProgram; + this.accountCompressionAuthority = accountCompressionAuthority; + this.registeredProgramPda = registeredProgramPda; + this.noopProgram = noopProgram; + this.solPoolPda = solPoolPda; + } + + static default(): SystemAccountPubkeys { + return new SystemAccountPubkeys( + LightSystemProgram.programId, + SystemProgram.programId, + defaultStaticAccountsStruct().accountCompressionProgram, + defaultStaticAccountsStruct().accountCompressionAuthority, + defaultStaticAccountsStruct().registeredProgramPda, + defaultStaticAccountsStruct().noopProgram, + PublicKey.default, + ); + } +} diff --git a/js/stateless.js/src/utils/validation.ts b/js/stateless.js/src/utils/validation.ts index 39ea74e319..66adf4f56a 100644 --- a/js/stateless.js/src/utils/validation.ts +++ b/js/stateless.js/src/utils/validation.ts @@ -4,6 +4,7 @@ import { CompressedAccountWithMerkleContext, bn, } from '../state'; +import { featureFlags } from '../constants'; export const validateSufficientBalance = (balance: BN) => { if (balance.lt(bn(0))) { @@ -38,7 +39,15 @@ export const validateNumbersForProof = ( `Invalid number of compressed accounts for proof: ${hashesLength}. Allowed numbers: ${[1, 2, 3, 4].join(', ')}`, ); } - validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + if (!featureFlags.isV2()) { + validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + } else { + validateNumbers( + hashesLength, + [1, 2, 3, 4, 8], + 'compressed accounts', + ); + } validateNumbersForNonInclusionProof(newAddressesLength); } else { if (hashesLength > 0) { @@ -51,14 +60,26 @@ export const validateNumbersForProof = ( /// Ensure that the amount if compressed accounts is allowed. export const validateNumbersForInclusionProof = (hashesLength: number) => { - validateNumbers(hashesLength, [1, 2, 3, 4, 8], 'compressed accounts'); + if (!featureFlags.isV2()) { + validateNumbers(hashesLength, [1, 2, 3, 4], 'compressed accounts'); + } else { + validateNumbers( + hashesLength, + [1, 2, 3, 4, 5, 8], + 'compressed accounts', + ); + } }; /// Ensure that the amount if new addresses is allowed. export const validateNumbersForNonInclusionProof = ( newAddressesLength: number, ) => { - validateNumbers(newAddressesLength, [1, 2], 'new addresses'); + if (!featureFlags.isV2()) { + validateNumbers(newAddressesLength, [1, 2], 'new addresses'); + } else { + validateNumbers(newAddressesLength, [1, 2, 3, 4], 'new addresses'); + } }; /// V1 circuit safeguards. diff --git a/package.json b/package.json index e5294f6c3f..35f7f3a408 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "devDependencies": { + "@solana/web3.js": "1.98.0", "husky": "^9.1.7", "nx": "^20.8.1", "playwright": "^1.54.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 151624bd4d..5d46855e82 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ importers: .: devDependencies: + '@solana/web3.js': + specifier: 1.98.0 + version: 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) husky: specifier: ^9.1.7 version: 9.1.7 @@ -34,7 +37,7 @@ importers: dependencies: '@coral-xyz/anchor': specifier: 0.29.0 - version: 0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@lightprotocol/compressed-token': specifier: workspace:* version: link:../js/compressed-token @@ -63,8 +66,8 @@ importers: specifier: ^0.4.14 version: 0.4.14(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@solana/web3.js': - specifier: 1.98.4 - version: 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + specifier: 1.98.0 + version: 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) axios: specifier: 1.12.2 version: 1.12.2 @@ -116,7 +119,7 @@ importers: version: 4.1.14(@oclif/core@4.5.4) '@solana/spl-token': specifier: ^0.3.11 - version: 0.3.11(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.3.11(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10) '@types/bn.js': specifier: ^5.1.5 version: 5.2.0 @@ -191,10 +194,13 @@ importers: dependencies: '@coral-xyz/borsh': specifier: ^0.29.0 - version: 0.29.0(@solana/web3.js@1.98.4(typescript@5.9.2)) + version: 0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@lightprotocol/stateless.js': specifier: workspace:* version: link:../stateless.js + '@solana/web3.js': + specifier: '>=1.73.5' + version: 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: specifier: ^5.2.1 version: 5.2.1 @@ -204,13 +210,10 @@ importers: devDependencies: '@coral-xyz/anchor': specifier: ^0.29.0 - version: 0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@esbuild-plugins/node-globals-polyfill': specifier: ^0.2.3 version: 0.2.3(esbuild@0.25.10) - '@eslint/js': - specifier: 9.36.0 - version: 9.36.0 '@lightprotocol/hasher.rs': specifier: 0.2.1 version: 0.2.1 @@ -243,10 +246,7 @@ importers: version: 11.1.6(rollup@4.21.3)(tslib@2.7.0)(typescript@5.9.2) '@solana/spl-token': specifier: 0.4.8 - version: 0.4.8(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/web3.js': - specifier: 1.98.4 - version: 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.4.8(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@types/bn.js': specifier: ^5.1.5 version: 5.2.0 @@ -254,11 +254,11 @@ importers: specifier: ^22.5.5 version: 22.16.5 '@typescript-eslint/eslint-plugin': - specifier: ^8.44.0 - version: 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + specifier: ^7.13.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/parser': - specifier: ^8.44.0 - version: 8.44.0(eslint@9.36.0)(typescript@5.9.2) + specifier: ^7.13.1 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.2) add: specifier: ^2.0.6 version: 2.0.6 @@ -266,20 +266,20 @@ importers: specifier: ^3.12.0 version: 3.12.0 eslint: - specifier: ^9.36.0 - version: 9.36.0 + specifier: ^8.56.0 + version: 8.57.1 eslint-plugin-import: specifier: ^2.30.0 - version: 2.30.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0) + version: 2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1) eslint-plugin-n: specifier: ^17.10.2 - version: 17.10.2(eslint@9.36.0) + version: 17.10.2(eslint@8.57.1) eslint-plugin-promise: specifier: ^7.1.0 - version: 7.1.0(eslint@9.36.0) + version: 7.1.0(eslint@8.57.1) eslint-plugin-vitest: specifier: ^0.5.4 - version: 0.5.4(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)) + version: 0.5.4(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)) prettier: specifier: ^3.3.3 version: 3.6.2 @@ -318,13 +318,13 @@ importers: dependencies: '@coral-xyz/borsh': specifier: ^0.29.0 - version: 0.29.0(@solana/web3.js@1.98.4(typescript@5.9.2)) + version: 0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@noble/hashes': specifier: 1.5.0 version: 1.5.0 - bn.js: - specifier: ^5.2.1 - version: 5.2.1 + '@solana/web3.js': + specifier: '>=1.73.5' + version: 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bs58: specifier: ^6.0.0 version: 6.0.0 @@ -346,13 +346,10 @@ importers: devDependencies: '@coral-xyz/anchor': specifier: 0.29.0 - version: 0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) '@esbuild-plugins/node-globals-polyfill': specifier: ^0.2.3 version: 0.2.3(esbuild@0.25.10) - '@eslint/js': - specifier: 9.36.0 - version: 9.36.0 '@lightprotocol/hasher.rs': specifier: 0.2.1 version: 0.2.1 @@ -383,9 +380,6 @@ importers: '@rollup/plugin-typescript': specifier: ^11.1.6 version: 11.1.6(rollup@4.21.3)(tslib@2.7.0)(typescript@5.9.2) - '@solana/web3.js': - specifier: 1.98.4 - version: 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) '@types/bn.js': specifier: ^5.1.5 version: 5.2.0 @@ -393,23 +387,26 @@ importers: specifier: ^22.5.5 version: 22.16.5 '@typescript-eslint/eslint-plugin': - specifier: ^8.44.0 - version: 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + specifier: ^7.13.1 + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) '@typescript-eslint/parser': - specifier: ^8.44.0 - version: 8.44.0(eslint@9.36.0)(typescript@5.9.2) + specifier: ^7.13.1 + version: 7.18.0(eslint@8.57.1)(typescript@5.9.2) + bn.js: + specifier: ^5.1.2 + version: 5.2.1 eslint: - specifier: ^9.36.0 - version: 9.36.0 + specifier: ^8.56.0 + version: 8.57.1 eslint-plugin-n: specifier: ^17.10.2 - version: 17.10.2(eslint@9.36.0) + version: 17.10.2(eslint@8.57.1) eslint-plugin-promise: specifier: ^7.1.0 - version: 7.1.0(eslint@9.36.0) + version: 7.1.0(eslint@8.57.1) eslint-plugin-vitest: specifier: ^0.5.4 - version: 0.5.4(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)) + version: 0.5.4(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)) http-server: specifier: ^14.1.1 version: 14.1.1 @@ -453,7 +450,7 @@ importers: dependencies: '@coral-xyz/anchor': specifier: ^0.29.0 - version: 0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + version: 0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) devDependencies: '@lightprotocol/zk-compression-cli': specifier: workspace:* @@ -1105,10 +1102,18 @@ packages: resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/eslintrc@3.3.1': resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@eslint/js@9.36.0': resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1135,10 +1140,19 @@ packages: resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} engines: {node: '>=18.18.0'} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + '@humanwhocodes/retry@0.4.3': resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} @@ -1948,12 +1962,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/codecs-core@2.3.0': - resolution: {integrity: sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-data-structures@2.0.0-experimental.8618508': resolution: {integrity: sha512-sLpjL9sqzaDdkloBPV61Rht1tgaKq98BCtIKRuyscIrmVPu3wu0Bavk2n/QekmUzaTsj7K1pVSniM0YqCdnEBw==} @@ -1980,12 +1988,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/codecs-numbers@2.3.0': - resolution: {integrity: sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg==} - engines: {node: '>=20.18.0'} - peerDependencies: - typescript: '>=5.3.3' - '@solana/codecs-strings@2.0.0-experimental.8618508': resolution: {integrity: sha512-b2yhinr1+oe+JDmnnsV0641KQqqDG8AQ16Z/x7GVWO+AWHMpRlHWVXOq8U1yhPMA4VXxl7i+D+C6ql0VGFp0GA==} peerDependencies: @@ -2025,13 +2027,6 @@ packages: peerDependencies: typescript: '>=5' - '@solana/errors@2.3.0': - resolution: {integrity: sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ==} - engines: {node: '>=20.18.0'} - hasBin: true - peerDependencies: - typescript: '>=5.3.3' - '@solana/options@2.0.0-experimental.8618508': resolution: {integrity: sha512-fy/nIRAMC3QHvnKi63KEd86Xr/zFBVxNW4nEpVEU2OT0gCEKwHY4Z55YHf7XujhyuM3PNpiBKg/YYw5QlRU4vg==} @@ -2079,8 +2074,8 @@ packages: resolution: {integrity: sha512-JBMGB0oR4lPttOZ5XiUGyvylwLQjt1CPJa6qQ5oM+MBCndfjz2TKKkw0eATlLLcYmq1jBVsNlJ2cD6ns2GR7lA==} engines: {node: '>=16'} - '@solana/web3.js@1.98.4': - resolution: {integrity: sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw==} + '@solana/web3.js@1.98.0': + resolution: {integrity: sha512-nz3Q5OeyGFpFCR+erX2f6JPt3sKhzhYcSycBCSPkWjzSVDh/Rr1FqTVMRe58FKO16/ivTUcuJjeS5MyBvpkbzA==} '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -2194,6 +2189,17 @@ packages: '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} + '@typescript-eslint/eslint-plugin@7.18.0': + resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + '@typescript-eslint/parser': ^7.0.0 + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/eslint-plugin@8.44.0': resolution: {integrity: sha512-EGDAOGX+uwwekcS0iyxVDmRV9HX6FLSM5kzrAToLTsr9OWCIKG/y3lQheCq18yZ5Xh78rRKJiEpP0ZaCs4ryOQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2202,6 +2208,16 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/parser@7.18.0': + resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/parser@8.44.0': resolution: {integrity: sha512-VGMpFQGUQWYT9LfnPcX8ouFojyrZ/2w3K5BucvxL/spdNehccKhB4jUyB1yBCXpr2XFm0jkECxgrpXBW2ipoAw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2219,6 +2235,10 @@ packages: resolution: {integrity: sha512-adbXNVEs6GmbzaCpymHQ0MB6E4TqoiVbC0iqG3uijR8ZYfpAXMGttouQzF4Oat3P2GxDVIrg7bMI/P65LiQZdg==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@7.18.0': + resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} + engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/scope-manager@8.44.0': resolution: {integrity: sha512-87Jv3E+al8wpD+rIdVJm/ItDBe/Im09zXIjFoipOjr5gHUhJmTzfFLuTJ/nPTMc2Srsroy4IBXwcTCHyRR7KzA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2229,6 +2249,16 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' + '@typescript-eslint/type-utils@7.18.0': + resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/type-utils@8.44.0': resolution: {integrity: sha512-9cwsoSxJ8Sak67Be/hD2RNt/fsqmWnNE1iHohG8lxqLSNY8xNfyY7wloo5zpW3Nu9hxVgURevqfcH6vvKCt6yg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2240,6 +2270,10 @@ packages: resolution: {integrity: sha512-7K7HMcSQIAND6RBL4kDl24sG/xKM13cA85dc7JnmQXw2cBDngg7c19B++JzvJHRG3zG36n9j1i451GBzRuHchw==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@7.18.0': + resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} + engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/types@8.44.0': resolution: {integrity: sha512-ZSl2efn44VsYM0MfDQe68RKzBz75NPgLQXuGypmym6QVOWL5kegTZuZ02xRAT9T+onqvM6T8CdQk0OwYMB6ZvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2253,6 +2287,15 @@ packages: typescript: optional: true + '@typescript-eslint/typescript-estree@7.18.0': + resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + '@typescript-eslint/typescript-estree@8.44.0': resolution: {integrity: sha512-lqNj6SgnGcQZwL4/SBJ3xdPEfcBuhCG8zdcwCPgYcmiPLgokiNDKlbPzCwEwu7m279J/lBYWtDYL+87OEfn8Jw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2265,6 +2308,12 @@ packages: peerDependencies: eslint: ^8.56.0 + '@typescript-eslint/utils@7.18.0': + resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} + engines: {node: ^18.18.0 || >=20.0.0} + peerDependencies: + eslint: ^8.56.0 + '@typescript-eslint/utils@8.44.0': resolution: {integrity: sha512-nktOlVcg3ALo0mYlV+L7sWUD58KG4CMj1rb2HUVOO4aL3K/6wcD+NERqd0rrA5Vg06b42YhF6cFxeixsp9Riqg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -2276,10 +2325,17 @@ packages: resolution: {integrity: sha512-k/Bfne7lrP7hcb7m9zSsgcBmo+8eicqqfNAJ7uUY+jkTFpKeH2FSkWpFRtimBxgkyvqfu9jTPRbYOvud6isdXA==} engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@7.18.0': + resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} + engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/visitor-keys@8.44.0': resolution: {integrity: sha512-zaz9u8EJ4GBmnehlrpoKvj/E3dNbuQ7q0ucyZImm3cLqJ8INTc970B1qEqDX/Rzq65r3TvVTN7kHWPBoyW7DWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@ungap/structured-clone@1.3.0': + resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@vitest/expect@2.1.1': resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} @@ -2814,10 +2870,6 @@ packages: resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} engines: {node: '>=18'} - commander@14.0.1: - resolution: {integrity: sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==} - engines: {node: '>=20'} - commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -3053,6 +3105,10 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dot-case@3.0.4: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} @@ -3295,6 +3351,10 @@ packages: vitest: optional: true + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + eslint-scope@8.4.0: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3307,6 +3367,12 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + eslint@9.36.0: resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3321,6 +3387,10 @@ packages: resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@1.2.2: resolution: {integrity: sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==} engines: {node: '>=0.4.0'} @@ -3437,6 +3507,10 @@ packages: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} @@ -3462,6 +3536,10 @@ packages: find-yarn-workspace-root@2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + flat-cache@4.0.1: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} @@ -3650,6 +3728,10 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -4039,6 +4121,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -5037,6 +5123,11 @@ packages: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rimraf@3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -5416,6 +5507,9 @@ packages: text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -5554,6 +5648,10 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -6602,11 +6700,11 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.27.1 - '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@coral-xyz/anchor@0.29.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: - '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.98.4(typescript@5.9.2)) + '@coral-xyz/borsh': 0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)) '@noble/hashes': 1.5.0 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 bs58: 4.0.1 buffer-layout: 1.2.2 @@ -6621,12 +6719,11 @@ snapshots: transitivePeerDependencies: - bufferutil - encoding - - typescript - utf-8-validate - '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.4(typescript@5.9.2))': + '@coral-xyz/borsh@0.29.0(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))': dependencies: - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bn.js: 5.2.1 buffer-layout: 1.2.2 @@ -6795,14 +6892,19 @@ snapshots: '@esbuild/win32-x64@0.25.10': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.36.0)': + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': dependencies: - eslint: 9.36.0 + eslint: 8.57.1 eslint-visitor-keys: 3.4.3 - '@eslint-community/eslint-utils@4.7.0(eslint@9.36.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@8.57.1)': dependencies: - eslint: 9.36.0 + eslint: 8.57.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/eslint-utils@4.9.0(eslint@8.57.1)': + dependencies: + eslint: 8.57.1 eslint-visitor-keys: 3.4.3 '@eslint-community/eslint-utils@4.9.0(eslint@9.36.0)': @@ -6826,6 +6928,20 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 + '@eslint/eslintrc@2.1.4': + dependencies: + ajv: 6.12.6 + debug: 4.4.3(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 @@ -6840,6 +6956,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/js@8.57.1': {} + '@eslint/js@9.36.0': {} '@eslint/object-schema@2.1.6': {} @@ -6862,8 +6980,18 @@ snapshots: '@humanfs/core': 0.19.1 '@humanwhocodes/retry': 0.4.3 + '@humanwhocodes/config-array@0.13.0': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + '@humanwhocodes/module-importer@1.0.1': {} + '@humanwhocodes/object-schema@2.0.3': {} + '@humanwhocodes/retry@0.4.3': {} '@iden3/bigarray@0.0.2': {} @@ -7049,7 +7177,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -7764,16 +7892,15 @@ snapshots: '@smithy/types': 4.5.0 tslib: 2.8.1 - '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) bigint-buffer: 1.1.5 bignumber.js: 9.1.2 transitivePeerDependencies: - bufferutil - encoding - - typescript - utf-8-validate '@solana/buffer-layout@4.0.1': @@ -7792,11 +7919,6 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) typescript: 5.9.2 - '@solana/codecs-core@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/codecs-data-structures@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7832,12 +7954,6 @@ snapshots: '@solana/errors': 2.0.0-rc.1(typescript@5.9.2) typescript: 5.9.2 - '@solana/codecs-numbers@2.3.0(typescript@5.9.2)': - dependencies: - '@solana/codecs-core': 2.3.0(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - '@solana/codecs-strings@2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7894,12 +8010,6 @@ snapshots: commander: 12.1.0 typescript: 5.9.2 - '@solana/errors@2.3.0(typescript@5.9.2)': - dependencies: - chalk: 5.4.1 - commander: 14.0.1 - typescript: 5.9.2 - '@solana/options@2.0.0-experimental.8618508': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 @@ -7927,16 +8037,16 @@ snapshots: 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)': + '@solana/spl-token-group@0.0.5(@solana/web3.js@1.98.0(bufferutil@4.0.8)(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) '@solana/spl-type-length-value': 0.1.0 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': + '@solana/spl-token-metadata@0.1.2(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)': dependencies: '@solana/codecs-core': 2.0.0-experimental.8618508 '@solana/codecs-data-structures': 2.0.0-experimental.8618508 @@ -7944,40 +8054,39 @@ snapshots: '@solana/codecs-strings': 2.0.0-experimental.8618508(fastestsmallesttextencoderdecoder@1.0.22) '@solana/options': 2.0.0-experimental.8618508 '@solana/spl-type-length-value': 0.1.0 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/spl-token-metadata@0.1.5(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': + '@solana/spl-token-metadata@0.1.5(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: '@solana/codecs': 2.0.0-rc.1(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/spl-type-length-value': 0.1.0 - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) transitivePeerDependencies: - fastestsmallesttextencoderdecoder - typescript - '@solana/spl-token@0.3.11(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.3.11(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22) - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token-metadata': 0.1.2(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil - encoding - fastestsmallesttextencoderdecoder - - typescript - utf-8-validate - '@solana/spl-token@0.4.8(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@solana/spl-token@0.4.8(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@solana/buffer-layout': 4.0.1 - '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@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) - '@solana/spl-token-metadata': 0.1.5(@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/web3.js': 1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10) + '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) + '@solana/spl-token-group': 0.0.5(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/spl-token-metadata': 0.1.5(@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) + '@solana/web3.js': 1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10) buffer: 6.0.3 transitivePeerDependencies: - bufferutil @@ -7990,14 +8099,14 @@ snapshots: dependencies: buffer: 6.0.3 - '@solana/web3.js@1.98.4(bufferutil@4.0.8)(typescript@5.9.2)(utf-8-validate@5.0.10)': + '@solana/web3.js@1.98.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.25.6 '@noble/curves': 1.4.2 '@noble/hashes': 1.5.0 '@solana/buffer-layout': 4.0.1 - '@solana/codecs-numbers': 2.3.0(typescript@5.9.2) agentkeepalive: 4.5.0 + bigint-buffer: 1.1.5 bn.js: 5.2.1 borsh: 0.7.0 bs58: 4.0.1 @@ -8010,7 +8119,6 @@ snapshots: transitivePeerDependencies: - bufferutil - encoding - - typescript - utf-8-validate '@standard-schema/spec@1.0.0': {} @@ -8124,6 +8232,24 @@ snapshots: dependencies: '@types/node': 22.16.5 + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/type-utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 7.18.0 + eslint: 8.57.1 + graphemer: 1.4.0 + ignore: 5.3.2 + natural-compare: 1.4.0 + ts-api-utils: 1.3.0(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -8141,6 +8267,19 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3(supports-color@8.1.1) + eslint: 8.57.1 + optionalDependencies: + typescript: 5.9.2 + 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 @@ -8167,6 +8306,11 @@ snapshots: '@typescript-eslint/types': 7.13.1 '@typescript-eslint/visitor-keys': 7.13.1 + '@typescript-eslint/scope-manager@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + '@typescript-eslint/scope-manager@8.44.0': dependencies: '@typescript-eslint/types': 8.44.0 @@ -8176,6 +8320,18 @@ snapshots: dependencies: typescript: 5.9.2 + '@typescript-eslint/type-utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + '@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.2) + debug: 4.4.3(supports-color@8.1.1) + eslint: 8.57.1 + ts-api-utils: 1.3.0(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/type-utils@8.44.0(eslint@9.36.0)(typescript@5.9.2)': dependencies: '@typescript-eslint/types': 8.44.0 @@ -8190,6 +8346,8 @@ snapshots: '@typescript-eslint/types@7.13.1': {} + '@typescript-eslint/types@7.18.0': {} + '@typescript-eslint/types@8.44.0': {} '@typescript-eslint/typescript-estree@7.13.1(typescript@5.9.2)': @@ -8207,6 +8365,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@7.18.0(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/visitor-keys': 7.18.0 + debug: 4.4.3(supports-color@8.1.1) + globby: 11.1.0 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 1.3.0(typescript@5.9.2) + optionalDependencies: + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + '@typescript-eslint/typescript-estree@8.44.0(typescript@5.9.2)': dependencies: '@typescript-eslint/project-service': 8.44.0(typescript@5.9.2) @@ -8223,13 +8396,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.13.1(eslint@9.36.0)(typescript@5.9.2)': + '@typescript-eslint/utils@7.13.1(eslint@8.57.1)(typescript@5.9.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.36.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) '@typescript-eslint/scope-manager': 7.13.1 '@typescript-eslint/types': 7.13.1 '@typescript-eslint/typescript-estree': 7.13.1(typescript@5.9.2) - eslint: 9.36.0 + eslint: 8.57.1 + transitivePeerDependencies: + - supports-color + - typescript + + '@typescript-eslint/utils@7.18.0(eslint@8.57.1)(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@typescript-eslint/scope-manager': 7.18.0 + '@typescript-eslint/types': 7.18.0 + '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.2) + eslint: 8.57.1 transitivePeerDependencies: - supports-color - typescript @@ -8250,11 +8434,18 @@ snapshots: '@typescript-eslint/types': 7.13.1 eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@7.18.0': + dependencies: + '@typescript-eslint/types': 7.18.0 + eslint-visitor-keys: 3.4.3 + '@typescript-eslint/visitor-keys@8.44.0': dependencies: '@typescript-eslint/types': 8.44.0 eslint-visitor-keys: 4.2.1 + '@ungap/structured-clone@1.3.0': {} + '@vitest/expect@2.1.1': dependencies: '@vitest/spy': 2.1.1 @@ -8867,8 +9058,6 @@ snapshots: commander@13.1.0: {} - commander@14.0.1: {} - commander@2.20.3: {} commander@5.1.0: {} @@ -9096,6 +9285,10 @@ snapshots: dependencies: esutils: 2.0.3 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dot-case@3.0.4: dependencies: no-case: 3.0.4 @@ -9455,9 +9648,9 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-compat-utils@0.5.1(eslint@9.36.0): + eslint-compat-utils@0.5.1(eslint@8.57.1): dependencies: - eslint: 9.36.0 + eslint: 8.57.1 semver: 7.7.1 eslint-config-prettier@10.1.8(eslint@9.36.0): @@ -9472,24 +9665,24 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.11.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0): + eslint-module-utils@2.11.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.44.0(eslint@9.36.0)(typescript@5.9.2) - eslint: 9.36.0 + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-es-x@7.8.0(eslint@9.36.0): + eslint-plugin-es-x@7.8.0(eslint@8.57.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.36.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@8.57.1) '@eslint-community/regexpp': 4.12.1 - eslint: 9.36.0 - eslint-compat-utils: 0.5.1(eslint@9.36.0) + eslint: 8.57.1 + eslint-compat-utils: 0.5.1(eslint@8.57.1) - eslint-plugin-import@2.30.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0): + eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -9498,9 +9691,9 @@ snapshots: array.prototype.flatmap: 1.3.2 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.36.0 + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.11.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@9.36.0) + eslint-module-utils: 2.11.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.15.1 is-glob: 4.0.3 @@ -9511,39 +9704,44 @@ snapshots: semver: 6.3.1 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.44.0(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/parser': 7.18.0(eslint@8.57.1)(typescript@5.9.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-n@17.10.2(eslint@9.36.0): + eslint-plugin-n@17.10.2(eslint@8.57.1): dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.36.0) + '@eslint-community/eslint-utils': 4.4.0(eslint@8.57.1) enhanced-resolve: 5.17.1 - eslint: 9.36.0 - eslint-plugin-es-x: 7.8.0(eslint@9.36.0) + eslint: 8.57.1 + eslint-plugin-es-x: 7.8.0(eslint@8.57.1) get-tsconfig: 4.7.2 globals: 15.9.0 ignore: 5.3.1 minimatch: 9.0.5 semver: 7.6.3 - eslint-plugin-promise@7.1.0(eslint@9.36.0): + eslint-plugin-promise@7.1.0(eslint@8.57.1): dependencies: - eslint: 9.36.0 + eslint: 8.57.1 - eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)): + eslint-plugin-vitest@0.5.4(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2)(vitest@2.1.1(@types/node@22.16.5)(terser@5.43.1)): dependencies: - '@typescript-eslint/utils': 7.13.1(eslint@9.36.0)(typescript@5.9.2) - eslint: 9.36.0 + '@typescript-eslint/utils': 7.13.1(eslint@8.57.1)(typescript@5.9.2) + eslint: 8.57.1 optionalDependencies: - '@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.36.0)(typescript@5.9.2))(eslint@9.36.0)(typescript@5.9.2) + '@typescript-eslint/eslint-plugin': 7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.9.2))(eslint@8.57.1)(typescript@5.9.2) vitest: 2.1.1(@types/node@22.16.5)(terser@5.43.1) transitivePeerDependencies: - supports-color - typescript + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 @@ -9553,6 +9751,49 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint@8.57.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@8.57.1) + '@eslint-community/regexpp': 4.12.1 + '@eslint/eslintrc': 2.1.4 + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.0 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + eslint@9.36.0: dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.36.0) @@ -9599,6 +9840,12 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 4.2.1 + espree@9.6.1: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 3.4.3 + esprima@1.2.2: {} esprima@4.0.1: {} @@ -9705,6 +9952,10 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 @@ -9736,6 +9987,12 @@ snapshots: dependencies: micromatch: 4.0.8 + flat-cache@3.2.0: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + rimraf: 3.0.2 + flat-cache@4.0.1: dependencies: flatted: 3.3.3 @@ -9941,6 +10198,10 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + globals@14.0.0: {} globals@15.9.0: {} @@ -10371,6 +10632,8 @@ snapshots: is-number@7.0.0: {} + is-path-inside@3.0.3: {} + is-plain-obj@2.1.0: {} is-plain-obj@4.1.0: {} @@ -10475,7 +10738,7 @@ snapshots: isexe@3.1.1: {} - isomorphic-ws@4.0.1(ws@7.5.10): + isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)): dependencies: ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -10508,7 +10771,7 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@7.5.10) + isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)) json-stringify-safe: 5.0.1 uuid: 8.3.2 ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10) @@ -11388,6 +11651,10 @@ snapshots: reusify@1.0.4: {} + rimraf@3.0.2: + dependencies: + glob: 7.2.3 + rimraf@6.0.1: dependencies: glob: 11.0.0 @@ -11720,7 +11987,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 string-width@7.2.0: dependencies: @@ -11875,6 +12142,8 @@ snapshots: text-encoding-utf-8@1.0.2: {} + text-table@0.2.0: {} + through@2.3.8: {} tightrope@0.2.0: {} @@ -12019,6 +12288,8 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-fest@0.20.2: {} + type-fest@0.21.3: {} type-fest@4.35.0: {} @@ -12393,7 +12664,7 @@ snapshots: dependencies: ansi-styles: 6.2.1 string-width: 5.1.2 - strip-ansi: 7.1.0 + strip-ansi: 7.1.2 wrap-ansi@9.0.2: dependencies: diff --git a/program-libs/compressed-account/Cargo.toml b/program-libs/compressed-account/Cargo.toml index 8623b20991..567c88aa97 100644 --- a/program-libs/compressed-account/Cargo.toml +++ b/program-libs/compressed-account/Cargo.toml @@ -18,7 +18,7 @@ new-unique = ["dep:solana-pubkey"] thiserror = { workspace = true } zerocopy = { workspace = true, features = ["derive"] } light-hasher = { workspace = true } -light-zero-copy = { workspace = true, features = ["std"] } +light-zero-copy = { workspace = true, features = ["std", "derive", "mut"] } light-macros = { workspace = true } pinocchio = { workspace = true, optional = true } solana-program-error = { workspace = true, optional = true } diff --git a/program-libs/compressed-account/src/address.rs b/program-libs/compressed-account/src/address.rs index 1e3f633ee0..8b1ff9fd94 100644 --- a/program-libs/compressed-account/src/address.rs +++ b/program-libs/compressed-account/src/address.rs @@ -40,6 +40,19 @@ pub fn derive_address( hashv_to_bn254_field_size_be_const_array::<4>(&slices).unwrap() } +/// Convenience function for calling derive_address with Pubkey types. +pub fn derive_compressed_address( + account_address: &Pubkey, + address_tree_pubkey: &Pubkey, + program_id: &Pubkey, +) -> [u8; 32] { + derive_address( + &account_address.to_bytes(), + &address_tree_pubkey.to_bytes(), + &program_id.to_bytes(), + ) +} + pub fn add_and_get_remaining_account_indices( pubkeys: &[Pubkey], remaining_accounts: &mut HashMap, diff --git a/program-libs/compressed-account/src/compressed_account.rs b/program-libs/compressed-account/src/compressed_account.rs index 8e2cfc13c5..a2dbb7318b 100644 --- a/program-libs/compressed-account/src/compressed_account.rs +++ b/program-libs/compressed-account/src/compressed_account.rs @@ -234,7 +234,7 @@ pub struct InCompressedAccount { pub address: Option<[u8; 32]>, } -#[derive(Debug, PartialEq, Default, Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Debug, PartialEq, Default, Hash, Clone, AnchorSerialize, AnchorDeserialize)] pub struct CompressedAccountData { pub discriminator: [u8; 8], pub data: Vec, diff --git a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs index 33434e5752..e8d1e577ca 100644 --- a/program-libs/compressed-account/src/instruction_data/compressed_proof.rs +++ b/program-libs/compressed-account/src/instruction_data/compressed_proof.rs @@ -1,4 +1,4 @@ -use light_zero_copy::{errors::ZeroCopyError, traits::ZeroCopyAt}; +use light_zero_copy::{errors::ZeroCopyError, traits::ZeroCopyAt, ZeroCopyMut}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned}; use crate::{AnchorDeserialize, AnchorSerialize}; @@ -17,6 +17,7 @@ use crate::{AnchorDeserialize, AnchorSerialize}; FromBytes, IntoBytes, Unaligned, + ZeroCopyMut, )] pub struct CompressedProof { pub a: [u8; 32], @@ -79,3 +80,83 @@ impl Into> for ValidityProof { self.0 } } + +// Borsh compatible validity proof implementation. Use this in your anchor +// program unless you have zero-copy instruction data. Convert to zero-copy via +// `let proof = compression_params.proof.into();`. +// +// TODO: make the zerocopy implementation compatible with borsh serde via +// Anchor. +pub mod borsh_compat { + use crate::{AnchorDeserialize, AnchorSerialize}; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + pub struct CompressedProof { + pub a: [u8; 32], + pub b: [u8; 64], + pub c: [u8; 32], + } + + impl Default for CompressedProof { + fn default() -> Self { + Self { + a: [0; 32], + b: [0; 64], + c: [0; 32], + } + } + } + + #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, AnchorDeserialize, AnchorSerialize)] + pub struct ValidityProof(pub Option); + + impl ValidityProof { + pub fn new(proof: Option) -> Self { + Self(proof) + } + } + + impl From for CompressedProof { + fn from(proof: super::CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for super::CompressedProof { + fn from(proof: CompressedProof) -> Self { + Self { + a: proof.a, + b: proof.b, + c: proof.c, + } + } + } + + impl From for ValidityProof { + fn from(proof: super::ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for super::ValidityProof { + fn from(proof: ValidityProof) -> Self { + Self(proof.0.map(|p| p.into())) + } + } + + impl From for ValidityProof { + fn from(proof: CompressedProof) -> Self { + Self(Some(proof)) + } + } + + impl From> for ValidityProof { + fn from(proof: Option) -> Self { + Self(proof) + } + } +} diff --git a/program-libs/compressed-account/src/pubkey.rs b/program-libs/compressed-account/src/pubkey.rs index 92511adf5f..821ef878f6 100644 --- a/program-libs/compressed-account/src/pubkey.rs +++ b/program-libs/compressed-account/src/pubkey.rs @@ -1,6 +1,10 @@ #[cfg(feature = "bytemuck-des")] use bytemuck::{Pod, Zeroable}; -use light_zero_copy::{errors::ZeroCopyError, traits::ZeroCopyAt}; +use light_zero_copy::{ + errors::ZeroCopyError, + traits::{ZeroCopyAt, ZeroCopyAtMut, ZeroCopyStructInner, ZeroCopyStructInnerMut}, + ZeroCopyNew, +}; use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Ref, Unaligned}; use crate::{AnchorDeserialize, AnchorSerialize}; @@ -46,6 +50,20 @@ pub struct Pubkey(pub(crate) [u8; 32]); #[repr(C)] pub struct Pubkey(pub(crate) [u8; 32]); +impl<'a> ZeroCopyNew<'a> for Pubkey { + type ZeroCopyConfig = (); + type Output = >::ZeroCopyAtMut; + fn byte_len(_config: &Self::ZeroCopyConfig) -> Result { + Ok(32) + } + fn new_zero_copy( + bytes: &'a mut [u8], + _config: Self::ZeroCopyConfig, + ) -> Result<(Self::Output, &'a mut [u8]), ZeroCopyError> { + >::zero_copy_at_mut(bytes) + } +} + impl Pubkey { pub fn new_from_array(array: [u8; 32]) -> Self { Self(array) @@ -91,6 +109,25 @@ impl<'a> ZeroCopyAt<'a> for Pubkey { Ok(Ref::<&[u8], Pubkey>::from_prefix(bytes)?) } } + +impl<'a> ZeroCopyAtMut<'a> for Pubkey { + type ZeroCopyAtMut = Ref<&'a mut [u8], Pubkey>; + + #[inline] + fn zero_copy_at_mut( + bytes: &'a mut [u8], + ) -> Result<(Self::ZeroCopyAtMut, &'a mut [u8]), ZeroCopyError> { + Ok(Ref::<&mut [u8], Pubkey>::from_prefix(bytes)?) + } +} + +impl ZeroCopyStructInner for Pubkey { + type ZeroCopyInner = Pubkey; +} + +impl ZeroCopyStructInnerMut for Pubkey { + type ZeroCopyInnerMut = Pubkey; +} impl From for [u8; 32] { fn from(pubkey: Pubkey) -> Self { pubkey.to_bytes() @@ -136,6 +173,22 @@ impl From for anchor_lang::prelude::Pubkey { } } +#[cfg(feature = "solana")] +#[cfg(not(feature = "anchor"))] +impl From for Pubkey { + fn from(pubkey: solana_pubkey::Pubkey) -> Self { + Self::new_from_array(pubkey.to_bytes()) + } +} + +#[cfg(feature = "solana")] +#[cfg(not(feature = "anchor"))] +impl From<&solana_pubkey::Pubkey> for Pubkey { + fn from(pubkey: &solana_pubkey::Pubkey) -> Self { + Self::new_from_array(pubkey.to_bytes()) + } +} + #[cfg(feature = "anchor")] impl From<&Pubkey> for anchor_lang::prelude::Pubkey { fn from(pubkey: &Pubkey) -> Self { diff --git a/program-libs/hasher/src/keccak.rs b/program-libs/hasher/src/keccak.rs index 81d81d810c..ab1c666ee8 100644 --- a/program-libs/hasher/src/keccak.rs +++ b/program-libs/hasher/src/keccak.rs @@ -9,6 +9,8 @@ use crate::{ pub struct Keccak; impl Hasher for Keccak { + const ID: u8 = 2; + fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } diff --git a/program-libs/hasher/src/lib.rs b/program-libs/hasher/src/lib.rs index 9f4e4758c0..83a0875ae9 100644 --- a/program-libs/hasher/src/lib.rs +++ b/program-libs/hasher/src/lib.rs @@ -24,6 +24,7 @@ pub const HASH_BYTES: usize = 32; pub type Hash = [u8; HASH_BYTES]; pub trait Hasher { + const ID: u8; fn hash(val: &[u8]) -> Result; fn hashv(vals: &[&[u8]]) -> Result; fn zero_bytes() -> ZeroBytes; diff --git a/program-libs/hasher/src/poseidon.rs b/program-libs/hasher/src/poseidon.rs index 0cd6c670da..b228764ef6 100644 --- a/program-libs/hasher/src/poseidon.rs +++ b/program-libs/hasher/src/poseidon.rs @@ -78,6 +78,8 @@ impl From for u64 { pub struct Poseidon; impl Hasher for Poseidon { + const ID: u8 = 1; + fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } diff --git a/program-libs/hasher/src/sha256.rs b/program-libs/hasher/src/sha256.rs index 8a4b985a52..ba44069c76 100644 --- a/program-libs/hasher/src/sha256.rs +++ b/program-libs/hasher/src/sha256.rs @@ -9,6 +9,8 @@ use crate::{ pub struct Sha256; impl Hasher for Sha256 { + const ID: u8 = 0; + fn hash(val: &[u8]) -> Result { Self::hashv(&[val]) } diff --git a/program-tests/create-address-test-program/src/lib.rs b/program-tests/create-address-test-program/src/lib.rs index 0da6b37a5d..0348d079de 100644 --- a/program-tests/create-address-test-program/src/lib.rs +++ b/program-tests/create-address-test-program/src/lib.rs @@ -67,11 +67,7 @@ pub mod system_cpi_test { use light_sdk::cpi::CpiAccountsV2; let cpi_accounts = CpiAccountsV2::new_with_config(&fee_payer, ctx.remaining_accounts, config); - let account_infos = cpi_accounts - .to_account_infos() - .into_iter() - .cloned() - .collect::>(); + let account_infos = cpi_accounts.to_account_infos(); let account_metas = to_account_metas_v2(cpi_accounts).map_err(|_| ErrorCode::AccountNotEnoughKeys)?; @@ -81,11 +77,7 @@ pub mod system_cpi_test { let cpi_accounts = CpiAccounts::new_with_config(&fee_payer, ctx.remaining_accounts, config); - let account_infos = cpi_accounts - .to_account_infos() - .into_iter() - .cloned() - .collect::>(); + let account_infos = cpi_accounts.to_account_infos(); let config = CpiInstructionConfig::try_from(&cpi_accounts) .map_err(|_| ErrorCode::AccountNotEnoughKeys)?; diff --git a/sdk-libs/client/Cargo.toml b/sdk-libs/client/Cargo.toml index 2bc1708dd7..a33eecfbb3 100644 --- a/sdk-libs/client/Cargo.toml +++ b/sdk-libs/client/Cargo.toml @@ -30,6 +30,7 @@ solana-account = { workspace = true } solana-epoch-info = { workspace = true } solana-keypair = { workspace = true } solana-compute-budget-interface = { workspace = true } +solana-signer = { workspace = true } solana-banks-client = { workspace = true, optional = true } solana-address-lookup-table-interface = { version = "2.2.1", features = [ "bytemuck", diff --git a/sdk-libs/client/src/constants.rs b/sdk-libs/client/src/constants.rs index 9c6e41699e..3d8fdf0d43 100644 --- a/sdk-libs/client/src/constants.rs +++ b/sdk-libs/client/src/constants.rs @@ -9,3 +9,27 @@ pub const STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = pubkey!("8n8rH2bFRVA6cSGNDpgqcKHCndbFCT1bXxAQG89ejVsh"); pub const NULLIFIED_STATE_TREE_LOOKUP_TABLE_DEVNET: Pubkey = pubkey!("5dhaJLBjnVBQFErr8oiCJmcVsx3Zj6xDekGB2zULPsnP"); + +/// Address lookup table with zk compression related keys. Use to reduce +/// transaction size. +/// +/// Keys include: all protocol pubkeys, default state trees, address trees, and +/// more. +/// +/// Example usage: +/// ```bash +/// +/// # By cloning from mainnet +/// light test-validator --validator-args "\ +/// --clone 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// # With a local LUT file +/// light test-validator --validator-args "\ +/// --account 9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ ./scripts/lut.json \ +/// --url https://api.mainnet-beta.solana.com \ +/// --upgradeable-program ~/.config/solana/id.json" +/// +/// ``` +pub const LOOKUP_TABLE_ADDRESS: Pubkey = pubkey!("9NYFyEqPkyXUhkerbGHXUXkvb4qpzeEdHuGpgbgpH1NJ"); diff --git a/sdk-libs/client/src/indexer/indexer_trait.rs b/sdk-libs/client/src/indexer/indexer_trait.rs index c2f5c873bc..4c9365c18f 100644 --- a/sdk-libs/client/src/indexer/indexer_trait.rs +++ b/sdk-libs/client/src/indexer/indexer_trait.rs @@ -5,8 +5,8 @@ use solana_pubkey::Pubkey; use super::{ response::{Items, ItemsWithCursor, Response}, types::{ - CompressedAccount, OwnerBalance, SignatureWithMetadata, TokenAccount, TokenBalance, - ValidityProofWithContext, + CompressedAccount, CompressedTokenAccount, OwnerBalance, SignatureWithMetadata, + TokenBalance, ValidityProofWithContext, }, Address, AddressWithTree, BatchAddressUpdateIndexerResponse, GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, @@ -75,14 +75,14 @@ pub trait Indexer: std::marker::Send + std::marker::Sync { delegate: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError>; + ) -> Result>, IndexerError>; async fn get_compressed_token_accounts_by_owner( &self, owner: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError>; + ) -> Result>, IndexerError>; /// Returns the token balances for a given owner. async fn get_compressed_token_balances_by_owner_v2( diff --git a/sdk-libs/client/src/indexer/mod.rs b/sdk-libs/client/src/indexer/mod.rs index c66baf2d0b..745b512beb 100644 --- a/sdk-libs/client/src/indexer/mod.rs +++ b/sdk-libs/client/src/indexer/mod.rs @@ -15,10 +15,10 @@ pub use indexer_trait::Indexer; pub use response::{Context, Items, ItemsWithCursor, Response}; pub use types::{ AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, AddressQueueIndex, - AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, Hash, MerkleProof, - MerkleProofWithContext, NewAddressProofWithContext, NextTreeInfo, OwnerBalance, ProofOfLeaf, - RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, TokenAccount, TokenBalance, - TreeInfo, ValidityProofWithContext, + AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, CompressedTokenAccount, + Hash, MerkleProof, MerkleProofWithContext, NewAddressProofWithContext, NextTreeInfo, + OwnerBalance, ProofOfLeaf, RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, + TokenBalance, TreeInfo, ValidityProofWithContext, }; mod options; pub use options::*; diff --git a/sdk-libs/client/src/indexer/photon_indexer.rs b/sdk-libs/client/src/indexer/photon_indexer.rs index f06355f5d5..d57e9d8952 100644 --- a/sdk-libs/client/src/indexer/photon_indexer.rs +++ b/sdk-libs/client/src/indexer/photon_indexer.rs @@ -11,7 +11,10 @@ use solana_pubkey::Pubkey; use tracing::{debug, error, warn}; use super::{ - types::{CompressedAccount, OwnerBalance, SignatureWithMetadata, TokenAccount, TokenBalance}, + types::{ + CompressedAccount, CompressedTokenAccount, OwnerBalance, SignatureWithMetadata, + TokenBalance, + }, BatchAddressUpdateIndexerResponse, MerkleProofWithContext, }; use crate::indexer::{ @@ -542,7 +545,7 @@ impl Indexer for PhotonIndexer { delegate: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { let config = config.unwrap_or_default(); self.retry(config.retry_config, || async { #[cfg(feature = "v2")] @@ -574,7 +577,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -618,7 +621,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -642,7 +645,7 @@ impl Indexer for PhotonIndexer { owner: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { let config = config.unwrap_or_default(); self.retry(config.retry_config, || async { #[cfg(feature = "v2")] @@ -675,7 +678,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -726,7 +729,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; diff --git a/sdk-libs/client/src/indexer/tree_info.rs b/sdk-libs/client/src/indexer/tree_info.rs index a4a0a29cdc..a11eedf3a4 100644 --- a/sdk-libs/client/src/indexer/tree_info.rs +++ b/sdk-libs/client/src/indexer/tree_info.rs @@ -259,28 +259,44 @@ lazy_static! { ); } + + // v2 tree 1 m.insert( "6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU".to_string(), TreeInfo { tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: None, + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), tree_type: TreeType::StateV2, next_tree_info: None, }, ); + // v2 queue 1 m.insert( "HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu".to_string(), TreeInfo { tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: None, + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), tree_type: TreeType::StateV2, next_tree_info: None, }, ); + // v2 cpi context 1 + m.insert( + "7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj".to_string(), + TreeInfo { + tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), + queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // address v2 tree m.insert( "EzKE84aVTkCUhDHLELqyJaq1Y7UVVmqxXqZjVHwHY3rK".to_string(), TreeInfo { @@ -292,6 +308,42 @@ lazy_static! { }, ); + // v2 queue 2 + m.insert( + "12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // v2 tree 2 + m.insert( + "2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + + // v2 cpi context + m.insert( + "HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R".to_string(), + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + tree_type: TreeType::StateV2, + next_tree_info: None, + }, + ); + m }; } diff --git a/sdk-libs/client/src/indexer/types.rs b/sdk-libs/client/src/indexer/types.rs index 8a408dc77e..0766f3c3f9 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -519,10 +519,18 @@ impl TryFrom for CompressedAccount { let hash = account .hash() .map_err(|_| IndexerError::InvalidResponseData)?; - // Breaks light-program-test - // let tree_info = QUEUE_TREE_MAPPING - // .get(&account.merkle_context.merkle_tree_pubkey.to_string()) - // .ok_or(IndexerError::InvalidResponseData)?; + + let tree_pubkey = + Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()); + let tree_info = QUEUE_TREE_MAPPING + .get(&tree_pubkey.to_string()) + .ok_or_else(|| { + println!( + "ERROR: No tree_info found for tree pubkey: {:?}", + tree_pubkey.to_string() + ); + IndexerError::InvalidResponseData + })?; Ok(CompressedAccount { address: account.compressed_account.address, @@ -531,10 +539,10 @@ impl TryFrom for CompressedAccount { lamports: account.compressed_account.lamports, leaf_index: account.merkle_context.leaf_index, tree_info: TreeInfo { - tree: Pubkey::new_from_array(account.merkle_context.merkle_tree_pubkey.to_bytes()), + tree: tree_pubkey, queue: Pubkey::new_from_array(account.merkle_context.queue_pubkey.to_bytes()), tree_type: account.merkle_context.tree_type, - cpi_context: None, + cpi_context: tree_info.cpi_context, next_tree_info: None, }, owner: Pubkey::new_from_array(account.compressed_account.owner.to_bytes()), @@ -581,6 +589,18 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { Ok::, IndexerError>(None) }?; + let tree_pubkey = + Pubkey::new_from_array(decode_base58_to_fixed_array(&account.merkle_context.tree)?); + let tree_info = QUEUE_TREE_MAPPING + .get(&tree_pubkey.to_string()) + .ok_or_else(|| { + println!( + "ERROR: No tree_info found for tree pubkey: {}", + account.merkle_context.tree + ); + IndexerError::InvalidResponseData + })?; + let owner = Pubkey::new_from_array(decode_base58_to_fixed_array(&account.owner)?); let address = account .address @@ -590,14 +610,12 @@ impl TryFrom<&photon_api::models::AccountV2> for CompressedAccount { let hash = decode_base58_to_fixed_array(&account.hash)?; let tree_info = TreeInfo { - tree: Pubkey::new_from_array(decode_base58_to_fixed_array( - &account.merkle_context.tree, - )?), + tree: tree_pubkey, queue: Pubkey::new_from_array(decode_base58_to_fixed_array( &account.merkle_context.queue, )?), tree_type: TreeType::from(account.merkle_context.tree_type as u64), - cpi_context: decode_base58_option_to_pubkey(&account.merkle_context.cpi_context)?, + cpi_context: tree_info.cpi_context, next_tree_info: account .merkle_context .next_tree_context @@ -715,14 +733,14 @@ pub struct AddressMerkleTreeAccounts { } #[derive(Clone, Default, Debug, PartialEq)] -pub struct TokenAccount { +pub struct CompressedTokenAccount { /// Token-specific data (mint, owner, amount, delegate, state, tlv) pub token: TokenData, /// General account information (address, hash, lamports, merkle context, etc.) pub account: CompressedAccount, } -impl TryFrom<&photon_api::models::TokenAccount> for TokenAccount { +impl TryFrom<&photon_api::models::TokenAccount> for CompressedTokenAccount { type Error = IndexerError; fn try_from(token_account: &photon_api::models::TokenAccount) -> Result { @@ -755,11 +773,11 @@ impl TryFrom<&photon_api::models::TokenAccount> for TokenAccount { .map_err(|_| IndexerError::InvalidResponseData)?, }; - Ok(TokenAccount { token, account }) + Ok(CompressedTokenAccount { token, account }) } } -impl TryFrom<&photon_api::models::TokenAccountV2> for TokenAccount { +impl TryFrom<&photon_api::models::TokenAccountV2> for CompressedTokenAccount { type Error = IndexerError; fn try_from(token_account: &photon_api::models::TokenAccountV2) -> Result { @@ -792,12 +810,12 @@ impl TryFrom<&photon_api::models::TokenAccountV2> for TokenAccount { .map_err(|_| IndexerError::InvalidResponseData)?, }; - Ok(TokenAccount { token, account }) + Ok(CompressedTokenAccount { token, account }) } } #[allow(clippy::from_over_into)] -impl Into for TokenAccount { +impl Into for CompressedTokenAccount { fn into(self) -> light_sdk::token::TokenDataWithMerkleContext { let compressed_account = CompressedAccountWithMerkleContext::from(self.account); @@ -810,7 +828,7 @@ impl Into for TokenAccount { #[allow(clippy::from_over_into)] impl Into> - for super::response::Response> + for super::response::Response> { fn into(self) -> Vec { self.value @@ -828,7 +846,7 @@ impl Into> } } -impl TryFrom for TokenAccount { +impl TryFrom for CompressedTokenAccount { type Error = IndexerError; fn try_from( @@ -836,7 +854,7 @@ impl TryFrom for TokenAccount { ) -> Result { let account = CompressedAccount::try_from(token_data_with_context.compressed_account)?; - Ok(TokenAccount { + Ok(CompressedTokenAccount { token: token_data_with_context.token_data, account, }) diff --git a/sdk-libs/client/src/rpc/client.rs b/sdk-libs/client/src/rpc/client.rs index af3fcb1641..5413f2f940 100644 --- a/sdk-libs/client/src/rpc/client.rs +++ b/sdk-libs/client/src/rpc/client.rs @@ -691,13 +691,22 @@ impl Rpc for LightClient { use crate::indexer::TreeInfo; #[cfg(feature = "v2")] - let default_trees = vec![TreeInfo { - tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), - queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), - cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), - next_tree_info: None, - tree_type: TreeType::StateV2, - }]; + let default_trees = vec![ + TreeInfo { + tree: pubkey!("HLKs5NJ8FXkJg8BrzJt56adFYYuwg5etzDtBbQYTsixu"), + queue: pubkey!("6L7SzhYB3anwEQ9cphpJ1U7Scwj57bx2xueReg7R9cKU"), + cpi_context: Some(pubkey!("7Hp52chxaew8bW1ApR4fck2bh6Y8qA1pu3qwH6N9zaLj")), + next_tree_info: None, + tree_type: TreeType::StateV2, + }, + TreeInfo { + tree: pubkey!("2Yb3fGo2E9aWLjY8KuESaqurYpGGhEeJr7eynKrSgXwS"), + queue: pubkey!("12wJT3xYd46rtjeqDU6CrtT8unqLjPiheggzqhN9YsyB"), + cpi_context: Some(pubkey!("HwtjxDvFEXiWnzeMeWkMBzpQN45A95rTJNZmz1Z3pe8R")), + next_tree_info: None, + tree_type: TreeType::StateV2, + }, + ]; #[cfg(not(feature = "v2"))] let default_trees = vec![TreeInfo { @@ -786,6 +795,7 @@ impl MerkleTreeExt for LightClient {} /// let mut rng = thread_rng(); /// let selected_tree = select_state_tree_info(&mut rng, &tree_infos)?; /// ``` +#[allow(clippy::result_large_err)] pub fn select_state_tree_info( rng: &mut R, state_trees: &[TreeInfo], diff --git a/sdk-libs/client/src/rpc/errors.rs b/sdk-libs/client/src/rpc/errors.rs index a336fe32ca..2875b0ecb2 100644 --- a/sdk-libs/client/src/rpc/errors.rs +++ b/sdk-libs/client/src/rpc/errors.rs @@ -33,9 +33,6 @@ pub enum RpcError { #[error("Error: `{0}`")] CustomError(String), - #[error("Signing error: {0}")] - SigningError(String), - #[error("Assert Rpc Error: {0}")] AssertRpcError(String), @@ -43,10 +40,6 @@ pub enum RpcError { #[error("Warp slot not in the future")] InvalidWarpSlot, - #[cfg(feature = "program-test")] - #[error("LiteSVM Error: {0}")] - LiteSvmError(String), - #[error("Account {0} does not exist")] AccountDoesNotExist(String), @@ -80,7 +73,6 @@ impl Clone for RpcError { RpcError::ClientError(_) => RpcError::CustomError("ClientError".to_string()), RpcError::IoError(e) => RpcError::IoError(e.kind().into()), RpcError::CustomError(e) => RpcError::CustomError(e.clone()), - RpcError::SigningError(e) => RpcError::SigningError(e.clone()), RpcError::AssertRpcError(e) => RpcError::AssertRpcError(e.clone()), RpcError::InvalidWarpSlot => RpcError::InvalidWarpSlot, RpcError::AccountDoesNotExist(e) => RpcError::AccountDoesNotExist(e.clone()), @@ -91,15 +83,6 @@ impl Clone for RpcError { RpcError::InvalidStateTreeLookupTable => RpcError::InvalidStateTreeLookupTable, RpcError::NullifyTableNotFound => RpcError::NullifyTableNotFound, RpcError::NoStateTreesAvailable => RpcError::NoStateTreesAvailable, - #[cfg(feature = "program-test")] - RpcError::LiteSvmError(e) => RpcError::LiteSvmError(e.clone()), } } } - -#[cfg(feature = "program-test")] -impl From for RpcError { - fn from(e: litesvm::error::LiteSVMError) -> Self { - RpcError::LiteSvmError(e.to_string()) - } -} diff --git a/sdk-libs/client/src/rpc/get_light_state_tree_infos.rs b/sdk-libs/client/src/rpc/get_light_state_tree_infos.rs index 8d247c2426..fa77eed58c 100644 --- a/sdk-libs/client/src/rpc/get_light_state_tree_infos.rs +++ b/sdk-libs/client/src/rpc/get_light_state_tree_infos.rs @@ -41,6 +41,7 @@ pub fn default_state_tree_lookup_tables() -> (Vec, Vec Result<(Pubkey, Pubkey), RpcError> { let length = info.len(); if length == 0 { diff --git a/sdk-libs/client/src/rpc/indexer.rs b/sdk-libs/client/src/rpc/indexer.rs index fbcba6c5ab..c4258f3ab8 100644 --- a/sdk-libs/client/src/rpc/indexer.rs +++ b/sdk-libs/client/src/rpc/indexer.rs @@ -5,10 +5,11 @@ use solana_pubkey::Pubkey; use super::LightClient; use crate::indexer::{ Address, AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, - MerkleProofWithContext, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, Response, - RetryConfig, SignatureWithMetadata, TokenAccount, TokenBalance, ValidityProofWithContext, + CompressedTokenAccount, GetCompressedAccountsByOwnerConfig, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MerkleProofWithContext, + NewAddressProofWithContext, OwnerBalance, PaginatedOptions, Response, RetryConfig, + SignatureWithMetadata, TokenBalance, ValidityProofWithContext, }; #[async_trait] @@ -94,7 +95,7 @@ impl Indexer for LightClient { owner: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { Ok(self .indexer .as_ref() @@ -268,7 +269,7 @@ impl Indexer for LightClient { delegate: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { Ok(self .indexer .as_ref() diff --git a/sdk-libs/client/src/rpc/mod.rs b/sdk-libs/client/src/rpc/mod.rs index 0b968c26c5..7912056568 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -1,5 +1,3 @@ -#![allow(clippy::result_large_err)] - pub mod client; pub mod errors; pub mod indexer; diff --git a/sdk-libs/client/src/rpc/rpc_trait.rs b/sdk-libs/client/src/rpc/rpc_trait.rs index 900ed08bab..9d0b20c6cc 100644 --- a/sdk-libs/client/src/rpc/rpc_trait.rs +++ b/sdk-libs/client/src/rpc/rpc_trait.rs @@ -58,7 +58,7 @@ impl LightClientConfig { commitment_config: Some(CommitmentConfig::confirmed()), photon_url: Some("http://127.0.0.1:8784".to_string()), api_key: None, - fetch_active_tree: false, + fetch_active_tree: true, } } @@ -83,8 +83,6 @@ pub trait Rpc: Send + Sync + Debug + 'static { match error { // Do not retry transaction errors. RpcError::ClientError(error) => error.kind.get_transaction_error().is_none(), - // Do not retry signing errors. - RpcError::SigningError(_) => false, _ => true, } } @@ -172,9 +170,16 @@ pub trait Rpc: Send + Sync + Debug + 'static { ) -> Result { let blockhash = self.get_latest_blockhash().await?.0; let mut transaction = Transaction::new_with_payer(instructions, Some(payer)); - transaction - .try_sign(signers, blockhash) - .map_err(|e| RpcError::SigningError(e.to_string()))?; + transaction.try_sign(signers, blockhash).map_err(|e| { + let message = transaction.message(); + let num_required_signatures = message.header.num_required_signatures as usize; + println!( + "Expected signers (first {} accounts in message): {:?}", + num_required_signatures, + message.account_keys[..num_required_signatures].to_vec() + ); + RpcError::CustomError(e.to_string()) + })?; self.process_transaction(transaction).await } @@ -191,8 +196,9 @@ pub trait Rpc: Send + Sync + Debug + 'static { payer: &Pubkey, signers: &[&Keypair], ) -> Result, Signature, Slot)>, RpcError>; - + #[allow(clippy::result_large_err)] fn indexer(&self) -> Result<&impl Indexer, RpcError>; + #[allow(clippy::result_large_err)] fn indexer_mut(&mut self) -> Result<&mut impl Indexer, RpcError>; /// Fetch the latest state tree addresses from the cluster. @@ -204,6 +210,7 @@ pub trait Rpc: Send + Sync + Debug + 'static { /// Gets a random state tree info. /// State trees are cached and have to be fetched or set. + #[allow(clippy::result_large_err)] fn get_random_state_tree_info(&self) -> Result; fn get_address_tree_v1(&self) -> TreeInfo; diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index eae058dc6d..e9e76d3dab 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -16,12 +16,13 @@ use light_client::{ fee::FeeConfig, indexer::{ AccountProofInputs, Address, AddressMerkleTreeAccounts, AddressProofInputs, - AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, Context, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, - MerkleProofWithContext, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, - Response, RetryConfig, RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, - TokenAccount, TokenBalance, ValidityProofWithContext, + AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, + CompressedTokenAccount, Context, GetCompressedAccountsByOwnerConfig, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MerkleProofWithContext, + NewAddressProofWithContext, OwnerBalance, PaginatedOptions, Response, RetryConfig, + RootIndex, SignatureWithMetadata, StateMerkleTreeAccounts, TokenBalance, + ValidityProofWithContext, }, rpc::{Rpc, RpcError}, }; @@ -246,15 +247,15 @@ impl Indexer for TestIndexer { owner: &Pubkey, options: Option, _config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { let mint = options.as_ref().and_then(|opts| opts.mint); - let token_accounts: Result, IndexerError> = self + let token_accounts: Result, IndexerError> = self .token_compressed_accounts .iter() .filter(|acc| { acc.token_data.owner == *owner && mint.is_none_or(|m| acc.token_data.mint == m) }) - .map(|acc| TokenAccount::try_from(acc.clone())) + .map(|acc| CompressedTokenAccount::try_from(acc.clone())) .collect(); let token_accounts = token_accounts?; let token_accounts = if let Some(options) = options { @@ -960,7 +961,7 @@ impl Indexer for TestIndexer { _delegate: &Pubkey, _options: Option, _config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { todo!("get_compressed_token_accounts_by_delegate not implemented") } diff --git a/sdk-libs/program-test/src/program_test/indexer.rs b/sdk-libs/program-test/src/program_test/indexer.rs index 28232fd616..49930682cd 100644 --- a/sdk-libs/program-test/src/program_test/indexer.rs +++ b/sdk-libs/program-test/src/program_test/indexer.rs @@ -1,10 +1,11 @@ use async_trait::async_trait; use light_client::indexer::{ Address, AddressWithTree, BatchAddressUpdateIndexerResponse, CompressedAccount, - GetCompressedAccountsByOwnerConfig, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, - Indexer, IndexerError, IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, - MerkleProofWithContext, NewAddressProofWithContext, OwnerBalance, PaginatedOptions, Response, - RetryConfig, SignatureWithMetadata, TokenAccount, TokenBalance, ValidityProofWithContext, + CompressedTokenAccount, GetCompressedAccountsByOwnerConfig, + GetCompressedTokenAccountsByOwnerOrDelegateOptions, Hash, Indexer, IndexerError, + IndexerRpcConfig, Items, ItemsWithCursor, MerkleProof, MerkleProofWithContext, + NewAddressProofWithContext, OwnerBalance, PaginatedOptions, Response, RetryConfig, + SignatureWithMetadata, TokenBalance, ValidityProofWithContext, }; use light_compressed_account::QueueType; use solana_sdk::pubkey::Pubkey; @@ -94,7 +95,7 @@ impl Indexer for LightProgramTest { owner: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { Ok(self .indexer .as_ref() @@ -265,7 +266,7 @@ impl Indexer for LightProgramTest { delegate: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { Ok(self .indexer .as_ref() diff --git a/sdk-libs/program-test/src/utils/setup_light_programs.rs b/sdk-libs/program-test/src/utils/setup_light_programs.rs index f5d67323d7..9d1621c631 100644 --- a/sdk-libs/program-test/src/utils/setup_light_programs.rs +++ b/sdk-libs/program-test/src/utils/setup_light_programs.rs @@ -49,25 +49,38 @@ pub fn setup_light_programs( .add_program_from_file(light_registry::ID, path.clone()) .inspect_err(|_| { println!("Program light_registry bin not found in {}", path); + }) + .map_err(|e| { + RpcError::CustomError(format!("Failed to add light_registry program: {}", e)) })?; let path = format!("{}/account_compression.so", light_bin_path); program_test .add_program_from_file(account_compression::ID, path.clone()) .inspect_err(|_| { println!("Program account_compression bin not found in {}", path); + }) + .map_err(|e| { + RpcError::CustomError(format!("Failed to add account_compression program: {}", e)) })?; let path = format!("{}/light_compressed_token.so", light_bin_path); program_test .add_program_from_file(light_compressed_token::ID, path.clone()) .inspect_err(|_| { println!("Program light_compressed_token bin not found in {}", path); + }) + .map_err(|e| { + RpcError::CustomError(format!( + "Failed to add light_compressed_token program: {}", + e + )) })?; let path = format!("{}/spl_noop.so", light_bin_path); program_test .add_program_from_file(NOOP_PROGRAM_ID, path.clone()) .inspect_err(|_| { println!("Program spl_noop bin not found in {}", path); - })?; + }) + .map_err(|e| RpcError::CustomError(format!("Failed to add spl_noop program: {}", e)))?; let path = format!("{}/light_system_program_pinocchio.so", light_bin_path); program_test @@ -77,6 +90,12 @@ pub fn setup_light_programs( "Program light_system_program_pinocchio bin not found in {}", path ); + }) + .map_err(|e| { + RpcError::CustomError(format!( + "Failed to add light_system_program_pinocchio: {}", + e + )) })?; let registered_program = registered_program_test_account_system_program(); @@ -104,6 +123,9 @@ pub fn setup_light_programs( .add_program_from_file(id, path.clone()) .inspect_err(|_| { println!("Program {} bin not found in {}", name, path); + }) + .map_err(|e| { + RpcError::CustomError(format!("Failed to add program {}: {}", name, e)) })?; } } diff --git a/sdk-libs/sdk-pinocchio/src/cpi/accounts_v2.rs b/sdk-libs/sdk-pinocchio/src/cpi/accounts_v2.rs index 89e124cb8b..e21d731911 100644 --- a/sdk-libs/sdk-pinocchio/src/cpi/accounts_v2.rs +++ b/sdk-libs/sdk-pinocchio/src/cpi/accounts_v2.rs @@ -1,5 +1,5 @@ use light_sdk_types::{ - CompressionCpiAccountIndexV2, CpiAccountsV2 as GenericCpiAccountsV2, PROGRAM_ACCOUNTS_LEN, + CompressionCpiAccountIndexV2, CpiAccountsV2 as GenericCpiAccountsV2, V2_PROGRAM_ACCOUNTS_LEN, }; use pinocchio::{account_info::AccountInfo, instruction::AccountMeta}; @@ -9,7 +9,7 @@ pub type CpiAccountsV2<'a> = GenericCpiAccountsV2<'a, AccountInfo>; pub fn to_account_metas_v2<'a>(cpi_accounts: &CpiAccountsV2<'a>) -> Result>> { let mut account_metas = - Vec::with_capacity(1 + cpi_accounts.account_infos().len() - PROGRAM_ACCOUNTS_LEN); + Vec::with_capacity(1 + cpi_accounts.account_infos().len() - V2_PROGRAM_ACCOUNTS_LEN); account_metas.push(AccountMeta::writable_signer(cpi_accounts.fee_payer().key())); account_metas.push(AccountMeta::readonly_signer( diff --git a/sdk-libs/sdk-types/src/constants.rs b/sdk-libs/sdk-types/src/constants.rs index 80e36ab550..497913f254 100644 --- a/sdk-libs/sdk-types/src/constants.rs +++ b/sdk-libs/sdk-types/src/constants.rs @@ -14,6 +14,12 @@ pub const ACCOUNT_COMPRESSION_AUTHORITY_PDA: [u8; 32] = /// ID of the light-compressed-token program. pub const C_TOKEN_PROGRAM_ID: [u8; 32] = pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); +pub const COMPRESSED_TOKEN_PROGRAM_ID: [u8; 32] = + pubkey_array!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"); + +/// ID of the compressed token program CPI authority PDA. +pub const COMPRESSED_TOKEN_PROGRAM_CPI_AUTHORITY: [u8; 32] = + pubkey_array!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"); /// Seed of the CPI authority. pub const CPI_AUTHORITY_PDA_SEED: &[u8] = b"cpi_authority"; @@ -34,7 +40,12 @@ pub const TOKEN_COMPRESSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [2, 0, 0, 0, 0, 0, 0 pub const ADDRESS_TREE_V1: [u8; 32] = pubkey_array!("amt1Ayt45jfbdw5YSo7iz6WZxUmnZsQTYXy82hVwyC2"); pub const ADDRESS_QUEUE_V1: [u8; 32] = pubkey_array!("aq1S9z4reTSQAdgWHGD2zDaS39sjGrAxbR31vxJ2F4F"); - -pub const CPI_CONTEXT_ACCOUNT_DISCRIMINATOR: [u8; 8] = [22, 20, 149, 218, 74, 204, 128, 166]; +pub const CPI_CONTEXT_ACCOUNT_DISCRIMINATOR_V1: [u8; 8] = [22, 20, 149, 218, 74, 204, 128, 166]; +pub const CPI_CONTEXT_ACCOUNT_DISCRIMINATOR: [u8; 8] = [34, 184, 183, 14, 100, 80, 183, 124]; pub const SOL_POOL_PDA: [u8; 32] = pubkey_array!("CHK57ywWSDncAoRu1F8QgwYJeXuAJyyBYT4LixLXvMZ1"); + +// For input accounts with empty data. +pub const DEFAULT_DATA_HASH: [u8; 32] = [ + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +]; diff --git a/sdk-libs/sdk-types/src/cpi_accounts.rs b/sdk-libs/sdk-types/src/cpi_accounts.rs index 7750603a3d..4aed17ab90 100644 --- a/sdk-libs/sdk-types/src/cpi_accounts.rs +++ b/sdk-libs/sdk-types/src/cpi_accounts.rs @@ -9,7 +9,7 @@ use crate::{ CpiSigner, CPI_CONTEXT_ACCOUNT_DISCRIMINATOR, LIGHT_SYSTEM_PROGRAM_ID, SOL_POOL_PDA, }; -#[derive(Debug, Copy, Clone, AnchorSerialize, AnchorDeserialize)] +#[derive(Debug, Copy, Clone, PartialEq, AnchorSerialize, AnchorDeserialize)] pub struct CpiAccountsConfig { pub cpi_context: bool, pub sol_compression_recipient: bool, @@ -61,14 +61,14 @@ pub enum CompressionCpiAccountIndex { } pub const SYSTEM_ACCOUNTS_LEN: usize = 11; - -pub struct CpiAccounts<'a, T: AccountInfoTrait> { +#[derive(Debug, Clone, PartialEq)] +pub struct CpiAccounts<'a, T: AccountInfoTrait + Clone> { fee_payer: &'a T, accounts: &'a [T], - config: CpiAccountsConfig, + pub config: CpiAccountsConfig, } -impl<'a, T: AccountInfoTrait> CpiAccounts<'a, T> { +impl<'a, T: AccountInfoTrait + Clone> CpiAccounts<'a, T> { pub fn new(fee_payer: &'a T, accounts: &'a [T], cpi_signer: CpiSigner) -> Self { Self { fee_payer, @@ -255,6 +255,14 @@ impl<'a, T: AccountInfoTrait> CpiAccounts<'a, T> { .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(system_len)) } + pub fn tree_pubkeys(&self) -> Result> { + Ok(self + .tree_accounts()? + .iter() + .map(|x| x.pubkey()) + .collect::>()) + } + pub fn get_tree_account_info(&self, tree_index: usize) -> Result<&'a T> { let tree_accounts = self.tree_accounts()?; tree_accounts @@ -265,12 +273,12 @@ impl<'a, T: AccountInfoTrait> CpiAccounts<'a, T> { } /// Create a vector of account info references - pub fn to_account_infos(&self) -> Vec<&'a T> { - let mut account_infos = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); - account_infos.push(self.fee_payer()); - self.account_infos()[1..] - .iter() - .for_each(|acc| account_infos.push(acc)); + pub fn to_account_infos(&self) -> Vec { + // Skip system light program + let refs = &self.account_infos()[1..]; + let mut account_infos = Vec::with_capacity(1 + refs.len()); + account_infos.push(self.fee_payer().clone()); + account_infos.extend_from_slice(refs); account_infos } } diff --git a/sdk-libs/sdk-types/src/cpi_accounts_small.rs b/sdk-libs/sdk-types/src/cpi_accounts_small.rs new file mode 100644 index 0000000000..8258fa537e --- /dev/null +++ b/sdk-libs/sdk-types/src/cpi_accounts_small.rs @@ -0,0 +1,223 @@ +use light_account_checks::AccountInfoTrait; + +use crate::{ + error::{LightSdkTypesError, Result}, + CpiAccountsConfig, CpiSigner, +}; + +#[repr(usize)] +pub enum CompressionCpiAccountIndexSmall { + LightSystemProgram, // index 0 - hardcoded in cpi hence no getter. + Authority, // index 1 - Cpi authority of the custom program, used to invoke the light system program. + RegisteredProgramPda, // index 2 - registered_program_pda + AccountCompressionAuthority, // index 3 - account_compression_authority + AccountCompressionProgram, // index 4 - account_compression_program + SystemProgram, // index 5 - system_program + SolPoolPda, // index 6 - Optional + DecompressionRecipient, // index 7 - Optional + CpiContext, // index 8 - Optional +} + +pub const PROGRAM_ACCOUNTS_LEN: usize = 0; // No program accounts in CPI + // 6 base accounts + 3 optional accounts +pub const SMALL_SYSTEM_ACCOUNTS_LEN: usize = 9; + +#[derive(Clone)] +pub struct CpiAccountsSmall<'a, T: AccountInfoTrait + Clone> { + fee_payer: &'a T, + accounts: &'a [T], + config: CpiAccountsConfig, +} + +impl<'a, T: AccountInfoTrait + Clone> CpiAccountsSmall<'a, T> { + #[inline(never)] + pub fn new(fee_payer: &'a T, accounts: &'a [T], cpi_signer: CpiSigner) -> Self { + Self { + fee_payer, + accounts, + config: CpiAccountsConfig::new(cpi_signer), + } + } + #[inline(never)] + #[cold] + pub fn new_with_config(fee_payer: &'a T, accounts: &'a [T], config: CpiAccountsConfig) -> Self { + Self { + fee_payer, + accounts, + config, + } + } + + pub fn fee_payer(&self) -> &'a T { + self.fee_payer + } + + pub fn authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::Authority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn light_system_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::LightSystemProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn registered_program_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::RegisteredProgramPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn account_compression_authority(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::AccountCompressionAuthority as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn account_compression_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::AccountCompressionProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn system_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::SystemProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn sol_pool_pda(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::SolPoolPda as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn decompression_recipient(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndexSmall::DecompressionRecipient as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn cpi_context(&self) -> Result<&'a T> { + let mut index = CompressionCpiAccountIndexSmall::CpiContext as usize; + if !self.config.sol_pool_pda { + index -= 1; + } + if !self.config.sol_compression_recipient { + index -= 1; + } + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn self_program_id(&self) -> T::Pubkey { + T::pubkey_from_bytes(self.config.cpi_signer.program_id) + } + + pub fn config(&self) -> &CpiAccountsConfig { + &self.config + } + + pub fn system_accounts_end_offset(&self) -> usize { + let mut len = SMALL_SYSTEM_ACCOUNTS_LEN; + if !self.config.sol_pool_pda { + len -= 1; + } + if !self.config.sol_compression_recipient { + len -= 1; + } + if !self.config.cpi_context { + len -= 1; + } + len + } + + pub fn account_infos(&self) -> &'a [T] { + self.accounts + } + + pub fn get_account_info(&self, index: usize) -> Result<&'a T> { + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + + pub fn tree_accounts(&self) -> Result<&'a [T]> { + let system_offset = self.system_accounts_end_offset(); + self.accounts + .get(system_offset..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + system_offset, + )) + } + + /// Returns accounts after the system accounts; instruction-specific + /// remaining_accounts start at this offset. + pub fn post_system_accounts(&self) -> Result<&'a [T]> { + let system_offset = self.system_accounts_end_offset(); + self.accounts + .get(system_offset..) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + system_offset, + )) + } + + pub fn get_tree_account_info(&self, tree_index: usize) -> Result<&'a T> { + let tree_accounts = self.tree_accounts()?; + tree_accounts + .get(tree_index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds( + self.system_accounts_end_offset() + tree_index, + )) + } + + // TODO: unify with get_tree_account_info + pub fn get_tree_address(&self, tree_index: u8) -> Result<&'a T> { + let tree_accounts = self.tree_accounts()?; + tree_accounts.get(tree_index as usize).ok_or( + LightSdkTypesError::CpiAccountsIndexOutOfBounds( + self.system_accounts_end_offset() + tree_index as usize, + ), + ) + } + + /// Create a vector of account info references + pub fn to_account_infos(&self) -> Vec { + let mut account_infos = Vec::with_capacity(1 + self.accounts.len()); + account_infos.push(self.fee_payer().clone()); + // Skip system light program + self.accounts[1..] + .iter() + .for_each(|acc| account_infos.push(acc.clone())); + account_infos + } + pub fn bump(&self) -> u8 { + self.config.cpi_signer.bump + } + + pub fn invoking_program(&self) -> [u8; 32] { + self.config.cpi_signer.program_id + } + pub fn account_infos_slice(&self) -> &[T] { + &self.accounts[PROGRAM_ACCOUNTS_LEN..] + } + + pub fn tree_pubkeys(&self) -> Result> { + Ok(self + .tree_accounts()? + .iter() + .map(|x| x.pubkey()) + .collect::>()) + } +} diff --git a/sdk-libs/sdk-types/src/cpi_accounts_v2.rs b/sdk-libs/sdk-types/src/cpi_accounts_v2.rs index 314f8c17ac..1ca88e7fd4 100644 --- a/sdk-libs/sdk-types/src/cpi_accounts_v2.rs +++ b/sdk-libs/sdk-types/src/cpi_accounts_v2.rs @@ -142,12 +142,15 @@ impl<'a, T: AccountInfoTrait> CpiAccountsV2<'a, T> { } /// Create a vector of account info references - pub fn to_account_infos(&self) -> Vec<&'a T> { + pub fn to_account_infos(&self) -> Vec + where + T: Clone, + { let mut account_infos = Vec::with_capacity(1 + self.accounts.len() - PROGRAM_ACCOUNTS_LEN); - account_infos.push(self.fee_payer()); + account_infos.push(self.fee_payer().clone()); self.accounts[PROGRAM_ACCOUNTS_LEN..] .iter() - .for_each(|acc| account_infos.push(acc)); + .for_each(|acc| account_infos.push(acc.clone())); account_infos } diff --git a/sdk-libs/sdk-types/src/cpi_context_write.rs b/sdk-libs/sdk-types/src/cpi_context_write.rs new file mode 100644 index 0000000000..0b34c60590 --- /dev/null +++ b/sdk-libs/sdk-types/src/cpi_context_write.rs @@ -0,0 +1,33 @@ +use light_account_checks::AccountInfoTrait; + +use crate::CpiSigner; +// TODO: move to ctoken types +#[derive(Clone, Debug)] +pub struct CpiContextWriteAccounts<'a, T: AccountInfoTrait + Clone> { + pub fee_payer: &'a T, + pub authority: &'a T, + pub cpi_context: &'a T, + pub cpi_signer: CpiSigner, +} + +impl CpiContextWriteAccounts<'_, T> { + pub fn bump(&self) -> u8 { + self.cpi_signer.bump + } + + pub fn invoking_program(&self) -> [u8; 32] { + self.cpi_signer.program_id + } + + pub fn to_account_infos(&self) -> [T; 3] { + [ + self.fee_payer.clone(), + self.authority.clone(), + self.cpi_context.clone(), + ] + } + + pub fn to_account_info_refs(&self) -> [&T; 3] { + [self.fee_payer, self.authority, self.cpi_context] + } +} diff --git a/sdk-libs/sdk-types/src/instruction/tree_info.rs b/sdk-libs/sdk-types/src/instruction/tree_info.rs index 8cdcc7fed0..39dab2190c 100644 --- a/sdk-libs/sdk-types/src/instruction/tree_info.rs +++ b/sdk-libs/sdk-types/src/instruction/tree_info.rs @@ -1,6 +1,10 @@ use light_account_checks::AccountInfoTrait; +#[cfg(feature = "v2")] +use light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked; use light_compressed_account::instruction_data::data::NewAddressParamsPacked; +#[cfg(feature = "v2")] +use crate::CpiAccountsSmall; use crate::{AnchorDeserialize, AnchorSerialize, CpiAccounts}; #[derive(Debug, Clone, Copy, AnchorDeserialize, AnchorSerialize, PartialEq, Default)] @@ -29,7 +33,24 @@ impl PackedAddressTreeInfo { } } - pub fn get_tree_pubkey( + #[cfg(feature = "v2")] + pub fn into_new_address_params_assigned_packed( + self, + seed: [u8; 32], + assigned_to_account: bool, + assigned_account_index: Option, + ) -> NewAddressParamsAssignedPacked { + NewAddressParamsAssignedPacked { + address_merkle_tree_account_index: self.address_merkle_tree_pubkey_index, + address_queue_account_index: self.address_queue_pubkey_index, + address_merkle_tree_root_index: self.root_index, + seed, + assigned_to_account, + assigned_account_index: assigned_account_index.unwrap_or_default(), + } + } + + pub fn get_tree_pubkey( &self, cpi_accounts: &CpiAccounts<'_, T>, ) -> Result { @@ -37,4 +58,14 @@ impl PackedAddressTreeInfo { cpi_accounts.get_tree_account_info(self.address_merkle_tree_pubkey_index as usize)?; Ok(account.pubkey()) } + + #[cfg(feature = "v2")] + pub fn get_tree_pubkey_small( + &self, + cpi_accounts: &CpiAccountsSmall<'_, T>, + ) -> Result { + let account = + cpi_accounts.get_tree_account_info(self.address_merkle_tree_pubkey_index as usize)?; + Ok(account.pubkey()) + } } diff --git a/sdk-libs/sdk-types/src/lib.rs b/sdk-libs/sdk-types/src/lib.rs index 36341f5d02..16a9eb4e94 100644 --- a/sdk-libs/sdk-types/src/lib.rs +++ b/sdk-libs/sdk-types/src/lib.rs @@ -1,8 +1,11 @@ pub mod address; pub mod constants; pub mod cpi_accounts; +#[cfg(feature = "v2")] +pub mod cpi_accounts_small; #[cfg(feature = "v2_ix")] pub mod cpi_accounts_v2; +pub mod cpi_context_write; pub mod error; pub mod instruction; @@ -13,9 +16,15 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use constants::*; pub use cpi_accounts::*; +#[cfg(feature = "v2")] +pub use cpi_accounts_small::{ + CompressionCpiAccountIndexSmall, CpiAccountsSmall, + PROGRAM_ACCOUNTS_LEN as SMALL_PROGRAM_ACCOUNTS_LEN, SMALL_SYSTEM_ACCOUNTS_LEN, +}; #[cfg(feature = "v2_ix")] pub use cpi_accounts_v2::{ - CompressionCpiAccountIndexV2, CpiAccountsV2, PROGRAM_ACCOUNTS_LEN, V2_SYSTEM_ACCOUNTS_LEN, + CompressionCpiAccountIndexV2, CpiAccountsV2, PROGRAM_ACCOUNTS_LEN as V2_PROGRAM_ACCOUNTS_LEN, + V2_SYSTEM_ACCOUNTS_LEN, }; /// Configuration struct containing program ID, CPI signer, and bump for Light Protocol diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index ace487331c..bed0e657b7 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -39,7 +39,7 @@ thiserror = { workspace = true } light-sdk-macros = { workspace = true } light-sdk-types = { workspace = true } light-macros = { workspace = true } -light-compressed-account = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } light-hasher = { workspace = true } light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } diff --git a/sdk-libs/sdk/src/account.rs b/sdk-libs/sdk/src/account.rs index 8206696040..9cd82cf6e1 100644 --- a/sdk-libs/sdk/src/account.rs +++ b/sdk-libs/sdk/src/account.rs @@ -65,33 +65,54 @@ //! ``` // TODO: add example for manual hashing -use std::ops::{Deref, DerefMut}; +use std::{ + marker::PhantomData, + ops::{Deref, DerefMut}, +}; use light_compressed_account::{ compressed_account::PackedMerkleContext, instruction_data::with_account_info::{CompressedAccountInfo, InAccountInfo, OutAccountInfo}, }; -use light_sdk_types::instruction::account_meta::CompressedAccountMetaTrait; +use light_sdk_types::{instruction::account_meta::CompressedAccountMetaTrait, DEFAULT_DATA_HASH}; use solana_pubkey::Pubkey; use crate::{ error::LightSdkError, - light_hasher::{DataHasher, Poseidon}, + light_hasher::{DataHasher, Hasher, Poseidon, Sha256}, AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; +pub trait Size { + fn size(&self) -> usize; +} + +pub type LightAccount<'a, A> = LightAccountInner<'a, Poseidon, A>; + +pub mod sha { + use super::*; + /// LightAccount variant that uses SHA256 hashing + pub type LightAccount<'a, A> = super::LightAccountInner<'a, Sha256, A>; +} + #[derive(Debug, PartialEq)] -pub struct LightAccount< +pub struct LightAccountInner< 'a, + H: Hasher, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, > { owner: &'a Pubkey, pub account: A, account_info: CompressedAccountInfo, + should_remove_data: bool, + _hasher: PhantomData, } -impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default> - LightAccount<'a, A> +impl< + 'a, + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > LightAccountInner<'a, H, A> { pub fn new_init( owner: &'a Pubkey, @@ -111,6 +132,8 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: None, output: Some(output_account_info), }, + should_remove_data: false, + _hasher: PhantomData, } } @@ -120,7 +143,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input_account: A, ) -> Result { let input_account_info = { - let input_data_hash = input_account.hash::()?; + let input_data_hash = input_account.hash::()?; let tree_info = input_account_meta.get_tree_info(); InAccountInfo { data_hash: input_data_hash, @@ -155,6 +178,57 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: Some(input_account_info), output: Some(output_account_info), }, + should_remove_data: false, + _hasher: PhantomData, + }) + } + + /// Create a new LightAccount for compression from an empty compressed + /// account. This is used when compressing a PDA - we know the compressed + /// account exists but is empty (data: [], data_hash: [0, 1, 1, 1, 1, 1, 1, + /// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + /// 1]). + pub fn new_mut_without_data( + owner: &'a Pubkey, + input_account_meta: &impl CompressedAccountMetaTrait, + ) -> Result { + let input_account_info = { + let tree_info = input_account_meta.get_tree_info(); + InAccountInfo { + data_hash: DEFAULT_DATA_HASH, // TODO: review security. + lamports: input_account_meta.get_lamports().unwrap_or_default(), + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + leaf_index: tree_info.leaf_index, + prove_by_index: tree_info.prove_by_index, + }, + root_index: input_account_meta.get_root_index().unwrap_or_default(), + discriminator: A::LIGHT_DISCRIMINATOR, + } + }; + let output_account_info = { + let output_merkle_tree_index = input_account_meta + .get_output_state_tree_index() + .ok_or(LightSdkError::OutputStateTreeIndexIsNone)?; + OutAccountInfo { + lamports: input_account_meta.get_lamports().unwrap_or_default(), + output_merkle_tree_index, + discriminator: A::LIGHT_DISCRIMINATOR, + ..Default::default() + } + }; + + Ok(Self { + owner, + account: A::default(), // Start with default, will be filled with PDA data + account_info: CompressedAccountInfo { + address: input_account_meta.get_address(), + input: Some(input_account_info), + output: Some(output_account_info), + }, + should_remove_data: false, + _hasher: PhantomData, }) } @@ -164,7 +238,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input_account: A, ) -> Result { let input_account_info = { - let input_data_hash = input_account.hash::()?; + let input_data_hash = input_account.hash::()?; let tree_info = input_account_meta.get_tree_info(); InAccountInfo { data_hash: input_data_hash, @@ -179,6 +253,7 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe discriminator: A::LIGHT_DISCRIMINATOR, } }; + Ok(Self { owner, account: input_account, @@ -187,6 +262,8 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe input: Some(input_account_info), output: None, }, + should_remove_data: false, + _hasher: PhantomData, }) } @@ -230,6 +307,20 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe &self.account_info.output } + /// Get the byte size of the account type. + pub fn size(&self) -> Result + where + A: Size, + { + Ok(self.account.size()) + } + + /// Remove the data from this account by setting it to default. + /// This is used when decompressing to ensure the compressed account is properly zeroed. + pub fn remove_data(&mut self) { + self.should_remove_data = true; + } + /// 1. Serializes the account data and sets the output data hash. /// 2. Returns CompressedAccountInfo. /// @@ -237,18 +328,28 @@ impl<'a, A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHashe /// that should only be called once per instruction. pub fn to_account_info(mut self) -> Result { if let Some(output) = self.account_info.output.as_mut() { - output.data_hash = self.account.hash::()?; - output.data = self - .account - .try_to_vec() - .map_err(|_| LightSdkError::Borsh)?; + if self.should_remove_data { + // TODO: review security. + output.data_hash = DEFAULT_DATA_HASH; + } else { + output.data_hash = self.account.hash::()?; + if H::ID != 0 { + output.data_hash[0] = 0; + } + output.data = self + .account + .try_to_vec() + .map_err(|_| LightSdkError::Borsh)?; + } } Ok(self.account_info) } } -impl Deref - for LightAccount<'_, A> +impl< + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > Deref for LightAccountInner<'_, H, A> { type Target = A; @@ -257,8 +358,10 @@ impl DerefMut - for LightAccount<'_, A> +impl< + H: Hasher, + A: AnchorSerialize + AnchorDeserialize + LightDiscriminator + DataHasher + Default, + > DerefMut for LightAccountInner<'_, H, A> { fn deref_mut(&mut self) -> &mut ::Target { &mut self.account diff --git a/sdk-libs/sdk/src/cpi/accounts.rs b/sdk-libs/sdk/src/cpi/accounts.rs index a35c084c7d..c12b09f3ea 100644 --- a/sdk-libs/sdk/src/cpi/accounts.rs +++ b/sdk-libs/sdk/src/cpi/accounts.rs @@ -23,6 +23,27 @@ pub struct CpiInstructionConfig<'a, 'info> { pub type CpiAccounts<'c, 'info> = GenericCpiAccounts<'c, AccountInfo<'info>>; +/// Trait to provide convenient access to packed account metas from CpiAccounts +pub trait CpiAccountsExt { + /// Returns AccountMetas for all tree accounts (packed accounts) + fn get_packed_account_metas(&self) -> Result>; +} + +impl CpiAccountsExt for CpiAccounts<'_, '_> { + fn get_packed_account_metas(&self) -> Result> { + let tree_accounts = self.tree_accounts()?; + let mut metas = Vec::with_capacity(tree_accounts.len()); + for info in tree_accounts { + metas.push(AccountMeta { + pubkey: *info.key, + is_signer: info.is_signer, + is_writable: info.is_writable, + }); + } + Ok(metas) + } +} + pub fn get_account_metas_from_config(config: CpiInstructionConfig<'_, '_>) -> Vec { let mut account_metas = Vec::with_capacity(1 + SYSTEM_ACCOUNTS_LEN); diff --git a/sdk-libs/sdk/src/cpi/accounts_cpi_context.rs b/sdk-libs/sdk/src/cpi/accounts_cpi_context.rs new file mode 100644 index 0000000000..46b6ccd7a2 --- /dev/null +++ b/sdk-libs/sdk/src/cpi/accounts_cpi_context.rs @@ -0,0 +1,13 @@ +use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; +use solana_account_info::AccountInfo; +use solana_instruction::AccountMeta; + +pub fn get_account_metas_from_config_cpi_context( + config: CpiContextWriteAccounts, +) -> [AccountMeta; 3] { + [ + AccountMeta::new(*config.fee_payer.key, true), + AccountMeta::new_readonly(config.cpi_signer.cpi_signer.into(), true), + AccountMeta::new(*config.cpi_context.key, false), + ] +} diff --git a/sdk-libs/sdk/src/cpi/accounts_small_ix.rs b/sdk-libs/sdk/src/cpi/accounts_small_ix.rs new file mode 100644 index 0000000000..60667db102 --- /dev/null +++ b/sdk-libs/sdk/src/cpi/accounts_small_ix.rs @@ -0,0 +1,134 @@ +use light_sdk_types::{ + CpiAccountsSmall as GenericCpiAccountsSmall, ACCOUNT_COMPRESSION_AUTHORITY_PDA, + ACCOUNT_COMPRESSION_PROGRAM_ID, REGISTERED_PROGRAM_PDA, SMALL_SYSTEM_ACCOUNTS_LEN, + SOL_POOL_PDA, +}; + +use crate::{ + error::{LightSdkError, Result}, + AccountInfo, AccountMeta, Pubkey, +}; + +#[derive(Debug)] +pub struct CpiInstructionConfigSmall<'a, 'info> { + pub fee_payer: Pubkey, + pub cpi_signer: Pubkey, + pub sol_pool_pda: bool, + pub sol_compression_recipient_pubkey: Option, + pub cpi_context_pubkey: Option, + pub packed_accounts: &'a [AccountInfo<'info>], +} + +pub type CpiAccountsSmall<'c, 'info> = GenericCpiAccountsSmall<'c, AccountInfo<'info>>; + +pub fn get_account_metas_from_config_small( + config: CpiInstructionConfigSmall<'_, '_>, +) -> Vec { + let mut account_metas = Vec::with_capacity(1 + SMALL_SYSTEM_ACCOUNTS_LEN); + + // 1. Fee payer (signer, writable) + account_metas.push(AccountMeta { + pubkey: config.fee_payer, + is_signer: true, + is_writable: true, + }); + + // 2. Authority/CPI Signer (signer, readonly) + account_metas.push(AccountMeta { + pubkey: config.cpi_signer, + is_signer: true, + is_writable: false, + }); + + // 3. Registered Program PDA (readonly) - hardcoded constant + account_metas.push(AccountMeta { + pubkey: Pubkey::from(REGISTERED_PROGRAM_PDA), + is_signer: false, + is_writable: false, + }); + + // 4. Account Compression Authority (readonly) - hardcoded constant + account_metas.push(AccountMeta { + pubkey: Pubkey::from(ACCOUNT_COMPRESSION_AUTHORITY_PDA), + is_signer: false, + is_writable: false, + }); + + // 5. Account Compression Program (readonly) - hardcoded constant + account_metas.push(AccountMeta { + pubkey: Pubkey::from(ACCOUNT_COMPRESSION_PROGRAM_ID), + is_signer: false, + is_writable: false, + }); + + // 6. System Program (readonly) - always default pubkey + account_metas.push(AccountMeta { + pubkey: Pubkey::default(), + is_signer: false, + is_writable: false, + }); + + // Optional accounts based on config + if config.sol_pool_pda { + account_metas.push(AccountMeta { + pubkey: Pubkey::from(SOL_POOL_PDA), + is_signer: false, + is_writable: true, + }); + } + + if let Some(sol_compression_recipient_pubkey) = config.sol_compression_recipient_pubkey { + account_metas.push(AccountMeta { + pubkey: sol_compression_recipient_pubkey, + is_signer: false, + is_writable: true, + }); + } + + if let Some(cpi_context_pubkey) = config.cpi_context_pubkey { + account_metas.push(AccountMeta { + pubkey: cpi_context_pubkey, + is_signer: false, + is_writable: true, + }); + } + + // Add tree accounts + for acc in config.packed_accounts { + account_metas.push(AccountMeta { + pubkey: *acc.key, + is_signer: false, + is_writable: acc.is_writable, + }); + } + + account_metas +} + +impl<'a, 'info> TryFrom<&'a CpiAccountsSmall<'a, 'info>> for CpiInstructionConfigSmall<'a, 'info> { + type Error = LightSdkError; + + fn try_from(cpi_accounts: &'a CpiAccountsSmall<'a, 'info>) -> Result { + Ok(CpiInstructionConfigSmall { + fee_payer: *cpi_accounts.fee_payer().key, + cpi_signer: cpi_accounts.config().cpi_signer().into(), + sol_pool_pda: cpi_accounts.config().sol_pool_pda, + sol_compression_recipient_pubkey: if cpi_accounts.config().sol_compression_recipient { + Some(*cpi_accounts.decompression_recipient()?.key) + } else { + None + }, + cpi_context_pubkey: if cpi_accounts.config().cpi_context { + Some(*cpi_accounts.cpi_context()?.key) + } else { + None + }, + packed_accounts: cpi_accounts.tree_accounts().unwrap_or(&[]), + }) + } +} + +pub fn to_account_metas_small(cpi_accounts: CpiAccountsSmall<'_, '_>) -> Result> { + let config = CpiInstructionConfigSmall::try_from(&cpi_accounts)?; + Ok(get_account_metas_from_config_small(config)) +} diff --git a/sdk-libs/sdk/src/cpi/accounts_v2_ix.rs b/sdk-libs/sdk/src/cpi/accounts_v2_ix.rs index 9efc0d2930..5f18024edd 100644 --- a/sdk-libs/sdk/src/cpi/accounts_v2_ix.rs +++ b/sdk-libs/sdk/src/cpi/accounts_v2_ix.rs @@ -1,5 +1,5 @@ use light_sdk_types::{ - CompressionCpiAccountIndexV2, CpiAccountsV2 as GenericCpiAccountsV2, PROGRAM_ACCOUNTS_LEN, + CompressionCpiAccountIndexV2, CpiAccountsV2 as GenericCpiAccountsV2, V2_PROGRAM_ACCOUNTS_LEN, }; use crate::{error::Result, AccountInfo, AccountMeta}; @@ -9,7 +9,7 @@ pub type CpiAccountsV2<'c, 'info> = GenericCpiAccountsV2<'c, AccountInfo<'info>> pub fn to_account_metas_v2(cpi_accounts: CpiAccountsV2<'_, '_>) -> Result> { // TODO: do a version with a const array instead of vector. let mut account_metas = - Vec::with_capacity(1 + cpi_accounts.account_infos().len() - PROGRAM_ACCOUNTS_LEN); + Vec::with_capacity(1 + cpi_accounts.account_infos().len() - V2_PROGRAM_ACCOUNTS_LEN); account_metas.push(AccountMeta { pubkey: *cpi_accounts.fee_payer().key, diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 39796a8da0..30cbd14548 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -1,16 +1,26 @@ use light_compressed_account::{ - compressed_account::ReadOnlyCompressedAccount, + compressed_account::PackedReadOnlyCompressedAccount, instruction_data::{ cpi_context::CompressedCpiContext, - data::{NewAddressParamsPacked, ReadOnlyAddress}, + data::{NewAddressParamsAssignedPacked, NewAddressParamsPacked, PackedReadOnlyAddress}, invoke_cpi::InstructionDataInvokeCpi, - with_account_info::CompressedAccountInfo, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, }, }; -use light_sdk_types::constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}; +use light_sdk_types::{ + constants::{CPI_AUTHORITY_PDA_SEED, LIGHT_SYSTEM_PROGRAM_ID}, + cpi_context_write::CpiContextWriteAccounts, +}; +#[allow(unused_imports)] // TODO: Remove. +use solana_msg::msg; +#[cfg(feature = "v2")] +use crate::cpi::{to_account_metas_small, CpiAccountsSmall}; use crate::{ - cpi::{get_account_metas_from_config, CpiAccounts, CpiInstructionConfig}, + cpi::{ + accounts_cpi_context::get_account_metas_from_config_cpi_context, + get_account_metas_from_config, CpiAccounts, CpiInstructionConfig, + }, error::{LightSdkError, Result}, instruction::{account_info::CompressedAccountInfoTrait, ValidityProof}, invoke_signed, AccountInfo, AnchorSerialize, Instruction, @@ -20,14 +30,47 @@ use crate::{ pub struct CpiInputs { pub proof: ValidityProof, pub account_infos: Option>, - pub read_only_accounts: Option>, + pub read_only_accounts: Option>, pub new_addresses: Option>, - pub read_only_address: Option>, + pub new_assigned_addresses: Option>, + pub read_only_address: Option>, pub compress_or_decompress_lamports: Option, pub is_compress: bool, pub cpi_context: Option, } +/// Builder pattern implementation for CpiInputs. +/// +/// This provides a fluent API for constructing CPI inputs with various configurations. +/// The most common pattern is to use one of the constructor methods and then chain +/// builder methods to add additional configuration. +/// +/// # Examples +/// +/// Most common CPI context usage (no proof, assigned addresses): +/// ```rust +/// let cpi_inputs = CpiInputs::new_for_cpi_context( +/// all_compressed_infos, +/// vec![pool_new_address_params, observation_new_address_params], +/// ); +/// ``` +/// +/// Basic usage with CPI context and custom proof: +/// ```rust +/// let cpi_inputs = CpiInputs::new_with_assigned_address( +/// light_proof, +/// all_compressed_infos, +/// vec![pool_new_address_params, observation_new_address_params], +/// ) +/// .with_first_set_cpi_context(); +/// ``` +/// +/// Advanced usage with multiple configurations: +/// ```rust +/// let cpi_inputs = CpiInputs::new(proof, account_infos) +/// .with_first_set_cpi_context() +/// .with_compress_lamports(1000000); +/// ``` impl CpiInputs { pub fn new(proof: ValidityProof, account_infos: Vec) -> Self { Self { @@ -50,13 +93,219 @@ impl CpiInputs { } } + pub fn new_with_assigned_address( + proof: ValidityProof, + account_infos: Vec, + new_addresses: Vec, + ) -> Self { + Self { + proof, + account_infos: Some(account_infos), + new_assigned_addresses: Some(new_addresses), + ..Default::default() + } + } + + // TODO: check if always unused! + /// Creates CpiInputs for the common CPI context pattern: no proof (None), + /// assigned addresses, and first set CPI context. + /// + /// This is the most common pattern when using CPI context for cross-program + /// compressed account operations. + /// + /// # Example + /// ```rust + /// let cpi_inputs = CpiInputs::new_for_cpi_context( + /// all_compressed_infos, + /// vec![user_new_address_params, game_new_address_params], + /// ); + /// ``` + pub fn new_first_cpi( + account_infos: Vec, + new_addresses: Vec, + ) -> Self { + Self { + proof: ValidityProof(None), + account_infos: Some(account_infos), + new_assigned_addresses: Some(new_addresses), + cpi_context: Some(CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index: 0, // unused + }), + ..Default::default() + } + } + + /// Sets a custom CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_cpi_context(CompressedCpiContext { + /// set_context: true, + /// first_set_context: false, + /// cpi_context_account_index: 1, + /// }); + /// ``` + pub fn with_cpi_context(mut self, cpi_context: CompressedCpiContext) -> Self { + self.cpi_context = Some(cpi_context); + self + } + + // TODO: check if always unused! + /// Sets CPI context to first set context (clears any existing context). + /// This is the most common pattern for initializing CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_first_set_cpi_context(); + /// ``` + pub fn with_first_set_cpi_context(mut self) -> Self { + self.cpi_context = Some(CompressedCpiContext { + set_context: false, + first_set_context: true, + cpi_context_account_index: 0, // unused. + }); + self + } + + /// Sets CPI context to set context (updates existing context). + /// Use this when you want to update an existing CPI context. + /// + /// # Example + /// ``` + /// let cpi_inputs = CpiInputs::new_with_assigned_address(proof, infos, addresses) + /// .with_set_cpi_context(0); + /// ``` + pub fn with_last_cpi_context(mut self, cpi_context_account_index: u8) -> Self { + self.cpi_context = Some(CompressedCpiContext { + set_context: true, + first_set_context: false, + cpi_context_account_index, + }); + self + } + pub fn invoke_light_system_program(self, cpi_accounts: CpiAccounts<'_, '_>) -> Result<()> { let bump = cpi_accounts.bump(); - let account_info_refs = cpi_accounts.to_account_infos(); + let account_infos = cpi_accounts.to_account_infos(); let instruction = create_light_system_progam_instruction_invoke_cpi(self, cpi_accounts)?; - let account_infos: Vec = account_info_refs.into_iter().cloned().collect(); invoke_light_system_program(account_infos.as_slice(), instruction, bump) } + + #[cfg(feature = "v2")] + pub fn invoke_light_system_program_small( + self, + cpi_accounts: CpiAccountsSmall<'_, '_>, + ) -> Result<()> { + let bump = cpi_accounts.bump(); + let account_infos = cpi_accounts.to_account_infos(); + let instruction = + create_light_system_progam_instruction_invoke_cpi_small(self, cpi_accounts)?; + invoke_light_system_program(account_infos.as_slice(), instruction, bump) + } + #[inline(never)] + #[cold] + pub fn invoke_light_system_program_cpi_context( + self, + cpi_accounts: CpiContextWriteAccounts, + ) -> Result<()> { + let bump = cpi_accounts.bump(); + let account_infos = cpi_accounts.to_account_infos(); + let instruction = + create_light_system_progam_instruction_invoke_cpi_context_write(self, cpi_accounts)?; + invoke_light_system_program(account_infos.as_slice(), instruction, bump) + } +} + +#[cfg(feature = "v2")] +pub fn create_light_system_progam_instruction_invoke_cpi_small( + cpi_inputs: CpiInputs, + cpi_accounts: CpiAccountsSmall<'_, '_>, +) -> Result { + if cpi_inputs.new_addresses.is_some() { + unimplemented!("new_addresses must be new assigned addresses."); + } + + let inputs = InstructionDataInvokeCpiWithAccountInfo { + proof: cpi_inputs.proof.into(), + mode: 1, + bump: cpi_accounts.bump(), + invoking_program_id: cpi_accounts.invoking_program().into(), + new_address_params: cpi_inputs.new_assigned_addresses.unwrap_or_default(), + read_only_accounts: cpi_inputs.read_only_accounts.unwrap_or_default(), + read_only_addresses: cpi_inputs.read_only_address.unwrap_or_default(), + account_infos: cpi_inputs.account_infos.unwrap_or_default(), + with_transaction_hash: false, + compress_or_decompress_lamports: cpi_inputs + .compress_or_decompress_lamports + .unwrap_or_default(), + is_compress: cpi_inputs.is_compress, + with_cpi_context: cpi_inputs.cpi_context.is_some(), + cpi_context: cpi_inputs.cpi_context.unwrap_or_default(), + }; + // TODO: bench vs zero copy and set. + let inputs = inputs.try_to_vec().map_err(|_| LightSdkError::Borsh)?; + + let mut data = Vec::with_capacity(8 + inputs.len()); + data.extend_from_slice( + &light_compressed_account::discriminators::INVOKE_CPI_WITH_ACCOUNT_INFO_INSTRUCTION, + ); + data.extend(inputs); + + let account_metas = to_account_metas_small(cpi_accounts)?; + + Ok(Instruction { + program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), + accounts: account_metas, + data, + }) +} + +#[inline(never)] +#[cold] +pub fn create_light_system_progam_instruction_invoke_cpi_context_write( + cpi_inputs: CpiInputs, + cpi_accounts: CpiContextWriteAccounts, +) -> Result { + if cpi_inputs.new_addresses.is_some() { + unimplemented!("new_addresses must be new assigned addresses."); + } + + let inputs = InstructionDataInvokeCpiWithAccountInfo { + proof: cpi_inputs.proof.into(), + mode: 1, + bump: cpi_accounts.bump(), + invoking_program_id: cpi_accounts.invoking_program().into(), + new_address_params: cpi_inputs.new_assigned_addresses.unwrap_or_default(), + read_only_accounts: cpi_inputs.read_only_accounts.unwrap_or_default(), + read_only_addresses: cpi_inputs.read_only_address.unwrap_or_default(), + account_infos: cpi_inputs.account_infos.unwrap_or_default(), + with_transaction_hash: false, + compress_or_decompress_lamports: cpi_inputs + .compress_or_decompress_lamports + .unwrap_or_default(), + is_compress: cpi_inputs.is_compress, + with_cpi_context: cpi_inputs.cpi_context.is_some(), + cpi_context: cpi_inputs.cpi_context.unwrap_or_default(), + }; + // TODO: bench vs zero copy and set. + let inputs = inputs.try_to_vec().map_err(|_| LightSdkError::Borsh)?; + + let mut data = Vec::with_capacity(8 + inputs.len()); + data.extend_from_slice( + &light_compressed_account::discriminators::INVOKE_CPI_WITH_ACCOUNT_INFO_INSTRUCTION, + ); + data.extend(inputs); + + let account_metas = get_account_metas_from_config_cpi_context(cpi_accounts); + Ok(Instruction { + program_id: LIGHT_SYSTEM_PROGRAM_ID.into(), + accounts: account_metas.to_vec(), + data, + }) } pub fn create_light_system_progam_instruction_invoke_cpi( @@ -138,8 +387,7 @@ where data.extend_from_slice(&light_compressed_account::discriminators::DISCRIMINATOR_INVOKE_CPI); data.extend_from_slice(&(inputs.len() as u32).to_le_bytes()); data.extend(inputs); - let account_info_refs = cpi_accounts.to_account_infos(); - let account_infos: Vec = account_info_refs.into_iter().cloned().collect(); + let account_infos = cpi_accounts.to_account_infos(); let bump = cpi_accounts.bump(); let config = CpiInstructionConfig::try_from(&cpi_accounts)?; diff --git a/sdk-libs/sdk/src/cpi/mod.rs b/sdk-libs/sdk/src/cpi/mod.rs index c96da72c8b..87a9eee7f5 100644 --- a/sdk-libs/sdk/src/cpi/mod.rs +++ b/sdk-libs/sdk/src/cpi/mod.rs @@ -8,12 +8,11 @@ //! pub const LIGHT_CPI_SIGNER: CpiSigner = //! derive_light_cpi_signer!("2tzfijPBGbrR5PboyFUFKzfEoLTwdDSHUjANCw929wyt"); //! -//! let light_cpi_accounts = CpiAccounts::new( +//! let light_cpi_accounts = CpiAccountsSmall::new( //! ctx.accounts.fee_payer.as_ref(), //! ctx.remaining_accounts, //! crate::LIGHT_CPI_SIGNER, -//! ) -//! .map_err(ProgramError::from)?; +//! ); //! //! let (address, address_seed) = derive_address( //! &[b"compressed", name.as_bytes()], @@ -43,16 +42,20 @@ //! ); //! //! cpi_inputs -//! .invoke_light_system_program(light_cpi_accounts) -//! .map_err(ProgramError::from)?; +//! .invoke_light_system_program_small(light_cpi_accounts)?; //! ``` mod accounts; +mod accounts_cpi_context; +#[cfg(feature = "v2")] +mod accounts_small_ix; #[cfg(feature = "v2_ix")] mod accounts_v2_ix; mod invoke; pub use accounts::*; +#[cfg(feature = "v2")] +pub use accounts_small_ix::*; #[cfg(feature = "v2_ix")] pub use accounts_v2_ix::*; pub use invoke::*; diff --git a/sdk-libs/sdk/src/instruction/mod.rs b/sdk-libs/sdk/src/instruction/mod.rs index 49cd82bd60..69745da9ce 100644 --- a/sdk-libs/sdk/src/instruction/mod.rs +++ b/sdk-libs/sdk/src/instruction/mod.rs @@ -176,6 +176,9 @@ mod pack_accounts; mod system_accounts; mod tree_info; +/// Borsh compatible validity proof implementation. Proves the validity of +/// existing compressed accounts and new addresses. +pub use light_compressed_account::instruction_data::compressed_proof::borsh_compat; /// Zero-knowledge proof to prove the validity of existing compressed accounts and new addresses. pub use light_compressed_account::instruction_data::compressed_proof::ValidityProof; pub use light_sdk_types::instruction::*; diff --git a/sdk-libs/sdk/src/token.rs b/sdk-libs/sdk/src/token.rs index 2edf2311d6..0b1af95c15 100644 --- a/sdk-libs/sdk/src/token.rs +++ b/sdk-libs/sdk/src/token.rs @@ -2,7 +2,7 @@ use light_compressed_account::compressed_account::CompressedAccountWithMerkleCon use crate::{AnchorDeserialize, AnchorSerialize, Pubkey}; -#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Default)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, AnchorDeserialize, AnchorSerialize, Default)] #[repr(u8)] pub enum AccountState { #[default] @@ -10,7 +10,7 @@ pub enum AccountState { Frozen, } // TODO: extract token data from program into into a separate crate, import it and remove this file. -#[derive(Debug, PartialEq, Eq, AnchorDeserialize, AnchorSerialize, Clone, Default)] +#[derive(Debug, PartialEq, Eq, Hash, AnchorDeserialize, AnchorSerialize, Clone, Default)] pub struct TokenData { /// The mint associated with this account pub mint: Pubkey, diff --git a/sdk-libs/sdk/src/utils.rs b/sdk-libs/sdk/src/utils.rs index 70dea91527..0639b3dfc3 100644 --- a/sdk-libs/sdk/src/utils.rs +++ b/sdk-libs/sdk/src/utils.rs @@ -1,3 +1,5 @@ +use solana_pubkey::Pubkey; + #[allow(unused_imports)] use crate::constants::CPI_AUTHORITY_PDA_SEED; #[macro_export] @@ -6,3 +8,18 @@ macro_rules! find_cpi_signer_macro { Pubkey::find_program_address([CPI_AUTHORITY_PDA_SEED].as_slice(), $program_id) }; } + +pub fn get_light_cpi_signer_seeds(program_id: &Pubkey) -> (Vec>, Pubkey) { + let seeds = &[b"cpi_authority".as_slice()]; + + // Compute the PDA at compile time + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); + + let token_signer_seeds_bump = bump; + + let token_signer_seeds: Vec> = vec![ + CPI_AUTHORITY_PDA_SEED.to_vec(), + vec![token_signer_seeds_bump], + ]; + (token_signer_seeds, pda) +}