diff --git a/js/compressed-token/package.json b/js/compressed-token/package.json index 664f916e91..0434f5aa43 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,14 @@ "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", "@solana/spl-token": "0.4.8", - "@solana/web3.js": "1.98.4", + "@solana/web3.js": "1.98.0", "@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..9311caa058 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,12 @@ "@rollup/plugin-replace": "^5.0.7", "@rollup/plugin-terser": "^0.4.4", "@rollup/plugin-typescript": "^11.1.6", - "@solana/web3.js": "1.98.4", + "@solana/web3.js": "1.98.0", "@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..b3295e46e7 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 @@ -1,7 +1,7 @@ import { PublicKey } from '@solana/web3.js'; import { getParsedEvents } from './get-parsed-events'; import BN from 'bn.js'; -import { COMPRESSED_TOKEN_PROGRAM_ID, featureFlags } from '../../constants'; +import { CTOKEN_PROGRAM_ID, featureFlags } from '../../constants'; import { Rpc } from '../../rpc'; import { getStateTreeInfoByPubkey } from '../../utils/get-state-tree-infos'; import { ParsedTokenAccount, WithCursor } from '../../rpc-interface'; @@ -54,7 +54,7 @@ export type EventWithParsedTokenTlvData = { */ export function parseTokenLayoutWithIdl( compressedAccount: CompressedAccountLegacy, - programId: PublicKey = COMPRESSED_TOKEN_PROGRAM_ID, + programId: PublicKey = CTOKEN_PROGRAM_ID, ): TokenData | null { if (compressedAccount.data === null) return null; @@ -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..7dcf62bf5a 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 { + CTOKEN_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; @@ -951,4 +959,31 @@ export class TestRpc extends Connection implements CompressionApiInterface { newAddresses.map(address => address.address), ); } + + async getCompressibleAccountInfo( + _address: PublicKey, + _programId: PublicKey, + _addressTreeInfo: TreeInfo, + ): Promise<{ + accountInfo: AccountInfo; + isCompressed: boolean; + merkleContext?: MerkleContext; + } | null> { + throw new Error( + 'getCompressibleAccountInfo not implemented in test-rpc', + ); + } + async getCompressibleTokenAccount( + _address: PublicKey, + _tokenProgramId: PublicKey = CTOKEN_PROGRAM_ID, + ): Promise<{ + accountInfo: AccountInfo; + parsed: TokenData; + isCompressed: boolean; + merkleContext?: MerkleContext; + } | null> { + throw new Error( + 'getCompressibleTokenAccount not implemented in test-rpc', + ); + } } 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/js/stateless.js/tests/e2e/layout.test.ts b/js/stateless.js/tests/e2e/layout.test.ts index a0a5ef4bad..230bac157f 100644 --- a/js/stateless.js/tests/e2e/layout.test.ts +++ b/js/stateless.js/tests/e2e/layout.test.ts @@ -17,7 +17,7 @@ import { import { PublicTransactionEvent } from '../../src/state'; import { - COMPRESSED_TOKEN_PROGRAM_ID, + CTOKEN_PROGRAM_ID, defaultStaticAccountsStruct, IDL, LightSystemProgramIDL, @@ -35,7 +35,7 @@ const getTestProgram = (): Program => { }, ); setProvider(mockProvider); - return new Program(IDL, COMPRESSED_TOKEN_PROGRAM_ID, mockProvider); + return new Program(IDL, CTOKEN_PROGRAM_ID, mockProvider); }; function deepEqual(ref: any, val: any) { 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/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/ctoken-types/src/instructions/mint_action/cpi_context.rs b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs new file mode 100644 index 0000000000..1f91314fdd --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/mint_action/cpi_context.rs @@ -0,0 +1,55 @@ +use light_compressed_account::instruction_data::zero_copy_set::CompressedCpiContextTrait; +use light_zero_copy::{ZeroCopy, ZeroCopyMut}; + +use crate::{AnchorDeserialize, AnchorSerialize}; + +#[repr(C)] +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut, PartialEq)] +pub struct CpiContext { + pub set_context: bool, + pub first_set_context: bool, + // Used as address tree index if create mint + pub in_tree_index: u8, + pub in_queue_index: u8, + pub out_queue_index: u8, + pub token_out_queue_index: u8, + // Index of the compressed account that should receive the new address (0 = mint, 1+ = token accounts) + pub assigned_account_index: u8, +} + +impl CompressedCpiContextTrait for ZCpiContext<'_> { + fn first_set_context(&self) -> u8 { + if self.first_set_context == 0 { + 0 + } else { + 1 + } + } + + fn set_context(&self) -> u8 { + if self.set_context == 0 { + 0 + } else { + 1 + } + } +} + +impl CpiContext { + /// Specific helper for creating a cmint as last use of cpi context. + pub fn last_cpi_create_mint( + address_tree_index: u8, + output_state_queue_index: u8, + mint_account_index: u8, + ) -> Self { + Self { + set_context: false, + first_set_context: false, + in_tree_index: address_tree_index, + in_queue_index: 0, // unused + out_queue_index: output_state_queue_index, + token_out_queue_index: output_state_queue_index, + assigned_account_index: mint_account_index, + } + } +} diff --git a/program-libs/ctoken-types/src/instructions/transfer2/compression.rs b/program-libs/ctoken-types/src/instructions/transfer2/compression.rs new file mode 100644 index 0000000000..50018dc9f8 --- /dev/null +++ b/program-libs/ctoken-types/src/instructions/transfer2/compression.rs @@ -0,0 +1,252 @@ +use std::fmt::Debug; + +use light_zero_copy::{ + errors::ZeroCopyError, traits::ZeroCopyAtMut, ZeroCopy, ZeroCopyMut, ZeroCopyNew, +}; +use zerocopy::Ref; + +use crate::{AnchorDeserialize, AnchorSerialize, CTokenError}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy)] +#[repr(C)] +pub enum CompressionMode { + Compress, + Decompress, + /// Compresses ctoken account and closes it + /// Signer must be owner or rent authority, if rent authority ctoken account must be compressible + /// Not implemented for spl token accounts. + CompressAndClose, +} + +pub const COMPRESS: u8 = 0u8; +pub const DECOMPRESS: u8 = 1u8; +pub const COMPRESS_AND_CLOSE: u8 = 2u8; + +impl<'a> ZeroCopyAtMut<'a> for CompressionMode { + type ZeroCopyAtMut = Ref<&'a mut [u8], u8>; + fn zero_copy_at_mut( + bytes: &'a mut [u8], + ) -> Result<(Self::ZeroCopyAtMut, &'a mut [u8]), ZeroCopyError> { + let (mode, bytes) = zerocopy::Ref::<&mut [u8], u8>::from_prefix(bytes)?; + + Ok((mode, bytes)) + } +} + +impl<'a> ZeroCopyNew<'a> for CompressionMode { + type ZeroCopyConfig = (); + type Output = Ref<&'a mut [u8], u8>; + + fn byte_len(_config: &Self::ZeroCopyConfig) -> Result { + Ok(1) // CompressionMode enum size is always 1 byte + } + + fn new_zero_copy( + bytes: &'a mut [u8], + _config: Self::ZeroCopyConfig, + ) -> Result<(Self::Output, &'a mut [u8]), ZeroCopyError> { + let (mode, remaining_bytes) = zerocopy::Ref::<&mut [u8], u8>::from_prefix(bytes)?; + + Ok((mode, remaining_bytes)) + } +} + +#[repr(C)] +#[derive( + Clone, Copy, Debug, PartialEq, Eq, AnchorSerialize, AnchorDeserialize, ZeroCopy, ZeroCopyMut, +)] +pub struct Compression { + pub mode: CompressionMode, + pub amount: u64, + pub mint: u8, + pub source_or_recipient: u8, + pub authority: u8, // Index of owner or delegate account + /// pool account index for spl token Compression/Decompression + /// rent_sponsor_index for CompressAndClose + pub pool_account_index: u8, // This account is not necessary to decompress ctokens because there are no token pools + /// pool index for spl token Compression/Decompression + /// compressed account index for CompressAndClose + pub pool_index: u8, // This account is not necessary to decompress ctokens because there are no token pools + pub bump: u8, // This account is not necessary to decompress ctokens because there are no token pools +} + +impl ZCompression<'_> { + pub fn get_rent_sponsor_index(&self) -> Result { + match self.mode { + ZCompressionMode::CompressAndClose => Ok(self.pool_account_index), + _ => Err(CTokenError::InvalidCompressionMode), + } + } + pub fn get_compressed_token_account_index(&self) -> Result { + match self.mode { + ZCompressionMode::CompressAndClose => Ok(self.pool_index), + _ => Err(CTokenError::InvalidCompressionMode), + } + } + pub fn get_destination_index(&self) -> Result { + match self.mode { + ZCompressionMode::CompressAndClose => Ok(self.bump), + _ => Err(CTokenError::InvalidCompressionMode), + } + } +} + +impl Compression { + pub fn compress_and_close( + amount: u64, + mint: u8, + recipient_index: u8, + authority: u8, + rent_sponsor_index: u8, + compressed_account_index: u8, + destination_index: u8, + ) -> Self { + Compression { + amount, // the full balance of the ctoken account to be compressed + mode: CompressionMode::CompressAndClose, + mint, + source_or_recipient: recipient_index, + authority, + pool_account_index: rent_sponsor_index, + pool_index: compressed_account_index, + bump: destination_index, + } + } + pub fn compress(amount: u64, mint: u8, source_or_recipient: u8, authority: u8) -> Self { + Compression { + amount, + mode: CompressionMode::Compress, + mint, + source_or_recipient, + authority, + pool_account_index: 0, + pool_index: 0, + bump: 0, + } + } + pub fn compress_spl( + amount: u64, + mint: u8, + source_or_recipient: u8, + authority: u8, + pool_account_index: u8, + pool_index: u8, + bump: u8, + ) -> Self { + Compression { + amount, + mode: CompressionMode::Compress, + mint, + source_or_recipient, + authority, + pool_account_index, + pool_index, + bump, + } + } + pub fn compress_ctoken(amount: u64, mint: u8, source_or_recipient: u8, authority: u8) -> Self { + Compression { + amount, + mode: CompressionMode::Compress, + mint, + source_or_recipient, + authority, + pool_account_index: 0, + pool_index: 0, + bump: 0, + } + } + pub fn decompress(amount: u64, mint: u8, source_or_recipient: u8) -> Self { + Compression { + amount, + mode: CompressionMode::Decompress, + mint, + source_or_recipient, + authority: 0, + pool_account_index: 0, + pool_index: 0, + bump: 0, + } + } + pub fn decompress_spl( + amount: u64, + mint: u8, + source_or_recipient: u8, + pool_account_index: u8, + pool_index: u8, + bump: u8, + ) -> Self { + Compression { + amount, + mode: CompressionMode::Decompress, + mint, + source_or_recipient, + authority: 0, + pool_account_index, + pool_index, + bump, + } + } + + pub fn decompress_ctoken(amount: u64, mint: u8, source_or_recipient: u8) -> Self { + Compression { + amount, + mode: CompressionMode::Decompress, + mint, + source_or_recipient, + authority: 0, + pool_account_index: 0, + pool_index: 0, + bump: 0, + } + } +} + +impl ZCompressionMut<'_> { + pub fn mode(&self) -> Result { + match *self.mode { + COMPRESS => Ok(CompressionMode::Compress), + DECOMPRESS => Ok(CompressionMode::Decompress), + COMPRESS_AND_CLOSE => Ok(CompressionMode::CompressAndClose), + _ => Err(CTokenError::InvalidCompressionMode), + } + } +} + +impl ZCompression<'_> { + pub fn new_balance_compressed_account(&self, current_balance: u64) -> Result { + let new_balance = match self.mode { + ZCompressionMode::Compress | ZCompressionMode::CompressAndClose => { + // Compress: add to balance (tokens are being added to compressed pool) + current_balance + .checked_add((*self.amount).into()) + .ok_or(CTokenError::ArithmeticOverflow) + } + ZCompressionMode::Decompress => { + // Decompress: subtract from balance (tokens are being removed from compressed pool) + current_balance + .checked_sub((*self.amount).into()) + .ok_or(CTokenError::CompressInsufficientFunds) + } + }?; + Ok(new_balance) + } + + pub fn new_balance_solana_account(&self, current_balance: u64) -> Result { + let new_balance = match self.mode { + ZCompressionMode::Compress | ZCompressionMode::CompressAndClose => { + // Compress: add to balance (tokens are being added to compressed pool) + current_balance + .checked_sub((*self.amount).into()) + .ok_or(CTokenError::InsufficientSupply) + } + ZCompressionMode::Decompress => { + // Decompress: subtract from balance (tokens are being removed from compressed pool) + current_balance + .checked_add((*self.amount).into()) + .ok_or(CTokenError::ArithmeticOverflow) + } + }?; + Ok(new_balance) + } +} 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..d2686d640d 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, @@ -21,14 +21,14 @@ pub trait Indexer: std::marker::Send + std::marker::Sync { &self, address: Address, config: Option, - ) -> Result>, IndexerError>; + ) -> Result, IndexerError>; /// Returns the compressed account with the given address or hash. async fn get_compressed_account_by_hash( &self, hash: Hash, config: Option, - ) -> Result>, IndexerError>; + ) -> Result, IndexerError>; /// Returns the owner’s compressed accounts. async fn get_compressed_accounts_by_owner( @@ -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( @@ -153,7 +153,7 @@ pub trait Indexer: std::marker::Send + std::marker::Sync { addresses: Option>, hashes: Option>, config: Option, - ) -> Result>>, IndexerError>; + ) -> Result>, IndexerError>; /// Returns proofs that the new addresses are not taken already and can be created. async fn get_multiple_new_address_proofs( 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..644e8a0018 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::{ @@ -179,7 +182,7 @@ impl Indexer for PhotonIndexer { &self, address: Address, config: Option, - ) -> Result>, IndexerError> { + ) -> Result, IndexerError> { let config = config.unwrap_or_default(); self.retry(config.retry_config, || async { let params = self.build_account_params(Some(address), None)?; @@ -201,10 +204,11 @@ impl Indexer for PhotonIndexer { if api_response.context.slot < config.slot { return Err(IndexerError::IndexerNotSyncedToSlot); } - let account = match api_response.value { - Some(boxed) => Some(CompressedAccount::try_from(&*boxed)?), - None => None, - }; + let account_data = api_response + .value + .ok_or(IndexerError::AccountNotFound) + .map(|boxed| *boxed)?; + let account = CompressedAccount::try_from(&account_data)?; Ok(Response { context: Context { @@ -220,7 +224,7 @@ impl Indexer for PhotonIndexer { &self, hash: Hash, config: Option, - ) -> Result>, IndexerError> { + ) -> Result, IndexerError> { let config = config.unwrap_or_default(); self.retry(config.retry_config, || async { let params = self.build_account_params(None, Some(hash))?; @@ -242,10 +246,11 @@ impl Indexer for PhotonIndexer { if api_response.context.slot < config.slot { return Err(IndexerError::IndexerNotSyncedToSlot); } - let account = match api_response.value { - Some(boxed) => Some(CompressedAccount::try_from(&*boxed)?), - None => None, - }; + let account_data = api_response + .value + .ok_or(IndexerError::AccountNotFound) + .map(|boxed| *boxed)?; + let account = CompressedAccount::try_from(&account_data)?; Ok(Response { context: Context { @@ -542,7 +547,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 +579,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -618,7 +623,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -642,7 +647,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 +680,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -726,7 +731,7 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(TokenAccount::try_from) + .map(CompressedTokenAccount::try_from) .collect(); let cursor = response.value.cursor; @@ -1207,7 +1212,7 @@ impl Indexer for PhotonIndexer { addresses: Option>, hashes: Option>, config: Option, - ) -> Result>>, IndexerError> { + ) -> Result>, IndexerError> { let config = config.unwrap_or_default(); self.retry(config.retry_config, || async { let hashes = hashes.clone(); @@ -1240,11 +1245,8 @@ impl Indexer for PhotonIndexer { .value .items .iter() - .map(|account_opt| match account_opt { - Some(account) => CompressedAccount::try_from(account).map(Some), - None => Ok(None), - }) - .collect::>, IndexerError>>()?; + .map(CompressedAccount::try_from) + .collect::, IndexerError>>()?; Ok(Response { context: Context { 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..c3cd5de17d 100644 --- a/sdk-libs/client/src/indexer/types.rs +++ b/sdk-libs/client/src/indexer/types.rs @@ -383,7 +383,7 @@ impl ValidityProofWithContext { } } -#[derive(Clone, Copy, Default, Debug, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, Default, Debug, PartialEq)] pub struct NextTreeInfo { pub cpi_context: Option, pub queue: Pubkey, @@ -432,7 +432,7 @@ impl TryFrom<&photon_api::models::TreeContextInfo> for NextTreeInfo { } } -#[derive(Clone, Copy, Default, Debug, PartialEq)] +#[derive(Clone, Copy, Hash, Eq, Default, Debug, PartialEq)] pub struct TreeInfo { pub cpi_context: Option, pub next_tree_info: Option, @@ -498,7 +498,7 @@ impl TreeInfo { } } -#[derive(Clone, Default, Debug, PartialEq)] +#[derive(Clone, Default, Eq, Hash, Debug, PartialEq)] pub struct CompressedAccount { pub address: Option<[u8; 32]>, pub data: Option, @@ -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 @@ -714,15 +732,15 @@ pub struct AddressMerkleTreeAccounts { pub queue: Pubkey, } -#[derive(Clone, Default, Debug, PartialEq)] -pub struct TokenAccount { +#[derive(Clone, Default, Eq, Hash, Debug, PartialEq)] +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..25d2de1692 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 { 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/indexer.rs b/sdk-libs/client/src/rpc/indexer.rs index fbcba6c5ab..1a9c764e68 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] @@ -67,7 +68,7 @@ impl Indexer for LightClient { &self, address: Address, config: Option, - ) -> Result>, IndexerError> { + ) -> Result, IndexerError> { Ok(self .indexer .as_ref() @@ -80,7 +81,7 @@ impl Indexer for LightClient { &self, hash: Hash, config: Option, - ) -> Result>, IndexerError> { + ) -> Result, IndexerError> { Ok(self .indexer .as_ref() @@ -94,7 +95,7 @@ impl Indexer for LightClient { owner: &Pubkey, options: Option, config: Option, - ) -> Result>, IndexerError> { + ) -> Result>, IndexerError> { Ok(self .indexer .as_ref() @@ -136,7 +137,7 @@ impl Indexer for LightClient { addresses: Option>, hashes: 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/lookup_table.rs b/sdk-libs/client/src/rpc/lookup_table.rs new file mode 100644 index 0000000000..e1adfe9872 --- /dev/null +++ b/sdk-libs/client/src/rpc/lookup_table.rs @@ -0,0 +1,37 @@ +pub use solana_address_lookup_table_interface::{ + error, instruction, program, state::AddressLookupTable, +}; +use solana_message::AddressLookupTableAccount; +use solana_pubkey::Pubkey; +use solana_rpc_client::rpc_client::RpcClient; + +use crate::rpc::errors::RpcError; + +/// Gets a lookup table account state from the network. +/// +/// # Arguments +/// +/// * `client` - The RPC client to use to get the lookup table account state. +/// * `lookup_table_address` - The address of the lookup table account to get. +/// +/// # Returns +/// +/// * `AddressLookupTableAccount` - The lookup table account state. +pub fn load_lookup_table( + client: &RpcClient, + lookup_table_address: &Pubkey, +) -> Result { + let raw_account = client.get_account(lookup_table_address)?; + let address_lookup_table = AddressLookupTable::deserialize(&raw_account.data).map_err(|e| { + RpcError::CustomError(format!("Failed to deserialize AddressLookupTable: {e:?}")) + })?; + let address_lookup_table_account = AddressLookupTableAccount { + key: lookup_table_address.to_bytes().into(), + addresses: address_lookup_table + .addresses + .iter() + .map(|p| p.to_bytes().into()) + .collect(), + }; + Ok(address_lookup_table_account) +} diff --git a/sdk-libs/client/src/rpc/mod.rs b/sdk-libs/client/src/rpc/mod.rs index 0b968c26c5..19a2e67f40 100644 --- a/sdk-libs/client/src/rpc/mod.rs +++ b/sdk-libs/client/src/rpc/mod.rs @@ -1,8 +1,7 @@ -#![allow(clippy::result_large_err)] - pub mod client; pub mod errors; pub mod indexer; +pub mod lookup_table; pub mod merkle_tree; mod rpc_trait; pub mod state; @@ -11,3 +10,4 @@ pub use client::{LightClient, RetryConfig}; pub use errors::RpcError; pub use rpc_trait::{LightClientConfig, Rpc}; pub mod get_light_state_tree_infos; +pub use lookup_table::load_lookup_table; diff --git a/sdk-libs/client/src/rpc/rpc_trait.rs b/sdk-libs/client/src/rpc/rpc_trait.rs index 900ed08bab..e78ae84c67 100644 --- a/sdk-libs/client/src/rpc/rpc_trait.rs +++ b/sdk-libs/client/src/rpc/rpc_trait.rs @@ -14,6 +14,7 @@ use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_rpc_client_api::config::RpcSendTransactionConfig; use solana_signature::Signature; +use solana_signer::Signer; use solana_transaction::Transaction; use solana_transaction_status_client_types::TransactionStatus; @@ -58,7 +59,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 +84,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 +171,21 @@ 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| { + println!( + "Provided signers: {:?}", + signers.iter().map(|s| s.pubkey()).collect::>() + ); + + 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 } diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs new file mode 100644 index 0000000000..4878e90e1e --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer/account_infos.rs @@ -0,0 +1,113 @@ +use arrayvec::ArrayVec; +use solana_account_info::AccountInfo; +use solana_instruction::Instruction; +use solana_msg::msg; + +use crate::{account::CTokenAccount, error::Result}; + +pub const MAX_ACCOUNT_INFOS: usize = 20; + +// TODO: test with delegate +// For pinocchio we will need to build the accounts in oder +// The easiest is probably just pass the accounts multiple times since deserialization is zero copy. +pub struct TransferAccountInfos<'a, 'info, const N: usize = MAX_ACCOUNT_INFOS> { + pub fee_payer: &'a AccountInfo<'info>, + pub authority: &'a AccountInfo<'info>, + pub ctoken_accounts: &'a [AccountInfo<'info>], + pub cpi_context: Option<&'a AccountInfo<'info>>, + // TODO: rename tree accounts to packed accounts + pub packed_accounts: &'a [AccountInfo<'info>], +} + +impl<'info, const N: usize> TransferAccountInfos<'_, 'info, N> { + // 874 with std::vec + // 722 with array vec + pub fn into_account_infos(self) -> ArrayVec, N> { + let mut capacity = 2 + self.ctoken_accounts.len() + self.packed_accounts.len(); + let ctoken_program_id_index = self.ctoken_accounts.len() - 2; + if self.cpi_context.is_some() { + capacity += 1; + } + + // Check if capacity exceeds ArrayVec limit + if capacity > N { + panic!("Account infos capacity {} exceeds limit {}", capacity, N); + } + + let mut account_infos = ArrayVec::, N>::new(); + account_infos.push(self.fee_payer.clone()); + account_infos.push(self.authority.clone()); + + // Add ctoken accounts + for account in self.ctoken_accounts { + account_infos.push(account.clone()); + } + + if let Some(cpi_context) = self.cpi_context { + account_infos.push(cpi_context.clone()); + } else { + account_infos.push(self.ctoken_accounts[ctoken_program_id_index].clone()); + } + + // Add tree accounts + for account in self.packed_accounts { + account_infos.push(account.clone()); + } + + account_infos + } + + // 1528 + pub fn into_account_infos_checked( + self, + ix: &Instruction, + ) -> Result, N>> { + let account_infos = self.into_account_infos(); + for (account_meta, account_info) in ix.accounts.iter().zip(account_infos.iter()) { + if account_meta.pubkey != *account_info.key { + msg!("account info and meta don't match."); + msg!("account meta {:?}", account_meta); + msg!("account info {:?}", account_info); + + msg!("account metas {:?}", ix.accounts); + msg!("account infos {:?}", account_infos); + panic!("account info and meta don't match."); + } + } + Ok(account_infos) + } +} + +// Note: maybe it is not useful for removing accounts results in loss of order +// other than doing [..end] so let's just do that in the first place. +// TODO: test +/// Filter packed accounts for accounts necessary for token accounts. +/// Note accounts still need to be in the correct order. +pub fn filter_packed_accounts<'info>( + token_accounts: &[&CTokenAccount], + account_infos: &[AccountInfo<'info>], +) -> Vec> { + let mut selected_account_infos = Vec::with_capacity(account_infos.len()); + account_infos + .iter() + .enumerate() + .filter(|(i, _)| { + let i = *i as u8; + token_accounts.iter().any(|y| { + y.merkle_tree_index == i + || y.input_metas().iter().any(|z| { + z.packed_tree_info.merkle_tree_pubkey_index == i + || z.packed_tree_info.queue_pubkey_index == i + || { + if let Some(delegate_index) = z.delegate_index { + delegate_index == i + } else { + false + } + } + }) + }) + }) + .for_each(|x| selected_account_infos.push(x.1.clone())); + selected_account_infos +} diff --git a/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs new file mode 100644 index 0000000000..f8c4d57273 --- /dev/null +++ b/sdk-libs/compressed-token-sdk/src/instructions/transfer2/account_metas.rs @@ -0,0 +1,143 @@ +use light_compressed_token_types::CPI_AUTHORITY_PDA; +use light_sdk::constants::LIGHT_SYSTEM_PROGRAM_ID; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; + +use crate::instructions::CTokenDefaultAccounts; + +/// Account metadata configuration for compressed token multi-transfer instructions +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Transfer2AccountsMetaConfig { + pub fee_payer: Option, + pub sol_pool_pda: Option, + pub sol_decompression_recipient: Option, + pub cpi_context: Option, + pub with_sol_pool: bool, + pub decompressed_accounts_only: bool, + pub packed_accounts: Option>, // TODO: check whether this can ever be None +} + +impl Transfer2AccountsMetaConfig { + pub fn new(fee_payer: Pubkey, packed_accounts: Vec) -> Self { + Self { + fee_payer: Some(fee_payer), + decompressed_accounts_only: false, + sol_pool_pda: None, + sol_decompression_recipient: None, + cpi_context: None, + with_sol_pool: false, + packed_accounts: Some(packed_accounts), + } + } + pub fn new_with_cpi_context( + fee_payer: Pubkey, + packed_accounts: Vec, + cpi_context: Pubkey, + ) -> Self { + Self { + fee_payer: Some(fee_payer), + decompressed_accounts_only: false, + sol_pool_pda: None, + sol_decompression_recipient: None, + cpi_context: Some(cpi_context), + with_sol_pool: false, + packed_accounts: Some(packed_accounts), + } + } + pub fn new_decompressed_accounts_only( + fee_payer: Pubkey, + packed_accounts: Vec, + ) -> Self { + Self { + fee_payer: Some(fee_payer), + sol_pool_pda: None, + sol_decompression_recipient: None, + cpi_context: None, + with_sol_pool: false, + decompressed_accounts_only: true, + packed_accounts: Some(packed_accounts), + } + } +} + +/// Get the standard account metas for a compressed token multi-transfer instruction +#[inline(never)] +pub fn get_transfer2_instruction_account_metas( + config: Transfer2AccountsMetaConfig, +) -> Vec { + let default_pubkeys = CTokenDefaultAccounts::default(); + + let packed_accounts_len = if let Some(packed_accounts) = config.packed_accounts.as_ref() { + packed_accounts.len() + } else { + 0 + }; + + // Build the account metas following the order expected by Transfer2ValidatedAccounts + let mut metas = Vec::with_capacity(10 + packed_accounts_len); + if !config.decompressed_accounts_only { + metas.push(AccountMeta::new_readonly( + Pubkey::new_from_array(LIGHT_SYSTEM_PROGRAM_ID), + false, + )); + // Add fee payer and authority if provided (for direct invoke) + if let Some(fee_payer) = config.fee_payer { + metas.push(AccountMeta::new(fee_payer, true)); + } + + // Core system accounts (always present) + metas.extend([ + AccountMeta::new_readonly(Pubkey::new_from_array(CPI_AUTHORITY_PDA), false), + // registered_program_pda + AccountMeta::new_readonly(default_pubkeys.registered_program_pda, false), + // account_compression_authority + AccountMeta::new_readonly(default_pubkeys.account_compression_authority, false), + // account_compression_program + AccountMeta::new_readonly(default_pubkeys.account_compression_program, false), + ]); + + // system_program (always present) + metas.push(AccountMeta::new_readonly( + default_pubkeys.system_program, + false, + )); + + // Optional sol pool accounts + if config.with_sol_pool { + if let Some(sol_pool_pda) = config.sol_pool_pda { + metas.push(AccountMeta::new(sol_pool_pda, false)); + } + if let Some(sol_decompression_recipient) = config.sol_decompression_recipient { + metas.push(AccountMeta::new(sol_decompression_recipient, false)); + } + } + if let Some(cpi_context) = config.cpi_context { + metas.push(AccountMeta::new(cpi_context, false)); + } + } else if config.cpi_context.is_some() || config.with_sol_pool { + // TODO: replace with error + unimplemented!( + "config.cpi_context.is_some() {}, config.with_sol_pool {} must both be false", + config.cpi_context.is_some(), + config.with_sol_pool + ); + } else { + // For decompressed accounts only, add compressions_only_cpi_authority_pda first + metas.push(AccountMeta::new_readonly( + Pubkey::new_from_array(CPI_AUTHORITY_PDA), + false, + )); + // Then add compressions_only_fee_payer if provided + if let Some(fee_payer) = config.fee_payer { + metas.push(AccountMeta::new(fee_payer, true)); + } + } + // always add packed accounts + if let Some(packed_accounts) = config.packed_accounts.as_ref() { + for account in packed_accounts { + metas.push(account.clone()); + } + } + + metas +} diff --git a/sdk-libs/sdk-types/Cargo.toml b/sdk-libs/sdk-types/Cargo.toml index a4a2d34eba..a436dc4a87 100644 --- a/sdk-libs/sdk-types/Cargo.toml +++ b/sdk-libs/sdk-types/Cargo.toml @@ -9,7 +9,6 @@ description = "Core types for Light Protocol SDK" [features] anchor = ["anchor-lang", "light-compressed-account/anchor"] v2 = [] -v2_ix = [] [dependencies] anchor-lang = { workspace = true, optional = true } diff --git a/sdk-libs/sdk-types/src/constants.rs b/sdk-libs/sdk-types/src/constants.rs index 80e36ab550..47222da986 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 CTOKEN_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_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..f73fda0450 100644 --- a/sdk-libs/sdk-types/src/lib.rs +++ b/sdk-libs/sdk-types/src/lib.rs @@ -1,8 +1,9 @@ pub mod address; pub mod constants; pub mod cpi_accounts; -#[cfg(feature = "v2_ix")] -pub mod cpi_accounts_v2; +#[cfg(feature = "v2")] +pub mod cpi_accounts_small; +pub mod cpi_context_write; pub mod error; pub mod instruction; @@ -13,9 +14,10 @@ use anchor_lang::{AnchorDeserialize, AnchorSerialize}; use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; pub use constants::*; pub use cpi_accounts::*; -#[cfg(feature = "v2_ix")] -pub use cpi_accounts_v2::{ - CompressionCpiAccountIndexV2, CpiAccountsV2, PROGRAM_ACCOUNTS_LEN, V2_SYSTEM_ACCOUNTS_LEN, +#[cfg(feature = "v2")] +pub use cpi_accounts_small::{ + CompressionCpiAccountIndexSmall, CpiAccountsSmall, PROGRAM_ACCOUNTS_LEN, + SMALL_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/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 39796a8da0..245b520aa6 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -1,16 +1,25 @@ 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; 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, to_account_metas_small, CpiAccounts, CpiAccountsSmall, + CpiInstructionConfig, + }, error::{LightSdkError, Result}, instruction::{account_info::CompressedAccountInfoTrait, ValidityProof}, invoke_signed, AccountInfo, AnchorSerialize, Instruction, @@ -20,14 +29,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 +92,217 @@ 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) } + + 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) + } +} + +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 +384,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..6461d83abd 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,18 +42,18 @@ //! ); //! //! cpi_inputs -//! .invoke_light_system_program(light_cpi_accounts) -//! .map_err(ProgramError::from)?; +//! .invoke_light_system_program_small(light_cpi_accounts)?; //! ``` mod accounts; -#[cfg(feature = "v2_ix")] -mod accounts_v2_ix; +mod accounts_cpi_context; +#[cfg(feature = "v2")] +mod accounts_small_ix; mod invoke; pub use accounts::*; -#[cfg(feature = "v2_ix")] -pub use accounts_v2_ix::*; +#[cfg(feature = "v2")] +pub use accounts_small_ix::*; pub use invoke::*; /// Derives cpi signer and bump to invoke the light system program at compile time. pub use light_sdk_macros::derive_light_cpi_signer; 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/utils.rs b/sdk-libs/sdk/src/utils.rs index 70dea91527..0d954b1928 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], + ]; + return (token_signer_seeds, pda); +}