diff --git a/.cargo/config.toml b/.cargo/config.toml index c00d2a43f4..018fc87b4b 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,6 +1,9 @@ [alias] xtask = "run --package xtask --" +[resolver] +incompatible-rust-versions = "fallback" + # On Windows # ``` # cargo install -f cargo-binutils diff --git a/Cargo.lock b/Cargo.lock index e465ced71e..8ce880c0c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1633,41 +1633,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "csdk-anchor-derived-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", - "bincode", - "borsh 0.10.4", - "light-client", - "light-compressed-account", - "light-compressible", - "light-compressible-client", - "light-hasher", - "light-macros", - "light-program-test", - "light-sdk", - "light-sdk-macros", - "light-sdk-types", - "light-test-utils", - "light-token-client", - "light-token-interface", - "light-token-sdk", - "light-token-types", - "solana-account", - "solana-instruction", - "solana-keypair", - "solana-logger", - "solana-program", - "solana-pubkey 2.4.0", - "solana-sdk", - "solana-signature", - "solana-signer", - "tokio", -] - [[package]] name = "csdk-anchor-full-derived-test" version = "0.1.0" @@ -3701,6 +3666,7 @@ dependencies = [ "light-heap", "light-macros", "light-program-profiler", + "light-sdk-types", "light-zero-copy", "pinocchio", "pinocchio-pubkey", @@ -3720,10 +3686,17 @@ dependencies = [ "anchor-lang", "borsh 0.10.4", "light-client", + "light-compressed-account", + "light-compressible", "light-sdk", + "light-token-interface", + "light-token-sdk", "solana-account", "solana-instruction", + "solana-program", + "solana-program-error 2.2.2", "solana-pubkey 2.4.0", + "spl-token-2022 7.0.0", "thiserror 2.0.17", ] @@ -6038,41 +6011,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "sdk-compressible-test" -version = "0.1.0" -dependencies = [ - "anchor-lang", - "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=d8a2b3d9)", - "bincode", - "borsh 0.10.4", - "light-client", - "light-compressed-account", - "light-compressible", - "light-compressible-client", - "light-hasher", - "light-macros", - "light-program-test", - "light-sdk", - "light-sdk-types", - "light-test-utils", - "light-token-client", - "light-token-interface", - "light-token-sdk", - "light-token-types", - "solana-account", - "solana-instruction", - "solana-keypair", - "solana-logger", - "solana-program", - "solana-pubkey 2.4.0", - "solana-sdk", - "solana-signature", - "solana-signer", - "solana-system-interface 1.0.0", - "tokio", -] - [[package]] name = "sdk-light-token-test" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 7a715a3dc1..a498d7a733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,9 +55,7 @@ members = [ "sdk-tests/sdk-native-test", "sdk-tests/sdk-v1-native-test", "sdk-tests/sdk-token-test", - "sdk-tests/sdk-compressible-test", "sdk-tests/sdk-light-token-test", - "sdk-tests/csdk-anchor-derived-test", "sdk-tests/csdk-anchor-full-derived-test", "forester-utils", "forester", diff --git a/ctoken_for_payments.md b/ctoken_for_payments.md index 696c6e5935..2cf501e8b3 100644 --- a/ctoken_for_payments.md +++ b/ctoken_for_payments.md @@ -76,7 +76,7 @@ import { createAssociatedTokenAccountInterfaceIdempotentInstruction, getAssociatedTokenAddressInterface, } from "@lightprotocol/compressed-token/unified"; -import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; +import { LIGHT_TOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; const ata = getAssociatedTokenAddressInterface(mint, recipient); @@ -86,7 +86,7 @@ const tx = new Transaction().add( ata, recipient, mint, - CTOKEN_PROGRAM_ID + LIGHT_TOKEN_PROGRAM_ID ) ); ``` @@ -203,7 +203,7 @@ import { getAssociatedTokenAddressInterface, createAssociatedTokenAccountInterfaceIdempotentInstruction, } from "@lightprotocol/compressed-token/unified"; -import { CTOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; +import { LIGHT_TOKEN_PROGRAM_ID } from "@lightprotocol/stateless.js"; const destinationAta = getAssociatedTokenAddressInterface(mint, recipient); const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( @@ -211,7 +211,7 @@ const createAtaIx = createAssociatedTokenAccountInterfaceIdempotentInstruction( destinationAta, recipient, mint, - CTOKEN_PROGRAM_ID + LIGHT_TOKEN_PROGRAM_ID ); new Transaction().add(createAtaIx, transferIx); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01de124b6e..3476b032c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -448,8 +448,6 @@ importers: programs: {} - sdk-tests/csdk-anchor-derived-test: {} - sdk-tests/csdk-anchor-full-derived-test: {} sdk-tests/sdk-anchor-test: @@ -489,8 +487,6 @@ importers: specifier: ^4.3.5 version: 4.9.5 - sdk-tests/sdk-compressible-test: {} - tsconfig: {} packages: diff --git a/program-libs/compressible/Cargo.toml b/program-libs/compressible/Cargo.toml index 49491d7cca..20be694df9 100644 --- a/program-libs/compressible/Cargo.toml +++ b/program-libs/compressible/Cargo.toml @@ -8,7 +8,7 @@ license = "MIT" [features] default = ["solana"] solana = ["dep:solana-program-error", "light-compressed-account/solana", "light-account-checks/solana", "solana-sysvar", "solana-msg"] -anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std", "light-account-checks/solana"] +anchor = ["anchor-lang", "light-compressed-account/anchor", "light-compressed-account/std", "light-account-checks/solana", "light-sdk-types/anchor"] pinocchio = ["dep:pinocchio", "light-compressed-account/pinocchio", "light-account-checks/pinocchio"] profile-program = [] profile-heap = ["dep:light-heap"] @@ -32,6 +32,7 @@ light-program-profiler = { workspace = true } light-heap = { workspace = true, optional = true } light-account-checks = { workspace= true } light-compressed-account = { workspace= true } +light-sdk-types = { workspace = true } aligned-sized = { workspace= true } solana-sysvar = {workspace = true, optional = true} diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index 0dff5dfd1f..dc16a63c3d 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -8,3 +8,18 @@ pub mod rent; use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_sdk_types::instruction::PackedAddressTreeInfo; + +/// Proof data for instruction params when creating new compressed accounts. +/// Used in the INIT flow - pass directly to instruction data. +/// All accounts use the same address tree, so only one `address_tree_info` is needed. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CreateAccountsProof { + /// The validity proof. + pub proof: ValidityProof, + /// Single packed address tree info (all accounts use same tree). + pub address_tree_info: PackedAddressTreeInfo, + /// Output state tree index for new compressed accounts. + pub output_state_tree_index: u8, +} diff --git a/programs/compressed-token/program/docs/ACCOUNTS.md b/programs/compressed-token/program/docs/ACCOUNTS.md index 6df1a8941e..6e45fcc5af 100644 --- a/programs/compressed-token/program/docs/ACCOUNTS.md +++ b/programs/compressed-token/program/docs/ACCOUNTS.md @@ -1,4 +1,5 @@ # Accounts + - Compressed tokens can be decompressed to spl tokens. Spl tokens are not explicitly listed here. - **description** - **discriminator** @@ -8,11 +9,12 @@ - **derivation:** (only for pdas) - **associated instructions** (create, close, update) - ## Solana Accounts + - The compressed token program uses ### CToken + - **description** struct `CToken` ctoken solana account with spl token compatible state layout @@ -40,8 +42,9 @@ - **serialization example** borsh and zero copy deserialization deserialize the compressible extension, spl serialization only deserialize the base token data. zero copy: (always use in programs) + ```rust - use light_token_interface::state::ctoken::CToken; + use light_token_interface::state::token::CToken; use light_zero_copy::traits::{ZeroCopyAt, ZeroCopyAtMut}; let (token, _) = CToken::zero_copy_at(&account_data)?; @@ -49,14 +52,16 @@ ``` borsh: (always use in client non solana program code) + ```rust use borsh::BorshDeserialize; - use light_token_interface::state::ctoken::CToken; + use light_token_interface::state::token::CToken; let token = CToken::deserialize(&mut &account_data[..])?; ``` spl serialization: (preferably use other serialization) + ```rust use spl_pod::bytemuck::pod_from_bytes; use spl_token_2022::pod::PodAccount; @@ -64,41 +69,43 @@ let pod_account = pod_from_bytes::(&account_data[..165])?; ``` - ### Associated CToken + - **description** struct `CToken` ctoken solana account with spl token compatible state layout - **derivation:** - seeds: [owner, ctoken_program_id, mint] + seeds: [owner, light_token_program_id, mint] - the same as `CToken` - ### Compressible Config + - owned by the LightRegistry program - defined in path `program-libs/compressible/src/config.rs` - crate: `light-compressible` - ## Compressed Accounts ### Compressed Token + - compressed token account. - version describes the hashing and the discriminator. (program-libs/token-interface/src/state/compressed_token/token_data_version.rs) - pub enum TokenDataVersion { - V1 = 1u8, // discriminator [2, 0, 0, 0, 0, 0, 0, 0], // 2 le (Poseidon hashed) - V2 = 2u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 3], // 3 be (Poseidon hashed) - ShaFlat = 3u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 4], // 4 be (Sha256 hash of borsh serialized data truncated to 31 bytes so that hash is less than be bn254 field size) - } + pub enum TokenDataVersion { + V1 = 1u8, // discriminator [2, 0, 0, 0, 0, 0, 0, 0], // 2 le (Poseidon hashed) + V2 = 2u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 3], // 3 be (Poseidon hashed) + ShaFlat = 3u8, // discriminator [0, 0, 0, 0, 0, 0, 0, 4], // 4 be (Sha256 hash of borsh serialized data truncated to 31 bytes so that hash is less than be bn254 field size) + } ### Compressed Mint ## Extensions + The compressed token program supports multiple extensions defined in `program-libs/token-interface/src/state/extensions/`. ### Mint Extensions #### TokenMetadata + - Mint extension, compatible with TokenMetadata extension of Token2022. - Only available in compressed mints. - Path: `program-libs/token-interface/src/state/extensions/token_metadata.rs` @@ -106,27 +113,33 @@ The compressed token program supports multiple extensions defined in `program-li ### Token Account Extensions #### Compressible + - Token account extension, Token2022 does not have an equivalent extension. - Only available in ctoken solana accounts (decompressed ctokens), not in compressed token accounts. - Stores compression info (rent sponsor, config, creation slot, etc.) for rent management. - Path: `program-libs/token-interface/src/state/extensions/compressible.rs` #### CompressedOnly + - Marker extension indicating the account can only exist in compressed form. - Path: `program-libs/token-interface/src/state/extensions/compressed_only.rs` #### Pausable + - Token account extension compatible with Token2022 PausableAccount extension. - Path: `program-libs/token-interface/src/state/extensions/pausable.rs` #### PermanentDelegate + - Token account extension compatible with Token2022 PermanentDelegate extension. - Path: `program-libs/token-interface/src/state/extensions/permanent_delegate.rs` #### TransferFee + - Token account extension compatible with Token2022 TransferFee extension. - Path: `program-libs/token-interface/src/state/extensions/transfer_fee.rs` #### TransferHook + - Token account extension compatible with Token2022 TransferHook extension. - Path: `program-libs/token-interface/src/state/extensions/transfer_hook.rs` diff --git a/programs/compressed-token/program/docs/ctoken/CREATE.md b/programs/compressed-token/program/docs/ctoken/CREATE.md index ca6c9540df..31f04ce78d 100644 --- a/programs/compressed-token/program/docs/ctoken/CREATE.md +++ b/programs/compressed-token/program/docs/ctoken/CREATE.md @@ -1,34 +1,46 @@ - # Instructions + - This file documents create ctoken account and create associated ctoken account. ## 1. create ctoken account - **discriminator:** 18 - **enum:** `CTokenInstruction::CreateTokenAccount` - **path:** programs/compressed-token/program/src/ctoken/create.rs - - **description:** - 1. creates ctoken solana accounts with and without Compressible extension - 2. account layout `CToken` is defined in path: program-libs/token-interface/src/state/ctoken/ctoken_struct.rs - 3. extension layout `CompressionInfo` is defined in path: - program-libs/token-interface/src/state/extensions/compressible.rs - 4. A compressible token means that the ctoken solana account can be compressed by the rent authority as soon as the account balance is insufficient. - 5. Account creation without the compressible extension: +**discriminator:** 18 +**enum:** `CTokenInstruction::CreateTokenAccount` +**path:** programs/compressed-token/program/src/ctoken/create.rs + +**description:** + +1. creates ctoken solana accounts with and without Compressible extension +2. account layout `CToken` is defined in path: program-libs/token-interface/src/state/ctoken/ctoken_struct.rs +3. extension layout `CompressionInfo` is defined in path: + program-libs/token-interface/src/state/extensions/compressible.rs +4. A compressible token means that the ctoken solana account can be compressed by the rent authority as soon as the account balance is insufficient. +5. Account creation without the compressible extension: + + - Initializes an existing 165-byte solana account as a ctoken account (SPL-compatible size) - Only sets mint, owner, and state fields - no extension data - Account must already exist and be owned by the program - 6. Account creation with compressible extension: + +6. Account creation with compressible extension: + + - creates the ctoken account via cpi within the instruction, then initializes it. - expects a CompressibleConfig account to read the rent authority, rent recipient and RentConfig from. - if the payer is not the rent recipient the fee payer pays the rent and becomes the rent recipient (the rent recipient is a ctoken program pda that funds rent exemption for compressible ctoken solana accounts) - **Instruction data:** - 1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_ctoken_account.rs +**Instruction data:** + +1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_ctoken_account.rs + + - `owner`: The owner pubkey for the token account (32 bytes) - `compressible_config`: Optional `CompressibleExtensionInstructionData` (None = non-compressible account) - 2. Instruction data with compressible extension - program-libs/token-interface/src/instructions/extensions/compressible.rs + +2. Instruction data with compressible extension + program-libs/token-interface/src/instructions/extensions/compressible.rs + + - `token_account_version`: Version of the compressed token account hashing scheme (u8). Must be 3 (ShaFlat) - only version 3 is supported. - `rent_payment`: Number of epochs to prepay for rent (u8) - `rent_payment = 1` is explicitly forbidden to prevent epoch boundary timing edge case (its rent for the current rent epoch) @@ -38,168 +50,154 @@ - `write_top_up`: Additional lamports allocated for future write operations on the compressed account (u32) - `compress_to_account_pubkey`: Optional `CompressToPubkey` for compressing to account pubkey instead of owner - **Accounts:** - 1. token_account +**Accounts:** + +1. token_account + + - (signer for compressible, mutable) - The ctoken account being created - For compressible accounts: must be signer (account created via CPI) - For non-compressible accounts: doesn't need to be signer (SPL compatibility) - 2. mint + +2. mint + + - (non-mutable) - Mint pubkey is used for token account initialization and extension detection - Account is unchecked and doesn't need to be initialized, allowing compressed mints to be used without providing the compressed account - Optional accounts required to initialize ctoken account with compressible extension: - 3. payer - - (signer, mutable) - - User account, pays for the compression incentive when using rent_sponsor - 4. config - - (non-mutable) - - Owned by LightRegistry program, CompressibleConfig::discriminator matches - - Used to read RentConfig, rent_sponsor, and compression_authority - - Must be in ACTIVE state - 5. system_program - - (non-mutable) - - Required for account creation and rent transfer - 6. rent_payer - - (mutable) - - Either rent_sponsor PDA or custom fee payer - - If custom fee payer: must be signer, pays rent exemption + compression incentive - - If rent_sponsor: not signer, pays only rent exemption (payer pays compression incentive) - - **Instruction Logic and Checks:** - 1. Deserialize instruction data +Optional accounts required to initialize ctoken account with compressible extension: 3. payer - (signer, mutable) - User account, pays for the compression incentive when using rent_sponsor 4. config - (non-mutable) - Owned by LightRegistry program, CompressibleConfig::discriminator matches - Used to read RentConfig, rent_sponsor, and compression_authority - Must be in ACTIVE state 5. system_program - (non-mutable) - Required for account creation and rent transfer 6. rent_payer - (mutable) - Either rent_sponsor PDA or custom fee payer - If custom fee payer: must be signer, pays rent exemption + compression incentive - If rent_sponsor: not signer, pays only rent exemption (payer pays compression incentive) + +**Instruction Logic and Checks:** + +1. Deserialize instruction data + + - If instruction data len == 32 bytes, treat as owner-only (SPL Token initialize_account3 compatibility) - Otherwise, deserialize as `CreateTokenAccountInstructionData` - 2. Parse and check accounts based on is_compressible flag + +2. Parse and check accounts based on is_compressible flag + + - For compressible: token_account must be signer - 3. Check mint extensions using `has_mint_extensions()` - 4. If with compressible account: - 4.1. Parse payer, config, system_program, and rent_payer accounts - 4.2. Validate CompressibleConfig is active (not inactive or deprecated) - - Error: `CompressibleError::InvalidState` if not active - 4.3. If with compress_to_pubkey: - - Validates: derives address from provided seeds/bump and verifies it matches token_account pubkey - - Security: ensures account is a derivable PDA, preventing compression to non-signable addresses - 4.4. Validate compression_only requirement for restricted extensions: - - If mint has restricted extensions (e.g., TransferFee) and compression_only == 0 - - Error: `ErrorCode::CompressionOnlyRequired` - 4.5. Validate compression_only is only set for mints with restricted extensions: - - If compression_only != 0 and mint has no restricted extensions - - Error: `ErrorCode::CompressionOnlyNotAllowed` - 4.6. Validate rent_payment is not exactly 1 epoch (must cover more than the current rent epoch or be 0) - - Check: `compressible_config.rent_payment != 1` - - Error: `ErrorCode::OneEpochPrefundingNotAllowed` if validation fails - - Purpose: Prevent accounts from becoming immediately compressible due to epoch boundary timing - 4.7. Calculate account size based on mint extensions (includes Compressible extension) - 4.8. Calculate rent (rent exemption + prepaid epochs rent + compression incentive) - 4.9. Check whether rent_payer is custom fee payer (rent_payer != config.rent_sponsor) - 4.10. If custom rent payer: - - Verify rent_payer is signer (prevents executable accounts as rent_sponsor) - - Create account with custom rent_payer via CPI (pays both rent exemption + additional lamports) - 4.11. If using protocol rent_sponsor: - - Create account with rent_sponsor PDA as fee payer via CPI (pays only rent exemption) - - Transfer compression incentive to created ctoken account from payer via CPI - 4.12. `initialize_ctoken_account` (programs/compressed-token/program/src/shared/initialize_ctoken_account.rs) - - Build extensions Vec including Compressible extension and any mint extension markers - - Copy version from config (used to match config PDA version in subsequent instructions) - - If custom fee payer, set custom fee payer as ctoken account rent_sponsor - - Else set config.rent_sponsor as ctoken account rent_sponsor - - Set `last_claimed_slot` to current slot (tracks when rent was last claimed/initialized) - - Validate token_account_version == 3 (ShaFlat) - - Error: `ProgramError::InvalidInstructionData` if version != 3 - - Validate write_top_up <= config.rent_config.max_top_up - - Error: `CTokenError::WriteTopUpExceedsMaximum` if exceeded - - Validate mint account (if initialized): - - Check mint owner is SPL Token, Token-2022, or CToken program - - Error: `ProgramError::IncorrectProgramId` if invalid owner - - Check mint structure is valid (82 bytes for SPL, or has AccountType marker for T22) - - Error: `ProgramError::InvalidAccountData` if invalid structure - - Cache decimals from mint account in extension - 5. If without compressible account (non-compressible path): - 5.1. Validate mint does not have restricted extensions - - Check: `!mint_extensions.has_restricted_extensions()` - - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension - - **Errors:** - - `ProgramError::BorshIoError` (error code: 15) - Failed to deserialize CreateTokenAccountInstructionData from instruction_data bytes - - `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts - - `AccountError::InvalidSigner` (error code: 12015) - token_account or payer is not a signer when required - - `AccountError::AccountNotMutable` (error code: 12008) - token_account or payer is not mutable when required - - `AccountError::AccountOwnedByWrongProgram` (error code: 12007) - Config account not owned by LightRegistry program - - `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails, compress_to_pubkey.check_seeds() fails, or invalid mint structure - - `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, extension data invalid, or token_account_version != 3 - - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - - `ProgramError::UnsupportedSysvar` (error code: 17) - Failed to get Clock sysvar - - `ProgramError::IncorrectProgramId` (error code: 1) - Mint account owner is not SPL Token, Token-2022, or CToken program - - `CompressibleError::InvalidState` (error code: 19002) - CompressibleConfig is not in active state - - `ErrorCode::InsufficientAccountSize` (error code: 6077) - token_account data length < 165 bytes (non-compressible) or < COMPRESSIBLE_TOKEN_ACCOUNT_SIZE (compressible) - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case - - `ErrorCode::CompressionOnlyRequired` (error code: 6131) - Mint has restricted extensions (e.g., TransferFee) but compression_only is not set in instruction data - - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - Either: (1) compressible_config is Some in instruction data but compressible accounts are missing, or (2) non-compressible account creation attempted for mint with restricted extensions - - `ErrorCode::CompressionOnlyNotAllowed` (error code: 6151) - compression_only is set but mint has no restricted extensions - - `CTokenError::WriteTopUpExceedsMaximum` (error code: 18042) - write_top_up exceeds config.rent_config.max_top_up - - `CTokenError::MissingCompressibleExtension` (error code: 18056) - Compressible extension initialization failed internally +3. Check mint extensions using `has_mint_extensions()` +4. If with compressible account: + 4.1. Parse payer, config, system_program, and rent_payer accounts + 4.2. Validate CompressibleConfig is active (not inactive or deprecated) - Error: `CompressibleError::InvalidState` if not active + 4.3. If with compress_to_pubkey: - Validates: derives address from provided seeds/bump and verifies it matches token_account pubkey - Security: ensures account is a derivable PDA, preventing compression to non-signable addresses + 4.4. Validate compression_only requirement for restricted extensions: - If mint has restricted extensions (e.g., TransferFee) and compression_only == 0 - Error: `ErrorCode::CompressionOnlyRequired` + 4.5. Validate compression_only is only set for mints with restricted extensions: - If compression_only != 0 and mint has no restricted extensions - Error: `ErrorCode::CompressionOnlyNotAllowed` + 4.6. Validate rent_payment is not exactly 1 epoch (must cover more than the current rent epoch or be 0) - Check: `compressible_config.rent_payment != 1` - Error: `ErrorCode::OneEpochPrefundingNotAllowed` if validation fails - Purpose: Prevent accounts from becoming immediately compressible due to epoch boundary timing + 4.7. Calculate account size based on mint extensions (includes Compressible extension) + 4.8. Calculate rent (rent exemption + prepaid epochs rent + compression incentive) + 4.9. Check whether rent_payer is custom fee payer (rent_payer != config.rent_sponsor) + 4.10. If custom rent payer: - Verify rent_payer is signer (prevents executable accounts as rent_sponsor) - Create account with custom rent_payer via CPI (pays both rent exemption + additional lamports) + 4.11. If using protocol rent_sponsor: - Create account with rent_sponsor PDA as fee payer via CPI (pays only rent exemption) - Transfer compression incentive to created ctoken account from payer via CPI + 4.12. `initialize_ctoken_account` (programs/compressed-token/program/src/shared/initialize_ctoken_account.rs) - Build extensions Vec including Compressible extension and any mint extension markers - Copy version from config (used to match config PDA version in subsequent instructions) - If custom fee payer, set custom fee payer as ctoken account rent_sponsor - Else set config.rent_sponsor as ctoken account rent_sponsor - Set `last_claimed_slot` to current slot (tracks when rent was last claimed/initialized) - Validate token_account_version == 3 (ShaFlat) - Error: `ProgramError::InvalidInstructionData` if version != 3 - Validate write_top_up <= config.rent_config.max_top_up - Error: `CTokenError::WriteTopUpExceedsMaximum` if exceeded - Validate mint account (if initialized): - Check mint owner is SPL Token, Token-2022, or CToken program - Error: `ProgramError::IncorrectProgramId` if invalid owner - Check mint structure is valid (82 bytes for SPL, or has AccountType marker for T22) - Error: `ProgramError::InvalidAccountData` if invalid structure - Cache decimals from mint account in extension +5. If without compressible account (non-compressible path): + 5.1. Validate mint does not have restricted extensions - Check: `!mint_extensions.has_restricted_extensions()` - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension + +**Errors:** + +- `ProgramError::BorshIoError` (error code: 15) - Failed to deserialize CreateTokenAccountInstructionData from instruction_data bytes +- `AccountError::NotEnoughAccountKeys` (error code: 12020) - Missing required accounts +- `AccountError::InvalidSigner` (error code: 12015) - token_account or payer is not a signer when required +- `AccountError::AccountNotMutable` (error code: 12008) - token_account or payer is not mutable when required +- `AccountError::AccountOwnedByWrongProgram` (error code: 12007) - Config account not owned by LightRegistry program +- `ProgramError::InvalidAccountData` (error code: 4) - CompressibleConfig pod deserialization fails, compress_to_pubkey.check_seeds() fails, or invalid mint structure +- `ProgramError::InvalidInstructionData` (error code: 3) - compressible_config is None in instruction data when compressible accounts provided, extension data invalid, or token_account_version != 3 +- `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer +- `ProgramError::UnsupportedSysvar` (error code: 17) - Failed to get Clock sysvar +- `ProgramError::IncorrectProgramId` (error code: 1) - Mint account owner is not SPL Token, Token-2022, or CToken program +- `CompressibleError::InvalidState` (error code: 19002) - CompressibleConfig is not in active state +- `ErrorCode::InsufficientAccountSize` (error code: 6077) - token_account data length < 165 bytes (non-compressible) or < COMPRESSIBLE_TOKEN_ACCOUNT_SIZE (compressible) +- `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch, which is forbidden due to epoch boundary timing edge case +- `ErrorCode::CompressionOnlyRequired` (error code: 6131) - Mint has restricted extensions (e.g., TransferFee) but compression_only is not set in instruction data +- `ErrorCode::MissingCompressibleConfig` (error code: 6115) - Either: (1) compressible_config is Some in instruction data but compressible accounts are missing, or (2) non-compressible account creation attempted for mint with restricted extensions +- `ErrorCode::CompressionOnlyNotAllowed` (error code: 6151) - compression_only is set but mint has no restricted extensions +- `CTokenError::WriteTopUpExceedsMaximum` (error code: 18042) - write_top_up exceeds config.rent_config.max_top_up +- `CTokenError::MissingCompressibleExtension` (error code: 18056) - Compressible extension initialization failed internally ## 2. create associated ctoken account - **discriminator:** 100 (non-idempotent), 102 (idempotent) - **enum:** `CTokenInstruction::CreateAssociatedTokenAccount` (non-idempotent), `CTokenInstruction::CreateAssociatedTokenAccountIdempotent` (idempotent) - **path:** programs/compressed-token/program/src/ctoken/create_ata.rs - - **description:** - 1. Creates deterministic ctoken PDA accounts derived from [owner, ctoken_program_id, mint] - 2. Supports both non-idempotent (fails if exists) and idempotent (succeeds if exists) modes - 3. Account layout same as create ctoken account: `CToken` with optional `CompressionInfo` - 4. Associated token accounts cannot use compress_to_pubkey (always compress to owner) - 5. Owner and mint are provided as accounts, bump is provided via instruction data - 6. Token account must be uninitialized (owned by system program) unless idempotent mode - 7. ATAs for mints with restricted extensions must be compressible (the compression_only marker is part of the Compressible extension) - - **Instruction data:** - 1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_associated_token_account.rs +**discriminator:** 100 (non-idempotent), 102 (idempotent) +**enum:** `CTokenInstruction::CreateAssociatedTokenAccount` (non-idempotent), `CTokenInstruction::CreateAssociatedTokenAccountIdempotent` (idempotent) +**path:** programs/compressed-token/program/src/ctoken/create_ata.rs + +**description:** + +1. Creates deterministic ctoken PDA accounts derived from [owner, light_token_program_id, mint] +2. Supports both non-idempotent (fails if exists) and idempotent (succeeds if exists) modes +3. Account layout same as create ctoken account: `CToken` with optional `CompressionInfo` +4. Associated token accounts cannot use compress_to_pubkey (always compress to owner) +5. Owner and mint are provided as accounts, bump is provided via instruction data +6. Token account must be uninitialized (owned by system program) unless idempotent mode +7. ATAs for mints with restricted extensions must be compressible (the compression_only marker is part of the Compressible extension) + +**Instruction data:** + +1. instruction data is defined in path: program-libs/token-interface/src/instructions/create_associated_token_account.rs + + - `bump`: PDA bump seed for derivation (u8) - `compressible_config`: Optional `CompressibleExtensionInstructionData`, same as create ctoken account but: - `compress_to_account_pubkey` must be None (ATAs always compress to owner) - `compression_only` must be non-zero (compressible ATAs require compression_only) - **Accounts:** - 1. owner +**Accounts:** + +1. owner + + - (non-mutable, non-signer) - The owner of the associated token account (used for PDA derivation and initialization) - 2. mint + +2. mint + + - (non-mutable, non-signer) - The mint for the token account (used for PDA derivation and initialization) - 3. fee_payer + +3. fee_payer + + - (signer, mutable) - Pays for account creation and compression incentive - 4. associated_token_account + +4. associated_token_account + + - (mutable, NOT signer) - The PDA being created, must be system-owned (uninitialized) unless idempotent - 5. system_program + +5. system_program + + - (non-mutable) - Required for account creation - Optional accounts for compressible extension (same as create ctoken account): - 6. config - - (non-mutable) - - Owned by LightRegistry program, CompressibleConfig::discriminator matches - - Used to read RentConfig, rent_sponsor, and compression_authority - 7. rent_payer - - (mutable) - - Either rent_sponsor PDA or custom fee payer (must be signer if custom) - - **Instruction Logic and Checks:** - 1. Deserialize instruction data - 2. Parse accounts: owner, mint, fee_payer, associated_token_account, system_program - 3. If idempotent mode: +Optional accounts for compressible extension (same as create ctoken account): 6. config - (non-mutable) - Owned by LightRegistry program, CompressibleConfig::discriminator matches - Used to read RentConfig, rent_sponsor, and compression_authority 7. rent_payer - (mutable) - Either rent_sponsor PDA or custom fee payer (must be signer if custom) + +**Instruction Logic and Checks:** + +1. Deserialize instruction data +2. Parse accounts: owner, mint, fee_payer, associated_token_account, system_program +3. If idempotent mode: + + - Validate PDA derivation matches [owner, program_id, mint] with provided bump - Return success if account already owned by ctoken program - 4. Verify account is system-owned (uninitialized) + +4. Verify account is system-owned (uninitialized) + + - Error: `ProgramError::IllegalOwner` if not owned by system program - 5. If compressible: + +5. If compressible: + + - Reject if compress_to_account_pubkey is Some (not allowed for ATAs) - Error: `ProgramError::InvalidInstructionData` if compress_to_account_pubkey is Some - Validate compression_only is set (required for compressible ATAs) @@ -219,21 +217,20 @@ - If using protocol rent_sponsor: - Create ATA PDA with rent_sponsor PDA paying rent exemption - Transfer compression incentive from fee_payer to account via CPI - 6. If not compressible: - 6.1. Validate mint does not have restricted extensions - - Check: `!mint_extensions.has_restricted_extensions()` - - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension - 6.2. Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) - 7. Initialize token account with is_ata flag set (same as ## 1. create ctoken account step 4.12, but with is_ata=true) - - **Errors:** - Same as create ctoken account with additions: - - `ProgramError::IllegalOwner` (error code: 18) - Associated token account not owned by system program when creating - - `ProgramError::InvalidInstructionData` (error code: 3) - compress_to_account_pubkey is Some (forbidden for ATAs) - - `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer - - `AccountError::InvalidSigner` (error code: 12015) - fee_payer is not a signer - - `AccountError::AccountNotMutable` (error code: 12008) - fee_payer or associated_token_account is not mutable - - `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch (see create ctoken account errors) - - `ErrorCode::AtaRequiresCompressionOnly` (error code: 6152) - compressible ATA must have compression_only set (compression_only == 0 is not allowed) - - `ErrorCode::MissingCompressibleConfig` (error code: 6115) - non-compressible ATA creation attempted for mint with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) + +6. If not compressible: + 6.1. Validate mint does not have restricted extensions - Check: `!mint_extensions.has_restricted_extensions()` - Error: `ErrorCode::MissingCompressibleConfig` if mint has restricted extensions - Rationale: Mints with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) must be marked as compression_only, and that marker is part of the Compressible extension + 6.2. Create ATA PDA with fee_payer paying rent exemption (base 165-byte SPL layout) +7. Initialize token account with is_ata flag set (same as ## 1. create ctoken account step 4.12, but with is_ata=true) + +**Errors:** +Same as create ctoken account with additions: + +- `ProgramError::IllegalOwner` (error code: 18) - Associated token account not owned by system program when creating +- `ProgramError::InvalidInstructionData` (error code: 3) - compress_to_account_pubkey is Some (forbidden for ATAs) +- `ProgramError::MissingRequiredSignature` (error code: 8) - Custom rent_payer is not a signer +- `AccountError::InvalidSigner` (error code: 12015) - fee_payer is not a signer +- `AccountError::AccountNotMutable` (error code: 12008) - fee_payer or associated_token_account is not mutable +- `ErrorCode::OneEpochPrefundingNotAllowed` (error code: 6101) - rent_payment is exactly 1 epoch (see create ctoken account errors) +- `ErrorCode::AtaRequiresCompressionOnly` (error code: 6152) - compressible ATA must have compression_only set (compression_only == 0 is not allowed) +- `ErrorCode::MissingCompressibleConfig` (error code: 6115) - non-compressible ATA creation attempted for mint with restricted extensions (Pausable, PermanentDelegate, TransferFee, TransferHook) diff --git a/programs/compressed-token/program/tests/compress_and_close.rs b/programs/compressed-token/program/tests/compress_and_close.rs index aadb5c9160..ae3fa7ea89 100644 --- a/programs/compressed-token/program/tests/compress_and_close.rs +++ b/programs/compressed-token/program/tests/compress_and_close.rs @@ -5,7 +5,7 @@ use light_account_checks::{ packed_accounts::ProgramPackedAccounts, }; use light_compressed_token::compressed_token::transfer2::{ - accounts::Transfer2Accounts, compression::ctoken::close_for_compress_and_close, + accounts::Transfer2Accounts, compression::close_for_compress_and_close, }; use light_token_interface::{ instructions::transfer2::{Compression, CompressionMode}, diff --git a/rebase.md b/rebase.md new file mode 100644 index 0000000000..8f1a57f45d --- /dev/null +++ b/rebase.md @@ -0,0 +1,95 @@ +# Rebase Resolution Notes + +## Context + +Rebasing `swen/clean-decompress-base` onto `main`. + +## Main Branch Changes (affecting this rebase) + +### 1. Package Renames + +- `ctoken-sdk` → `token-sdk` +- `light_token_sdk` → `light_token_sdk` +- `ctoken` module → `token` module in APIs +- `sdk-ctoken-test` → `sdk-light-token-test` +- `light_token_interface` → `light_token_interface` +- Type names: `CToken` → `Token` in some places + +### 2. Deleted Tests/Directories in Main + +Main deleted these that HEAD modified: + +- `sdk-tests/csdk-anchor-derived-test/` - Deleted in main +- `sdk-tests/sdk-compressible-test/` - Deleted in main +- `sdk-libs/macros/src/compressible/GUIDE.md` - Deleted in main + +### 3. User's Branch (HEAD) Changes to Preserve + +- **Phase 8 refactor**: `TokenSeedProvider` trait simplified - no accounts struct needed +- Seed pubkeys embedded directly in enum variants +- `HasTokenVariant::is_packed_token()` → `is_packed_ctoken()` in some places +- Various API simplifications in decompress_runtime + +## Resolution Decisions + +### Content Conflicts + +1. **sdk-libs/macros/src/compressible/instructions.rs** + - Keep user's Phase 8 changes (simplified API) + - Use main's naming (`light_token_sdk` not `light_token_sdk`) + - Resolution: Take HEAD's code, update package names to main's convention + +2. **sdk-libs/macros/src/compressible/decompress_context.rs** + - Keep user's `RentFreeAccountData` naming + - Use `light_token_sdk::compat::PackedCTokenData` (main's package name) + - Fix trait method naming to match + +3. **sdk-libs/macros/src/compressible/seed_providers.rs** + - Keep user's Phase 8 implementation (simpler trait) + - Update imports to `light_token_sdk` + +4. **sdk-libs/macros/src/compressible/variant_enum.rs** + - Keep user's variant structure with idx fields + - Use `light_token_sdk::compat::*` imports + +5. **sdk-libs/sdk/src/compressible/decompress_runtime.rs** + - Keep user's simplified `TokenSeedProvider` trait (no accounts struct) + - Update to `ctoken_program()` accessor name + +6. **sdk-libs/token-sdk/src/compressible/decompress_runtime.rs** + - Keep user's implementation with `TokenSeedProvider` re-export + - Fix variable names (`token_accounts` vs `ctoken_accounts`) + +7. **sdk-libs/token-sdk/src/pack.rs** + - Keep both main's and user's Pack impls (they're compatible) + - Use `light_token_interface` imports + +8. **sdk-libs/token-sdk/src/token/create.rs, create_ata.rs** + - Keep user's new builder pattern APIs + - Use main's `light_token_interface` imports + +9. **sdk-libs/program-test/src/compressible.rs** + - Keep user's `compression_only` field addition + - Use `CToken` type with zerocopy (main's approach for Token parsing) + +10. **Cargo.toml** + - Keep main's member list (sdk-light-token-test) + - Add back sdk-compressible-test and csdk-anchor-derived-test if still needed + - Resolution: User tests appear consolidated - use main's list + +### Modify/Delete Conflicts + +1. **sdk-tests/csdk-anchor-derived-test/\*** - DELETE (main removed, tests moved to csdk-anchor-full-derived-test) +2. **sdk-tests/sdk-compressible-test/\*** - DELETE (main removed, functionality consolidated) +3. **sdk-libs/macros/src/compressible/GUIDE.md** - DELETE (main removed docs) + +### Test Files + +- **csdk-anchor-full-derived-test** - Keep user's changes, update imports to `light_token_sdk` → `light_token_sdk` + +## Confidence Level + +- **High confidence**: Package renames are mechanical +- **High confidence**: Phase 8 API simplifications are the user's intended changes +- **Medium confidence**: Deleted test directories - assuming main's consolidation is correct +- **Note**: Variable naming (`token_accounts` vs `ctoken_accounts`) - using main's `token_` prefix consistently diff --git a/scripts/lint.sh b/scripts/lint.sh index 3358832702..d31acd7d4a 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -36,8 +36,6 @@ NO_DEFAULT_CRATES=( "light-token-sdk" "light-token-types" "light-sdk" - "sdk-compressible-test" - "csdk-anchor-derived-test" "csdk-anchor-full-derived-test" ) diff --git a/sdk-libs/compressible-client/Cargo.toml b/sdk-libs/compressible-client/Cargo.toml index e148bae684..fb1a3980d9 100644 --- a/sdk-libs/compressible-client/Cargo.toml +++ b/sdk-libs/compressible-client/Cargo.toml @@ -7,15 +7,22 @@ repository = "https://github.com/lightprotocol/light-protocol" description = "Client instruction builders for Light Protocol compressible accounts" [features] -anchor = ["anchor-lang", "light-sdk/anchor"] +anchor = ["anchor-lang", "light-sdk/anchor", "light-token-sdk/anchor"] [dependencies] solana-instruction = { workspace = true } solana-pubkey = { workspace = true } solana-account = { workspace = true } +solana-program-error = { workspace = true } +solana-program = { workspace = true } +spl-token-2022 = { workspace = true } light-client = { workspace = true, features = ["v2"] } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } +light-token-sdk = { workspace = true, features = ["cpi-context"] } +light-token-interface = { workspace = true } +light-compressed-account = { workspace = true } +light-compressible = { workspace = true } anchor-lang = { workspace = true, features = ["idl-build"], optional = true } borsh = { workspace = true } diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs new file mode 100644 index 0000000000..7019a7871d --- /dev/null +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -0,0 +1,207 @@ +//! Helper for getting validity proofs for creating new compressed accounts (INIT flow). +//! +//! This module provides an opinionated helper that: +//! - Uses a single address tree (V2) for all addresses +//! - Handles address derivation internally based on input type +//! - Packs proof into remaining accounts +//! - Returns a single `address_tree_info` since all accounts use the same tree + +use light_client::{ + indexer::{AddressWithTree, Indexer, IndexerError}, + rpc::{Rpc, RpcError}, +}; +use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; +use thiserror::Error; + +use crate::pack::{pack_proof, PackError}; + +/// Error type for create accounts proof operations. +#[derive(Debug, Error)] +pub enum CreateAccountsProofError { + #[error("Inputs cannot be empty")] + EmptyInputs, + + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("RPC error: {0}")] + Rpc(RpcError), + + #[error("Pack error: {0}")] + Pack(#[from] PackError), +} + +/// Input for creating new compressed accounts. +/// `program_id` from main function is used as default owner for `Pda` variant. +#[derive(Clone, Debug)] +pub enum CreateAccountsProofInput { + /// PDA owned by the calling program (uses program_id from main fn) + Pda(Pubkey), + /// PDA with explicit owner (for cross-program accounts) + PdaWithOwner { pda: Pubkey, owner: Pubkey }, + /// CMint (always uses LIGHT_TOKEN_PROGRAM_ID internally) + Mint(Pubkey), +} + +impl CreateAccountsProofInput { + /// Standard PDA owned by calling program. + /// Address derived: `derive_address(&pda, &tree, &program_id)` + pub fn pda(pda: Pubkey) -> Self { + Self::Pda(pda) + } + + /// PDA with explicit owner (rare: cross-program accounts). + /// Address derived: `derive_address(&pda, &tree, &owner)` + pub fn pda_with_owner(pda: Pubkey, owner: Pubkey) -> Self { + Self::PdaWithOwner { pda, owner } + } + + /// Compressed mint (CMint). + /// Address derived: `derive_mint_compressed_address(&mint_signer, &tree)` + pub fn mint(mint_signer: Pubkey) -> Self { + Self::Mint(mint_signer) + } + + /// Derive the compressed address. + fn derive_address(&self, address_tree: &Pubkey, program_id: &Pubkey) -> [u8; 32] { + match self { + Self::Pda(pda) => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &program_id.to_bytes(), + ), + Self::PdaWithOwner { pda, owner } => light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree.to_bytes(), + &owner.to_bytes(), + ), + Self::Mint(signer) => derive_mint_compressed_address(signer, address_tree), + } + } +} + +// Re-export from light-compressible (SBF-compatible) +pub use light_compressible::CreateAccountsProof; + +/// Result of `get_create_accounts_proof`. +pub struct CreateAccountsProofResult { + /// Proof data to include in instruction data. + pub create_accounts_proof: CreateAccountsProof, + /// Remaining accounts to append to instruction accounts. + pub remaining_accounts: Vec, +} + +/// Gets validity proof for creating new compressed accounts (INIT flow). +/// +/// Opinionated helper that: +/// - Uses a single address tree (V2) for all addresses +/// - Handles address derivation internally based on input type +/// - Packs proof into remaining accounts +/// +/// # Arguments +/// * `rpc` - RPC client implementing `Rpc + Indexer` traits +/// * `program_id` - Your program's ID (used as default owner for Pda inputs + system config) +/// * `inputs` - Vec of `CreateAccountsProofInput` describing accounts to create +/// +/// # Returns +/// `CreateAccountsProofResult` containing proof and remaining accounts. +/// +/// # Example +/// ```rust,ignore +/// let result = get_create_accounts_proof( +/// &rpc, +/// &program_id, +/// vec![ +/// CreateAccountsProofInput::pda(user_pda), +/// CreateAccountsProofInput::pda(game_pda), +/// CreateAccountsProofInput::mint(mint_signer_pda), +/// ], +/// ).await?; +/// +/// // Just pass create_accounts_proof to instruction - macros use defaults +/// let ix = Instruction { +/// program_id, +/// accounts: [my_accounts.to_account_metas(None), result.remaining_accounts].concat(), +/// data: MyInstruction { +/// create_accounts_proof: result.create_accounts_proof, +/// // ... other params +/// }.data(), +/// }; +/// ``` +pub async fn get_create_accounts_proof( + rpc: &R, + program_id: &Pubkey, + inputs: Vec, +) -> Result { + if inputs.is_empty() { + return Err(CreateAccountsProofError::EmptyInputs); + } + + // 1. Get address tree (opinionated: always V2) + let address_tree = rpc.get_address_tree_v2(); + let address_tree_pubkey = address_tree.tree; + + // 2. Derive all compressed addresses (program_id used as default owner for Pda) + let derived_addresses: Vec<[u8; 32]> = inputs + .iter() + .map(|input| input.derive_address(&address_tree_pubkey, program_id)) + .collect(); + + // 3. Build AddressWithTree for each (all use same tree) + let addresses_with_trees: Vec = derived_addresses + .iter() + .map(|&address| AddressWithTree { + address, + tree: address_tree_pubkey, + }) + .collect(); + + // 4. Get validity proof (empty hashes = INIT flow) + let validity_proof = rpc + .get_validity_proof(vec![], addresses_with_trees, None) + .await? + .value; + + // 5. Get output state tree + let state_tree_info = rpc + .get_random_state_tree_info() + .map_err(CreateAccountsProofError::Rpc)?; + + // 6. Determine CPI context + // For INIT with mints: need CPI context for cross-program invocation + let has_mints = inputs + .iter() + .any(|i| matches!(i, CreateAccountsProofInput::Mint(_))); + let cpi_context = if has_mints { + state_tree_info.cpi_context + } else { + None + }; + + // 7. Pack proof + let packed = pack_proof( + program_id, + validity_proof.clone(), + &state_tree_info, + cpi_context, + )?; + + // All addresses use the same tree, so just take the first packed info + let address_tree_info = packed + .packed_tree_infos + .address_trees + .first() + .copied() + .ok_or(CreateAccountsProofError::EmptyInputs)?; + + Ok(CreateAccountsProofResult { + create_accounts_proof: CreateAccountsProof { + proof: validity_proof.proof, + address_tree_info, + output_state_tree_index: packed.output_tree_index, + }, + remaining_accounts: packed.remaining_accounts, + }) +} diff --git a/sdk-libs/compressible-client/src/decompress_atas.rs b/sdk-libs/compressible-client/src/decompress_atas.rs new file mode 100644 index 0000000000..300ea73afd --- /dev/null +++ b/sdk-libs/compressible-client/src/decompress_atas.rs @@ -0,0 +1,786 @@ +//! Decompress ATA-owned compressed tokens. +//! +//! This module provides client-side functionality to decompress multiple +//! ATA-owned compressed token accounts in a single instruction with one proof. +//! +//! Two API patterns are provided: +//! +//! ## High-level async API +//! - `decompress_atas`: Async, fetches state + proof internally +//! +//! ## High-performance sync API (for apps that pre-fetch state) +//! ```ignore +//! // 1. Fetch raw account interfaces (async) +//! let account = rpc.get_ata_account_interface(&mint, &owner).await?; +//! +//! // 2. Parse into token account interface (sync) +//! let parsed = parse_token_account_interface(&account)?; +//! +//! // 3. If cold, get proof and build instructions (sync) +//! if parsed.is_cold { +//! let proof = rpc.get_validity_proof(...).await?; +//! let ixs = build_decompress_atas(&[parsed], fee_payer, Some(proof))?; +//! } +//! ``` + +use light_client::indexer::{ + CompressedTokenAccount, GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer, + IndexerError, ValidityProofWithContext, +}; +use light_compressed_account::compressed_account::PackedMerkleContext; +use light_sdk::instruction::PackedAccounts; +use light_token_interface::{ + instructions::{ + extensions::{CompressedOnlyExtensionInstructionData, ExtensionInstructionData}, + transfer2::MultiInputTokenDataWithContext, + }, + state::{ExtensionStruct, TokenDataVersion}, +}; +use light_token_sdk::{ + compat::{AccountState, TokenData}, + compressed_token::{ + transfer2::{ + create_transfer2_instruction, Transfer2AccountsMetaConfig, Transfer2Config, + Transfer2Inputs, + }, + CTokenAccount2, + }, + error::TokenSdkError, + token::{derive_token_ata, CreateAssociatedTokenAccount}, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; +use spl_token_2022::state::Account as SplTokenAccount; +use thiserror::Error; + +/// Error type for decompress ATA operations. +#[derive(Debug, Error)] +pub enum DecompressAtaError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("Token SDK error: {0}")] + TokenSdk(#[from] TokenSdkError), + + #[error("No state trees in proof")] + NoStateTreesInProof, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), + + #[error("Cold ATA missing compressed data at index {0}")] + MissingCompressedData(usize), + + #[error("Proof required for cold ATAs")] + ProofRequired, + + #[error("Invalid account data")] + InvalidAccountData, +} + +// ============================================================================ +// Raw Account Interface +// ============================================================================ + +/// Context for decompressing a cold ATA. +/// Contains all data needed to build decompression instructions. +#[derive(Debug, Clone)] +pub struct AtaDecompressionContext { + /// Full compressed token account from indexer. + pub compressed: CompressedTokenAccount, + /// Wallet owner (signer for decompression). + pub wallet_owner: Pubkey, + /// Token mint. + pub mint: Pubkey, + /// ATA derivation bump. + pub bump: u8, +} + +/// Raw ATA account interface - Account bytes are ALWAYS present. +/// +/// For hot accounts: actual on-chain bytes. +/// For cold accounts: synthetic SPL Token Account format bytes. +/// +/// Use `parse_token_account_interface()` to extract typed `TokenData`. +#[derive(Debug, Clone)] +pub struct AtaAccountInterface { + /// The ATA pubkey. + pub pubkey: Pubkey, + /// Raw Solana Account - always present. + /// Hot: actual on-chain bytes. + /// Cold: synthetic bytes (TokenData packed as SPL Token Account format). + pub account: Account, + /// Whether this account is compressed (needs decompression). + pub is_cold: bool, + /// Decompression context (only if cold). + pub decompression_context: Option, +} + +/// Pack TokenData into SPL Token Account format bytes (165 bytes). +pub fn pack_token_data_to_spl_bytes( + mint: &Pubkey, + owner: &Pubkey, + token_data: &TokenData, +) -> [u8; 165] { + use solana_program::program_pack::Pack; + let spl_account = SplTokenAccount { + mint: *mint, + owner: *owner, + amount: token_data.amount, + delegate: token_data.delegate.into(), + state: match token_data.state { + AccountState::Frozen => spl_token_2022::state::AccountState::Frozen, + _ => spl_token_2022::state::AccountState::Initialized, + }, + is_native: solana_program::program_option::COption::None, + delegated_amount: 0, + close_authority: solana_program::program_option::COption::None, + }; + let mut buf = [0u8; 165]; + SplTokenAccount::pack(spl_account, &mut buf).expect("pack should never fail"); + buf +} + +// ============================================================================ +// Parsed Token Account Interface +// ============================================================================ + +/// Parsed token account with decompression metadata. +/// +/// Returned by `parse_token_account_interface()`. +/// If `is_cold` is true (or `decompression_context` is Some), the account +/// needs decompression before it can be used on-chain. +#[derive(Debug, Clone)] +pub struct TokenAccountInterface { + /// Parsed token data (standard SPL-compatible type). + pub token_data: TokenData, + /// Whether this account is compressed. + pub is_cold: bool, + /// Decompression context if cold (contains all data for instruction building). + pub decompression_context: Option, +} + +impl TokenAccountInterface { + /// Convenience: get amount. + #[inline] + pub fn amount(&self) -> u64 { + self.token_data.amount + } + + /// Convenience: get delegate. + #[inline] + pub fn delegate(&self) -> Option { + self.token_data.delegate + } + + /// Convenience: get state. + #[inline] + pub fn state(&self) -> AccountState { + self.token_data.state + } + + /// Returns the compressed account hash if cold (for validity proof). + pub fn hash(&self) -> Option<[u8; 32]> { + self.decompression_context + .as_ref() + .map(|d| d.compressed.account.hash) + } +} + +/// Parse raw account interface into typed TokenAccountInterface. +/// +/// For hot accounts: unpacks SPL Token Account bytes. +/// For cold accounts: uses TokenData from decompression context. +pub fn parse_token_account_interface( + interface: &AtaAccountInterface, +) -> Result { + use solana_program::program_pack::Pack; + + if interface.is_cold { + // Cold: use TokenData from decompression context + let ctx = interface + .decompression_context + .as_ref() + .ok_or(DecompressAtaError::InvalidAccountData)?; + + Ok(TokenAccountInterface { + token_data: ctx.compressed.token.clone(), + is_cold: true, + decompression_context: Some(ctx.clone()), + }) + } else { + // Hot: unpack SPL Token Account from raw bytes + let data = &interface.account.data; + if data.len() < 165 { + return Err(DecompressAtaError::InvalidAccountData); + } + + let spl_account = SplTokenAccount::unpack(&data[..165]) + .map_err(|_| DecompressAtaError::InvalidAccountData)?; + + let token_data = TokenData { + mint: spl_account.mint, + owner: spl_account.owner, + amount: spl_account.amount, + delegate: spl_account.delegate.into(), + state: match spl_account.state { + spl_token_2022::state::AccountState::Frozen => AccountState::Frozen, + _ => AccountState::Initialized, + }, + tlv: None, + }; + + Ok(TokenAccountInterface { + token_data, + is_cold: false, + decompression_context: None, + }) + } +} + +// ============================================================================ +// Legacy AtaInterface (for backward compatibility) +// ============================================================================ + +/// Legacy decompression context. +#[derive(Debug, Clone)] +pub struct DecompressionContext { + pub compressed: CompressedTokenAccount, +} + +/// Legacy ATA interface. +/// Prefer `AtaAccountInterface` + `parse_token_account_interface()` for new code. +#[derive(Debug, Clone)] +pub struct AtaInterface { + pub ata: Pubkey, + pub owner: Pubkey, + pub mint: Pubkey, + pub bump: u8, + pub is_cold: bool, + pub token_data: TokenData, + pub raw_account: Option, + pub decompression: Option, +} + +impl AtaInterface { + #[inline] + pub fn is_cold(&self) -> bool { + self.is_cold + } + + #[inline] + pub fn is_hot(&self) -> bool { + self.raw_account.is_some() + } + + #[inline] + pub fn is_none(&self) -> bool { + !self.is_cold && self.raw_account.is_none() + } + + pub fn hash(&self) -> Option<[u8; 32]> { + self.decompression + .as_ref() + .map(|d| d.compressed.account.hash) + } + + pub fn account(&self) -> Option<&Account> { + self.raw_account.as_ref() + } + + pub fn compressed(&self) -> Option<&CompressedTokenAccount> { + self.decompression.as_ref().map(|d| &d.compressed) + } + + #[inline] + pub fn amount(&self) -> u64 { + self.token_data.amount + } + + #[inline] + pub fn delegate(&self) -> Option { + self.token_data.delegate + } + + #[inline] + pub fn state(&self) -> AccountState { + self.token_data.state + } +} + +/// Internal context for each ATA to decompress. +struct InternalAtaDecompressContext { + token_account: CompressedTokenAccount, + ata_pubkey: Pubkey, + wallet_owner: Pubkey, + ata_bump: u8, +} + +// ============================================================================ +// New API: TokenAccountInterface-based +// ============================================================================ + +/// Builds decompress instructions from parsed TokenAccountInterfaces (sync). +/// +/// High-performance API pattern: +/// 1. Fetch raw accounts: `get_ata_account_interface()` +/// 2. Parse: `parse_token_account_interface()` +/// 3. Get proof for cold accounts (async) +/// 4. Build instructions (this function, sync) +/// +/// Returns empty vec if all accounts are hot - fast exit. +/// +/// # Example +/// ```ignore +/// // 1. Fetch raw account interfaces (async) +/// let account = rpc.get_ata_account_interface(&mint, &owner).await?; +/// +/// // 2. Parse into token account interface (sync) +/// let parsed = parse_token_account_interface(&account)?; +/// +/// // 3. Collect cold hashes for proof +/// let cold_hashes: Vec<_> = [&parsed].iter() +/// .filter_map(|p| p.hash()) +/// .collect(); +/// +/// // 4. If any cold, get proof (async) +/// let proof = if cold_hashes.is_empty() { +/// None +/// } else { +/// Some(rpc.get_validity_proof(cold_hashes, vec![], None).await?.value) +/// }; +/// +/// // 5. Build instructions (sync) +/// let instructions = build_decompress_token_accounts(&[parsed], fee_payer, proof)?; +/// ``` +pub fn build_decompress_token_accounts( + token_accounts: &[TokenAccountInterface], + fee_payer: Pubkey, + validity_proof: Option, +) -> Result, DecompressAtaError> { + let mut cold_contexts: Vec = Vec::new(); + let mut create_ata_instructions = Vec::new(); + + for token_account in token_accounts.iter() { + if let Some(ctx) = &token_account.decompression_context { + // Derive ATA for destination + let (ata_pubkey, _) = derive_token_ata(&ctx.wallet_owner, &ctx.mint); + + // Create ATA idempotently + let create_ata = + CreateAssociatedTokenAccount::new(fee_payer, ctx.wallet_owner, ctx.mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + cold_contexts.push(InternalAtaDecompressContext { + token_account: ctx.compressed.clone(), + ata_pubkey, + wallet_owner: ctx.wallet_owner, + ata_bump: ctx.bump, + }); + } + } + + // Fast exit if all hot + if cold_contexts.is_empty() { + return Ok(vec![]); + } + + // Proof required for cold accounts + let proof = validity_proof.ok_or(DecompressAtaError::ProofRequired)?; + + // Build decompress instruction + let decompress_ix = build_batch_decompress_instruction(fee_payer, &cold_contexts, proof)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +/// Async wrapper: decompress parsed TokenAccountInterfaces. +/// +/// Takes parsed interfaces, fetches proof internally, builds instructions. +/// Returns empty vec if all accounts are hot - fast exit. +/// +/// # Example +/// ```ignore +/// // Fetch and parse +/// let account = rpc.get_ata_account_interface(&mint, &owner).await?; +/// let parsed = parse_token_account_interface(&account)?; +/// +/// // Decompress (fetches proof internally if needed) +/// let instructions = decompress_token_accounts(&[parsed], fee_payer, &rpc).await?; +/// ``` +pub async fn decompress_token_accounts( + token_accounts: &[TokenAccountInterface], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let cold_hashes: Vec<[u8; 32]> = token_accounts.iter().filter_map(|a| a.hash()).collect(); + + if cold_hashes.is_empty() { + return Ok(vec![]); + } + + let proof = indexer + .get_validity_proof(cold_hashes, vec![], None) + .await? + .value; + + build_decompress_token_accounts(token_accounts, fee_payer, Some(proof)) +} + +// ============================================================================ +// Legacy API: AtaInterface-based (backward compatibility) +// ============================================================================ + +/// Builds decompress instructions for ATAs synchronously (legacy API). +/// +/// Prefer `build_decompress_token_accounts` with `TokenAccountInterface` for new code. +pub fn build_decompress_atas( + atas: &[AtaInterface], + fee_payer: Pubkey, + validity_proof: Option, +) -> Result, DecompressAtaError> { + let mut cold_contexts: Vec = Vec::new(); + let mut create_ata_instructions = Vec::new(); + + for ata in atas.iter() { + if ata.is_cold { + if let Some(decompression) = &ata.decompression { + let create_ata = CreateAssociatedTokenAccount::new(fee_payer, ata.owner, ata.mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + cold_contexts.push(InternalAtaDecompressContext { + token_account: decompression.compressed.clone(), + ata_pubkey: ata.ata, + wallet_owner: ata.owner, + ata_bump: ata.bump, + }); + } + } + } + + if cold_contexts.is_empty() { + return Ok(vec![]); + } + + let proof = validity_proof.ok_or(DecompressAtaError::ProofRequired)?; + let decompress_ix = build_batch_decompress_instruction(fee_payer, &cold_contexts, proof)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +/// Async wrapper for legacy AtaInterface API. +pub async fn decompress_atas( + atas: &[AtaInterface], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let cold_hashes: Vec<[u8; 32]> = atas.iter().filter_map(|a| a.hash()).collect(); + + if cold_hashes.is_empty() { + return Ok(vec![]); + } + + let proof = indexer + .get_validity_proof(cold_hashes, vec![], None) + .await? + .value; + + build_decompress_atas(atas, fee_payer, Some(proof)) +} + +/// Decompresses ATA-owned compressed tokens for multiple (mint, owner) pairs. +/// +/// This is a convenience async API that fetches state and proof internally. +/// For high-performance apps, use `build_decompress_atas` with pre-fetched state. +/// +/// For each (mint, wallet_owner) pair: +/// 1. Derives the ATA address +/// 2. Fetches compressed token accounts owned by that ATA +/// 3. Gets a single validity proof for all accounts +/// 4. Creates destination ATAs if needed (idempotent) +/// 5. Builds single decompress instruction +/// +/// # Arguments +/// * `mint_owner_pairs` - List of (mint, wallet_owner) pairs to decompress +/// * `fee_payer` - Fee payer pubkey +/// * `indexer` - Indexer for fetching accounts and proofs +/// +/// # Returns +/// * Vec of instructions: [create_ata_idempotent..., decompress_all] +/// * Returns empty vec if no compressed tokens found +pub async fn decompress_atas_idempotent( + mint_owner_pairs: &[(Pubkey, Pubkey)], + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressAtaError> { + let mut create_ata_instructions = Vec::new(); + let mut all_accounts: Vec = Vec::new(); + + // Phase 1: Gather compressed token accounts and prepare ATA creation + for (mint, wallet_owner) in mint_owner_pairs { + let (ata_pubkey, ata_bump) = derive_token_ata(wallet_owner, mint); + + // Query compressed tokens owned by this ATA + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); + let result = indexer + .get_compressed_token_accounts_by_owner(&ata_pubkey, options, None) + .await?; + + let accounts = result.value.items; + if accounts.is_empty() { + continue; + } + + // Create ATA idempotently + let create_ata = CreateAssociatedTokenAccount::new(fee_payer, *wallet_owner, *mint) + .idempotent() + .instruction()?; + create_ata_instructions.push(create_ata); + + // Collect context for each account + for acc in accounts { + all_accounts.push(InternalAtaDecompressContext { + token_account: acc, + ata_pubkey, + wallet_owner: *wallet_owner, + ata_bump, + }); + } + } + + if all_accounts.is_empty() { + return Ok(create_ata_instructions); + } + + // Phase 2: Get validity proof for all accounts + let hashes: Vec<[u8; 32]> = all_accounts + .iter() + .map(|ctx| ctx.token_account.account.hash) + .collect(); + + let proof_result = indexer + .get_validity_proof(hashes, vec![], None) + .await? + .value; + + // Phase 3: Build decompress instruction + let decompress_ix = build_batch_decompress_instruction(fee_payer, &all_accounts, proof_result)?; + + let mut instructions = create_ata_instructions; + instructions.push(decompress_ix); + Ok(instructions) +} + +fn build_batch_decompress_instruction( + fee_payer: Pubkey, + accounts: &[InternalAtaDecompressContext], + proof: ValidityProofWithContext, +) -> Result { + let mut packed_accounts = PackedAccounts::default(); + + // Pack tree infos first (inserts trees and queues) + let packed_tree_infos = proof.pack_tree_infos(&mut packed_accounts); + let tree_infos = packed_tree_infos + .state_trees + .as_ref() + .ok_or(DecompressAtaError::NoStateTreesInProof)?; + + let mut token_accounts_vec = Vec::with_capacity(accounts.len()); + let mut in_tlv_data: Vec> = Vec::with_capacity(accounts.len()); + let mut has_any_tlv = false; + + for (i, ctx) in accounts.iter().enumerate() { + let token = &ctx.token_account.token; + let tree_info = &tree_infos.packed_tree_infos[i]; + + // Insert wallet_owner as signer (for ATA, wallet signs, not ATA pubkey) + let owner_index = packed_accounts.insert_or_get_config(ctx.wallet_owner, true, false); + + // Insert ATA pubkey (as the token owner in TokenData - not a signer!) + let ata_index = packed_accounts.insert_or_get(ctx.ata_pubkey); + + // Insert mint + let mint_index = packed_accounts.insert_or_get(token.mint); + + // Insert delegate if present + let delegate_index = token + .delegate + .map(|d| packed_accounts.insert_or_get(d)) + .unwrap_or(0); + + // Insert destination ATA (same as ata_index since we decompress to the same ATA) + let destination_index = ata_index; + + // Build MultiInputTokenDataWithContext + // NOTE: prove_by_index comes from tree_info (the proof), not account (the query) + // The query may have stale prove_by_index values, but the proof is authoritative. + let source = MultiInputTokenDataWithContext { + owner: ata_index, // Token owner is ATA pubkey (not wallet!) + amount: token.amount, + has_delegate: token.delegate.is_some(), + delegate: delegate_index, + mint: mint_index, + version: TokenDataVersion::ShaFlat as u8, + merkle_context: PackedMerkleContext { + merkle_tree_pubkey_index: tree_info.merkle_tree_pubkey_index, + queue_pubkey_index: tree_info.queue_pubkey_index, + prove_by_index: tree_info.prove_by_index, + leaf_index: tree_info.leaf_index, + }, + root_index: tree_info.root_index, + }; + + // Build CTokenAccount2 for decompress + let mut ctoken_account = CTokenAccount2::new(vec![source])?; + ctoken_account.decompress(token.amount, destination_index)?; + token_accounts_vec.push(ctoken_account); + + // Build TLV for this input (CompressedOnly extension for ATAs) + let is_frozen = token.state == AccountState::Frozen; + let tlv_vec: Vec = token + .tlv + .as_ref() + .map(|exts| { + exts.iter() + .filter_map(|ext| match ext { + ExtensionStruct::CompressedOnly(co) => { + Some(ExtensionInstructionData::CompressedOnly( + CompressedOnlyExtensionInstructionData { + delegated_amount: co.delegated_amount, + withheld_transfer_fee: co.withheld_transfer_fee, + is_frozen, + compression_index: 0, + is_ata: true, + bump: ctx.ata_bump, + owner_index, // Wallet owner who signs + }, + )) + } + _ => None, + }) + .collect() + }) + .unwrap_or_default(); + + if !tlv_vec.is_empty() { + has_any_tlv = true; + } + in_tlv_data.push(tlv_vec); + } + + // Convert packed_accounts to AccountMetas + let (packed_account_metas, _, _) = packed_accounts.to_account_metas(); + + // Build Transfer2 instruction + let meta_config = Transfer2AccountsMetaConfig::new(fee_payer, packed_account_metas); + let transfer_config = Transfer2Config::default().filter_zero_amount_outputs(); + + let inputs = Transfer2Inputs { + meta_config, + token_accounts: token_accounts_vec, + transfer_config, + validity_proof: proof.proof, + in_tlv: if has_any_tlv { Some(in_tlv_data) } else { None }, + ..Default::default() + }; + + create_transfer2_instruction(inputs).map_err(DecompressAtaError::from) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_derive_ata() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_token_ata(&wallet, &mint); + assert_ne!(ata, wallet); + assert_ne!(ata, mint); + let _ = bump; + } + + #[test] + fn test_ata_interface_is_cold() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_token_ata(&wallet, &mint); + + let hot_ata = AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData { + mint, + owner: ata, + amount: 100, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }, + raw_account: Some(Account::default()), + decompression: None, + }; + assert!(!hot_ata.is_cold()); + assert!(hot_ata.is_hot()); + assert_eq!(hot_ata.amount(), 100); + + let none_ata = AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData::default(), + raw_account: None, + decompression: None, + }; + assert!(!none_ata.is_cold()); + assert!(!none_ata.is_hot()); + assert!(none_ata.is_none()); + } + + #[test] + fn test_build_decompress_atas_fast_exit() { + let wallet = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let (ata, bump) = derive_token_ata(&wallet, &mint); + + // All hot - should return empty vec + let hot_atas = vec![AtaInterface { + ata, + owner: wallet, + mint, + bump, + is_cold: false, + token_data: TokenData { + mint, + owner: ata, + amount: 50, + delegate: None, + state: AccountState::Initialized, + tlv: None, + }, + raw_account: Some(Account::default()), + decompression: None, + }]; + + let result = build_decompress_atas(&hot_atas, wallet, None).unwrap(); + assert!(result.is_empty()); + } +} diff --git a/sdk-libs/compressible-client/src/decompress_mint.rs b/sdk-libs/compressible-client/src/decompress_mint.rs new file mode 100644 index 0000000000..42bc878653 --- /dev/null +++ b/sdk-libs/compressible-client/src/decompress_mint.rs @@ -0,0 +1,413 @@ +//! Decompress compressed CMint accounts. +//! +//! This module provides client-side functionality to decompress compressed +//! CMint accounts (mints created via `#[compressible]` macro that have been +//! auto-compressed by forester). +//! +//! DecompressMint is permissionless - any fee_payer can decompress any +//! compressed mint. The mint_seed_pubkey is required for PDA derivation. +//! +//! Three APIs are provided: +//! - `decompress_mint`: Simple async API (fetches state + proof internally) +//! - `build_decompress_mint`: Sync, caller provides pre-fetched state + proof +//! - `decompress_mint`: High-perf wrapper (takes MintInterface, fetches proof internally) + +use borsh::BorshDeserialize; +use light_client::indexer::{CompressedAccount, Indexer, IndexerError, ValidityProofWithContext}; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_token_interface::{ + instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, + state::CompressedMint, + CMINT_ADDRESS_TREE, +}; +use light_token_sdk::{ + compressed_token::create_compressed_mint::derive_mint_compressed_address, + token::{find_mint_address, DecompressMint}, +}; +use solana_account::Account; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; +use thiserror::Error; + +/// Error type for decompress mint operations. +#[derive(Debug, Error)] +pub enum DecompressMintError { + #[error("Indexer error: {0}")] + Indexer(#[from] IndexerError), + + #[error("Compressed mint not found for signer {signer:?}")] + MintNotFound { signer: Pubkey }, + + #[error("Missing compressed mint data in account")] + MissingMintData, + + #[error("Program error: {0}")] + ProgramError(#[from] ProgramError), + + #[error("Proof required for cold mint")] + ProofRequired, +} + +/// State of a CMint - either on-chain (hot), compressed (cold), or non-existent. +#[derive(Debug, Clone)] +#[allow(clippy::large_enum_variant)] +pub enum MintState { + /// CMint exists on-chain - no decompression needed. + Hot { account: Account }, + /// CMint is compressed - needs decompression. + Cold { + compressed: CompressedAccount, + mint_data: CompressedMint, + }, + /// CMint doesn't exist (neither on-chain nor compressed). + None, +} + +/// Interface for a CMint that provides all info needed for decompression. +/// +/// This is a superset of the solana Account type, containing: +/// - CMint pubkey (derived from signer) +/// - Signer pubkey (mint authority seed) +/// - State: Hot (on-chain), Cold (compressed), or None +#[derive(Debug, Clone)] +pub struct MintInterface { + /// The CMint PDA pubkey. + pub cmint: Pubkey, + /// The mint signer pubkey (used to derive CMint). + pub signer: Pubkey, + /// Address tree where compressed mint lives. + pub address_tree: Pubkey, + /// Compressed address (for proof). + pub compressed_address: [u8; 32], + /// Current state of the CMint. + pub state: MintState, +} + +impl MintInterface { + /// Returns true if this CMint needs decompression (is cold). + #[inline] + pub fn is_cold(&self) -> bool { + matches!(self.state, MintState::Cold { .. }) + } + + /// Returns true if this CMint exists on-chain (is hot). + #[inline] + pub fn is_hot(&self) -> bool { + matches!(self.state, MintState::Hot { .. }) + } + + /// Returns the compressed account hash if cold. + pub fn hash(&self) -> Option<[u8; 32]> { + match &self.state { + MintState::Cold { compressed, .. } => Some(compressed.hash), + _ => None, + } + } + + /// Returns the on-chain account if hot. + pub fn account(&self) -> Option<&Account> { + match &self.state { + MintState::Hot { account } => Some(account), + _ => None, + } + } + + /// Returns the compressed account and mint data if cold. + pub fn compressed(&self) -> Option<(&CompressedAccount, &CompressedMint)> { + match &self.state { + MintState::Cold { + compressed, + mint_data, + } => Some((compressed, mint_data)), + _ => None, + } + } +} + +/// Default rent payment in epochs (~24 hours per epoch) +pub const DEFAULT_RENT_PAYMENT: u8 = 2; +/// Default write top-up lamports (~3 hours rent per write) +pub const DEFAULT_WRITE_TOP_UP: u32 = 766; + +/// Builds decompress instruction for a CMint synchronously. +/// +/// This is a high-performance API for apps that pre-fetch mint state. +/// Returns empty vec if mint is hot (on-chain) - fast exit. +/// +/// # Arguments +/// * `mint` - Pre-fetched MintInterface (from `get_mint_interface`) +/// * `fee_payer` - Fee payer pubkey +/// * `validity_proof` - Proof for cold mint (required if cold, ignored if hot) +/// * `rent_payment` - Rent payment in epochs (default: 2) +/// * `write_top_up` - Lamports for future writes (default: 766) +/// +/// # Returns +/// * Vec with single decompress instruction +/// * Empty vec if mint is hot +pub fn build_decompress_mint( + mint: &MintInterface, + fee_payer: Pubkey, + validity_proof: Option, + rent_payment: Option, + write_top_up: Option, +) -> Result, DecompressMintError> { + // Fast exit if hot + let mint_data = match &mint.state { + MintState::Hot { .. } | MintState::None => return Ok(vec![]), + MintState::Cold { mint_data, .. } => mint_data, + }; + + // Check if already decompressed flag is set - return empty vec (idempotent) + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + + // Proof required for cold mint + let proof_result = validity_proof.ok_or(DecompressMintError::ProofRequired)?; + + // Extract tree info from proof result + let account_info = &proof_result.accounts[0]; + let state_tree = account_info.tree_info.tree; + let input_queue = account_info.tree_info.queue; + let output_queue = account_info + .tree_info + .next_tree_info + .as_ref() + .map(|next| next.queue) + .unwrap_or(input_queue); + + // Build CompressedMintWithContext + let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data.clone()) + .map_err(|_| DecompressMintError::MissingMintData)?; + + let compressed_mint_with_context = CompressedMintWithContext { + leaf_index: account_info.leaf_index as u32, + prove_by_index: account_info.root_index.proof_by_index(), + root_index: account_info.root_index.root_index().unwrap_or_default(), + address: mint.compressed_address, + mint: Some(mint_instruction_data), + }; + + // Build DecompressMint instruction + let decompress = DecompressMint { + payer: fee_payer, + authority: fee_payer, // Permissionless - any signer works + state_tree, + input_queue, + output_queue, + compressed_mint_with_context, + proof: ValidityProof(proof_result.proof.into()), + rent_payment: rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), + write_top_up: write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), + }; + + let ix = decompress + .instruction() + .map_err(DecompressMintError::from)?; + Ok(vec![ix]) +} + +/// High-performance wrapper: decompress pre-fetched mint. +/// +/// Takes pre-fetched `MintInterface`, fetches proof internally, builds instruction. +/// Returns empty vec if mint is hot (on-chain) - fast exit. +/// +/// # Example +/// ```ignore +/// // Pre-fetch mint state +/// let mint = rpc.get_mint_interface(&signer).await?; +/// +/// // Decompress if cold (fetches proof internally) +/// let instructions = decompress_mint(&mint, fee_payer, &rpc).await?; +/// ``` +pub async fn decompress_mint( + mint: &MintInterface, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + // Fast exit if hot or doesn't exist + let hash = match mint.hash() { + Some(h) => h, + None => return Ok(vec![]), + }; + + // Check decompressed flag before fetching proof + if let Some((_, mint_data)) = mint.compressed() { + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + } + + // Get validity proof + let proof = indexer + .get_validity_proof(vec![hash], vec![], None) + .await? + .value; + + // Build instruction (sync) + build_decompress_mint(mint, fee_payer, Some(proof), None, None) +} + +/// Request to decompress a compressed CMint. +#[derive(Debug, Clone)] +pub struct DecompressMintRequest { + /// The seed pubkey used to derive the CMint PDA. + /// This is the same value passed as `mint_signer` when the mint was created. + pub mint_seed_pubkey: Pubkey, + /// Address tree where the compressed mint was created. + /// If None, uses the default cmint address tree. + pub address_tree: Option, + /// Rent payment in epochs (must be 0 or >= 2). Default: 2 + pub rent_payment: Option, + /// Lamports for future write operations. Default: 766 + pub write_top_up: Option, +} + +impl DecompressMintRequest { + pub fn new(mint_seed_pubkey: Pubkey) -> Self { + Self { + mint_seed_pubkey, + address_tree: None, + rent_payment: None, + write_top_up: None, + } + } + + pub fn with_address_tree(mut self, address_tree: Pubkey) -> Self { + self.address_tree = Some(address_tree); + self + } + + pub fn with_rent_payment(mut self, rent_payment: u8) -> Self { + self.rent_payment = Some(rent_payment); + self + } + + pub fn with_write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = Some(write_top_up); + self + } +} + +/// Decompresses a compressed Mint to an on-chain Mint Solana account. +/// +/// This is permissionless - any fee_payer can decompress any compressed mint. +/// Returns empty vec if already decompressed (idempotent). +pub async fn decompress_mint_idempotent( + request: DecompressMintRequest, + fee_payer: Pubkey, + indexer: &I, +) -> Result, DecompressMintError> { + // 1. Derive addresses + let address_tree = request + .address_tree + .unwrap_or(Pubkey::new_from_array(CMINT_ADDRESS_TREE)); + let compressed_address = + derive_mint_compressed_address(&request.mint_seed_pubkey, &address_tree); + + // 2. Fetch compressed mint account from indexer + let compressed_account = indexer + .get_compressed_account(compressed_address, None) + .await? + .value + .ok_or(DecompressMintError::MintNotFound { + signer: request.mint_seed_pubkey, + })?; + + // 3. Check if data is empty (already decompressed - empty shell remains) + // After decompression, the compressed account has empty data but the address persists. + let data = match compressed_account.data.as_ref() { + Some(d) if !d.data.is_empty() => d, + _ => return Ok(vec![]), // Empty data = already decompressed (idempotent) + }; + + // 4. Parse mint data from compressed account + let mint_data = CompressedMint::try_from_slice(&data.data) + .map_err(|_| DecompressMintError::MissingMintData)?; + + // 5. Check if already decompressed flag is set - return empty vec (idempotent) + if mint_data.metadata.cmint_decompressed { + return Ok(vec![]); + } + + // 5. Get validity proof + let proof_result = indexer + .get_validity_proof(vec![compressed_account.hash], vec![], None) + .await? + .value; + + // 6. Extract tree info from proof result + let account_info = &proof_result.accounts[0]; + let state_tree = account_info.tree_info.tree; + let input_queue = account_info.tree_info.queue; + let output_queue = account_info + .tree_info + .next_tree_info + .as_ref() + .map(|next| next.queue) + .unwrap_or(input_queue); + + // 7. Build CompressedMintWithContext + // NOTE: prove_by_index and leaf_index come from account_info (the proof), not compressed_account + // The query may have stale values, but the proof is authoritative. + let mint_instruction_data = CompressedMintInstructionData::try_from(mint_data) + .map_err(|_| DecompressMintError::MissingMintData)?; + + let compressed_mint_with_context = CompressedMintWithContext { + leaf_index: account_info.leaf_index as u32, + prove_by_index: account_info.root_index.proof_by_index(), + root_index: account_info.root_index.root_index().unwrap_or_default(), + address: compressed_address, + mint: Some(mint_instruction_data), + }; + + // 8. Build DecompressMint instruction + let decompress = DecompressMint { + payer: fee_payer, + authority: fee_payer, // Permissionless - any signer works + state_tree, + input_queue, + output_queue, + compressed_mint_with_context, + proof: ValidityProof(proof_result.proof.into()), + rent_payment: request.rent_payment.unwrap_or(DEFAULT_RENT_PAYMENT), + write_top_up: request.write_top_up.unwrap_or(DEFAULT_WRITE_TOP_UP), + }; + + let ix = decompress + .instruction() + .map_err(DecompressMintError::from)?; + Ok(vec![ix]) +} + +/// Derive MintInterface from signer pubkey and on-chain/compressed state. +/// Helper for creating MintInterface when you have the data. +pub fn create_mint_interface( + signer: Pubkey, + address_tree: Pubkey, + onchain_account: Option, + compressed: Option<(CompressedAccount, CompressedMint)>, +) -> MintInterface { + let (cmint, _) = find_mint_address(&signer); + let compressed_address = derive_mint_compressed_address(&signer, &address_tree); + + let state = if let Some(account) = onchain_account { + MintState::Hot { account } + } else if let Some((compressed, mint_data)) = compressed { + MintState::Cold { + compressed, + mint_data, + } + } else { + MintState::None + }; + + MintInterface { + cmint, + signer, + address_tree, + compressed_address, + state, + } +} diff --git a/sdk-libs/compressible-client/src/initialize_config.rs b/sdk-libs/compressible-client/src/initialize_config.rs new file mode 100644 index 0000000000..97a0756c0b --- /dev/null +++ b/sdk-libs/compressible-client/src/initialize_config.rs @@ -0,0 +1,164 @@ +//! Helper for initializing compression config with sensible defaults. + +#[cfg(feature = "anchor")] +use anchor_lang::{AnchorDeserialize, AnchorSerialize}; +#[cfg(not(feature = "anchor"))] +use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +use light_sdk::compressible::config::CompressibleConfig; +use solana_instruction::{AccountMeta, Instruction}; +use solana_pubkey::Pubkey; + +/// Default address tree v2 pubkey. +pub const ADDRESS_TREE_V2: Pubkey = + solana_pubkey::pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx"); + +/// Default write top-up value (5000 lamports). +pub const DEFAULT_INIT_WRITE_TOP_UP: u32 = 5_000; + +/// Instruction data format matching anchor-generated `initialize_compression_config`. +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct InitializeCompressionConfigAnchorData { + pub write_top_up: u32, + pub rent_sponsor: Pubkey, + pub compression_authority: Pubkey, + pub rent_config: light_compressible::rent::RentConfig, + pub address_space: Vec, +} + +/// Builder for creating `initialize_compression_config` instruction with sensible defaults. +/// +/// Uses: +/// - Address tree v2 (`amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx`) +/// - Default rent config +/// - Default write top-up (5000 lamports) +/// +/// # Example +/// ```ignore +/// let (instruction, config_pda) = InitializeRentFreeConfig::new( +/// &program_id, +/// &fee_payer, +/// &program_data_pda, +/// rent_sponsor_pubkey, +/// compression_authority_pubkey, +/// ).build(); +/// ``` +pub struct InitializeRentFreeConfig { + program_id: Pubkey, + fee_payer: Pubkey, + program_data_pda: Pubkey, + authority: Option, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + rent_config: light_compressible::rent::RentConfig, + write_top_up: u32, + address_space: Vec, + config_bump: u8, +} + +impl InitializeRentFreeConfig { + /// Creates a new builder with required fields and default values. + /// + /// # Arguments + /// * `program_id` - The program that owns the compression config + /// * `fee_payer` - The account paying for the transaction + /// * `program_data_pda` - The program data PDA (BPF upgradeable loader) + /// * `rent_sponsor` - The rent sponsor pubkey + /// * `compression_authority` - The compression authority pubkey + pub fn new( + program_id: &Pubkey, + fee_payer: &Pubkey, + program_data_pda: &Pubkey, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + ) -> Self { + Self { + program_id: *program_id, + fee_payer: *fee_payer, + program_data_pda: *program_data_pda, + authority: None, + rent_sponsor, + compression_authority, + rent_config: light_compressible::rent::RentConfig::default(), + write_top_up: DEFAULT_INIT_WRITE_TOP_UP, + address_space: vec![ADDRESS_TREE_V2], + config_bump: 0, + } + } + + /// Sets the authority signer (defaults to fee_payer if not set). + pub fn authority(mut self, authority: Pubkey) -> Self { + self.authority = Some(authority); + self + } + + /// Overrides the default rent config. + pub fn rent_config(mut self, rent_config: light_compressible::rent::RentConfig) -> Self { + self.rent_config = rent_config; + self + } + + /// Overrides the default write top-up value. + pub fn write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = write_top_up; + self + } + + /// Overrides the default address space (address tree v2). + pub fn address_space(mut self, address_space: Vec) -> Self { + self.address_space = address_space; + self + } + + /// Sets the config bump (default 0). + pub fn config_bump(mut self, config_bump: u8) -> Self { + self.config_bump = config_bump; + self + } + + /// Builds the instruction and returns (instruction, config_pda). + /// + /// The returned instruction is ready to send with Anchor's generated discriminator. + pub fn build(self) -> (Instruction, Pubkey) { + let authority = self.authority.unwrap_or(self.fee_payer); + let (config_pda, _) = CompressibleConfig::derive_pda(&self.program_id, self.config_bump); + + let accounts = vec![ + AccountMeta::new(self.fee_payer, true), // payer + AccountMeta::new(config_pda, false), // config + AccountMeta::new_readonly(self.program_data_pda, false), // program_data + AccountMeta::new_readonly(authority, true), // authority + AccountMeta::new_readonly( + solana_pubkey::pubkey!("11111111111111111111111111111111"), + false, + ), // system_program + ]; + + let instruction_data = InitializeCompressionConfigAnchorData { + write_top_up: self.write_top_up, + rent_sponsor: self.rent_sponsor, + compression_authority: self.compression_authority, + rent_config: self.rent_config, + address_space: self.address_space, + }; + + // Anchor discriminator for "initialize_compression_config" + // SHA256("global:initialize_compression_config")[..8] + const DISCRIMINATOR: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; + + let serialized_data = instruction_data + .try_to_vec() + .expect("Failed to serialize instruction data"); + + let mut data = Vec::with_capacity(DISCRIMINATOR.len() + serialized_data.len()); + data.extend_from_slice(&DISCRIMINATOR); + data.extend_from_slice(&serialized_data); + + let instruction = Instruction { + program_id: self.program_id, + accounts, + data, + }; + + (instruction, config_pda) + } +} diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index f621868df0..ebe3cb0f8f 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -1,19 +1,58 @@ +pub mod create_accounts_proof; +pub mod decompress_atas; +pub mod decompress_mint; pub mod get_compressible_account; +pub mod initialize_config; +pub mod pack; #[cfg(feature = "anchor")] use anchor_lang::{AnchorDeserialize, AnchorSerialize}; #[cfg(not(feature = "anchor"))] use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; +pub use create_accounts_proof::{ + get_create_accounts_proof, CreateAccountsProofError, CreateAccountsProofInput, + CreateAccountsProofResult, +}; +// Re-export from light-compressible for convenience (SBF-compatible definition) +pub use decompress_atas::{ + // Legacy API (backward compatible) + build_decompress_atas, + // New API (recommended) + build_decompress_token_accounts, + decompress_atas, + decompress_atas_idempotent, + decompress_token_accounts, + pack_token_data_to_spl_bytes, + parse_token_account_interface, + AtaAccountInterface, + AtaDecompressionContext, + AtaInterface, + DecompressAtaError, + DecompressionContext, + TokenAccountInterface, +}; +// Re-export TokenData for convenience (standard SPL-compatible type) +pub use decompress_mint::{ + build_decompress_mint, create_mint_interface, decompress_mint, decompress_mint_idempotent, + DecompressMintError, DecompressMintRequest, MintInterface, MintState, DEFAULT_RENT_PAYMENT, + DEFAULT_WRITE_TOP_UP, +}; +pub use initialize_config::InitializeRentFreeConfig; use light_client::indexer::{CompressedAccount, TreeInfo, ValidityProofWithContext}; +pub use light_compressible::CreateAccountsProof; pub use light_sdk::compressible::config::CompressibleConfig; use light_sdk::{ compressible::{compression_info::CompressedAccountData, Pack}, - constants::LIGHT_TOKEN_PROGRAM_ID, instruction::{ account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, SystemAccountMetaConfig, ValidityProof, }, }; +pub use light_token_sdk::compat::TokenData; +use light_token_sdk::token::{ + COMPRESSIBLE_CONFIG_V1, LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, RENT_SPONSOR, +}; +pub use pack::{pack_proof, PackError, PackedProofResult}; use solana_account::Account; use solana_instruction::{AccountMeta, Instruction}; use solana_pubkey::Pubkey; @@ -58,10 +97,178 @@ pub struct CompressAccountsIdempotentData { pub system_accounts_offset: u8, } +/// Account interface for unified hot/cold account handling. +/// Represents an account that may be on-chain (hot) or compressed (cold). +#[derive(Clone, Debug)] +pub struct AccountInterface { + /// The account's public key (PDA address) + pub pubkey: Pubkey, + /// True if the account is compressed (cold), false if on-chain (hot) + pub is_cold: bool, + /// Context needed for decompression (only present when is_cold is true) + pub decompression_context: Option, +} + +/// Context needed to decompress a compressed PDA account. +#[derive(Clone, Debug)] +pub struct PdaDecompressionContext { + /// The compressed account data from indexer + pub compressed_account: CompressedAccount, +} + +impl AccountInterface { + /// Create a new cold (compressed) account interface + pub fn cold(pubkey: Pubkey, compressed_account: CompressedAccount) -> Self { + Self { + pubkey, + is_cold: true, + decompression_context: Some(PdaDecompressionContext { compressed_account }), + } + } + + /// Create a new hot (on-chain) account interface + pub fn hot(pubkey: Pubkey) -> Self { + Self { + pubkey, + is_cold: false, + decompression_context: None, + } + } + + /// Get the compressed account data bytes if available + pub fn compressed_data(&self) -> Option<&[u8]> { + self.decompression_context + .as_ref() + .and_then(|ctx| ctx.compressed_account.data.as_ref()) + .map(|d| d.data.as_slice()) + } +} + +/// A rent-free decompression request combining account interface and variant. +/// Generic over V (the CompressedAccountVariant type from the program). +#[derive(Clone, Debug)] +pub struct RentFreeDecompressAccount { + /// The account interface (contains pubkey and cold/hot state) + pub account_interface: AccountInterface, + /// The typed variant (e.g., CompressedAccountVariant::UserRecord { ... }) + pub variant: V, +} + +impl RentFreeDecompressAccount { + /// Create a new decompression request + pub fn new(account_interface: AccountInterface, variant: V) -> Self { + Self { + account_interface, + variant, + } + } + + /// Create decompression request from account interface and seeds. + /// + /// The seeds type determines which variant constructor to call. + /// Data is extracted from interface, passed to `IntoVariant::into_variant()`. + /// + /// # Arguments + /// * `interface` - The account interface (must be cold/compressed) + /// * `seeds` - Seeds struct (e.g., `UserRecordSeeds`) that implements `IntoVariant` + /// + /// # Example + /// ```ignore + /// RentFreeDecompressAccount::from_seeds( + /// AccountInterface::cold(user_record_pda, compressed_user), + /// UserRecordSeeds { authority, mint_authority, owner, category_id }, + /// )? + /// ``` + #[cfg(feature = "anchor")] + pub fn from_seeds( + interface: AccountInterface, + seeds: S, + ) -> Result + where + S: light_sdk::compressible::IntoVariant, + { + let data = interface.compressed_data().ok_or_else(|| { + anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotInitialized) + })?; + let variant = seeds.into_variant(data)?; + Ok(Self::new(interface, variant)) + } + + /// Create decompression request for CToken account. + /// + /// Parses TokenData from interface.compressed_data() internally. + /// The CToken variant type determines how to wrap into the full variant. + /// + /// # Arguments + /// * `interface` - The account interface (must be cold/compressed) + /// * `ctoken_variant` - CToken variant (e.g., `TokenAccountVariant::Vault { cmint }`) + /// + /// # Example + /// ```ignore + /// RentFreeDecompressAccount::from_ctoken( + /// AccountInterface::cold(vault_pda, compressed_vault.account), + /// TokenAccountVariant::Vault { cmint: cmint_pda }, + /// )? + /// ``` + #[cfg(feature = "anchor")] + pub fn from_ctoken( + interface: AccountInterface, + ctoken_variant: T, + ) -> Result + where + T: light_sdk::compressible::IntoCTokenVariant, + { + use anchor_lang::AnchorDeserialize; + + let data = interface.compressed_data().ok_or_else(|| { + anchor_lang::error::Error::from(anchor_lang::error::ErrorCode::AccountNotInitialized) + })?; + let token_data = TokenData::deserialize(&mut &data[..])?; + let variant = ctoken_variant.into_ctoken_variant(token_data); + Ok(Self::new(interface, variant)) + } +} + /// Instruction builders for compressible accounts pub mod compressible_instruction { use super::*; + /// Helpers for decompress_accounts_idempotent instruction + pub mod decompress { + use super::*; + + /// Returns program account metas for decompress_accounts_idempotent with CToken support. + /// Includes ctoken_rent_sponsor, light_token_program, ctoken_cpi_authority, ctoken_config. + pub fn accounts( + fee_payer: Pubkey, + config: Pubkey, + rent_sponsor: Pubkey, + ) -> Vec { + vec![ + AccountMeta::new(fee_payer, true), + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + AccountMeta::new(RENT_SPONSOR, false), + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false), + AccountMeta::new_readonly(COMPRESSIBLE_CONFIG_V1, false), + ] + } + + /// Returns program account metas for PDA-only decompression (no CToken accounts). + pub fn accounts_pda_only( + fee_payer: Pubkey, + config: Pubkey, + rent_sponsor: Pubkey, + ) -> Vec { + vec![ + AccountMeta::new(fee_payer, true), + AccountMeta::new_readonly(config, false), + AccountMeta::new(rent_sponsor, false), + ] + } + } + /// SHA256("global:initialize_compression_config")[..8] pub const INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR: [u8; 8] = [133, 228, 12, 169, 56, 76, 222, 61]; @@ -163,9 +370,9 @@ pub mod compressible_instruction { } } - /// Builds decompress_accounts_idempotent instruction + /// Builds decompress_accounts_idempotent instruction (raw version with explicit discriminator) #[allow(clippy::too_many_arguments)] - pub fn decompress_accounts_idempotent( + pub fn build_decompress_idempotent_raw( program_id: &Pubkey, discriminator: &[u8], decompressed_account_addresses: &[Pubkey], @@ -185,7 +392,7 @@ pub mod compressible_instruction { let mut has_tokens = false; let mut has_pdas = false; for (compressed_account, _) in compressed_accounts.iter() { - if compressed_account.owner == LIGHT_TOKEN_PROGRAM_ID.into() { + if compressed_account.owner == LIGHT_TOKEN_PROGRAM_ID { has_tokens = true; } else { has_pdas = true; @@ -202,13 +409,18 @@ pub mod compressible_instruction { } // pack cpi_context_account if required. + // CRITICAL: When both PDAs and tokens exist, tokens execute LAST (consuming the CPI context). + // CPI context validation checks: cpi_context.associated_tree == first_input_of_executor.tree + // So we must use the FIRST TOKEN's cpi_context, not the first PDA's. if has_pdas && has_tokens { - let cpi_context_of_first_input = - compressed_accounts[0].0.tree_info.cpi_context.unwrap(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - cpi_context_of_first_input, - ); + // Find the first token account's CPI context + let first_token_cpi_context = compressed_accounts + .iter() + .find(|(acc, _)| acc.owner == LIGHT_TOKEN_PROGRAM_ID) + .map(|(acc, _)| acc.tree_info.cpi_context.unwrap()) + .expect("has_tokens is true so there must be a token"); + let system_config = + SystemAccountMetaConfig::new_with_cpi_context(*program_id, first_token_cpi_context); remaining_accounts.add_system_accounts_v2(system_config)?; } else { let system_config = SystemAccountMetaConfig::new(*program_id); @@ -234,19 +446,19 @@ pub mod compressible_instruction { let mut typed_compressed_accounts = Vec::with_capacity(compressed_accounts.len()); - for (compressed_account, data) in compressed_accounts { - let queue_index = remaining_accounts.insert_or_get(compressed_account.tree_info.queue); + // The compressed_accounts are expected to be in the SAME ORDER as the + // validity_proof_with_context.accounts. This is because both are derived + // from the same hash order passed to get_validity_proof(). + // We use index-based matching instead of queue+leaf_index to handle + // accounts on different trees with potentially colliding indices. + for (i, (compressed_account, data)) in compressed_accounts.iter().enumerate() { + // Insert the queue for this account (needed for the packed context) + let _queue_index = remaining_accounts.insert_or_get(compressed_account.tree_info.queue); - let tree_info = packed_tree_infos_slice - .iter() - .find(|pti| { - pti.queue_pubkey_index == queue_index - && pti.leaf_index == compressed_account.leaf_index - }) - .copied() - .ok_or( - "Matching PackedStateTreeInfo (queue_pubkey_index + leaf_index) not found", - )?; + // Use index-based matching - the i-th compressed account uses the i-th tree info + let tree_info = packed_tree_infos_slice.get(i).copied().ok_or( + "Tree info index out of bounds - compressed_accounts length must match validity proof accounts length", + )?; let packed_data = data.pack(&mut remaining_accounts); typed_compressed_accounts.push(CompressedAccountData { @@ -356,4 +568,63 @@ pub mod compressible_instruction { data, }) } + + /// Builds decompress_accounts_idempotent instruction from RentFreeDecompressAccount items. + /// Automatically filters cold accounts and returns None if no accounts need decompression. + /// + /// # Arguments + /// * `program_id` - The program ID + /// * `accounts` - Vec of RentFreeDecompressAccount (cold accounts will be decompressed, hot skipped) + /// * `program_account_metas` - Account metas from generated .to_account_metas(None) + /// * `validity_proof_with_context` - The validity proof for the cold accounts + #[allow(clippy::too_many_arguments)] + pub fn build_decompress_idempotent( + program_id: &Pubkey, + accounts: Vec>, + program_account_metas: Vec, + validity_proof_with_context: ValidityProofWithContext, + ) -> Result, Box> + where + V: Pack + Clone + std::fmt::Debug, + { + // Filter to only cold accounts + let cold_accounts: Vec<_> = accounts + .into_iter() + .filter(|a| a.account_interface.is_cold) + .collect(); + + if cold_accounts.is_empty() { + return Ok(None); + } + + // Extract pubkeys and (CompressedAccount, variant) pairs + let decompressed_account_addresses: Vec = cold_accounts + .iter() + .map(|a| a.account_interface.pubkey) + .collect(); + + let compressed_accounts: Vec<(CompressedAccount, V)> = cold_accounts + .into_iter() + .map(|a| { + let compressed_account = a + .account_interface + .decompression_context + .expect("Cold account must have decompression context") + .compressed_account; + (compressed_account, a.variant) + }) + .collect(); + + // Build instruction using raw function + let instruction = build_decompress_idempotent_raw( + program_id, + &DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, + &decompressed_account_addresses, + &compressed_accounts, + &program_account_metas, + validity_proof_with_context, + )?; + + Ok(Some(instruction)) + } } diff --git a/sdk-libs/compressible-client/src/pack.rs b/sdk-libs/compressible-client/src/pack.rs new file mode 100644 index 0000000000..180a28462e --- /dev/null +++ b/sdk-libs/compressible-client/src/pack.rs @@ -0,0 +1,120 @@ +//! Helper for packing validity proofs into remaining accounts. +//! +//! # Usage +//! +//! ```rust,ignore +//! // 1. Derive addresses & get proof +//! let proof = rpc.get_validity_proof(hashes, addresses, None).await?.value; +//! +//! // 2. Pack into remaining accounts +//! let packed = pack_proof(&program_id, proof.clone(), &output_tree, cpi_context)?; +//! +//! // 3. Build instruction +//! let ix = Instruction { +//! program_id, +//! accounts: [my_accounts.to_account_metas(None), packed.remaining_accounts].concat(), +//! data: MyInstruction { +//! proof: proof.proof, +//! address_tree_infos: packed.packed_tree_infos.address_trees, +//! output_tree_index: packed.output_tree_index, +//! }.data(), +//! }; +//! ``` + +use light_client::indexer::{TreeInfo, ValidityProofWithContext}; +use light_sdk::instruction::{PackedAccounts, SystemAccountMetaConfig}; +pub use light_sdk::instruction::{PackedAddressTreeInfo, PackedStateTreeInfo}; +use solana_instruction::AccountMeta; +use solana_pubkey::Pubkey; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum PackError { + #[error("Failed to add system accounts: {0}")] + SystemAccounts(#[from] light_sdk::error::LightSdkError), +} + +/// Packed state tree infos from validity proof. +#[derive(Clone, Default, Debug)] +pub struct PackedStateTreeInfos { + pub packed_tree_infos: Vec, + pub output_tree_index: u8, +} + +/// Packed tree infos from validity proof. +#[derive(Clone, Default, Debug)] +pub struct PackedTreeInfos { + pub state_trees: Option, + pub address_trees: Vec, +} + +/// Result of packing a validity proof into remaining accounts. +pub struct PackedProofResult { + /// Remaining accounts to append to your instruction's accounts. + pub remaining_accounts: Vec, + /// Packed tree infos from the proof. Use `.address_trees` or `.state_trees` as needed. + pub packed_tree_infos: PackedTreeInfos, + /// Index of output tree in remaining accounts. Pass to instruction data. + pub output_tree_index: u8, + /// Offset where system accounts start. Pass to instruction data if needed. + pub system_accounts_offset: u8, +} + +/// Packs a validity proof into remaining accounts for instruction building. +/// +/// Handles all the `PackedAccounts` boilerplate: +/// - Adds system accounts (with optional CPI context) +/// - Inserts output tree queue +/// - Packs tree infos from proof +/// +/// # Arguments +/// - `program_id`: Your program's ID +/// - `proof`: Validity proof from `get_validity_proof()` +/// - `output_tree`: Tree info for writing outputs (from `get_random_state_tree_info()`) +/// - `cpi_context`: CPI context pubkey. Required when mixing PDAs with tokens in same tx. +/// Get from `tree_info.cpi_context`. +/// +/// # Returns +/// `PackedProofResult` containing remaining accounts and indices for instruction data. +pub fn pack_proof( + program_id: &Pubkey, + proof: ValidityProofWithContext, + output_tree: &TreeInfo, + cpi_context: Option, +) -> Result { + let mut packed = PackedAccounts::default(); + + let system_config = match cpi_context { + Some(ctx) => SystemAccountMetaConfig::new_with_cpi_context(*program_id, ctx), + None => SystemAccountMetaConfig::new(*program_id), + }; + packed.add_system_accounts_v2(system_config)?; + + let output_queue = output_tree + .next_tree_info + .as_ref() + .map(|n| n.queue) + .unwrap_or(output_tree.queue); + let output_tree_index = packed.insert_or_get(output_queue); + + let client_packed_tree_infos = proof.pack_tree_infos(&mut packed); + let (remaining_accounts, system_offset, _) = packed.to_account_metas(); + + // Convert from light_client's types to our local types + let packed_tree_infos = PackedTreeInfos { + state_trees: client_packed_tree_infos + .state_trees + .map(|st| PackedStateTreeInfos { + packed_tree_infos: st.packed_tree_infos, + output_tree_index: st.output_tree_index, + }), + address_trees: client_packed_tree_infos.address_trees, + }; + + Ok(PackedProofResult { + remaining_accounts, + packed_tree_infos, + output_tree_index, + system_accounts_offset: system_offset as u8, + }) +} diff --git a/sdk-libs/macros/src/accounts.rs b/sdk-libs/macros/src/accounts.rs deleted file mode 100644 index a4d236e36e..0000000000 --- a/sdk-libs/macros/src/accounts.rs +++ /dev/null @@ -1,641 +0,0 @@ -use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{ - parse::{Parse, ParseStream}, - parse_quote, - punctuated::Punctuated, - token::PathSep, - Error, Expr, Fields, Ident, ItemStruct, Meta, Path, PathSegment, Result, Stmt, Token, Type, - TypePath, -}; - -pub(crate) fn process_light_system_accounts(input: ItemStruct) -> Result { - let mut output = input.clone(); - - let fields = - match output.fields { - Fields::Named(ref mut fields) => fields, - _ => return Err(Error::new_spanned( - input, - "`light_system_accounts` attribute can only be used with structs that have named fields.", - )), - }; - - let fields_to_add = [ - ("light_system_program", "AccountInfo<'info>"), - ("system_program", "Program<'info, System>"), - ("account_compression_program", "AccountInfo<'info>"), - ]; - let fields_to_add_check = [ - ("registered_program_pda", "AccountInfo<'info>"), - ("noop_program", "AccountInfo<'info>"), - ("account_compression_authority", "AccountInfo<'info>"), - ]; - let existing_field_names: Vec<_> = fields - .named - .iter() - .map(|f| f.ident.as_ref().unwrap().to_string()) - .collect(); - - // TODO: Eventually we want to provide flexibility to override. - // Until then, we error if the fields are manually defined. - for (field_name, field_type) in fields_to_add.iter().chain(fields_to_add_check.iter()) { - if existing_field_names.contains(&field_name.to_string()) { - return Err(syn::Error::new_spanned( - &output, - format!("Field `{}` already exists in the struct.", field_name), - )); - } - - let new_field = syn::Field { - attrs: vec![], - vis: syn::Visibility::Public(syn::token::Pub { - span: proc_macro2::Span::call_site(), - }), - mutability: syn::FieldMutability::None, - ident: Some(syn::Ident::new(field_name, proc_macro2::Span::call_site())), - colon_token: Some(syn::Token![:](proc_macro2::Span::call_site())), - ty: syn::parse_str(field_type)?, - }; - fields.named.push(new_field); - } - - let expanded = quote! { - #output - }; - - Ok(expanded) -} - -struct ParamTypeCheck { - ident: Ident, - ty: Type, -} - -impl ToTokens for ParamTypeCheck { - fn to_tokens(&self, tokens: &mut TokenStream) { - let Self { ident, ty } = self; - let stmt: Stmt = parse_quote! { - let #ident: &#ty = #ident; - }; - stmt.to_tokens(tokens); - } -} - -pub struct InstructionArgs { - param_type_checks: Vec, - param_names: Vec, -} - -impl Parse for InstructionArgs { - fn parse(input: ParseStream) -> Result { - let mut param_type_checks = Vec::new(); - let mut param_names = Vec::new(); - - while !input.is_empty() { - let ident = input.parse::()?; - input.parse::()?; - let ty = input.parse::()?; - - param_names.push(ident.clone()); - param_type_checks.push(ParamTypeCheck { ident, ty }); - - if input.peek(Token![,]) { - input.parse::()?; - } - } - - Ok(InstructionArgs { - param_type_checks, - param_names, - }) - } -} - -/// Takes an input struct annotated with `#[light_accounts]` attribute and -/// then: -/// -/// - Creates a separate struct with `Light` prefix and moves compressed -/// account fields (annotated with `#[light_account]` attribute) to it. As a -/// result, the original struct, later processed by Anchor macros, contains -/// only regular accounts. -/// - Creates an extention trait, with `LightContextExt` prefix, which serves -/// as an extension to `LightContext` and defines these methods: -/// - `check_constraints`, where the checks extracted from `#[light_account]` -/// attributes are performed. -/// - `derive_address_seeds`, where the seeds extracted from -/// `#[light_account]` attributes are used to derive the address. -pub(crate) fn process_light_accounts(input: ItemStruct) -> Result { - let mut anchor_accounts_strct = input.clone(); - - let (_, type_gen, _) = input.generics.split_for_impl(); - - let anchor_accounts_name = input.ident.clone(); - let light_accounts_name = Ident::new(&format!("Light{}", input.ident), Span::call_site()); - let ext_trait_name = Ident::new( - &format!("LightContextExt{}", input.ident), - Span::call_site(), - ); - let params_name = Ident::new(&format!("Params{}", input.ident), Span::call_site()); - - let instruction_params = input - .attrs - .iter() - .find(|attribute| attribute.path().is_ident("instruction")) - .map(|attribute| attribute.parse_args::()) - .transpose()?; - - let mut light_accounts_fields: Punctuated = Punctuated::new(); - - let fields = - match anchor_accounts_strct.fields { - Fields::Named(ref mut fields) => fields, - _ => return Err(Error::new_spanned( - input, - "`light_accounts` attribute can only be used with structs that have named fields.", - )), - }; - - // Fields which should belong to the Anchor instruction struct. - let mut anchor_fields = Punctuated::new(); - // Names of fields which should belong to the Anchor instruction struct. - let mut anchor_field_idents = Vec::new(); - // Names of fields which should belong to the Light instruction struct. - let mut light_field_idents = Vec::new(); - // Names of fields of the Light instruction struct, which should be - // available in constraints. - let mut light_referrable_field_idents = Vec::new(); - let mut constraint_calls = Vec::new(); - let mut derive_address_seed_calls = Vec::new(); - let mut set_address_seed_calls = Vec::new(); - - for field in fields.named.iter() { - let mut light_account = false; - for attr in &field.attrs { - if attr.path().is_ident("light_account") { - light_account = true; - } - } - - if light_account { - light_accounts_fields.push(field.clone()); - light_field_idents.push(field.ident.clone()); - - let field_ident = &field.ident; - - let mut account_args = None; - for attribute in &field.attrs { - let attribute_list = match &attribute.meta { - Meta::List(attribute_list) => attribute_list, - _ => continue, - }; - account_args = Some(syn::parse2::( - attribute_list.tokens.clone(), - )?); - break; - } - let account_args = match account_args { - Some(account_args) => account_args, - None => { - return Err(Error::new_spanned( - input, - "no arguments provided in `light_account`", - )) - } - }; - - if account_args.action != LightAccountAction::Init { - light_referrable_field_idents.push(field.ident.clone()); - } - - if let Some(constraint) = account_args.constraint { - let Constraint { expr, error } = constraint; - let error = match error { - Some(error) => error, - None => parse_quote! { - ::light_sdk::error::LightSdkError::ConstraintViolation - }, - }; - constraint_calls.push(quote! { - if ! ( #expr ) { - return ::anchor_lang::prelude::err!(#error); - } - }); - } - - let seeds = account_args.seeds; - derive_address_seed_calls.push(quote! { - let address_seed = ::light_sdk::address::derive_address_seed( - &#seeds, - &crate::ID, - ); - }); - set_address_seed_calls.push(quote! { - #field_ident.set_address_seed(address_seed); - }) - } else { - anchor_fields.push(field.clone()); - anchor_field_idents.push(field.ident.clone()); - } - } - - fields.named = anchor_fields; - - let light_accounts_strct = if light_accounts_fields.is_empty() { - quote! { - #[derive(::light_sdk::LightAccounts)] - pub struct #light_accounts_name {} - } - } else { - quote! { - #[derive(::light_sdk::LightAccounts)] - pub struct #light_accounts_name { - #light_accounts_fields - } - } - }; - - let light_referrable_fields = if light_referrable_field_idents.is_empty() { - quote! {} - } else { - quote! { - let #light_accounts_name { - #(#light_referrable_field_idents),*, .. - } = &self.light_accounts; - } - }; - let input_fields = match instruction_params { - Some(instruction_params) => { - let param_names = instruction_params.param_names; - let param_type_checks = instruction_params.param_type_checks; - quote! { - let #params_name { #(#param_names),*, .. } = inputs; - #(#param_type_checks)* - } - } - None => quote! {}, - }; - - let expanded = quote! { - #[::light_sdk::light_system_accounts] - #[derive(::anchor_lang::Accounts, ::light_sdk::LightTraits)] - #anchor_accounts_strct - - #light_accounts_strct - - pub trait #ext_trait_name { - fn check_constraints( - &self, - inputs: &#params_name, - ) -> Result<()>; - fn derive_address_seeds( - &mut self, - address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo, - inputs: &#params_name, - ); - } - - impl<'a, 'b, 'c, 'info> #ext_trait_name for ::light_sdk::context::LightContext< - 'a, 'b, 'c, 'info, #anchor_accounts_name #type_gen, #light_accounts_name, - > { - #[allow(unused_parens)] - #[allow(unused_variables)] - fn check_constraints( - &self, - inputs: &#params_name, - ) -> Result<()> { - let #anchor_accounts_name { - #(#anchor_field_idents),*, .. - } = &self.anchor_context.accounts; - #light_referrable_fields - #input_fields - - #(#constraint_calls)* - - Ok(()) - } - - #[allow(unused_variables)] - fn derive_address_seeds( - &mut self, - address_merkle_context: PackedAddressTreeInfo, - inputs: &#params_name, - ) { - let #anchor_accounts_name { - #(#anchor_field_idents),*, .. - } = &self.anchor_context.accounts; - #light_referrable_fields - #input_fields - - let unpacked_address_merkle_context = - ::light_sdk::program_merkle_context::unpack_address_merkle_context( - address_merkle_context, self.anchor_context.remaining_accounts); - - #(#derive_address_seed_calls)* - - let #light_accounts_name { #(#light_field_idents),* } = &mut self.light_accounts; - - #(#set_address_seed_calls)* - } - } - }; - - Ok(expanded) -} - -mod light_account_kw { - // Action - syn::custom_keyword!(init); - syn::custom_keyword!(close); - // Constraint - syn::custom_keyword!(constraint); - // Seeds - syn::custom_keyword!(seeds); -} - -#[derive(Eq, PartialEq)] -pub(crate) enum LightAccountAction { - Init, - Mut, - Close, -} - -pub(crate) struct Constraint { - /// Expression of the constraint, e.g. - /// `my_compressed_acc.owner == signer.key()`. - expr: Expr, - /// Optional error to return. If not specified, the default - /// `LightSdkError::ConstraintViolation` will be used. - error: Option, -} - -pub(crate) struct LightAccountArgs { - action: LightAccountAction, - constraint: Option, - seeds: Option, -} - -impl Parse for LightAccountArgs { - fn parse(input: ParseStream) -> Result { - let mut action = None; - let mut constraint = None; - let mut seeds = None; - - while !input.is_empty() { - let lookahead = input.lookahead1(); - - // Actions - if lookahead.peek(light_account_kw::init) { - input.parse::()?; - action = Some(LightAccountAction::Init); - } else if lookahead.peek(Token![mut]) { - input.parse::()?; - action = Some(LightAccountAction::Mut); - } else if lookahead.peek(light_account_kw::close) { - input.parse::()?; - action = Some(LightAccountAction::Close); - } - // Constraint - else if lookahead.peek(light_account_kw::constraint) { - // Parse the constraint. - input.parse::()?; - input.parse::()?; - let expr: Expr = input.parse()?; - - // Parse an optional error. - let mut error = None; - if input.peek(Token![@]) { - input.parse::()?; - error = Some(input.parse::()?); - } - - constraint = Some(Constraint { expr, error }); - } - // Seeds - else if lookahead.peek(light_account_kw::seeds) { - input.parse::()?; - input.parse::()?; - seeds = Some(input.parse::()?); - } else { - return Err(lookahead.error()); - } - - if input.peek(Token![,]) { - input.parse::()?; - } - } - - let action = match action { - Some(action) => action, - None => { - return Err(Error::new( - Span::call_site(), - "Expected an action for the account (`init`, `mut` or `close`)", - )) - } - }; - - Ok(Self { - action, - constraint, - seeds, - }) - } -} - -pub(crate) fn process_light_accounts_derive(input: ItemStruct) -> Result { - let strct_name = &input.ident; - let (impl_gen, type_gen, where_clause) = input.generics.split_for_impl(); - - let mut try_from_slice_calls = Vec::new(); - let mut field_idents = Vec::new(); - let mut new_address_params_calls = Vec::new(); - let mut input_account_calls = Vec::new(); - let mut output_account_calls = Vec::new(); - - let fields = match input.fields { - Fields::Named(ref fields) => fields, - _ => { - return Err(Error::new_spanned( - input, - "Only structs with named fields can derive LightAccounts", - )) - } - }; - - for (i, field) in fields.named.iter().enumerate() { - let field_ident = &field.ident; - field_idents.push(field_ident); - - let account_args = field - .attrs - .iter() - .find(|attribute| attribute.path().is_ident("light_account")) - .map(|attribute| attribute.parse_args::()) - .transpose()? - .ok_or_else(|| { - Error::new_spanned(input.clone(), "no arguments provided in `light_account`") - })?; - - let type_path = match field.ty { - Type::Path(ref type_path) => type_path, - _ => { - return Err(Error::new_spanned( - input, - "Only struct with typed fields can derive LightAccounts", - )) - } - }; - - let type_path_without_args = TypePath { - qself: type_path.qself.clone(), - path: Path { - leading_colon: type_path.path.leading_colon, - segments: type_path - .path - .segments - .iter() - .map(|segment| PathSegment { - ident: segment.ident.clone(), - arguments: syn::PathArguments::None, - }) - .collect::>(), - }, - }; - let try_from_slice_call = match account_args.action { - LightAccountAction::Init => quote! { - let mut #field_ident: #type_path = #type_path_without_args::new_init( - &merkle_context, - &address_merkle_context, - address_merkle_tree_root_index, - ); - }, - LightAccountAction::Mut => quote! { - let mut #field_ident: #type_path = #type_path_without_args::try_from_slice_mut( - inputs[#i].as_slice(), - &merkle_context, - merkle_tree_root_index, - &address_merkle_context, - )?; - }, - LightAccountAction::Close => quote! { - let mut #field_ident: #type_path = #type_path_without_args::try_from_slice_close( - inputs[#i].as_slice(), - &merkle_context, - merkle_tree_root_index, - &address_merkle_context, - )?; - }, - }; - try_from_slice_calls.push(try_from_slice_call); - - new_address_params_calls.push(quote! { - if let Some(new_address_params_for_acc) = self.#field_ident.new_address_params() { - new_address_params.push(new_address_params_for_acc); - } - }); - input_account_calls.push(quote! { - if let Some(compressed_account) = self.#field_ident.input_compressed_account( - &crate::ID, - remaining_accounts, - )? { - accounts.push(compressed_account); - } - }); - output_account_calls.push(quote! { - if let Some(compressed_account) = self.#field_ident.output_compressed_account( - &crate::ID, - remaining_accounts, - )? { - accounts.push(compressed_account); - } - }); - } - - let expanded = quote! { - impl #impl_gen ::light_sdk::compressed_account::LightAccounts for #strct_name #type_gen #where_clause { - fn try_light_accounts( - inputs: Vec>, - merkle_context: ::light_sdk::merkle_context::PackedMerkleContext, - merkle_tree_root_index: u16, - address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo, - address_merkle_tree_root_index: u16, - remaining_accounts: &[::anchor_lang::prelude::AccountInfo], - ) -> Result { - let unpacked_address_merkle_context = - ::light_sdk::program_merkle_context::unpack_address_merkle_context( - address_merkle_context, remaining_accounts); - - #(#try_from_slice_calls)* - Ok(Self { - #(#field_idents),* - }) - } - - fn new_address_params(&self) -> Vec<::light_sdk::address::NewAddressParamsPacked> { - let mut new_address_params = Vec::new(); - #(#new_address_params_calls)* - new_address_params - } - - fn input_accounts(&self, remaining_accounts: &[::anchor_lang::prelude::AccountInfo]) -> Result> { - let mut accounts = Vec::new(); - #(#input_account_calls)* - Ok(accounts) - } - - fn output_accounts(&self, remaining_accounts: &[::anchor_lang::prelude::AccountInfo]) -> Result> { - let mut accounts = Vec::new(); - #(#output_account_calls)* - Ok(accounts) - } - } - }; - - Ok(expanded) -} - -#[cfg(test)] -mod tests { - use syn::{parse_quote, ItemStruct}; - - use super::*; - - #[test] - fn test_process_light_system_accounts_adds_fields_correctly() { - let input: ItemStruct = parse_quote! { - struct TestStruct { - #[light_account(mut)] - foo: u64, - existing_field: u32, - } - }; - - let output = process_light_system_accounts(input).unwrap(); - let output_string = output.to_string(); - - println!("{output_string}"); - - assert!(output_string.contains("light_system_program")); - assert!(output_string.contains("system_program")); - assert!(output_string.contains("account_compression_program")); - assert!(output_string.contains("registered_program_pda")); - assert!(output_string.contains("noop_program")); - assert!(output_string.contains("account_compression_authority")); - } - - #[test] - fn test_process_light_system_accounts_fails_on_existing_field() { - let input: ItemStruct = parse_quote! { - struct TestStruct { - existing_field: u32, - system_program: Program<'info, System>, - } - }; - - let result = process_light_system_accounts(input); - assert!(result.is_err()); - let error_message = result.unwrap_err().to_string(); - assert!(error_message.contains("Field `system_program` already exists in the struct.")); - } -} diff --git a/sdk-libs/macros/src/compressible/GUIDE.md b/sdk-libs/macros/src/compressible/GUIDE.md deleted file mode 100644 index 599db88b56..0000000000 --- a/sdk-libs/macros/src/compressible/GUIDE.md +++ /dev/null @@ -1,198 +0,0 @@ -## Compressible macros — caller program usage (first draft) - -Use this to add rent-free PDAs, cTokens, and cMints to your program with minimal boilerplate. - -### What you get (the interface) - -- `#[derive(Compressible)]`: makes a struct compressible. Expect a `compression_info: Option` field. -- `#[add_compressible_instructions(...)]`: generates ready-to-use `decompress_accounts_idempotent` and `compress_accounts_idempotent` entrypoints, PDA seed derivation, and optional cToken integration. -- `#[account]`: convenience macro for Anchor accounts adding `LightHasher` + `LightDiscriminator` derives. -- Rent tools: `derive_light_rent_sponsor_pda!`, `derive_light_rent_sponsor!` for compile‑time rent sponsor constants. -- Program config helpers: `process_initialize_compression_config_checked`, `process_update_compression_config`. - -### How to use — PDA only - -1. Define your PDAs - -```rust -use light_sdk::compressible::CompressionInfo; -use light_sdk_macros::{account, Compressible}; - -#[account] -#[derive(Compressible)] -pub struct UserRecord { - pub compression_info: Option, - pub owner: Pubkey, - pub name: String, - pub score: u64, -} -``` - -2. Generate compress/decompress instructions with auto seeds - -```rust -use light_sdk_macros::add_compressible_instructions; - -#[add_compressible_instructions( - UserRecord = ("user_record", data.owner.as_ref()) -)] -#[program] -pub mod my_program {} -``` - -3. Initialize your compression config (one-time) - -- Call the generated `initialize_compression_config` entrypoint or invoke: - - `process_initialize_compression_config_checked(config_pda, update_authority, program_data, rent_sponsor, compression_authority, rent_config, write_top_up, address_space, bump=0, payer, system_program, program_id)` -- Inputs you must pick: - - rent_sponsor: who receives rent when PDAs compress/close - - compression_authority: who can compress/close your PDAs - - rent_config + write_top_up: rent curve + write top‑up per write - - address_space: one address tree pubkey for your PDAs - -4. Use the generated entrypoints - -- `decompress_accounts_idempotent(...)` -- `compress_accounts_idempotent(...)` - -### How to use — mixed with cToken - -1. Extend the macro with token variants - -```rust -#[add_compressible_instructions( - // PDAs - UserRecord = ("user_record", data.owner.as_ref()), - // Program‑owned ctoken PDA (must provide authority seeds) - TreasuryCtoken = (is_token, "treasury_ctoken", ctx.fee_payer, authority = (ctx.treasury)), - // User ATA variant (no seeds, derived from owner+mint) - UserAta = (is_token, is_ata) -)] -#[program] -pub mod my_program {} -``` - -2. Create compressible token accounts (ATAs) on the client or via CPI - -- Inputs (client builder): `CreateCompressibleAssociatedTokenAccountInputs { payer, owner, mint, compressible_config, rent_sponsor, pre_pay_num_epochs, lamports_per_write, token_account_version }` -- Authority-less user ATAs use `derive_ctoken_ata(owner, mint)` under the hood. - -3. Decompress/compress flows - -- The generated `decompress_accounts_idempotent` and `compress_accounts_idempotent` accept packed token data alongside your PDAs. You only provide the standard accounts the macro adds (fee_payer, config, rent_sponsor, and optional ctoken config/cpi auth). - -### How to use — cMints (compressed mints) - -- Create a compressed mint: - - `create_compressed_mint(CreateCompressedMintInputs { decimals, mint_authority, freeze_authority, proof, address_merkle_tree_root_index, mint_signer, payer, address_tree_pubkey, output_queue, extensions, version })` - - Derive addresses with: - - `derive_mint_compressed_address(&mint_signer, &address_tree_pubkey)` - - `find_mint_address(&mint_signer)` -- Mint tokens to compressed accounts: - - `create_mint_to_compressed_instruction(MintToCompressedInputs { compressed_mint_inputs, recipients, mint_authority, payer, state_merkle_tree, input_queue, output_queue_cmint, output_queue_tokens, decompressed_mint_config, proof, token_account_version, cpi_context_pubkey, token_pool })` - -Keep it simple: create cMint → mint to recipients (compressed accounts or cToken ATAs) using the SDK helpers below. - -### cToken SDK (compressed-token-sdk) — the interfaces you actually call - -- Accounts - - `derive_ctoken_ata(owner, mint) -> (Pubkey, u8)` - - `create_compressible_associated_token_account(inputs)` / `_idempotent` (+ “2” variants if owner/mint passed as accounts) - - Low-level: `create_compressible_token_account_instruction(CreateCompressibleTokenAccount)` -- Mints - - `create_compressed_mint(CreateCompressedMintInputs)` - - `derive_mint_compressed_address(mint_seed, address_tree)` - - `find_mint_address(mint_seed)` -- Mint to recipients - - `create_mint_to_compressed_instruction(MintToCompressedInputs)` - - Types: `Recipient { recipient, amount }` -- Transfer SPL ↔ cToken - - `create_transfer_spl_to_ctoken_instruction(...)` - - `create_transfer_ctoken_to_spl_instruction(...)` - - `transfer_interface(...)` / `transfer_interface_signed(...)` -- Update compressed mint - - `update_compressed_mint(UpdateCompressedMintInputs)` - -### Rent — set/update for your PDAs and for cTokens - -- PDAs (your program) - - One-time config: `process_initialize_compression_config_checked(...)` (or use generated `initialize_compression_config` entrypoint) - - Update later: `process_update_compression_config(config, authority, new_update_authority?, new_rent_sponsor?, new_compression_authority?, new_rent_config?, new_write_top_up?, new_address_space?, program_id)` - - Use `light_compressible::rent::RentConfig` to define rent curve and distribution. Funds on close/compress go to `rent_sponsor` (completed epochs) and refund fee payer for partial epochs automatically. -- cTokens (account-level) - - When creating a compressible token account, you pass: - - `rent_sponsor`, `pre_pay_num_epochs`, optional `lamports_per_write`, and `compressible_config` (the registry’s or your chosen config PDA) - - For ATAs: `CreateCompressibleAssociatedTokenAccountInputs { ... }` - -### Rust client — the minimum you need - -1. Connect and fetch proofs - -```rust -use light_client::rpc::{LightClient, LightClientConfig, Rpc}; - -let mut rpc = LightClient::new(LightClientConfig::local()).await?; // or devnet/mainnet -// rpc.get_validity_proof(account_hashes, new_addresses, None).await? -``` - -2. Create a compressible ATA - -```rust -use light_token_sdk::instructions::{ - create_compressible_associated_token_account, CreateCompressibleAssociatedTokenAccountInputs -}; - -let ix = create_compressible_associated_token_account(CreateCompressibleAssociatedTokenAccountInputs { - payer, - owner, - mint, - compressible_config, - rent_sponsor, - pre_pay_num_epochs: 2, - lamports_per_write: Some(1_000), - token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat, -})?; -``` - -3. Create a cMint and mint to recipients - -```rust -use light_token_sdk::instructions::{ - create_compressed_mint, CreateCompressedMintInputs, - create_mint_to_compressed_instruction, MintToCompressedInputs -}; -use light_token_interface::instructions::mint_action::Recipient; - -let create_cmint_ix = create_compressed_mint(CreateCompressedMintInputs { /* fill from RPC + keys */ })?; -let mint_ix = create_mint_to_compressed_instruction(MintToCompressedInputs { - recipients: vec![Recipient { recipient: some_address, amount: 1000 }], - /* queues/tree/authority from RPC + keys */ -}, None)?; -``` - -4. High-level helpers (token-client) - -```rust -use light_token_client::actions::{create_compressible_token_account, CreateCompressibleTokenAccountInputs, mint_to_compressed}; - -let token_acc = create_compressible_token_account(&mut rpc, CreateCompressibleTokenAccountInputs { - owner, mint, num_prepaid_epochs: 2, payer: &payer, token_account_keypair: None, - lamports_per_write: None, token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat -}).await?; - -let sig = mint_to_compressed(&mut rpc, spl_mint_pda, vec![Recipient{ recipient: token_acc, amount: 1000 }], light_token_interface::state::TokenDataVersion::ShaFlat, &mint_authority, &payer).await?; -``` - -### TL;DR checklists - -- PDA only - - Add `#[derive(Compressible)]` + `compression_info` - - Add `#[add_compressible_instructions(...)]` with seeds - - Initialize config (rent_sponsor, compression_authority, rent_config, write_top_up, address_space) - - Call generated compress/decompress entrypoints -- Mixed with cToken - - Add token variants in `#[add_compressible_instructions(...)]` (program-owned with `authority = (...)` or `is_ata`) - - Use SDK to create cToken ATAs; pass rent fields - - Mint via cMints and `mint_to_compressed` or `mint_action` -- cMints - - `create_compressed_mint(...)` then `create_mint_to_compressed_instruction(...)` diff --git a/sdk-libs/macros/src/compressible/README.md b/sdk-libs/macros/src/compressible/README.md index eb7337d103..aac0ceb9e3 100644 --- a/sdk-libs/macros/src/compressible/README.md +++ b/sdk-libs/macros/src/compressible/README.md @@ -19,13 +19,13 @@ Procedural macros for generating rent-free account types and their hooks for Sol **`variant_enum.rs`** - Account variant enum -- Generates `CompressedAccountVariant` enum from account types +- Generates `RentFreeAccountVariant` enum from account types - Implements all required traits (Default, DataHasher, Size, Pack, Unpack) -- Creates `CompressedAccountData` wrapper struct +- Creates `RentFreeAccountData` wrapper struct **`instructions.rs`** - Instruction generation -- Main macro: `add_compressible_instructions` +- Main macro: `#[rentfree]` - Generates compress/decompress instruction handlers - Creates context structs and account validation - **Compress**: PDA-only (ctokens compressed via registry) diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs new file mode 100644 index 0000000000..f80596a6eb --- /dev/null +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -0,0 +1,658 @@ +//! Anchor seed extraction from #[account(seeds = [...])] attributes. +//! +//! This module extracts PDA seeds from Anchor's attribute syntax and classifies them +//! into the categories needed for compression: literals, ctx fields, data fields, etc. + +use syn::{Expr, Ident, ItemStruct, Type}; + +/// Classified seed element from Anchor's seeds array +#[derive(Clone, Debug)] +pub enum ClassifiedSeed { + /// b"literal" or "string" - hardcoded bytes + Literal(Vec), + /// CONSTANT - uppercase identifier, resolved as crate::CONSTANT + Constant(syn::Path), + /// account.key().as_ref() - reference to account in struct + CtxAccount(Ident), + /// params.field.as_ref() or params.field.to_le_bytes().as_ref() + DataField { + field_name: Ident, + /// Method like to_le_bytes, or None for direct .as_ref() + conversion: Option, + }, + /// Function call like max_key(&a.key(), &b.key()) + FunctionCall { + func: syn::Path, + /// Account references used as arguments + ctx_args: Vec, + }, +} + +/// Extracted seed specification for a compressible field +#[derive(Clone, Debug)] +pub struct ExtractedSeedSpec { + /// The variant name derived from field_name (snake_case -> CamelCase) + pub variant_name: Ident, + /// The inner type (e.g., UserRecord from Account<'info, UserRecord>) + pub inner_type: Ident, + /// Classified seeds from #[account(seeds = [...])] + pub seeds: Vec, +} + +/// Extracted token specification for a #[rentfree_token = Variant] field +#[derive(Clone, Debug)] +pub struct ExtractedTokenSpec { + /// The field name in the Accounts struct + pub field_name: Ident, + /// The variant name from #[rentfree_token = Variant] + pub variant_name: Ident, + /// Seeds from #[account(seeds = [...])] + pub seeds: Vec, + /// Authority field name (if specified or auto-detected) + pub authority_field: Option, + /// Authority seeds (from the authority field's #[account(seeds)]) + pub authority_seeds: Option>, +} + +/// All extracted info from an Accounts struct +#[derive(Clone, Debug)] +pub struct ExtractedAccountsInfo { + pub struct_name: Ident, + pub pda_fields: Vec, + pub token_fields: Vec, +} + +/// Extract rentfree field info from an Accounts struct +pub fn extract_from_accounts_struct( + item: &ItemStruct, +) -> syn::Result> { + let fields = match &item.fields { + syn::Fields::Named(named) => &named.named, + _ => return Ok(None), + }; + + let mut pda_fields = Vec::new(); + let mut token_fields = Vec::new(); + let mut all_fields = Vec::new(); + + for field in fields { + let field_ident = match &field.ident { + Some(id) => id.clone(), + None => continue, + }; + + all_fields.push((field_ident.clone(), field.ty.clone())); + + // Check for #[rentfree] attribute + let has_rentfree = field + .attrs + .iter() + .any(|attr| attr.path().is_ident("rentfree")); + + // Check for #[rentfree_token(...)] attribute + let token_attr = extract_rentfree_token_attr(&field.attrs); + + if has_rentfree { + // Extract inner type from Account<'info, T> or Box> + let (is_boxed, inner_type) = match extract_account_inner_type(&field.ty) { + Some(result) => result, + None => { + return Err(syn::Error::new_spanned( + &field.ty, + "#[rentfree] requires Account<'info, T> or Box>", + )); + } + }; + + // Extract seeds from #[account(seeds = [...])] + let seeds = extract_anchor_seeds(&field.attrs)?; + + // Derive variant name from field name: snake_case -> CamelCase + let variant_name = { + let camel = snake_to_camel_case(&field_ident.to_string()); + Ident::new(&camel, field_ident.span()) + }; + + let _ = (field_ident, is_boxed); // Suppress unused warnings + pda_fields.push(ExtractedSeedSpec { + variant_name, + inner_type, + seeds, + }); + } else if let Some(token_attr) = token_attr { + // Token field - derive variant name from field name if not provided + let seeds = extract_anchor_seeds(&field.attrs)?; + + // Derive variant name: snake_case field -> CamelCase variant + let variant_name = token_attr.variant_name.unwrap_or_else(|| { + let camel = snake_to_camel_case(&field_ident.to_string()); + Ident::new(&camel, field_ident.span()) + }); + + token_fields.push(ExtractedTokenSpec { + field_name: field_ident, + variant_name, + seeds, + authority_field: None, + // Use authority from attribute if provided + authority_seeds: token_attr.authority_seeds, + }); + } + } + + // If no rentfree fields found, return None + if pda_fields.is_empty() && token_fields.is_empty() { + return Ok(None); + } + + // Resolve authority for token fields (only if not already provided in attribute) + for token in &mut token_fields { + // Skip if authority was already provided in the attribute + if token.authority_seeds.is_some() { + continue; + } + + // Try to find authority field by convention: {field_name}_authority or vault_authority + let authority_candidates = [ + format!("{}_authority", token.field_name), + "vault_authority".to_string(), + "authority".to_string(), + ]; + + for candidate in &authority_candidates { + if let Some((auth_field, _)) = all_fields.iter().find(|(name, _)| name == candidate) { + token.authority_field = Some(auth_field.clone()); + + // Try to extract authority seeds from the authority field + if let Some(auth_field_info) = fields + .iter() + .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(candidate.clone())) + { + if let Ok(auth_seeds) = extract_anchor_seeds(&auth_field_info.attrs) { + if !auth_seeds.is_empty() { + token.authority_seeds = Some(auth_seeds); + } + } + } + break; + } + } + } + + let _ = all_fields; // Suppress unused warning + Ok(Some(ExtractedAccountsInfo { + struct_name: item.ident.clone(), + pda_fields, + token_fields, + })) +} + +/// Parsed #[rentfree_token(...)] attribute +struct RentFreeTokenAttr { + /// Optional variant name - if None, derived from field name + variant_name: Option, + authority_seeds: Option>, +} + +/// Convert snake_case field name to CamelCase variant name +/// e.g., token_0_vault -> Token0Vault, vault -> Vault +fn snake_to_camel_case(s: &str) -> String { + s.split('_') + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() +} + +/// Extract #[rentfree_token(authority = [...])] attribute +/// Variant name is now derived from field name, not specified in attribute +fn extract_rentfree_token_attr(attrs: &[syn::Attribute]) -> Option { + for attr in attrs { + if attr.path().is_ident("rentfree_token") { + match &attr.meta { + // #[rentfree_token = Variant] (deprecated but still supported) + syn::Meta::NameValue(nv) => { + if let Expr::Path(path) = &nv.value { + if let Some(ident) = path.path.get_ident() { + return Some(RentFreeTokenAttr { + variant_name: Some(ident.clone()), + authority_seeds: None, + }); + } + } + } + // #[rentfree_token(authority = [...])] or #[rentfree_token(Variant, authority = [...])] + syn::Meta::List(list) => { + if let Ok(parsed) = parse_rentfree_token_list(&list.tokens) { + return Some(parsed); + } + // Fallback: try parsing as just an identifier (deprecated) + if let Ok(ident) = syn::parse2::(list.tokens.clone()) { + return Some(RentFreeTokenAttr { + variant_name: Some(ident), + authority_seeds: None, + }); + } + } + // #[rentfree_token] with no arguments + syn::Meta::Path(_) => { + return Some(RentFreeTokenAttr { + variant_name: None, + authority_seeds: None, + }); + } + } + } + } + None +} + +/// Parse rentfree_token(authority = [...]) or rentfree_token(Variant, authority = [...]) content +fn parse_rentfree_token_list(tokens: &proc_macro2::TokenStream) -> syn::Result { + use syn::parse::Parser; + + let parser = |input: syn::parse::ParseStream| -> syn::Result { + let mut variant_name = None; + let mut authority_seeds = None; + + // Check if first token is authority = [...] or a variant name + if input.peek(Ident) { + let ident: Ident = input.parse()?; + + if ident == "authority" { + // First token is authority, parse the seeds + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + let mut seeds = Vec::new(); + for elem in &array.elems { + if let Ok(seed) = classify_seed_expr(elem) { + seeds.push(seed); + } + } + authority_seeds = Some(seeds); + } else { + // First token is variant name (deprecated but supported) + variant_name = Some(ident); + + // Check for comma and additional args + while input.peek(syn::Token![,]) { + input.parse::()?; + + // Look for authority = [...] + if input.peek(Ident) { + let key: Ident = input.parse()?; + if key == "authority" { + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + let mut seeds = Vec::new(); + for elem in &array.elems { + if let Ok(seed) = classify_seed_expr(elem) { + seeds.push(seed); + } + } + authority_seeds = Some(seeds); + } + } + } + } + } + + Ok(RentFreeTokenAttr { + variant_name, + authority_seeds, + }) + }; + + parser.parse2(tokens.clone()) +} + +/// Extract inner type T from Account<'info, T>, Box>, +/// AccountLoader<'info, T>, or InterfaceAccount<'info, T> +fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { + match ty { + Type::Path(type_path) => { + let segment = type_path.path.segments.last()?; + let ident_str = segment.ident.to_string(); + + match ident_str.as_str() { + "Account" | "AccountLoader" | "InterfaceAccount" => { + // Extract T from Account<'info, T> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(Type::Path(inner_path)) = arg { + if let Some(inner_seg) = inner_path.path.segments.last() { + // Skip lifetime 'info + if inner_seg.ident != "info" { + return Some((false, inner_seg.ident.clone())); + } + } + } + } + } + None + } + "Box" => { + // Check for Box> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + if let Some((_, inner_type)) = extract_account_inner_type(inner_ty) { + return Some((true, inner_type)); + } + } + } + None + } + _ => None, + } + } + _ => None, + } +} + +/// Extract seeds from #[account(seeds = [...], bump)] attribute +fn extract_anchor_seeds(attrs: &[syn::Attribute]) -> syn::Result> { + for attr in attrs { + if !attr.path().is_ident("account") { + continue; + } + + // Parse the attribute as a token stream and look for seeds = [...] + let tokens = match &attr.meta { + syn::Meta::List(list) => list.tokens.clone(), + _ => continue, + }; + + // Parse as comma-separated key-value pairs + let parsed: syn::Result> = + syn::parse::Parser::parse2( + syn::punctuated::Punctuated::parse_terminated, + tokens.clone(), + ); + + if let Ok(items) = &parsed { + for item in items { + if item.key == "seeds" { + return classify_seeds_array(&item.value); + } + } + } + } + + Ok(Vec::new()) +} + +/// Helper struct for parsing account attribute items +struct AccountAttrItem { + key: Ident, + value: Expr, +} + +impl syn::parse::Parse for AccountAttrItem { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + // Handle keywords like `mut` as well as identifiers + let key: Ident = if input.peek(syn::Token![mut]) { + input.parse::()?; + Ident::new("mut", proc_macro2::Span::call_site()) + } else { + input.parse()? + }; + + // Handle bare identifiers like `mut`, `init`, `bump` + if !input.peek(syn::Token![=]) { + return Ok(AccountAttrItem { + key: key.clone(), + value: syn::parse_quote!(true), + }); + } + + input.parse::()?; + let value: Expr = input.parse()?; + + Ok(AccountAttrItem { key, value }) + } +} + +/// Classify seeds from an array expression [seed1, seed2, ...] +fn classify_seeds_array(expr: &Expr) -> syn::Result> { + let array = match expr { + Expr::Array(arr) => arr, + Expr::Reference(r) => { + if let Expr::Array(arr) = &*r.expr { + arr + } else { + return Err(syn::Error::new_spanned(expr, "Expected seeds array")); + } + } + _ => return Err(syn::Error::new_spanned(expr, "Expected seeds array")), + }; + + let mut seeds = Vec::new(); + for elem in &array.elems { + seeds.push(classify_seed_expr(elem)?); + } + + Ok(seeds) +} + +/// Classify a single seed expression +fn classify_seed_expr(expr: &Expr) -> syn::Result { + match expr { + // b"literal" + Expr::Lit(lit) => { + if let syn::Lit::ByteStr(bs) = &lit.lit { + return Ok(ClassifiedSeed::Literal(bs.value())); + } + if let syn::Lit::Str(s) = &lit.lit { + return Ok(ClassifiedSeed::Literal(s.value().into_bytes())); + } + Err(syn::Error::new_spanned( + expr, + "Unsupported literal in seeds", + )) + } + + // CONSTANT (all uppercase path) + Expr::Path(path) => { + if let Some(ident) = path.path.get_ident() { + let name = ident.to_string(); + if name + .chars() + .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) + { + return Ok(ClassifiedSeed::Constant(path.path.clone())); + } + // Otherwise it's a variable reference - treat as ctx account + return Ok(ClassifiedSeed::CtxAccount(ident.clone())); + } + // Multi-segment path is a constant + Ok(ClassifiedSeed::Constant(path.path.clone())) + } + + // method_call.as_ref() - most common case + Expr::MethodCall(mc) => classify_method_call(mc), + + // Reference like &account.key() + Expr::Reference(r) => classify_seed_expr(&r.expr), + + // Field access like params.owner - direct field reference + Expr::Field(field) => { + if let syn::Member::Named(field_name) = &field.member { + if let Expr::Path(path) = &*field.base { + if let Some(base_ident) = path.path.get_ident() { + if base_ident == "params" { + return Ok(ClassifiedSeed::DataField { + field_name: field_name.clone(), + conversion: None, + }); + } + } + } + // ctx.field or account.field - treat as ctx account + return Ok(ClassifiedSeed::CtxAccount(field_name.clone())); + } + Err(syn::Error::new_spanned( + expr, + "Unsupported field expression", + )) + } + + // Function call like max_key(&a.key(), &b.key()).as_ref() + Expr::Call(call) => { + let func = match &*call.func { + Expr::Path(p) => p.path.clone(), + _ => { + return Err(syn::Error::new_spanned( + expr, + "Expected path for function call", + )) + } + }; + + let mut ctx_args = Vec::new(); + for arg in &call.args { + if let Some(ident) = extract_ctx_ident_from_expr(arg) { + ctx_args.push(ident); + } + } + + Ok(ClassifiedSeed::FunctionCall { func, ctx_args }) + } + + _ => Err(syn::Error::new_spanned( + expr, + format!("Unsupported seed expression: {:?}", expr), + )), + } +} + +/// Classify a method call expression like account.key().as_ref() +fn classify_method_call(mc: &syn::ExprMethodCall) -> syn::Result { + // Unwrap .as_ref() at the end + if mc.method == "as_ref" { + return classify_seed_expr(&mc.receiver); + } + + // Handle params.field.to_le_bytes() directly + if mc.method == "to_le_bytes" || mc.method == "to_be_bytes" { + if let Some((field_name, base)) = extract_params_field(&mc.receiver) { + if base == "params" { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: Some(mc.method.clone()), + }); + } + } + } + + // Handle account.key() + if mc.method == "key" { + if let Some(ident) = extract_receiver_ident(&mc.receiver) { + // Check if it's params.field or ctx.account + if let Expr::Field(field) = &*mc.receiver { + if let Expr::Path(path) = &*field.base { + if let Some(base_ident) = path.path.get_ident() { + if base_ident == "params" { + if let syn::Member::Named(field_name) = &field.member { + return Ok(ClassifiedSeed::DataField { + field_name: field_name.clone(), + conversion: None, + }); + } + } + } + } + } + return Ok(ClassifiedSeed::CtxAccount(ident)); + } + } + + // params.field.as_ref() directly + if let Some((field_name, base)) = extract_params_field(&mc.receiver) { + if base == "params" { + return Ok(ClassifiedSeed::DataField { + field_name, + conversion: None, + }); + } + } + + Err(syn::Error::new_spanned( + mc, + "Unsupported method call in seeds", + )) +} + +/// Extract field name from params.field or similar +fn extract_params_field(expr: &Expr) -> Option<(Ident, String)> { + if let Expr::Field(field) = expr { + if let syn::Member::Named(field_name) = &field.member { + if let Expr::Path(path) = &*field.base { + if let Some(base_ident) = path.path.get_ident() { + return Some((field_name.clone(), base_ident.to_string())); + } + } + } + } + None +} + +/// Extract the base identifier from an expression like account.key() -> account +fn extract_receiver_ident(expr: &Expr) -> Option { + match expr { + Expr::Path(path) => path.path.get_ident().cloned(), + Expr::Field(field) => { + if let syn::Member::Named(name) = &field.member { + Some(name.clone()) + } else { + None + } + } + Expr::MethodCall(mc) => extract_receiver_ident(&mc.receiver), + Expr::Reference(r) => extract_receiver_ident(&r.expr), + _ => None, + } +} + +/// Extract ctx account identifier from expression (for function args) +fn extract_ctx_ident_from_expr(expr: &Expr) -> Option { + match expr { + Expr::Reference(r) => extract_ctx_ident_from_expr(&r.expr), + Expr::MethodCall(mc) => { + if mc.method == "key" { + extract_receiver_ident(&mc.receiver) + } else { + None + } + } + Expr::Field(field) => { + if let syn::Member::Named(name) = &field.member { + Some(name.clone()) + } else { + None + } + } + Expr::Path(path) => path.path.get_ident().cloned(), + _ => None, + } +} + +/// Get data field names from classified seeds +pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> { + let mut fields = Vec::new(); + for seed in seeds { + if let ClassifiedSeed::DataField { + field_name, + conversion, + } = seed + { + if !fields.iter().any(|(f, _): &(Ident, _)| f == field_name) { + fields.push((field_name.clone(), conversion.clone())); + } + } + } + fields +} diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/compressible/decompress_context.rs index 5d5a37301e..cdccdfe8a3 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/compressible/decompress_context.rs @@ -2,78 +2,113 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - DeriveInput, Ident, Result, Token, -}; - -struct PdaTypesAttr { - types: Punctuated, -} - -impl Parse for PdaTypesAttr { - fn parse(input: ParseStream) -> Result { - Ok(PdaTypesAttr { - types: Punctuated::parse_terminated(input)?, - }) - } -} - -struct TokenVariantAttr { - variant: Ident, -} +use syn::{Ident, Result}; -impl Parse for TokenVariantAttr { - fn parse(input: ParseStream) -> Result { - Ok(TokenVariantAttr { - variant: input.parse()?, - }) - } -} +// Re-export from variant_enum for convenience +pub use crate::compressible::variant_enum::PdaCtxSeedInfo; pub fn generate_decompress_context_trait_impl( - pda_type_idents: Vec, + pda_ctx_seeds: Vec, token_variant_ident: Ident, lifetime: syn::Lifetime, ) -> Result { - let pda_match_arms: Vec<_> = pda_type_idents + // Generate match arms that extract idx fields, resolve Pubkeys, construct CtxSeeds + let pda_match_arms: Vec<_> = pda_ctx_seeds .iter() - .map(|pda_type| { + .map(|info| { + let pda_type = &info.type_name; let packed_name = format_ident!("Packed{}", pda_type); - quote! { - CompressedAccountVariant::#packed_name(packed) => { - match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( - &*self.rent_sponsor, - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &program_id, - self, // Pass the context itself as seed_accounts - seed_params, - ) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(e) => return std::result::Result::Err(e), + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", pda_type); + let ctx_fields = &info.ctx_seed_fields; + // Generate pattern to extract idx fields from packed variant + let idx_field_patterns: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field } + }).collect(); + // Generate code to resolve idx fields to Pubkeys + let resolve_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { + let #field = *post_system_accounts + .get(#idx_field as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }).collect(); + // Generate CtxSeeds struct construction + let ctx_seeds_construction = if ctx_fields.is_empty() { + quote! { let ctx_seeds = #ctx_seeds_struct_name; } + } else { + let field_inits: Vec<_> = ctx_fields.iter().map(|field| { + quote! { #field } + }).collect(); + quote! { let ctx_seeds = #ctx_seeds_struct_name { #(#field_inits),* }; } + }; + if ctx_fields.is_empty() { + quote! { + RentFreeAccountVariant::#packed_name { data: packed, .. } => { + #ctx_seeds_construction + match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + &*self.rent_sponsor, + cpi_accounts, + address_space, + &solana_accounts[i], + i, + &packed, + &meta, + post_system_accounts, + &mut compressed_pda_infos, + &program_id, + &ctx_seeds, + seed_params, + ) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(e) => return std::result::Result::Err(e), + } + } + RentFreeAccountVariant::#pda_type { .. } => { + unreachable!("Unpacked variants should not be present during decompression"); } } - CompressedAccountVariant::#pda_type(_) => { - unreachable!("Unpacked variants should not be present during decompression"); + } else { + quote! { + RentFreeAccountVariant::#packed_name { data: packed, #(#idx_field_patterns,)* .. } => { + #(#resolve_ctx_seeds)* + #ctx_seeds_construction + match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + &*self.rent_sponsor, + cpi_accounts, + address_space, + &solana_accounts[i], + i, + &packed, + &meta, + post_system_accounts, + &mut compressed_pda_infos, + &program_id, + &ctx_seeds, + seed_params, + ) { + std::result::Result::Ok(()) => {}, + std::result::Result::Err(e) => return std::result::Result::Err(e), + } + } + RentFreeAccountVariant::#pda_type { .. } => { + unreachable!("Unpacked variants should not be present during decompression"); + } } } }) .collect(); + let packed_token_variant_ident = format_ident!("Packed{}", token_variant_ident); + Ok(quote! { impl<#lifetime> light_sdk::compressible::DecompressContext<#lifetime> for DecompressAccountsIdempotent<#lifetime> { - type CompressedData = CompressedAccountData; - type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#token_variant_ident>; + type CompressedData = RentFreeAccountData; + type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; - type SeedParams = SeedParams; + type SeedParams = (); fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { &*self.fee_payer @@ -92,7 +127,7 @@ pub fn generate_decompress_context_trait_impl( } fn token_program(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> { - self.ctoken_program.as_ref().map(|a| &**a) + self.light_token_program.as_ref().map(|a| &**a) } fn token_cpi_authority(&self) -> std::option::Option<&solana_account_info::AccountInfo<#lifetime>> { @@ -126,11 +161,11 @@ pub fn generate_decompress_context_trait_impl( let meta = compressed_data.meta; match compressed_data.data { #(#pda_match_arms)* - CompressedAccountVariant::PackedCTokenData(mut data) => { + RentFreeAccountVariant::PackedCTokenData(mut data) => { data.token_data.version = 3; compressed_token_accounts.push((data, meta)); } - CompressedAccountVariant::CTokenData(_) => { + RentFreeAccountVariant::CTokenData(_) => { unreachable!(); } } @@ -154,10 +189,9 @@ pub fn generate_decompress_context_trait_impl( proof: light_sdk::instruction::ValidityProof, cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, #lifetime>, post_system_accounts: &[solana_account_info::AccountInfo<#lifetime>], - has_pdas: bool, + has_prior_context: bool, ) -> std::result::Result<(), solana_program_error::ProgramError> { light_token_sdk::compressible::process_decompress_tokens_runtime( - self, remaining_accounts, fee_payer, token_program, @@ -169,51 +203,10 @@ pub fn generate_decompress_context_trait_impl( proof, cpi_accounts, post_system_accounts, - has_pdas, + has_prior_context, &crate::ID, ) } } }) } - -pub fn derive_decompress_context(input: DeriveInput) -> Result { - let pda_types_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("pda_types")) - .ok_or_else(|| { - syn::Error::new_spanned( - &input, - "DecompressContext derive requires #[pda_types(Type1, Type2, ...)] attribute", - ) - })?; - - let pda_types: PdaTypesAttr = pda_types_attr.parse_args()?; - let pda_type_idents: Vec = pda_types.types.iter().cloned().collect(); - - let token_variant_attr = input - .attrs - .iter() - .find(|attr| attr.path().is_ident("token_variant")) - .ok_or_else(|| { - syn::Error::new_spanned( - &input, - "DecompressContext derive requires #[token_variant(CTokenAccountVariant)] attribute", - ) - })?; - - let token_variant: TokenVariantAttr = token_variant_attr.parse_args()?; - let token_variant_ident = token_variant.variant; - - let lifetime = if let Some(lt) = input.generics.lifetimes().next() { - lt.lifetime.clone() - } else { - return Err(syn::Error::new_spanned( - &input, - "DecompressContext requires a lifetime parameter (e.g., <'info>)", - )); - }; - - generate_decompress_context_trait_impl(pda_type_idents, token_variant_ident, lifetime) -} diff --git a/sdk-libs/macros/src/compressible/file_scanner.rs b/sdk-libs/macros/src/compressible/file_scanner.rs new file mode 100644 index 0000000000..ea52500298 --- /dev/null +++ b/sdk-libs/macros/src/compressible/file_scanner.rs @@ -0,0 +1,188 @@ +//! File scanning for #[rentfree_program] macro. +//! +//! This module reads external Rust source files to extract seed information +//! from Accounts structs that contain #[rentfree] fields. + +use std::path::{Path, PathBuf}; + +use syn::{Item, ItemMod, ItemStruct}; + +use crate::compressible::anchor_seeds::{ + extract_from_accounts_struct, ExtractedAccountsInfo, ExtractedSeedSpec, ExtractedTokenSpec, +}; + +/// Result of scanning a module and its external files +#[derive(Debug, Default)] +pub struct ScannedModuleInfo { + pub pda_specs: Vec, + pub token_specs: Vec, + pub errors: Vec, + /// Names of Accounts structs that have rentfree fields (for auto-wrapping handlers) + pub rentfree_struct_names: std::collections::HashSet, +} + +/// Scan the entire src/ directory for Accounts structs with #[rentfree] fields. +/// +/// This function scans all .rs files in the crate's src/ directory +/// and extracts seed information from Accounts structs. +pub fn scan_module_for_compressible( + _module: &ItemMod, + base_path: &Path, +) -> syn::Result { + let mut result = ScannedModuleInfo::default(); + + // Scan all .rs files in the src directory + scan_directory_recursive(base_path, &mut result); + + Ok(result) +} + +/// Recursively scan a directory for .rs files +fn scan_directory_recursive(dir: &Path, result: &mut ScannedModuleInfo) { + let entries = match std::fs::read_dir(dir) { + Ok(e) => e, + Err(e) => { + result + .errors + .push(format!("Failed to read directory {:?}: {}", dir, e)); + return; + } + }; + + for entry in entries.flatten() { + let path = entry.path(); + + if path.is_dir() { + scan_directory_recursive(&path, result); + } else if path.extension().map(|e| e == "rs").unwrap_or(false) { + scan_rust_file(&path, result); + } + } +} + +/// Scan a single Rust file for Accounts structs +fn scan_rust_file(path: &Path, result: &mut ScannedModuleInfo) { + let contents = match std::fs::read_to_string(path) { + Ok(c) => c, + Err(e) => { + result + .errors + .push(format!("Failed to read {:?}: {}", path, e)); + return; + } + }; + + let parsed: syn::File = match syn::parse_str(&contents) { + Ok(f) => f, + Err(e) => { + // Not all files may be valid on their own (e.g., test files with main) + // Just skip them silently + let _ = e; + return; + } + }; + + for item in parsed.items { + match item { + Item::Struct(item_struct) => { + if let Ok(Some((info, struct_name))) = try_extract_from_struct(&item_struct) { + result.pda_specs.extend(info.pda_fields); + result.token_specs.extend(info.token_fields); + result.rentfree_struct_names.insert(struct_name); + } + } + Item::Mod(inner_mod) if inner_mod.content.is_some() => { + // Inline module - recursively scan + scan_inline_module(&inner_mod, result); + } + _ => {} + } + } +} + +/// Scan an inline module for Accounts structs +fn scan_inline_module(module: &ItemMod, result: &mut ScannedModuleInfo) { + let content = match &module.content { + Some((_, items)) => items, + None => return, + }; + + for item in content { + match item { + Item::Struct(item_struct) => { + if let Ok(Some((info, struct_name))) = try_extract_from_struct(item_struct) { + result.pda_specs.extend(info.pda_fields); + result.token_specs.extend(info.token_fields); + result.rentfree_struct_names.insert(struct_name); + } + } + Item::Mod(inner_mod) if inner_mod.content.is_some() => { + scan_inline_module(inner_mod, result); + } + _ => {} + } + } +} + +/// Try to extract rentfree info from a struct. +/// Returns (ExtractedAccountsInfo, struct_name) if the struct has rentfree fields. +fn try_extract_from_struct( + item_struct: &ItemStruct, +) -> syn::Result> { + // Check if it has #[derive(Accounts)] + let has_accounts_derive = item_struct.attrs.iter().any(|attr| { + if attr.path().is_ident("derive") { + if let Ok(meta) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + return meta.iter().any(|p| p.is_ident("Accounts")); + } + } + false + }); + + if !has_accounts_derive { + return Ok(None); + } + + let info = extract_from_accounts_struct(item_struct)?; + match info { + Some(extracted) => { + let struct_name = extracted.struct_name.to_string(); + Ok(Some((extracted, struct_name))) + } + None => Ok(None), + } +} + +/// Resolve the base path for the crate source +/// +/// This attempts to find the src/ directory by looking at CARGO_MANIFEST_DIR +/// or falling back to current directory. +pub fn resolve_crate_src_path() -> PathBuf { + // Try CARGO_MANIFEST_DIR first (set during cargo build) + if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { + let src_path = PathBuf::from(&manifest_dir).join("src"); + if src_path.exists() { + return src_path; + } + // Fallback to manifest dir itself + return PathBuf::from(manifest_dir); + } + + // Fallback to current directory + std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join("src") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_resolve_path() { + let path = resolve_crate_src_path(); + println!("Resolved path: {:?}", path); + } +} diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 620b6d225c..50265641ae 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -8,6 +8,22 @@ use syn::{ Expr, Ident, Item, ItemMod, LitStr, Result, Token, }; +/// Convert PascalCase to snake_case (e.g., UserRecord -> user_record) +fn to_snake_case(s: &str) -> String { + let mut result = String::new(); + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() { + if i > 0 { + result.push('_'); + } + result.push(c.to_ascii_lowercase()); + } else { + result.push(c); + } + } + result +} + macro_rules! macro_error { ($span:expr, $msg:expr) => { syn::Error::new_spanned( @@ -45,145 +61,100 @@ pub struct TokenSeedSpec { pub variant: Ident, pub _eq: Token![=], pub is_token: Option, - pub is_ata: bool, pub seeds: Punctuated, pub authority: Option>, } impl Parse for TokenSeedSpec { fn parse(input: ParseStream) -> Result { - let variant = input.parse()?; - let _eq = input.parse()?; + let variant: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; let content; syn::parenthesized!(content in input); - let (is_token, is_ata, seeds, authority) = if content.peek(Ident) { - let first_ident: Ident = content.parse()?; - - match first_ident.to_string().as_str() { - "is_token" => { - let _comma: Token![,] = content.parse()?; - - if content.peek(Ident) { - let fork = content.fork(); - if let Ok(second_ident) = fork.parse::() { - if second_ident == "is_ata" { - let _: Ident = content.parse()?; - return Ok(TokenSeedSpec { - variant, - _eq, - is_token: Some(true), - is_ata: true, - seeds: Punctuated::new(), - authority: None, - }); - } - } + // New explicit syntax: + // PDA: TypeName = (seeds = (...)) + // Token: TypeName = (is_token, seeds = (...), authority = (...)) + let mut is_token = None; + let mut seeds = Punctuated::new(); + let mut authority = None; + + while !content.is_empty() { + if content.peek(Ident) { + let ident: Ident = content.parse()?; + let ident_str = ident.to_string(); + + match ident_str.as_str() { + "is_token" | "true" => { + is_token = Some(true); } - - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(true), false, seeds, authority) - } - "true" => { - let _comma: Token![,] = content.parse()?; - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(true), false, seeds, authority) - } - "is_pda" | "false" => { - let _comma: Token![,] = content.parse()?; - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (Some(false), false, seeds, authority) - } - _ => { - let mut seeds = Punctuated::new(); - // Allow function-call expressions starting with an identifier, e.g. max_key(...) - if content.peek(syn::token::Paren) { - let args_tokens; - syn::parenthesized!(args_tokens in content); - let inner_ts: proc_macro2::TokenStream = args_tokens.parse()?; - let call_expr: syn::Expr = - syn::parse2(quote! { #first_ident( #inner_ts ) })?; - seeds.push(SeedElement::Expression(Box::new(call_expr))); - } else { - seeds.push(SeedElement::Expression(Box::new(syn::Expr::Path( - syn::ExprPath { - attrs: vec![], - qself: None, - path: syn::Path::from(first_ident), - }, - )))); + "is_pda" | "false" => { + is_token = Some(false); } - - if content.peek(Token![,]) { - let _comma: Token![,] = content.parse()?; - let (rest, authority) = parse_seeds_with_authority(&content)?; - seeds.extend(rest); - (None, false, seeds, authority) - } else { - (None, false, seeds, None) + "seeds" => { + let _eq: Token![=] = content.parse()?; + let seeds_content; + syn::parenthesized!(seeds_content in content); + seeds = parse_seed_elements(&seeds_content)?; + } + "authority" => { + let _eq: Token![=] = content.parse()?; + authority = Some(parse_authority_seeds(&content)?); + } + _ => { + return Err(syn::Error::new_spanned( + &ident, + format!( + "Unknown keyword '{}'. Expected: is_token, seeds, or authority.\n\ + Use explicit syntax: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + For tokens: TypeName = (is_token, seeds = (...), authority = (...))", + ident_str + ), + )); } } + } else { + return Err(syn::Error::new( + content.span(), + "Expected keyword (is_token, seeds, or authority). Use explicit syntax:\n\ + - PDA: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + - Token: TypeName = (is_token, seeds = (...), authority = (...))", + )); } - } else { - let (seeds, authority) = parse_seeds_with_authority(&content)?; - (None, false, seeds, authority) - }; + + if content.peek(Token![,]) { + let _comma: Token![,] = content.parse()?; + } else { + break; + } + } + + if seeds.is_empty() { + return Err(syn::Error::new_spanned( + &variant, + format!( + "Missing seeds for '{}'. Use: {} = (seeds = (\"seed\", ctx.account, ...))", + variant, variant + ), + )); + } Ok(TokenSeedSpec { variant, _eq, is_token, - is_ata, seeds, authority, }) } } -#[allow(clippy::type_complexity)] -fn parse_seeds_with_authority( - content: ParseStream, -) -> Result<(Punctuated, Option>)> { +/// Parse seed elements from within seeds = (...) +fn parse_seed_elements(content: ParseStream) -> Result> { let mut seeds = Punctuated::new(); - let mut authority = None; while !content.is_empty() { - if content.peek(Ident) { - let fork = content.fork(); - if let Ok(ident) = fork.parse::() { - if ident == "authority" && fork.peek(Token![=]) { - let _: Ident = content.parse()?; - let _: Token![=] = content.parse()?; - - if content.peek(syn::token::Paren) { - let auth_content; - syn::parenthesized!(auth_content in content); - let mut auth_seeds = Vec::new(); - - while !auth_content.is_empty() { - auth_seeds.push(auth_content.parse::()?); - if auth_content.peek(Token![,]) { - let _: Token![,] = auth_content.parse()?; - } else { - break; - } - } - authority = Some(auth_seeds); - } else { - authority = Some(vec![content.parse::()?]); - } - - if content.peek(Token![,]) { - let _: Token![,] = content.parse()?; - continue; - } else { - break; - } - } - } - } - seeds.push(content.parse::()?); if content.peek(Token![,]) { @@ -196,7 +167,29 @@ fn parse_seeds_with_authority( } } - Ok((seeds, authority)) + Ok(seeds) +} + +/// Parse authority seeds - either parenthesized tuple or single expression +fn parse_authority_seeds(content: ParseStream) -> Result> { + if content.peek(syn::token::Paren) { + let auth_content; + syn::parenthesized!(auth_content in content); + let mut auth_seeds = Vec::new(); + + while !auth_content.is_empty() { + auth_seeds.push(auth_content.parse::()?); + if auth_content.peek(Token![,]) { + let _: Token![,] = auth_content.parse()?; + } else { + break; + } + } + Ok(auth_seeds) + } else { + // Single expression (e.g., LIGHT_CPI_SIGNER) + Ok(vec![content.parse::()?]) + } } #[derive(Clone)] @@ -215,1364 +208,1399 @@ impl Parse for SeedElement { } } -pub struct InstructionDataSpec { - pub field_name: Ident, - pub field_type: syn::Type, +/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. +/// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) +fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.XXX pattern (direct field access) + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + fields.push(field_name.clone()); + return; + } + } + } + // Check for ctx.accounts.XXX pattern (nested field access) + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + fields.push(field_name.clone()); + return; + } + } + } + } + } + } + } + // Recurse into base expression + extract_ctx_fields_from_expr(&field_expr.base, fields); + } + syn::Expr::MethodCall(method) => { + // Recurse into receiver and args + extract_ctx_fields_from_expr(&method.receiver, fields); + for arg in &method.args { + extract_ctx_fields_from_expr(arg, fields); + } + } + syn::Expr::Call(call) => { + // Recurse into function args + for arg in &call.args { + extract_ctx_fields_from_expr(arg, fields); + } + } + syn::Expr::Reference(ref_expr) => { + extract_ctx_fields_from_expr(&ref_expr.expr, fields); + } + syn::Expr::Paren(paren) => { + extract_ctx_fields_from_expr(&paren.expr, fields); + } + _ => {} + } } -impl Parse for InstructionDataSpec { - fn parse(input: ParseStream) -> Result { - let field_name: Ident = input.parse()?; - let _eq: Token![=] = input.parse()?; - let field_type: syn::Type = input.parse()?; - - Ok(InstructionDataSpec { - field_name, - field_type, - }) +/// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. +fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { + let mut fields = Vec::new(); + if let SeedElement::Expression(expr) = seed { + extract_ctx_fields_from_expr(expr, &mut fields); } + fields } -struct EnhancedMacroArgs { - account_types: Vec, - pda_seeds: Vec, - token_seeds: Vec, - instruction_data: Vec, +/// Extract all ctx.accounts.XXX field names from a list of seed elements. +/// Deduplicates the fields. +pub fn extract_ctx_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + all_fields.extend(extract_ctx_account_fields(seed)); + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() } -impl Parse for EnhancedMacroArgs { - fn parse(input: ParseStream) -> Result { - let mut account_types = Vec::new(); - let mut pda_seeds = Vec::new(); - let mut token_seeds = Vec::new(); - let mut instruction_data = Vec::new(); - - let mut _item_count = 0; - while !input.is_empty() { - let ident: Ident = input.parse()?; - - if input.peek(Token![=]) { - let _eq: Token![=] = input.parse()?; - - if input.peek(syn::token::Paren) { - let content; - syn::parenthesized!(content in input); - let inside: TokenStream = content.parse()?; - let seed_spec: TokenSeedSpec = syn::parse2(quote! { #ident = (#inside) })?; - - let is_token_account = seed_spec.is_token.unwrap_or(false); - if is_token_account { - token_seeds.push(seed_spec); - } else { - pda_seeds.push(seed_spec); - account_types.push(ident); +/// Phase 5: Extract data.XXX field names from an expression recursively. +fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for data.XXX pattern + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + fields.push(field_name.clone()); + return; + } } - } else { - let field_type: syn::Type = input.parse()?; - instruction_data.push(InstructionDataSpec { - field_name: ident, - field_type, - }); } - } else { - account_types.push(ident); } - - if input.peek(Token![,]) { - let _comma: Token![,] = input.parse()?; - } else { - break; + // Recurse into base expression + extract_data_fields_from_expr(&field_expr.base, fields); + } + syn::Expr::MethodCall(method) => { + extract_data_fields_from_expr(&method.receiver, fields); + for arg in &method.args { + extract_data_fields_from_expr(arg, fields); } - _item_count += 1; } - Ok(EnhancedMacroArgs { - account_types, - pda_seeds, - token_seeds, - instruction_data, - }) + syn::Expr::Call(call) => { + for arg in &call.args { + extract_data_fields_from_expr(arg, fields); + } + } + syn::Expr::Reference(ref_expr) => { + extract_data_fields_from_expr(&ref_expr.expr, fields); + } + syn::Expr::Paren(paren) => { + extract_data_fields_from_expr(&paren.expr, fields); + } + _ => {} } } -#[allow(clippy::too_many_arguments)] -#[inline(never)] -pub fn add_compressible_instructions( - args: TokenStream, - mut module: ItemMod, -) -> Result { - let enhanced_args = match syn::parse2::(args.clone()) { - Ok(args) => args, - Err(e) => { - eprintln!("ERROR: Failed to parse macro args: {}", e); - eprintln!("Args were: {}", args); - return Err(e); +/// Phase 5: Extract all data.XXX field names from a list of seed elements. +pub fn extract_data_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + if let SeedElement::Expression(expr) = seed { + extract_data_fields_from_expr(expr, &mut all_fields); } - }; + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() +} - let account_types = enhanced_args.account_types; - let pda_seeds = Some(enhanced_args.pda_seeds); - let token_seeds = Some(enhanced_args.token_seeds); - let instruction_data = enhanced_args.instruction_data; +pub struct InstructionDataSpec { + pub field_name: Ident, + pub field_type: syn::Type, +} - if module.content.is_none() { - return Err(macro_error!(&module, "Module must have a body")); - } +impl Parse for InstructionDataSpec { + fn parse(input: ParseStream) -> Result { + let field_name: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; + let field_type: syn::Type = input.parse()?; - if account_types.is_empty() { - return Err(macro_error!( - &module, - "At least one account type must be specified" - )); + Ok(InstructionDataSpec { + field_name, + field_type, + }) } +} - let size_validation_checks = validate_compressed_account_sizes(&account_types)?; - - let content = module.content.as_mut().unwrap(); +pub fn generate_decompress_context_impl( + _variant: InstructionVariant, + pda_ctx_seeds: Vec, + token_variant_ident: Ident, +) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); - let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { - if !token_seed_specs.is_empty() { - crate::compressible::seed_providers::generate_token_account_variant_enum( - token_seed_specs, - )? - } else { - crate::compressible::utils::generate_empty_ctoken_enum() - } - } else { - crate::compressible::utils::generate_empty_ctoken_enum() - }; + let trait_impl = + crate::compressible::decompress_context::generate_decompress_context_trait_impl( + pda_ctx_seeds, + token_variant_ident, + lifetime, + )?; - if let Some(ref token_seed_specs) = token_seeds { - for spec in token_seed_specs { - if spec.is_ata { - if !spec.seeds.is_empty() { - return Err(macro_error!( - &spec.variant, - "ATA variant '{}' must not have seeds - ATAs are derived from owner+mint only", - spec.variant - )); - } - if spec.authority.is_some() { - return Err(macro_error!( - &spec.variant, - "ATA variant '{}' must not have authority - ATAs are owned by user wallets", - spec.variant - )); - } - } else if spec.authority.is_none() { - return Err(macro_error!( - &spec.variant, - "Program-owned token account '{}' must specify authority = for compression signing. For user-owned ATAs, use is_ata flag instead.", - spec.variant - )); - } - } - } + Ok(syn::parse_quote! { + mod __decompress_context_impl { + use super::*; - let mut account_types_stream = TokenStream::new(); - for (i, account_type) in account_types.iter().enumerate() { - if i > 0 { - account_types_stream.extend(quote! { , }); + #trait_impl } - account_types_stream.extend(quote! { #account_type }); - } - let enum_and_traits = - crate::compressible::variant_enum::compressed_account_variant(account_types_stream)?; - - // Generate SeedParams struct for instruction data fields - let seed_params_struct = { - let param_fields: Vec<_> = instruction_data - .iter() - .map(|spec| { - let field_name = &spec.field_name; - let field_type = &spec.field_type; - quote! { - pub #field_name: #field_type - } - }) - .collect(); + }) +} - quote! { - #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] - pub struct SeedParams { - #(#param_fields,)* - } +pub fn generate_process_decompress_accounts_idempotent( + _variant: InstructionVariant, + _instruction_data: &[InstructionDataSpec], +) -> Result { + // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_decompress_accounts_idempotent<'info>( + accounts: &DecompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::process_decompress_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + proof, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + std::option::Option::None::<&()>, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) } - }; + }) +} - let has_pda_seeds = pda_seeds.as_ref().map(|p| !p.is_empty()).unwrap_or(false); - let has_token_seeds = token_seeds.as_ref().map(|t| !t.is_empty()).unwrap_or(false); +pub fn generate_decompress_instruction_entrypoint( + _variant: InstructionVariant, + _instruction_data: &[InstructionDataSpec], +) -> Result { + // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx - let instruction_variant = match (has_pda_seeds, has_token_seeds) { - (true, true) => InstructionVariant::Mixed, - (true, false) => InstructionVariant::PdaOnly, - (false, true) => InstructionVariant::TokenOnly, - (false, false) => { - return Err(macro_error!( - &module, - "At least one PDA or token seed specification must be provided" - )) + Ok(syn::parse_quote! { + #[inline(never)] + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_decompress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + proof, + compressed_accounts, + system_accounts_offset, + ) } - }; - - let error_codes = generate_error_codes(instruction_variant)?; - - let required_accounts = extract_required_accounts_from_seeds(&pda_seeds, &token_seeds)?; + }) +} - let decompress_accounts = - generate_decompress_accounts_struct(&required_accounts, instruction_variant)?; +pub fn generate_compress_context_impl( + _variant: InstructionVariant, + account_types: Vec, +) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); - let pda_seed_provider_impls: Result> = account_types - .iter() - .map(|name| { - let name_str = name.to_string(); - let spec = if let Some(ref pda_seed_specs) = pda_seeds { - pda_seed_specs - .iter() - .find(|s| s.variant == name_str) - .ok_or_else(|| { - macro_error!( - name, - "No seed specification for account type '{}'. All accounts must have seed specifications.", - name_str - ) - })? - } else { - return Err(macro_error!( - name, - "No seed specifications provided. Use: AccountType = (\"seed\", data.field)" - )); - }; - let seed_derivation = - generate_pda_seed_derivation_for_trait(spec, &instruction_data)?; - Ok(quote! { - impl<'info> light_sdk::compressible::PdaSeedDerivation, SeedParams> for #name { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - accounts: &DecompressAccountsIdempotent<'info>, - seed_params: &SeedParams, - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - }) - }) - .collect(); - let pda_seed_provider_impls = pda_seed_provider_impls?; - - let helper_packed_fns: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let func_name = format_ident!("handle_packed_{}", name); + let compress_arms: Vec<_> = account_types.iter().map(|name| { quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn #func_name<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, 'info>, - address_space: solana_pubkey::Pubkey, - solana_accounts: &[solana_account_info::AccountInfo<'info>], - i: usize, - packed: &#packed_name, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - post_system_accounts: &[solana_account_info::AccountInfo<'info>], - compressed_pda_infos: &mut Vec, - seed_accounts: &DecompressAccountsIdempotent<'info>, - seed_params: &SeedParams, - ) -> std::result::Result<(), solana_program_error::ProgramError> { - light_sdk::compressible::handle_packed_pda_variant::<#name, #packed_name, DecompressAccountsIdempotent<'info>, SeedParams>( - accounts.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - packed, - meta, - post_system_accounts, - compressed_pda_infos, - &crate::ID, - seed_accounts, - std::option::Option::Some(seed_params), - ) - } - } - }).collect(); + d if d == #name::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data().map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + drop(data_borrow); - let call_unpacked_arms: Vec<_> = account_types.iter().map(|name| { - quote! { - CompressedAccountVariant::#name(_) => { - unreachable!("Unpacked variants should not be present during decompression - accounts are always packed in-flight"); + let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( + program_id, + account_info, + &mut account_data, + meta, + cpi_accounts, + &compression_config.address_space, + )?; + // Lamport transfers are handled by close() in process_compress_pda_accounts_idempotent + // All lamports go to rent_sponsor for simplicity + Ok(Some(compressed_info)) } } }).collect(); - let call_packed_arms: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let func_name = format_ident!("handle_packed_{}", name); - quote! { - CompressedAccountVariant::#packed_name(packed) => { - match #func_name(accounts, &cpi_accounts, address_space, solana_accounts, i, &packed, &meta, post_system_accounts, &mut compressed_pda_infos, accounts, seed_params) { - std::result::Result::Ok(()) => {}, - std::result::Result::Err(e) => return std::result::Result::Err(e), - } - } - } - }).collect(); - let trait_impls: syn::ItemMod = syn::parse_quote! { - mod __trait_impls { + Ok(syn::parse_quote! { + mod __compress_context_impl { use super::*; + use light_sdk::LightDiscriminator; + use light_sdk::compressible::HasCompressionInfo; - impl light_sdk::compressible::HasTokenVariant for CompressedAccountData { - fn is_packed_token(&self) -> bool { - matches!(self.data, CompressedAccountVariant::PackedCTokenData(_)) + impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { + fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &*self.fee_payer } - } - - impl light_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), anchor_lang::prelude::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_seeds(self, &ctx).map_err(|e: anchor_lang::error::Error| -> anchor_lang::prelude::ProgramError { e.into() }) + fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.config } - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), anchor_lang::prelude::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_authority_seeds(self, &ctx).map_err(|e: anchor_lang::error::Error| -> anchor_lang::prelude::ProgramError { e.into() }) + fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.rent_sponsor } - } - - impl light_token_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_seeds(self, &ctx) - .map_err(|e: anchor_lang::error::Error| { - let program_error: anchor_lang::prelude::ProgramError = e.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - }) + fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.compression_authority } - fn get_authority_seeds<'a, 'info>( + fn compress_pda_account( &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - use super::ctoken_seed_system::{ - CTokenSeedContext, - CTokenSeedProvider as LocalProvider, - }; - let ctx = CTokenSeedContext { - accounts, - remaining_accounts, - }; - LocalProvider::get_authority_seeds(self, &ctx) - .map_err(|e: anchor_lang::error::Error| { - let program_error: anchor_lang::prelude::ProgramError = e.into(); + account_info: &solana_account_info::AccountInfo<#lifetime>, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, + compression_config: &light_sdk::compressible::CompressibleConfig, + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result, solana_program_error::ProgramError> { + let data = account_info.try_borrow_data().map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + let discriminator = &data[0..8]; + + match discriminator { + #(#compress_arms)* + _ => { + let err: anchor_lang::error::Error = anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); let code = match program_error { anchor_lang::prelude::ProgramError::Custom(code) => code, _ => 0, }; - solana_program_error::ProgramError::Custom(code) - }) + Err(solana_program_error::ProgramError::Custom(code)) + } + } } } } - }; + }) +} - let ctoken_trait_system: syn::ItemMod = syn::parse_quote! { - pub mod ctoken_seed_system { - use super::*; +pub fn generate_process_compress_accounts_idempotent( + _variant: InstructionVariant, +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_compress_accounts_idempotent<'info>( + accounts: &CompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) +} - pub struct CTokenSeedContext<'a, 'info> { - pub accounts: &'a DecompressAccountsIdempotent<'info>, - pub remaining_accounts: &'a [solana_account_info::AccountInfo<'info>], - } +pub fn generate_compress_instruction_entrypoint( + _variant: InstructionVariant, +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_compress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + compressed_accounts, + system_accounts_offset, + ) + } + }) +} - pub trait CTokenSeedProvider { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)>; +/// Phase 3: Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. +/// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) +#[inline(never)] +fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( + spec: &TokenSeedSpec, + _instruction_data: &[InstructionDataSpec], + ctx_seed_fields: &[syn::Ident], +) -> Result { + let mut bindings: Vec = Vec::new(); + let mut seed_refs = Vec::new(); - fn get_authority_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)>; - } - } - }; + // Convert ctx_seed_fields to a set for quick lookup + let ctx_field_names: std::collections::HashSet = + ctx_seed_fields.iter().map(|f| f.to_string()).collect(); - let helpers_module: syn::ItemMod = { - let helper_packed_fns = helper_packed_fns.clone(); - let call_unpacked_arms = call_unpacked_arms.clone(); - let call_packed_arms = call_packed_arms.clone(); - syn::parse_quote! { - mod __macro_helpers { - use super::*; - use crate::state::*; // Import Packed* types from state module - #(#helper_packed_fns)* - #[inline(never)] - pub fn collect_pda_and_token<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'b, 'info>, - address_space: solana_pubkey::Pubkey, - compressed_accounts: Vec, - solana_accounts: &[solana_account_info::AccountInfo<'info>], - seed_params: &SeedParams, - ) -> std::result::Result<( - Vec, - Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )>, - ), solana_program_error::ProgramError> { - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - let estimated_capacity = compressed_accounts.len(); - let mut compressed_pda_infos = Vec::with_capacity(estimated_capacity); - let mut compressed_token_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )> = Vec::with_capacity(estimated_capacity); - - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let meta = compressed_data.meta; - match compressed_data.data { - #(#call_unpacked_arms)* - #(#call_packed_arms)* - CompressedAccountVariant::PackedCTokenData(mut data) => { - data.token_data.version = 3; - compressed_token_accounts.push((data, meta)); + // Recursively rewrite expressions: + // - `data.` -> `self.` (from unpacked compressed account data - Phase 4) + // - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + // - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + fn map_pda_expr_to_ctx_seeds( + expr: &syn::Expr, + ctx_field_names: &std::collections::HashSet, + ) -> syn::Expr { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Handle nested field access: ctx.accounts.field_name -> ctx_seeds.field_name + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // ctx.accounts.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } } - CompressedAccountVariant::CTokenData(_) => { - unreachable!(); + } + } + // Handle direct field access + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + // Phase 4: data.field -> self.field (from unpacked compressed account data) + return syn::parse_quote! { self.#field_name }; + } else if segment.ident == "ctx" { + let field_str = field_name.to_string(); + if ctx_field_names.contains(&field_str) { + // ctx.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; + } } } } - - std::result::Result::Ok((compressed_pda_infos, compressed_token_accounts)) } + expr.clone() + } + syn::Expr::MethodCall(method_call) => { + let mut new_method_call = method_call.clone(); + new_method_call.receiver = Box::new(map_pda_expr_to_ctx_seeds( + &method_call.receiver, + ctx_field_names, + )); + new_method_call.args = method_call + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::MethodCall(new_method_call) + } + syn::Expr::Call(call_expr) => { + let mut new_call_expr = call_expr.clone(); + new_call_expr.args = call_expr + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::Call(new_call_expr) + } + syn::Expr::Reference(ref_expr) => { + let mut new_ref_expr = ref_expr.clone(); + new_ref_expr.expr = + Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); + syn::Expr::Reference(new_ref_expr) } + _ => expr.clone(), } - }; + } - let token_variant_name = format_ident!("CTokenAccountVariant"); + for (i, seed) in spec.seeds.iter().enumerate() { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + seed_refs.push(quote! { #value.as_bytes() }); + } + SeedElement::Expression(expr) => { + // Handle byte string literals: b"seed" -> use directly (no .as_bytes()) + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + seed_refs.push(quote! { &[#(#bytes),*] }); + continue; + } + } - let decompress_context_impl = generate_decompress_context_impl( - instruction_variant, - account_types.clone(), - token_variant_name, - )?; - let decompress_processor_fn = - generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; - let decompress_instruction = - generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { + seed_refs.push( + quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }, + ); + continue; + } + } + } - let compress_accounts: syn::ItemStruct = match instruction_variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => syn::parse_quote! { - #[derive(Accounts)] - pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, + let binding_name = + syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); + let mapped_expr = map_pda_expr_to_ctx_seeds(expr, &ctx_field_names); + bindings.push(quote! { + let #binding_name = #mapped_expr; + }); + seed_refs.push(quote! { (#binding_name).as_ref() }); + } } - }, - }; + } - let compress_context_impl = - generate_compress_context_impl(instruction_variant, account_types.clone())?; - let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; - let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; + let indices: Vec = (0..seed_refs.len()).collect(); - let processor_module: syn::ItemMod = syn::parse_quote! { - mod __processor_functions { - use super::*; - #decompress_processor_fn - #compress_processor_fn - } - }; + Ok(quote! { + #(#bindings)* + let seeds: &[&[u8]] = &[#(#seed_refs,)*]; + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + #( + seeds_vec.push(seeds[#indices].to_vec()); + )* + seeds_vec.push(vec![bump]); + Ok((seeds_vec, pda)) + }) +} - let init_config_accounts: syn::ItemStruct = syn::parse_quote! { - #[derive(Accounts)] - pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Checked by SDK +#[inline(never)] +fn generate_decompress_accounts_struct( + required_accounts: &[String], + variant: InstructionVariant, +) -> Result { + let mut account_fields = vec![ + quote! { #[account(mut)] - pub config: AccountInfo<'info>, + pub fee_payer: Signer<'info> + }, + quote! { /// CHECK: Checked by SDK - pub program_data: AccountInfo<'info>, - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, - } - }; + pub config: AccountInfo<'info> + }, + ]; - let update_config_accounts: syn::ItemStruct = syn::parse_quote! { - #[derive(Accounts)] - pub struct UpdateCompressionConfig<'info> { - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - pub authority: Signer<'info>, + match variant { + InstructionVariant::PdaOnly => { + unreachable!() } - }; - - let init_config_instruction: syn::ItemFn = syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn initialize_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, - write_top_up: u32, - rent_sponsor: Pubkey, - compression_authority: Pubkey, - rent_config: light_compressible::rent::RentConfig, - address_space: Vec, - ) -> Result<()> { - light_sdk::compressible::process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - Ok(()) + InstructionVariant::TokenOnly => { + unreachable!() } - }; - - let update_config_instruction: syn::ItemFn = syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn update_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - light_sdk::compressible::process_update_compression_config( - ctx.accounts.config.as_ref(), - ctx.accounts.authority.as_ref(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - Ok(()) + InstructionVariant::Mixed => { + account_fields.extend(vec![ + quote! { + /// CHECK: anyone can pay + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info> + }, + quote! { + /// CHECK: optional - only needed if decompressing tokens + #[account(mut)] + pub ctoken_rent_sponsor: Option> + }, + ]); } - }; + } - // Insert SeedParams struct - let seed_params_item: Item = syn::parse2(seed_params_struct)?; - content.1.push(seed_params_item); + match variant { + InstructionVariant::TokenOnly => { + unreachable!() + } + InstructionVariant::Mixed => { + account_fields.extend(vec![ + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] + pub light_token_program: Option> + }, + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] + pub ctoken_cpi_authority: Option> + }, + quote! { + /// CHECK: Checked by SDK + pub ctoken_config: Option> + }, + ]); + } + InstructionVariant::PdaOnly => { + unreachable!() + } + } - content.1.push(Item::Struct(decompress_accounts)); - content.1.push(Item::Mod(helpers_module)); - content.1.push(Item::Mod(ctoken_trait_system)); - content.1.push(Item::Mod(trait_impls)); - content.1.push(Item::Mod(decompress_context_impl)); - content.1.push(Item::Mod(processor_module)); - content.1.push(Item::Fn(decompress_instruction)); - content.1.push(Item::Struct(compress_accounts)); - content.1.push(Item::Mod(compress_context_impl)); - content.1.push(Item::Fn(compress_instruction)); - content.1.push(Item::Struct(init_config_accounts)); - content.1.push(Item::Struct(update_config_accounts)); - content.1.push(Item::Fn(init_config_instruction)); - content.1.push(Item::Fn(update_config_instruction)); + let standard_fields = [ + "fee_payer", + "rent_sponsor", + "ctoken_rent_sponsor", + "config", + "light_token_program", + "ctoken_cpi_authority", + "ctoken_config", + ]; - if let Some(ref seeds) = token_seeds { - if !seeds.is_empty() { - let impl_code = - crate::compressible::seed_providers::generate_token_seed_provider_implementation( - seeds, - )?; - let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code).map_err(|e| { - syn::Error::new_spanned( - &seeds[0].variant, - format!("Failed to parse ctoken implementation: {}", e), - ) - })?; - content.1.push(Item::Impl(ctoken_impl)); + for account_name in required_accounts { + if !standard_fields.contains(&account_name.as_str()) { + let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); + // Mark seed accounts as writable to support CPI calls that may need them writable + account_fields.push(quote! { + /// CHECK: optional seed account - may be used in CPIs + #[account(mut)] + pub #account_ident: Option> + }); } } - let client_seed_functions = - crate::compressible::seed_providers::generate_client_seed_functions( - &account_types, - &pda_seeds, - &token_seeds, - &instruction_data, - )?; - - // Add allow attribute to module itself to suppress clippy warnings - module.attrs.push(syn::parse_quote! { - #[allow(clippy::too_many_arguments)] - }); + let struct_def = quote! { + #[derive(Accounts)] + pub struct DecompressAccountsIdempotent<'info> { + #(#account_fields,)* + } + }; - Ok(quote! { - #size_validation_checks - #error_codes - #ctoken_enum - #enum_and_traits - #(#pda_seed_provider_impls)* - #[allow(non_snake_case)] - #module - #client_seed_functions - }) + syn::parse2(struct_def) } -pub fn generate_decompress_context_impl( - _variant: InstructionVariant, - pda_type_idents: Vec, - token_variant_ident: Ident, -) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); - - let trait_impl = - crate::compressible::decompress_context::generate_decompress_context_trait_impl( - pda_type_idents, - token_variant_ident, - lifetime, - )?; - - Ok(syn::parse_quote! { - mod __decompress_context_impl { - use super::*; - - #trait_impl +#[inline(never)] +fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result { + let size_checks: Vec<_> = account_types.iter().map(|account_type| { + quote! { + const _: () = { + const COMPRESSED_SIZE: usize = 8 + <#account_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; + if COMPRESSED_SIZE > 800 { + panic!(concat!( + "Compressed account '", stringify!(#account_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" + )); + } + }; } - }) + }).collect(); + + Ok(quote! { #(#size_checks)* }) } -pub fn generate_process_decompress_accounts_idempotent( - _variant: InstructionVariant, - instruction_data: &[InstructionDataSpec], -) -> Result { - // If we have seed parameters, accept them as a single struct - let (params, seed_params_arg) = if !instruction_data.is_empty() { - ( - quote! { seed_data: SeedParams, }, - quote! { std::option::Option::Some(&seed_data) }, - ) - } else { - (quote! {}, quote! { std::option::Option::None }) +#[inline(never)] +fn generate_error_codes(variant: InstructionVariant) -> Result { + let base_errors = quote! { + #[msg("Rent sponsor mismatch")] + InvalidRentSponsor, + #[msg("Missing seed account")] + MissingSeedAccount, + #[msg("Seed value does not match account data")] + SeedMismatch, }; - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_decompress_accounts_idempotent<'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - #params - ) -> Result<()> { - light_sdk::compressible::process_decompress_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - #seed_params_arg, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + let variant_specific_errors = match variant { + InstructionVariant::PdaOnly => unreachable!(), + InstructionVariant::TokenOnly => unreachable!(), + InstructionVariant::Mixed => quote! { + #[msg("Not implemented")] + CTokenDecompressionNotImplemented, + #[msg("Not implemented")] + PdaDecompressionNotImplemented, + #[msg("Not implemented")] + TokenCompressionNotImplemented, + #[msg("Not implemented")] + PdaCompressionNotImplemented, + }, + }; + + Ok(quote! { + #[error_code] + pub enum CompressibleInstructionError { + #base_errors + #variant_specific_errors } }) } -pub fn generate_decompress_instruction_entrypoint( - _variant: InstructionVariant, - instruction_data: &[InstructionDataSpec], -) -> Result { - // If we have seed parameters, pass them as a single struct - let (params, args) = if !instruction_data.is_empty() { - (quote! { seed_data: SeedParams, }, quote! { seed_data, }) - } else { - (quote! {}, quote! {}) - }; +/// Convert ClassifiedSeed to SeedElement (Punctuated) +fn convert_classified_to_seed_elements( + seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], +) -> Punctuated { + use crate::compressible::anchor_seeds::ClassifiedSeed; + + let mut result = Punctuated::new(); + for seed in seeds { + let elem = match seed { + ClassifiedSeed::Literal(bytes) => { + // Convert to string literal + if let Ok(s) = std::str::from_utf8(bytes) { + SeedElement::Literal(syn::LitStr::new(s, proc_macro2::Span::call_site())) + } else { + // Byte array - use expression + let byte_values: Vec<_> = bytes.iter().map(|b| quote!(#b)).collect(); + let expr: Expr = syn::parse_quote!(&[#(#byte_values),*]); + SeedElement::Expression(Box::new(expr)) + } + } + ClassifiedSeed::Constant(path) => { + let expr: Expr = syn::parse_quote!(#path); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::CtxAccount(ident) => { + let expr: Expr = syn::parse_quote!(ctx.#ident); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { + field_name, + conversion: None, + } => { + let expr: Expr = syn::parse_quote!(data.#field_name); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { + field_name, + conversion: Some(method), + } => { + let expr: Expr = syn::parse_quote!(data.#field_name.#method()); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::FunctionCall { func, ctx_args } => { + let args: Vec = ctx_args + .iter() + .map(|arg| syn::parse_quote!(&ctx.#arg.key())) + .collect(); + let expr: Expr = syn::parse_quote!(#func(#(#args),*)); + SeedElement::Expression(Box::new(expr)) + } + }; + result.push(elem); + } + result +} - Ok(syn::parse_quote! { - #[inline(never)] - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - #params - ) -> Result<()> { - __processor_functions::process_decompress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - proof, - compressed_accounts, - system_accounts_offset, - #args - ) - } - }) +fn convert_classified_to_seed_elements_vec( + seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], +) -> Vec { + convert_classified_to_seed_elements(seeds) + .into_iter() + .collect() } -pub fn generate_compress_context_impl( - _variant: InstructionVariant, +/// Generate all code from extracted seeds (shared logic with add_compressible_instructions) +#[inline(never)] +fn generate_from_extracted_seeds( + module: &mut ItemMod, account_types: Vec, -) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); + pda_seeds: Option>, + token_seeds: Option>, + instruction_data: Vec, +) -> Result { + let size_validation_checks = validate_compressed_account_sizes(&account_types)?; - let compress_arms: Vec<_> = account_types.iter().map(|name| { - quote! { - d if d == #name::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - drop(data_borrow); + let content = module.content.as_mut().unwrap(); + let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { + if !token_seed_specs.is_empty() { + crate::compressible::seed_providers::generate_ctoken_account_variant_enum( + token_seed_specs, + )? + } else { + crate::compressible::utils::generate_empty_ctoken_enum() + } + } else { + crate::compressible::utils::generate_empty_ctoken_enum() + }; - let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - // Compute rent-based close distribution and transfer lamports: - // - Completed epochs to rent sponsor - // - Partial epoch (unused) to fee payer (user refund) - #[cfg(target_os = "solana")] - let current_slot = anchor_lang::solana_program::sysvar::clock::Clock::get() - .map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)? - .slot; - #[cfg(not(target_os = "solana"))] - let current_slot = 0; - let bytes = account_info.data_len() as u64; - let current_lamports = account_info.lamports(); - let rent_exemption = anchor_lang::solana_program::sysvar::rent::Rent::get() - .map_err(|_| anchor_lang::prelude::ProgramError::UnsupportedSysvar)? - .minimum_balance(bytes as usize); - let ci_ref = account_data.compression_info(); - let state = light_compressible::rent::AccountRentState { - num_bytes: bytes, - current_slot, - current_lamports, - last_claimed_slot: ci_ref.last_claimed_slot(), - }; - let dist = state.calculate_close_distribution(&ci_ref.rent_config, rent_exemption); - // Transfer partial epoch back to fee payer (user) - if dist.to_user > 0 { - let fee_payer_info = self.fee_payer.to_account_info(); - let mut src = account_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - let mut dst = fee_payer_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - **src = src.checked_sub(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?; - **dst = dst.checked_add(dist.to_user).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?; - } - // Transfer completed epochs (and base) to rent sponsor - if dist.to_rent_sponsor > 0 { - let rent_sponsor_info = self.rent_sponsor.to_account_info(); - let mut src = account_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - let mut dst = rent_sponsor_info.try_borrow_mut_lamports().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - program_error - })?; - **src = src.checked_sub(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::InsufficientFunds)?; - **dst = dst.checked_add(dist.to_rent_sponsor).ok_or(anchor_lang::prelude::ProgramError::Custom(0))?; - } - Ok(Some(compressed_info)) + if let Some(ref token_seed_specs) = token_seeds { + for spec in token_seed_specs { + if spec.authority.is_none() { + return Err(macro_error!( + &spec.variant, + "Token account '{}' must specify authority = for compression signing.", + spec.variant + )); } } - }).collect(); - - Ok(syn::parse_quote! { - mod __compress_context_impl { - use super::*; - use light_sdk::LightDiscriminator; - use light_sdk::compressible::HasCompressionInfo; - - impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { - fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &*self.fee_payer - } + } - fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.config - } + let pda_ctx_seeds: Vec = pda_seeds + .as_ref() + .map(|specs| { + specs + .iter() + .map(|spec| { + let ctx_fields = extract_ctx_seed_fields(&spec.seeds); + crate::compressible::variant_enum::PdaCtxSeedInfo::new( + spec.variant.clone(), + ctx_fields, + ) + }) + .collect() + }) + .unwrap_or_default(); - fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.rent_sponsor - } + let account_type_refs: Vec<&Ident> = account_types.iter().collect(); + let enum_and_traits = + crate::compressible::variant_enum::compressed_account_variant_with_ctx_seeds( + &account_type_refs, + &pda_ctx_seeds, + )?; - fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.compression_authority - } + let seed_params_struct = quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] + pub struct SeedParams; + }; - fn compress_pda_account( - &self, - account_info: &solana_account_info::AccountInfo<#lifetime>, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, - compression_config: &light_sdk::compressible::CompressibleConfig, - program_id: &solana_pubkey::Pubkey, - ) -> std::result::Result, solana_program_error::ProgramError> { - let data = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - let discriminator = &data[0..8]; + let instruction_data_types: std::collections::HashMap = instruction_data + .iter() + .map(|spec| (spec.field_name.to_string(), &spec.field_type)) + .collect(); - match discriminator { - #(#compress_arms)* - _ => { - let err: anchor_lang::error::Error = anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - Err(solana_program_error::ProgramError::Custom(code)) + let seeds_structs_and_constructors: Vec = if let Some(ref pda_seed_specs) = + pda_seeds + { + pda_seed_specs + .iter() + .zip(pda_ctx_seeds.iter()) + .map(|(spec, ctx_info)| { + let type_name = &spec.variant; + let seeds_struct_name = format_ident!("{}Seeds", type_name); + let constructor_name = format_ident!("{}", to_snake_case(&type_name.to_string())); + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_field_decls: Vec<_> = ctx_fields.iter().map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }).collect(); + let data_fields = extract_data_seed_fields(&spec.seeds); + let data_field_decls: Vec<_> = data_fields.iter().filter_map(|field| { + let field_str = field.to_string(); + instruction_data_types.get(&field_str).map(|ty| { + quote! { pub #field: #ty } + }) + }).collect(); + let data_verifications: Vec<_> = data_fields.iter().map(|field| { + quote! { + if data.#field != seeds.#field { + return std::result::Result::Err(CompressibleInstructionError::SeedMismatch.into()); + } + } + }).collect(); + quote! { + #[derive(Clone, Debug)] + pub struct #seeds_struct_name { + #(#ctx_field_decls,)* + #(#data_field_decls,)* + } + impl RentFreeAccountVariant { + pub fn #constructor_name( + account_data: &[u8], + seeds: #seeds_struct_name, + ) -> std::result::Result { + use anchor_lang::AnchorDeserialize; + let data = #type_name::deserialize(&mut &account_data[..])?; + + #(#data_verifications)* + + std::result::Result::Ok(Self::#type_name { + data, + #(#ctx_fields: seeds.#ctx_fields,)* + }) + } + } + impl light_sdk::compressible::IntoVariant for #seeds_struct_name { + fn into_variant(self, data: &[u8]) -> std::result::Result { + RentFreeAccountVariant::#constructor_name(data, self) } } } - } - } - }) -} + }) + .collect() + } else { + Vec::new() + }; -pub fn generate_process_compress_accounts_idempotent( - _variant: InstructionVariant, -) -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_compress_accounts_idempotent<'info>( - accounts: &CompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} + let has_pda_seeds = pda_seeds.as_ref().map(|p| !p.is_empty()).unwrap_or(false); + let has_token_seeds = token_seeds.as_ref().map(|t| !t.is_empty()).unwrap_or(false); -pub fn generate_compress_instruction_entrypoint( - _variant: InstructionVariant, -) -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - __processor_functions::process_compress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - compressed_accounts, - system_accounts_offset, - ) + let instruction_variant = match (has_pda_seeds, has_token_seeds) { + (true, true) => InstructionVariant::Mixed, + (true, false) => InstructionVariant::PdaOnly, + (false, true) => InstructionVariant::TokenOnly, + (false, false) => { + return Err(macro_error!( + module, + "At least one PDA or token seed specification must be provided" + )) } - }) -} + }; -#[inline(never)] -fn generate_pda_seed_derivation_for_trait( - spec: &TokenSeedSpec, - _instruction_data: &[InstructionDataSpec], -) -> Result { - let mut bindings: Vec = Vec::new(); - let mut seed_refs = Vec::new(); + let error_codes = generate_error_codes(instruction_variant)?; + let decompress_accounts = generate_decompress_accounts_struct(&[], instruction_variant)?; - // Recursively rewrite expressions: - // - `data.` -> `seed_params.` (from instruction params, not struct fields!) - // - `ctx.accounts.` -> `accounts.` - // - `ctx.` -> `accounts.` - // While preserving function/method calls and references. - fn map_pda_expr_to_params(expr: &syn::Expr) -> syn::Expr { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Handle nested field access: ctx.accounts.field_name -> accounts.field_name.as_ref().ok_or(...)?.key() - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError { - let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into(); - err.into() - })?.key() }; - } - } - } - } - } + let pda_seed_provider_impls: Result> = account_types + .iter() + .zip(pda_ctx_seeds.iter()) + .map(|(name, ctx_info)| { + let name_str = name.to_string(); + let spec = if let Some(ref pda_seed_specs) = pda_seeds { + pda_seed_specs + .iter() + .find(|s| s.variant == name_str) + .ok_or_else(|| { + macro_error!(name, "No seed specification for account type '{}'", name_str) + })? + } else { + return Err(macro_error!(name, "No seed specifications provided")); + }; + + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_fields_decl: Vec<_> = ctx_fields.iter().map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }).collect(); + + let ctx_seeds_struct = if ctx_fields.is_empty() { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name; + } + } else { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name { + #(#ctx_fields_decl),* } - // Handle direct field access - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - // data.field -> seed_params.field (from instruction params!) - return syn::parse_quote! { seed_params.#field_name }; - } else if segment.ident == "ctx" { - // ctx.field -> accounts.field.as_ref().ok_or(...)?.key() (error if optional account is missing) - return syn::parse_quote! { accounts.#field_name.as_ref().ok_or_else(|| -> solana_program_error::ProgramError { - let err: anchor_lang::error::Error = CompressibleInstructionError::MissingSeedAccount.into(); - err.into() - })?.key() }; - } - } + } + }; + + let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, &instruction_data, ctx_fields)?; + Ok(quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &(), + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation } } - expr.clone() - } - syn::Expr::MethodCall(method_call) => { - // Special case: ctx.accounts.account_name.key() -> accounts.account_name.key() - // This is already handled by the Field case transforming ctx.accounts.X -> accounts.X - let mut new_method_call = method_call.clone(); - new_method_call.receiver = Box::new(map_pda_expr_to_params(&method_call.receiver)); - new_method_call.args = method_call - .args - .iter() - .map(map_pda_expr_to_params) - .collect(); - syn::Expr::MethodCall(new_method_call) - } - syn::Expr::Call(call_expr) => { - // Map function args recursively. We do not transform the function path. - let mut new_call_expr = call_expr.clone(); - new_call_expr.args = call_expr.args.iter().map(map_pda_expr_to_params).collect(); - syn::Expr::Call(new_call_expr) - } - syn::Expr::Reference(ref_expr) => { - let mut new_ref_expr = ref_expr.clone(); - new_ref_expr.expr = Box::new(map_pda_expr_to_params(&ref_expr.expr)); - syn::Expr::Reference(new_ref_expr) - } - _ => { - // For other expressions (constants, literals, paths), leave as-is - expr.clone() + }) + }) + .collect(); + let pda_seed_provider_impls = pda_seed_provider_impls?; + + let trait_impls: syn::ItemMod = syn::parse_quote! { + mod __trait_impls { + use super::*; + + impl light_sdk::compressible::HasTokenVariant for RentFreeAccountData { + fn is_packed_token(&self) -> bool { + matches!(self.data, RentFreeAccountVariant::PackedCTokenData(_)) + } } } - } + }; - for (i, seed) in spec.seeds.iter().enumerate() { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - seed_refs.push(quote! { #value.as_bytes() }); - } - SeedElement::Expression(expr) => { - if let syn::Expr::Path(path_expr) = &**expr { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { - seed_refs.push(quote! { #ident.as_bytes() }); - continue; - } - } - } + let token_variant_name = format_ident!("TokenAccountVariant"); - // Generic solution: rewrite any `data.*` occurrences recursively to `self.*`, - // then bind the result to a local to ensure lifetimes are valid, - // and use `.as_ref()` to convert into a seed `&[u8]`. - let binding_name = - syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = map_pda_expr_to_params(expr); - bindings.push(quote! { - let #binding_name = #mapped_expr; - }); - seed_refs.push(quote! { (#binding_name).as_ref() }); + let decompress_context_impl = generate_decompress_context_impl( + instruction_variant, + pda_ctx_seeds.clone(), + token_variant_name, + )?; + let decompress_processor_fn = + generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; + let decompress_instruction = + generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; + + let compress_accounts: syn::ItemStruct = match instruction_variant { + InstructionVariant::PdaOnly => unreachable!(), + InstructionVariant::TokenOnly => unreachable!(), + InstructionVariant::Mixed => syn::parse_quote! { + #[derive(Accounts)] + pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub compression_authority: AccountInfo<'info>, } + }, + }; + + let compress_context_impl = + generate_compress_context_impl(instruction_variant, account_types.clone())?; + let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; + let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; + + let module_tokens = quote! { + mod __processor_functions { + use super::*; + #decompress_processor_fn + #compress_processor_fn + } + }; + let processor_module: syn::ItemMod = syn::parse2(module_tokens)?; + + let init_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct InitializeCompressionConfig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + pub program_data: AccountInfo<'info>, + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, + } + }; + + let update_config_accounts: syn::ItemStruct = syn::parse_quote! { + #[derive(Accounts)] + pub struct UpdateCompressionConfig<'info> { + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + pub update_authority: Signer<'info>, + } + }; + + let init_config_instruction: syn::ItemFn = syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn initialize_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, + write_top_up: u32, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + rent_config: light_compressible::rent::RentConfig, + address_space: Vec, + ) -> Result<()> { + light_sdk::compressible::process_initialize_compression_config_checked( + &ctx.accounts.config.to_account_info(), + &ctx.accounts.authority.to_account_info(), + &ctx.accounts.program_data.to_account_info(), + &rent_sponsor, + &compression_authority, + rent_config, + write_top_up, + address_space, + 0, + &ctx.accounts.payer.to_account_info(), + &ctx.accounts.system_program.to_account_info(), + &crate::ID, + )?; + Ok(()) + } + }; + + let update_config_instruction: syn::ItemFn = syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn update_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, + new_rent_sponsor: Option, + new_compression_authority: Option, + new_rent_config: Option, + new_write_top_up: Option, + new_address_space: Option>, + new_update_authority: Option, + ) -> Result<()> { + light_sdk::compressible::process_update_compression_config( + ctx.accounts.config.as_ref(), + ctx.accounts.update_authority.as_ref(), + new_update_authority.as_ref(), + new_rent_sponsor.as_ref(), + new_compression_authority.as_ref(), + new_rent_config, + new_write_top_up, + new_address_space, + &crate::ID, + )?; + Ok(()) } - } + }; - let indices: Vec = (0..seed_refs.len()).collect(); + let client_functions = crate::compressible::seed_providers::generate_client_seed_functions( + &account_types, + &pda_seeds, + &token_seeds, + &instruction_data, + )?; - Ok(quote! { - #(#bindings)* - let seeds: &[&[u8]] = &[#(#seed_refs,)*]; - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - #( - seeds_vec.push(seeds[#indices].to_vec()); - )* - seeds_vec.push(vec![bump]); - Ok((seeds_vec, pda)) - }) -} + // Insert SeedParams struct + let seed_params_item: Item = syn::parse2(seed_params_struct)?; + content.1.push(seed_params_item); -#[inline(never)] -fn extract_required_accounts_from_seeds( - pda_seeds: &Option>, - token_seeds: &Option>, -) -> Result> { - let mut required_accounts: Vec = Vec::new(); - - #[inline(always)] - fn push_unique(list: &mut Vec, value: String) { - if !list.iter().any(|v| v == &value) { - list.push(value); + // Insert XxxSeeds structs and RentFreeAccountVariant constructors + for seeds_tokens in seeds_structs_and_constructors.into_iter() { + let wrapped: syn::File = syn::parse2(seeds_tokens)?; + for item in wrapped.items { + content.1.push(item); } } - #[inline(never)] - fn extract_accounts_from_seed_spec( - spec: &TokenSeedSpec, - ordered_accounts: &mut Vec, - ) -> Result> { - let mut spec_accounts = Vec::new(); - for seed in &spec.seeds { - if let SeedElement::Expression(expr) = seed { - let mut local_accounts = Vec::new(); - extract_account_from_expr(expr, &mut local_accounts); - for acc in local_accounts { - push_unique(ordered_accounts, acc.clone()); - push_unique(&mut spec_accounts, acc); - } - } - } - if let Some(authority_seeds) = &spec.authority { - for seed in authority_seeds { - if let SeedElement::Expression(expr) = seed { - let mut local_accounts = Vec::new(); - extract_account_from_expr(expr, &mut local_accounts); - for acc in local_accounts { - push_unique(ordered_accounts, acc.clone()); - push_unique(&mut spec_accounts, acc); - } - } - } - } - Ok(spec_accounts) - } + content.1.push(Item::Verbatim(size_validation_checks)); + content.1.push(Item::Verbatim(enum_and_traits)); + content.1.push(Item::Verbatim(ctoken_enum)); + content.1.push(Item::Struct(decompress_accounts)); + content.1.push(Item::Mod(trait_impls)); + content.1.push(Item::Mod(decompress_context_impl)); + content.1.push(Item::Mod(processor_module)); + content.1.push(Item::Fn(decompress_instruction)); + content.1.push(Item::Struct(compress_accounts)); + content.1.push(Item::Mod(compress_context_impl)); + content.1.push(Item::Fn(compress_instruction)); + content.1.push(Item::Struct(init_config_accounts)); + content.1.push(Item::Struct(update_config_accounts)); + content.1.push(Item::Fn(init_config_instruction)); + content.1.push(Item::Fn(update_config_instruction)); - if let Some(pda_seed_specs) = pda_seeds { - for spec in pda_seed_specs { - let _required_seeds = extract_accounts_from_seed_spec(spec, &mut required_accounts)?; + // Add pda seed provider impls + for pda_impl in pda_seed_provider_impls.into_iter() { + let wrapped: syn::File = syn::parse2(pda_impl)?; + for item in wrapped.items { + content.1.push(item); } } - if let Some(token_seed_specs) = token_seeds { - for spec in token_seed_specs { - let _required_seeds = extract_accounts_from_seed_spec(spec, &mut required_accounts)?; + // Add ctoken seed provider impl + if let Some(ref seeds) = token_seeds { + if !seeds.is_empty() { + let impl_code = + crate::compressible::seed_providers::generate_ctoken_seed_provider_implementation( + seeds, + )?; + let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; + content.1.push(Item::Impl(ctoken_impl)); } } - Ok(required_accounts) -} + // Add error codes + let error_item: syn::ItemEnum = syn::parse2(error_codes)?; + content.1.push(Item::Enum(error_item)); -#[inline(never)] -fn extract_account_from_expr(expr: &syn::Expr, ordered_accounts: &mut Vec) { - #[inline(always)] - fn push_unique(list: &mut Vec, value: String) { - if !list.iter().any(|v| v == &value) { - list.push(value); - } + // Add client functions (module + pub use statement) + let client_file: syn::File = syn::parse2(client_functions)?; + for item in client_file.items { + content.1.push(item); } - match expr { - syn::Expr::MethodCall(method_call) => { - extract_account_from_expr(&method_call.receiver, ordered_accounts); - } - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - push_unique(ordered_accounts, field_name.to_string()); + Ok(quote! { #module }) +} + +// ============================================================================= +// COMPRESSIBLE_PROGRAM: Auto-discovers seeds from external module files +// ============================================================================= + +/// Main entry point for #[rentfree_program] macro. +/// +/// This macro reads external module files to extract seed information from +/// Accounts structs with #[rentfree] fields. It also automatically wraps +/// instruction handlers that use these Accounts structs with pre_init/finalize logic. +/// +/// Usage: +/// ```ignore +/// #[rentfree_program] +/// #[program] +/// pub mod my_program { +/// pub mod instruction_accounts; // Macro reads this file! +/// pub mod state; +/// +/// use instruction_accounts::*; +/// use state::*; +/// +/// // No #[light_instruction] needed - auto-wrapped! +/// pub fn create_user(ctx: Context, params: Params) -> Result<()> { +/// // Your business logic +/// } +/// } +/// ``` +/// Extract the Context type name from a function's parameters. +/// Returns (struct_name, params_ident) if found. +fn extract_context_and_params(fn_item: &syn::ItemFn) -> Option<(String, Ident)> { + let mut context_type = None; + let mut params_ident = None; + + for input in &fn_item.sig.inputs { + if let syn::FnArg::Typed(pat_type) = input { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + // Check if this is a Context parameter + if let syn::Type::Path(type_path) = &*pat_type.ty { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Context" { + // Extract T from Context<'_, '_, '_, 'info, T<'info>> or Context + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + // Find the last type argument (T or T<'info>) + for arg in args.args.iter().rev() { + if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = + arg + { + if let Some(inner_seg) = inner_path.path.segments.last() { + context_type = Some(inner_seg.ident.to_string()); + break; + } } } } } } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" && field_name != "accounts" { - push_unique(ordered_accounts, field_name.to_string()); - } - } } - } - } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let name = ident.to_string(); - if name != "ctx" - && name != "data" - && !name - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { - push_unique(ordered_accounts, name); + + // Track potential params argument (not ctx, not signer-like names) + let name = pat_ident.ident.to_string(); + if name != "ctx" && !name.contains("signer") && !name.contains("bump") { + // Prefer "params" but accept others + if name == "params" || params_ident.is_none() { + params_ident = Some(pat_ident.ident.clone()); + } } } } - syn::Expr::Call(call_expr) => { - for arg in &call_expr.args { - extract_account_from_expr(arg, ordered_accounts); + } + + match (context_type, params_ident) { + (Some(ctx), Some(params)) => Some((ctx, params)), + _ => None, + } +} + +/// Wrap a function with pre_init/finalize logic. +fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> syn::ItemFn { + let fn_vis = &fn_item.vis; + let fn_sig = &fn_item.sig; + let fn_block = &fn_item.block; + let fn_attrs = &fn_item.attrs; + + let wrapped: syn::ItemFn = syn::parse_quote! { + #(#fn_attrs)* + #fn_vis #fn_sig { + // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) + use light_sdk::compressible::{LightPreInit, LightFinalize}; + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + + // Execute the original handler body in a closure + let __light_handler_result = (|| #fn_block)(); + + // Phase 2: On success, finalize compression + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; } + + __light_handler_result } - syn::Expr::Reference(ref_expr) => { - extract_account_from_expr(&ref_expr.expr, ordered_accounts); - } - _ => {} - } + }; + + wrapped } #[inline(never)] -fn generate_decompress_accounts_struct( - required_accounts: &[String], - variant: InstructionVariant, -) -> Result { - let mut account_fields = vec![ - quote! { - #[account(mut)] - pub fee_payer: Signer<'info> - }, - quote! { - /// CHECK: Checked by SDK - pub config: AccountInfo<'info> - }, - ]; +pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { + use crate::compressible::{ + anchor_seeds::get_data_fields, + file_scanner::{resolve_crate_src_path, scan_module_for_compressible}, + }; - match variant { - InstructionVariant::PdaOnly => { - unreachable!() - } - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: anyone can pay - #[account(mut)] - pub rent_sponsor: UncheckedAccount<'info> - }, - quote! { - /// CHECK: optional - only needed if decompressing tokens - #[account(mut)] - pub ctoken_rent_sponsor: Option> - }, - ]); - } + if module.content.is_none() { + return Err(macro_error!(&module, "Module must have a body")); } - match variant { - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub ctoken_program: Option> - }, - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option> - }, - quote! { - /// CHECK: Checked by SDK - pub ctoken_config: Option> - }, - ]); - } - InstructionVariant::PdaOnly => { - unreachable!() - } + // Resolve the crate's src/ directory + let base_path = resolve_crate_src_path(); + + // Scan the module (and external files) for compressible fields + let scanned = scan_module_for_compressible(&module, &base_path)?; + + // Report any errors from file scanning + if !scanned.errors.is_empty() { + let error_msg = scanned.errors.join("\n"); + return Err(macro_error!( + &module, + "Errors while scanning for rentfree types:\n{}", + error_msg + )); } - let standard_fields = [ - "fee_payer", - "rent_sponsor", - "ctoken_rent_sponsor", - "config", - "ctoken_program", - "ctoken_cpi_authority", - "ctoken_config", - ]; + // Check if we found anything + if scanned.pda_specs.is_empty() && scanned.token_specs.is_empty() { + return Err(macro_error!( + &module, + "No #[rentfree] or #[rentfree_token] fields found in any Accounts struct.\n\ + Ensure your Accounts structs are in modules declared with `pub mod xxx;`" + )); + } - for account_name in required_accounts { - if !standard_fields.contains(&account_name.as_str()) { - let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); - account_fields.push(quote! { - /// CHECK: optional seed account - pub #account_ident: Option> - }); + // Auto-wrap instruction handlers that use rentfree Accounts structs + if let Some((_, ref mut items)) = module.content { + for item in items.iter_mut() { + if let Item::Fn(fn_item) = item { + // Check if this function uses a rentfree Accounts struct + if let Some((context_type, params_ident)) = extract_context_and_params(fn_item) { + if scanned.rentfree_struct_names.contains(&context_type) { + // Wrap the function with pre_init/finalize logic + *fn_item = wrap_function_with_rentfree(fn_item, ¶ms_ident); + } + } + } } } - let struct_def = quote! { - #[derive(Accounts)] - pub struct DecompressAccountsIdempotent<'info> { - #(#account_fields,)* - } - }; + // Convert extracted specs to the format expected by generate_from_extracted_seeds + let mut found_pda_seeds: Vec = Vec::new(); + let mut found_data_fields: Vec = Vec::new(); + let mut account_types: Vec = Vec::new(); - syn::parse2(struct_def) -} + for pda in &scanned.pda_specs { + account_types.push(pda.inner_type.clone()); -#[inline(never)] -fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result { - let size_checks: Vec<_> = account_types.iter().map(|account_type| { - quote! { - const _: () = { - const COMPRESSED_SIZE: usize = 8 + <#account_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; - if COMPRESSED_SIZE > 800 { - panic!(concat!( - "Compressed account '", stringify!(#account_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" - )); - } + let seed_elements = convert_classified_to_seed_elements(&pda.seeds); + + // Extract data field types from seeds + for (field_name, conversion) in get_data_fields(&pda.seeds) { + let field_type: syn::Type = if conversion.is_some() { + syn::parse_quote!(u64) + } else { + syn::parse_quote!(solana_pubkey::Pubkey) }; + + if !found_data_fields.iter().any(|f| f.field_name == field_name) { + found_data_fields.push(InstructionDataSpec { + field_name, + field_type, + }); + } } - }).collect(); - Ok(quote! { #(#size_checks)* }) -} + found_pda_seeds.push(TokenSeedSpec { + variant: pda.variant_name.clone(), + _eq: syn::parse_quote!(=), + is_token: Some(false), + seeds: seed_elements, + authority: None, + }); + } -#[inline(never)] -fn generate_error_codes(variant: InstructionVariant) -> Result { - let base_errors = quote! { - #[msg("Rent sponsor mismatch")] - InvalidRentSponsor, - #[msg("Missing seed account")] - MissingSeedAccount, - #[msg("ATA uses SPL ATA derivation")] - AtaDoesNotUseSeedDerivation, + // Convert token specs + let mut found_token_seeds: Vec = Vec::new(); + for token in &scanned.token_specs { + let seed_elements = convert_classified_to_seed_elements(&token.seeds); + let authority_elements = token + .authority_seeds + .as_ref() + .map(|seeds| convert_classified_to_seed_elements_vec(seeds)); + + found_token_seeds.push(TokenSeedSpec { + variant: token.variant_name.clone(), + _eq: syn::parse_quote!(=), + is_token: Some(true), + seeds: seed_elements, + authority: authority_elements, + }); + } + + let pda_seeds = if found_pda_seeds.is_empty() { + None + } else { + Some(found_pda_seeds) }; - let variant_specific_errors = match variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => quote! { - #[msg("Not implemented")] - CTokenDecompressionNotImplemented, - #[msg("Not implemented")] - PdaDecompressionNotImplemented, - #[msg("Not implemented")] - TokenCompressionNotImplemented, - #[msg("Not implemented")] - PdaCompressionNotImplemented, - }, + let token_seeds = if found_token_seeds.is_empty() { + None + } else { + Some(found_token_seeds) }; - Ok(quote! { - #[error_code] - pub enum CompressibleInstructionError { - #base_errors - #variant_specific_errors - } - }) + // Use the shared generation logic + generate_from_extracted_seeds( + &mut module, + account_types, + pda_seeds, + token_seeds, + found_data_fields, + ) } diff --git a/sdk-libs/macros/src/compressible/light_compressible.rs b/sdk-libs/macros/src/compressible/light_compressible.rs new file mode 100644 index 0000000000..95f38f8fe5 --- /dev/null +++ b/sdk-libs/macros/src/compressible/light_compressible.rs @@ -0,0 +1,258 @@ +//! LightCompressible derive macro - consolidates all required traits for compressible accounts. +//! +//! This macro is equivalent to deriving: +//! - `LightHasherSha` (SHA256 hashing) +//! - `LightDiscriminator` (unique discriminator) +//! - `Compressible` (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) +//! - `CompressiblePack` (Pack + Unpack + Packed struct generation) + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{DeriveInput, Fields, ItemStruct, Result}; + +use crate::{ + compressible::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, + discriminator::discriminator, + hasher::derive_light_hasher_sha, +}; + +/// Derives all required traits for a compressible account. +/// +/// This is a convenience macro that combines: +/// - `LightHasherSha` - SHA256-based DataHasher and ToByteArray implementations (type 3 ShaFlat) +/// - `LightDiscriminator` - Unique 8-byte discriminator for the account type +/// - `Compressible` - HasCompressionInfo, CompressAs, Size, CompressedInitSpace traits +/// - `CompressiblePack` - Pack/Unpack traits with Packed struct generation for Pubkey compression +/// +/// # Example +/// +/// ```ignore +/// use light_sdk_macros::LightCompressible; +/// use light_sdk::compressible::CompressionInfo; +/// use solana_pubkey::Pubkey; +/// +/// #[derive(Default, Debug, InitSpace, LightCompressible)] +/// #[account] +/// pub struct UserRecord { +/// pub owner: Pubkey, +/// #[max_len(32)] +/// pub name: String, +/// pub score: u64, +/// pub compression_info: Option, +/// } +/// ``` +/// +/// This is equivalent to: +/// ```ignore +/// #[derive(Default, Debug, InitSpace, LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +/// #[account] +/// pub struct UserRecord { ... } +/// ``` +/// +/// ## Notes +/// +/// - The `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) +/// - SHA256 hashing serializes the entire struct, so `#[hash]` is not needed +pub fn derive_light_compressible(input: DeriveInput) -> Result { + // Convert DeriveInput to ItemStruct for macros that need it + let item_struct = derive_input_to_item_struct(&input)?; + + // Generate LightHasherSha implementation + let hasher_impl = derive_light_hasher_sha(item_struct.clone())?; + + // Generate LightDiscriminator implementation + let discriminator_impl = discriminator(item_struct)?; + + // Generate Compressible implementation (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) + let compressible_impl = derive_compressible(input.clone())?; + + // Generate CompressiblePack implementation (Pack + Unpack + Packed struct) + let pack_impl = derive_compressible_pack(input)?; + + // Combine all implementations + Ok(quote! { + #hasher_impl + #discriminator_impl + #compressible_impl + #pack_impl + }) +} + +/// Converts a DeriveInput to an ItemStruct. +/// +/// This is needed because some of our existing macros (like LightHasherSha) +/// expect ItemStruct while others (like Compressible) expect DeriveInput. +fn derive_input_to_item_struct(input: &DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + _ => { + return Err(syn::Error::new_spanned( + input, + "LightCompressible can only be derived for structs", + )) + } + }; + + let fields = match &data.fields { + Fields::Named(fields) => Fields::Named(fields.clone()), + Fields::Unnamed(fields) => Fields::Unnamed(fields.clone()), + Fields::Unit => Fields::Unit, + }; + + Ok(ItemStruct { + attrs: input.attrs.clone(), + vis: input.vis.clone(), + struct_token: data.struct_token, + ident: input.ident.clone(), + generics: input.generics.clone(), + fields, + semi_token: data.semi_token, + }) +} + +#[cfg(test)] +mod tests { + use syn::parse_quote; + + use super::*; + + #[test] + fn test_light_compressible_basic() { + // No #[hash] or #[skip] needed - SHA256 hashes entire struct, compression_info auto-skipped + let input: DeriveInput = parse_quote! { + pub struct UserRecord { + pub owner: Pubkey, + pub name: String, + pub score: u64, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!(result.is_ok(), "LightCompressible should succeed"); + + let output = result.unwrap().to_string(); + + // Should contain LightHasherSha output + assert!(output.contains("DataHasher"), "Should implement DataHasher"); + assert!( + output.contains("ToByteArray"), + "Should implement ToByteArray" + ); + + // Should contain LightDiscriminator output + assert!( + output.contains("LightDiscriminator"), + "Should implement LightDiscriminator" + ); + assert!( + output.contains("LIGHT_DISCRIMINATOR"), + "Should have discriminator constant" + ); + + // Should contain Compressible output (HasCompressionInfo, CompressAs, Size) + assert!( + output.contains("HasCompressionInfo"), + "Should implement HasCompressionInfo" + ); + assert!(output.contains("CompressAs"), "Should implement CompressAs"); + assert!(output.contains("Size"), "Should implement Size"); + + // Should contain CompressiblePack output (Pack, Unpack, Packed struct) + assert!(output.contains("Pack"), "Should implement Pack"); + assert!(output.contains("Unpack"), "Should implement Unpack"); + assert!( + output.contains("PackedUserRecord"), + "Should generate Packed struct" + ); + } + + #[test] + fn test_light_compressible_with_compress_as() { + // compress_as still works - no #[hash] or #[skip] needed + let input: DeriveInput = parse_quote! { + #[compress_as(start_time = 0, score = 0)] + pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, + pub score: u64, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!( + result.is_ok(), + "LightCompressible with compress_as should succeed" + ); + + let output = result.unwrap().to_string(); + + // compress_as attribute should be processed + assert!(output.contains("CompressAs"), "Should implement CompressAs"); + } + + #[test] + fn test_light_compressible_no_pubkey_fields() { + let input: DeriveInput = parse_quote! { + pub struct SimpleRecord { + pub id: u64, + pub value: u32, + pub compression_info: Option, + } + }; + + let result = derive_light_compressible(input); + assert!( + result.is_ok(), + "LightCompressible without Pubkey fields should succeed" + ); + + let output = result.unwrap().to_string(); + + // Should still generate everything + assert!(output.contains("DataHasher"), "Should implement DataHasher"); + assert!( + output.contains("LightDiscriminator"), + "Should implement LightDiscriminator" + ); + assert!( + output.contains("HasCompressionInfo"), + "Should implement HasCompressionInfo" + ); + + // For structs without Pubkey fields, PackedSimpleRecord should be a type alias + // (implementation detail of CompressiblePack) + } + + #[test] + fn test_light_compressible_enum_fails() { + let input: DeriveInput = parse_quote! { + pub enum NotAStruct { + A, + B, + } + }; + + let result = derive_light_compressible(input); + assert!(result.is_err(), "LightCompressible should fail for enums"); + } + + #[test] + fn test_light_compressible_missing_compression_info() { + let input: DeriveInput = parse_quote! { + pub struct MissingCompressionInfo { + pub id: u64, + pub value: u32, + } + }; + + let result = derive_light_compressible(input); + // Compressible derive validates compression_info field + assert!( + result.is_err(), + "Should fail without compression_info field" + ); + } +} diff --git a/sdk-libs/macros/src/compressible/mod.rs b/sdk-libs/macros/src/compressible/mod.rs index fb11aaa1b2..c02abdeb69 100644 --- a/sdk-libs/macros/src/compressible/mod.rs +++ b/sdk-libs/macros/src/compressible/mod.rs @@ -1,7 +1,10 @@ //! Compressible account macro generation. +pub mod anchor_seeds; pub mod decompress_context; +pub mod file_scanner; pub mod instructions; +pub mod light_compressible; pub mod pack_unpack; pub mod seed_providers; pub mod traits; diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/compressible/seed_providers.rs index 0f668cfa72..b2d54de2cc 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/compressible/seed_providers.rs @@ -2,29 +2,255 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{spanned::Spanned, Ident, Result}; +use syn::{Ident, Result}; use crate::compressible::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; -pub fn generate_token_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { - let variants = token_seeds.iter().enumerate().map(|(index, spec)| { +/// Extract ctx.* field names from seed elements (both token seeds and authority seeds) +fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { + let mut ctx_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + // Helper to extract ctx.* from a SeedElement + fn extract_from_seed( + seed: &SeedElement, + ctx_fields: &mut Vec, + seen: &mut std::collections::HashSet, + ) { + if let SeedElement::Expression(expr) = seed { + extract_ctx_from_expr(expr, ctx_fields, seen); + } + } + + fn extract_ctx_from_expr( + expr: &syn::Expr, + ctx_fields: &mut Vec, + seen: &mut std::collections::HashSet, + ) { + if let syn::Expr::Field(field_expr) = expr { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field pattern + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + let field_name_str = field_name.to_string(); + // Skip standard fields + if !matches!( + field_name_str.as_str(), + "fee_payer" + | "rent_sponsor" + | "config" + | "compression_authority" + ) && seen.insert(field_name_str) + { + ctx_fields.push(field_name.clone()); + } + } + } + } + } + } + } + // Check for ctx.field pattern (shorthand) + else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + let field_name_str = field_name.to_string(); + if !matches!( + field_name_str.as_str(), + "fee_payer" | "rent_sponsor" | "config" | "compression_authority" + ) && seen.insert(field_name_str) + { + ctx_fields.push(field_name.clone()); + } + } + } + } + } + } + // Recursively check method calls like max_key(&ctx.field.key(), ...) + else if let syn::Expr::Call(call_expr) = expr { + for arg in &call_expr.args { + extract_ctx_from_expr(arg, ctx_fields, seen); + } + } else if let syn::Expr::Reference(ref_expr) = expr { + extract_ctx_from_expr(&ref_expr.expr, ctx_fields, seen); + } else if let syn::Expr::MethodCall(method_call) = expr { + extract_ctx_from_expr(&method_call.receiver, ctx_fields, seen); + } + } + + // Extract from seeds + for seed in &spec.seeds { + extract_from_seed(seed, &mut ctx_fields, &mut seen); + } + + // Extract from authority seeds too + if let Some(auth_seeds) = &spec.authority { + for seed in auth_seeds { + extract_from_seed(seed, &mut ctx_fields, &mut seen); + } + } + + ctx_fields +} + +pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { + // Phase 8: Generate struct variants with ctx.* seed fields + + // Unpacked variants (with Pubkeys) + let unpacked_variants = token_seeds.iter().map(|spec| { let variant_name = &spec.variant; - let index_u8 = index as u8; - quote! { - #variant_name = #index_u8, + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let fields = ctx_fields.iter().map(|field| { + quote! { #field: Pubkey } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + // Packed variants (with u8 indices) + let packed_variants = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + // Pack impl match arms + let pack_arms = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + if ctx_fields.is_empty() { + quote! { + TokenAccountVariant::#variant_name => PackedTokenAccountVariant::#variant_name, + } + } else { + let field_bindings: Vec<_> = ctx_fields.iter().collect(); + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let pack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + quote! { let #idx = remaining_accounts.insert_or_get(*#field); } + }) + .collect(); + + quote! { + TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { + #(#pack_stmts)* + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } + } + } + } + }); + + // Unpack impl match arms + let unpack_arms = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + if ctx_fields.is_empty() { + quote! { + PackedTokenAccountVariant::#variant_name => Ok(TokenAccountVariant::#variant_name), + } + } else { + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let unpack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + // Dereference idx since match pattern gives us &u8 + quote! { + let #field = *remaining_accounts + .get(*#idx as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }) + .collect(); + let field_names: Vec<_> = ctx_fields.iter().collect(); + + quote! { + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { + #(#unpack_stmts)* + Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) + } + } } }); Ok(quote! { #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - #[repr(u8)] - pub enum CTokenAccountVariant { - #(#variants)* + pub enum TokenAccountVariant { + #(#unpacked_variants)* + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum PackedTokenAccountVariant { + #(#packed_variants)* + } + + impl light_token_sdk::pack::Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + match self { + #(#pack_arms)* + } + } + } + + impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; + + fn unpack( + &self, + remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + match self { + #(#unpack_arms)* + } + } + } + + impl light_sdk::compressible::IntoCTokenVariant for TokenAccountVariant { + fn into_ctoken_variant(self, token_data: light_token_sdk::compat::TokenData) -> RentFreeAccountVariant { + RentFreeAccountVariant::CTokenData(light_token_sdk::compat::CTokenData { + variant: self, + token_data, + }) + } } }) } -pub fn generate_token_seed_provider_implementation( +/// Phase 8: Generate TokenSeedProvider impl that uses self.field instead of ctx.accounts.field +pub fn generate_ctoken_seed_provider_implementation( token_seeds: &[TokenSeedSpec], ) -> Result { let mut get_seeds_match_arms = Vec::new(); @@ -32,149 +258,61 @@ pub fn generate_token_seed_provider_implementation( for spec in token_seeds { let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); - if spec.is_ata { - let get_seeds_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::AtaDoesNotUseSeedDerivation.into() - ).into()) - } - }; - get_seeds_match_arms.push(get_seeds_arm); - - let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::AtaDoesNotUseSeedDerivation.into() - ).into()) - } - }; - get_authority_seeds_match_arms.push(authority_arm); - continue; - } - - let mut token_bindings = Vec::new(); - let mut token_seed_refs = Vec::new(); + // Build match pattern with destructuring if there are ctx fields + let pattern = if ctx_fields.is_empty() { + quote! { TokenAccountVariant::#variant_name } + } else { + let field_names: Vec<_> = ctx_fields.iter().collect(); + quote! { TokenAccountVariant::#variant_name { #(#field_names,)* } } + }; - for (i, seed) in spec.seeds.iter().enumerate() { + // Build seed refs for get_seeds - use self.field directly for ctx.* seeds + let token_seed_refs: Vec = spec.seeds.iter().map(|seed| { match seed { SeedElement::Literal(lit) => { let value = lit.value(); - token_seed_refs.push(quote! { #value.as_bytes() }); + quote! { #value.as_bytes() } } SeedElement::Expression(expr) => { + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; + } + } + + // Handle uppercase constants if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); - if ident_str - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { + if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { if ident_str == "LIGHT_CPI_SIGNER" { - token_seed_refs.push(quote! { #ident.cpi_signer.as_ref() }); + return quote! { crate::#ident.cpi_signer.as_ref() }; } else { - token_seed_refs.push(quote! { #ident.as_bytes() }); + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; } - continue; } } } - let mut handled = false; - if let syn::Expr::Field(field_expr) = &**expr { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("seed_{}", i), - expr.span(), - ); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - token_seed_refs - .push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = - syn::Ident::new(&format!("seed_{}", i), expr.span()); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - token_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - token_seed_refs.push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } + // Handle ctx.accounts.field or ctx.field - use the destructured field directly + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; } - if !handled { - token_seed_refs.push(quote! { (#expr).as_ref() }); - } + // Fallback + quote! { (#expr).as_ref() } } } - } + }).collect(); let get_seeds_arm = quote! { - CTokenAccountVariant::#variant_name => { - #(#token_bindings)* + #pattern => { let seeds: &[&[u8]] = &[#(#token_seed_refs),*]; - let (token_account_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &crate::ID); + let (token_account_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); seeds_vec.push(vec![bump]); @@ -183,140 +321,52 @@ pub fn generate_token_seed_provider_implementation( }; get_seeds_match_arms.push(get_seeds_arm); + // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let mut auth_bindings: Vec = Vec::new(); - let mut auth_seed_refs = Vec::new(); - - for (i, authority_seed) in authority_seeds.iter().enumerate() { - match authority_seed { + let auth_seed_refs: Vec = authority_seeds.iter().map(|seed| { + match seed { SeedElement::Literal(lit) => { let value = lit.value(); - auth_seed_refs.push(quote! { #value.as_bytes() }); + quote! { #value.as_bytes() } } SeedElement::Expression(expr) => { - let mut handled = false; - match &**expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member - { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = - path.path.segments.first() - { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("authority_seed_{}", i), - expr.span(), - ); - let field_name_str = - field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - auth_seed_refs.push( - quote! { #binding_name.as_ref() }, - ); - handled = true; - } - } - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let binding_name = syn::Ident::new( - &format!("authority_seed_{}", i), - expr.span(), - ); - let field_name_str = field_name.to_string(); - let is_standard_field = matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ); - if is_standard_field { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name.key(); - }); - } else { - auth_bindings.push(quote! { - let #binding_name = ctx.accounts.#field_name - .as_ref() - .ok_or_else(|| -> anchor_lang::error::Error { - anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into() - })? - .key(); - }); - } - auth_seed_refs - .push(quote! { #binding_name.as_ref() }); - handled = true; - } - } - } - } - } - syn::Expr::MethodCall(_mc) => { - auth_seed_refs.push(quote! { (#expr).as_ref() }); - handled = true; + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { - if ident_str == "LIGHT_CPI_SIGNER" { - auth_seed_refs - .push(quote! { #ident.cpi_signer.as_ref() }); - } else { - auth_seed_refs.push(quote! { #ident.as_bytes() }); - } - handled = true; + } + + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { + if ident_str == "LIGHT_CPI_SIGNER" { + return quote! { crate::#ident.cpi_signer.as_ref() }; + } else { + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; } } } - _ => {} } - if !handled { - auth_seed_refs.push(quote! { (#expr).as_ref() }); + // Handle ctx.accounts.field or ctx.field - use the destructured field directly + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; } + + // Fallback + quote! { (#expr).as_ref() } } } - } + }).collect(); let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - #(#auth_bindings)* + #pattern => { let seeds: &[&[u8]] = &[#(#auth_seed_refs),*]; - let (authority_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, &crate::ID); + let (authority_pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); seeds_vec.push(vec![bump]); @@ -326,45 +376,71 @@ pub fn generate_token_seed_provider_implementation( get_authority_seeds_match_arms.push(authority_arm); } else { let authority_arm = quote! { - CTokenAccountVariant::#variant_name => { - Err(anchor_lang::prelude::ProgramError::Custom( + #pattern => { + Err(solana_program_error::ProgramError::Custom( CompressibleInstructionError::MissingSeedAccount.into() - ).into()) + )) } }; get_authority_seeds_match_arms.push(authority_arm); } } + // Phase 8: New trait signature - no ctx/accounts parameter needed Ok(quote! { - impl ctoken_seed_system::CTokenSeedProvider for CTokenAccountVariant { - fn get_seeds<'a, 'info>( + impl light_sdk::compressible::TokenSeedProvider for TokenAccountVariant { + fn get_seeds( &self, - ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)> { + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { match self { #(#get_seeds_match_arms)* - _ => Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into()) } } - fn get_authority_seeds<'a, 'info>( + fn get_authority_seeds( &self, - ctx: &ctoken_seed_system::CTokenSeedContext<'a, 'info>, - ) -> Result<(Vec>, solana_pubkey::Pubkey)> { + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { match self { #(#get_authority_seeds_match_arms)* - _ => Err(anchor_lang::prelude::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() - ).into()) } } } }) } +/// Extract the field name from a ctx.field or ctx.accounts.field expression +fn extract_ctx_field_name(expr: &syn::Expr) -> Option { + if let syn::Expr::Field(field_expr) = expr { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field pattern + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + return Some(field_name.clone()); + } + } + } + } + } + } + // Check for ctx.field pattern (shorthand) + else if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + return Some(field_name.clone()); + } + } + } + } + } + None +} + #[inline(never)] pub fn generate_client_seed_functions( _account_types: &[Ident], @@ -404,10 +480,6 @@ pub fn generate_client_seed_functions( for spec in token_seed_specs { let variant_name = &spec.variant; - if spec.is_ata { - continue; - } - let function_name = format_ident!("get_{}_seeds", variant_name.to_string().to_lowercase()); @@ -439,7 +511,6 @@ pub fn generate_client_seed_functions( variant: spec.variant.clone(), _eq: spec._eq, is_token: spec.is_token, - is_ata: spec.is_ata, seeds: syn::punctuated::Punctuated::new(), authority: None, }; @@ -639,6 +710,13 @@ fn analyze_seed_spec_for_client( } } } + syn::Expr::Lit(lit_expr) => { + // Handle byte string literals: b"seed" -> use directly + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + expressions.push(quote! { &[#(#bytes),*] }); + } + } syn::Expr::Path(path_expr) => { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); @@ -647,9 +725,10 @@ fn analyze_seed_spec_for_client( .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { if ident_str == "LIGHT_CPI_SIGNER" { - expressions.push(quote! { #ident.cpi_signer.as_ref() }); + expressions.push(quote! { crate::#ident.cpi_signer.as_ref() }); } else { - expressions.push(quote! { #ident.as_bytes() }); + // Use crate:: prefix and explicit type annotation + expressions.push(quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }); } } else { parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); diff --git a/sdk-libs/macros/src/compressible/traits.rs b/sdk-libs/macros/src/compressible/traits.rs index a59b97af53..4118b5b17e 100644 --- a/sdk-libs/macros/src/compressible/traits.rs +++ b/sdk-libs/macros/src/compressible/traits.rs @@ -83,7 +83,8 @@ fn generate_has_compression_info_impl(struct_name: &Ident) -> TokenStream { } } -/// Generates field assignments for CompressAs trait, handling overrides and copy types +/// Generates field assignments for CompressAs trait, handling overrides and copy types. +/// Auto-skips `compression_info` field and fields marked with `#[skip]`. fn generate_compress_as_field_assignments( fields: &Punctuated, compress_as_fields: &Option, @@ -94,6 +95,12 @@ fn generate_compress_as_field_assignments( let field_name = field.ident.as_ref().unwrap(); let field_type = &field.ty; + // Auto-skip compression_info field (handled separately in CompressAs impl) + if field_name == "compression_info" { + continue; + } + + // Also skip fields explicitly marked with #[skip] if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) { continue; } @@ -147,13 +154,20 @@ fn generate_compress_as_impl( } } -/// Generates size calculation fields for the Size trait +/// Generates size calculation fields for the Size trait. +/// Auto-skips `compression_info` field and fields marked with `#[skip]`. fn generate_size_fields(fields: &Punctuated) -> Vec { let mut size_fields = Vec::new(); for field in fields.iter() { let field_name = field.ident.as_ref().unwrap(); + // Auto-skip compression_info field (handled separately in Size impl) + if field_name == "compression_info" { + continue; + } + + // Also skip fields explicitly marked with #[skip] if field.attrs.iter().any(|attr| attr.path().is_ident("skip")) { continue; } diff --git a/sdk-libs/macros/src/compressible/utils.rs b/sdk-libs/macros/src/compressible/utils.rs index 3b337c232e..35c09e5d93 100644 --- a/sdk-libs/macros/src/compressible/utils.rs +++ b/sdk-libs/macros/src/compressible/utils.rs @@ -104,13 +104,13 @@ pub(crate) fn is_pubkey_type(ty: &Type) -> bool { } } -/// Generates an empty CTokenAccountVariant enum. +/// Generates an empty TokenAccountVariant enum. /// /// This is used when no token accounts are specified in compressible instructions. pub(crate) fn generate_empty_ctoken_enum() -> proc_macro2::TokenStream { quote::quote! { #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Debug, Clone, Copy)] #[repr(u8)] - pub enum CTokenAccountVariant {} + pub enum TokenAccountVariant {} } } diff --git a/sdk-libs/macros/src/compressible/variant_enum.rs b/sdk-libs/macros/src/compressible/variant_enum.rs index c220a8c15a..88aa578c36 100644 --- a/sdk-libs/macros/src/compressible/variant_enum.rs +++ b/sdk-libs/macros/src/compressible/variant_enum.rs @@ -1,70 +1,102 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Ident, Result, Token, -}; - -struct AccountTypeList { - types: Punctuated, +use quote::{format_ident, quote}; +use syn::{Ident, Result}; + +/// Info about ctx.* seeds for a PDA type +#[derive(Clone, Debug)] +pub struct PdaCtxSeedInfo { + pub type_name: Ident, + /// Field names from ctx.accounts.XXX references in seeds + pub ctx_seed_fields: Vec, } -impl Parse for AccountTypeList { - fn parse(input: ParseStream) -> Result { - Ok(AccountTypeList { - types: Punctuated::parse_terminated(input)?, - }) +impl PdaCtxSeedInfo { + pub fn new(type_name: Ident, ctx_seed_fields: Vec) -> Self { + Self { + type_name, + ctx_seed_fields, + } } } -pub fn compressed_account_variant(input: TokenStream) -> Result { - let type_list = syn::parse2::(input)?; - let account_types: Vec<&Ident> = type_list.types.iter().collect(); - +/// Enhanced function that generates variants with ctx.* seed fields +pub fn compressed_account_variant_with_ctx_seeds( + account_types: &[&Ident], + pda_ctx_seeds: &[PdaCtxSeedInfo], +) -> Result { if account_types.is_empty() { - return Err(syn::Error::new_spanned( - &type_list.types, + return Err(syn::Error::new( + proc_macro2::Span::call_site(), "At least one account type must be specified", )); } + // Build a map from type name to ctx seed fields + let ctx_seeds_map: std::collections::HashMap = pda_ctx_seeds + .iter() + .map(|info| (info.type_name.to_string(), info.ctx_seed_fields.as_slice())) + .collect(); + + // Phase 2: Generate struct variants with ctx.* seed fields let account_variants = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + // Unpacked variant: Pubkey fields for ctx.* seeds + // Note: Use bare Pubkey which is in scope via `use anchor_lang::prelude::*` + let unpacked_ctx_fields = ctx_fields.iter().map(|field| { + quote! { #field: Pubkey } + }); + + // Packed variant: u8 index fields for ctx.* seeds + let packed_ctx_fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + quote! { - #name(#name), - #packed_name(#packed_name), + #name { data: #name, #(#unpacked_ctx_fields,)* }, + #packed_name { data: #packed_name, #(#packed_ctx_fields,)* }, } }); + // Phase 8: PackedCTokenData uses PackedTokenAccountVariant (with idx fields) + // CTokenData uses TokenAccountVariant (with Pubkey fields) let enum_def = quote! { #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] - pub enum CompressedAccountVariant { + pub enum RentFreeAccountVariant { #(#account_variants)* - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), + PackedCTokenData(light_token_sdk::compat::PackedCTokenData), + CTokenData(light_token_sdk::compat::CTokenData), } }; let first_type = account_types[0]; + let first_ctx_fields = ctx_seeds_map + .get(&first_type.to_string()) + .copied() + .unwrap_or(&[]); + let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { + quote! { #field: Pubkey::default() } + }); let default_impl = quote! { - impl Default for CompressedAccountVariant { + impl Default for RentFreeAccountVariant { fn default() -> Self { - Self::#first_type(#first_type::default()) + Self::#first_type { data: #first_type::default(), #(#first_default_ctx_fields,)* } } } }; let hash_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_hasher::DataHasher>::hash::(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_hasher::DataHasher>::hash::(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let data_hasher_impl = quote! { - impl light_hasher::DataHasher for CompressedAccountVariant { + impl light_hasher::DataHasher for RentFreeAccountVariant { fn hash(&self) -> std::result::Result<[u8; 32], light_hasher::HasherError> { match self { #(#hash_match_arms)* @@ -76,46 +108,46 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { }; let light_discriminator_impl = quote! { - impl light_sdk::LightDiscriminator for CompressedAccountVariant { + impl light_sdk::LightDiscriminator for RentFreeAccountVariant { const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; } }; let compression_info_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let compression_info_mut_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let compression_info_mut_opt_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let set_compression_info_none_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let has_compression_info_impl = quote! { - impl light_sdk::compressible::HasCompressionInfo for CompressedAccountVariant { + impl light_sdk::compressible::HasCompressionInfo for RentFreeAccountVariant { fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { match self { #(#compression_info_match_arms)* @@ -151,15 +183,15 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { }; let size_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); + let packed_name = format_ident!("Packed{}", name); quote! { - CompressedAccountVariant::#name(data) => <#name as light_sdk::account::Size>::size(data), - CompressedAccountVariant::#packed_name(_) => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::account::Size>::size(data), + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), } }); let size_impl = quote! { - impl light_sdk::account::Size for CompressedAccountVariant { + impl light_sdk::account::Size for RentFreeAccountVariant { fn size(&self) -> usize { match self { #(#size_match_arms)* @@ -170,16 +202,44 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { } }; - let pack_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); - quote! { - CompressedAccountVariant::#packed_name(_) => unreachable!(), - CompressedAccountVariant::#name(data) => CompressedAccountVariant::#packed_name(<#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts)), + // Phase 2: Pack/Unpack with ctx seed fields + let pack_match_arms: Vec<_> = account_types.iter().map(|name| { + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + if ctx_fields.is_empty() { + // No ctx seeds - simple pack + quote! { + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#name { data, .. } => RentFreeAccountVariant::#packed_name { + data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + }, + } + } else { + // Has ctx seeds - pack data and ctx seed pubkeys + let field_names: Vec<_> = ctx_fields.iter().collect(); + let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); + let pack_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + // Dereference because we're matching on &self, so field is &Pubkey + quote! { let #idx_field = remaining_accounts.insert_or_get(*#field); } + }).collect(); + + quote! { + RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#name { data, #(#field_names,)* .. } => { + #(#pack_ctx_seeds)* + RentFreeAccountVariant::#packed_name { + data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + #(#idx_field_names,)* + } + }, + } } - }); + }).collect(); let pack_impl = quote! { - impl light_sdk::compressible::Pack for CompressedAccountVariant { + impl light_sdk::compressible::Pack for RentFreeAccountVariant { type Packed = Self; fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { @@ -187,6 +247,7 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { #(#pack_match_arms)* Self::PackedCTokenData(_) => unreachable!(), Self::CTokenData(data) => { + // Use ctoken-sdk's Pack trait for CTokenData Self::PackedCTokenData(light_token_sdk::pack::Pack::pack(data, remaining_accounts)) } } @@ -194,16 +255,47 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { } }; - let unpack_match_arms = account_types.iter().map(|name| { - let packed_name = quote::format_ident!("Packed{}", name); - quote! { - CompressedAccountVariant::#packed_name(data) => Ok(CompressedAccountVariant::#name(<#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?)), - CompressedAccountVariant::#name(_) => unreachable!(), + let unpack_match_arms: Vec<_> = account_types.iter().map(|name| { + let packed_name = format_ident!("Packed{}", name); + let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + + if ctx_fields.is_empty() { + // No ctx seeds - simple unpack + quote! { + RentFreeAccountVariant::#packed_name { data, .. } => Ok(RentFreeAccountVariant::#name { + data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + }), + RentFreeAccountVariant::#name { .. } => unreachable!(), + } + } else { + // Has ctx seeds - unpack data and resolve ctx seed pubkeys from indices + let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); + let field_names: Vec<_> = ctx_fields.iter().collect(); + let unpack_ctx_seeds: Vec<_> = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { + let #field = *remaining_accounts + .get(*#idx_field as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }).collect(); + + quote! { + RentFreeAccountVariant::#packed_name { data, #(#idx_field_names,)* .. } => { + #(#unpack_ctx_seeds)* + Ok(RentFreeAccountVariant::#name { + data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + #(#field_names,)* + }) + }, + RentFreeAccountVariant::#name { .. } => unreachable!(), + } } - }); + }).collect(); let unpack_impl = quote! { - impl light_sdk::compressible::Unpack for CompressedAccountVariant { + impl light_sdk::compressible::Unpack for RentFreeAccountVariant { type Unpacked = Self; fn unpack( @@ -219,15 +311,11 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { } }; - let compressed_account_data_struct = quote! { + let rentfree_account_data_struct = quote! { #[derive(Clone, Debug, anchor_lang::AnchorDeserialize, anchor_lang::AnchorSerialize)] - pub struct CompressedAccountData { + pub struct RentFreeAccountData { pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, - // /// Indices into remaining_accounts for seed account references (starting from seed_accounts_offset) - // pub seed_indices: Vec, - // /// Indices into remaining_accounts for authority seed references (for CTokens only) - // pub authority_indices: Vec, + pub data: RentFreeAccountVariant, } }; @@ -240,7 +328,7 @@ pub fn compressed_account_variant(input: TokenStream) -> Result { #size_impl #pack_impl #unpack_impl - #compressed_account_data_struct + #rentfree_account_data_struct }; Ok(expanded) diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/finalize/codegen.rs new file mode 100644 index 0000000000..5cc3a93f86 --- /dev/null +++ b/sdk-libs/macros/src/finalize/codegen.rs @@ -0,0 +1,685 @@ +//! Code generation for LightFinalize and LightPreInit trait implementations. +//! +//! Design for mints: +//! - At mint init, we CREATE + DECOMPRESS atomically +//! - After init, the CMint should always be in decompressed/"hot" state +//! +//! Flow for PDAs + mints: +//! 1. Pre-init: ALL compression logic executes here +//! a. Write PDAs to CPI context +//! b. Invoke mint_action with decompress + CPI context +//! c. CMint is now "hot" and usable +//! 2. Instruction body: Can use hot CMint (mintTo, transfers, etc.) +//! 3. Finalize: No-op (all work done in pre_init) + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; + +use super::parse::{ParsedCompressibleStruct, RentFreeField}; + +/// Generate both trait implementations +pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream { + let struct_name = &parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = parsed.generics.split_for_impl(); + + // Get the params type from instruction args (first arg) + let params_type = parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .map(|arg| &arg.ty); + + let params_type = match params_type { + Some(ty) => ty, + None => { + // No instruction args - generate no-op impls + return quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + ) -> std::result::Result { + Ok(false) + } + } + + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + Ok(()) + } + } + }; + } + }; + + let params_ident = parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .map(|arg| &arg.name) + .expect("params ident must exist if type exists"); + + let has_pdas = !parsed.rentfree_fields.is_empty(); + let has_mints = !parsed.light_mint_fields.is_empty(); + + // Get fee payer field + let fee_payer = parsed + .fee_payer_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { fee_payer }); + + let compression_config = parsed + .compression_config_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { compression_config }); + + // CToken accounts for decompress + let ctoken_config = parsed + .ctoken_config_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_compressible_config }); + + let ctoken_rent_sponsor = parsed + .ctoken_rent_sponsor_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_rent_sponsor }); + + let light_token_program = parsed + .ctoken_program_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { light_token_program }); + + let ctoken_cpi_authority = parsed + .ctoken_cpi_authority_field + .as_ref() + .map(|f| quote! { #f }) + .unwrap_or_else(|| quote! { ctoken_cpi_authority }); + + // Generate LightPreInit impl based on what we have + // ALL compression logic runs in pre_init so instruction body can use hot state + let pre_init_body = if has_pdas && has_mints { + // PDAs + mints: Write PDAs to CPI context, then invoke mint_action with decompress + generate_pre_init_pdas_and_mints( + parsed, + params_ident, + &fee_payer, + &compression_config, + &ctoken_config, + &ctoken_rent_sponsor, + &light_token_program, + &ctoken_cpi_authority, + ) + } else if has_mints { + // Mints only: Invoke mint_action with decompress (no CPI context) + generate_pre_init_mints_only( + parsed, + params_ident, + &fee_payer, + &ctoken_config, + &ctoken_rent_sponsor, + &light_token_program, + &ctoken_cpi_authority, + ) + } else if has_pdas { + // PDAs only: Direct invoke (no CPI context needed) + generate_pre_init_pdas_only(parsed, params_ident, &fee_payer, &compression_config) + } else { + quote! { Ok(false) } + }; + + // LightFinalize: No-op (all work done in pre_init) + let finalize_body = quote! { Ok(()) }; + + quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + ) -> std::result::Result { + use anchor_lang::ToAccountInfo; + #pre_init_body + } + } + + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + use anchor_lang::ToAccountInfo; + #finalize_body + } + } + } +} + +/// Generate LightPreInit body for PDAs + mints: +/// 1. Write PDAs to CPI context +/// 2. Invoke mint_action with decompress + CPI context +/// After this, Mint is "hot" and usable in instruction body +#[allow(clippy::too_many_arguments)] +fn generate_pre_init_pdas_and_mints( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + compression_config: &TokenStream, + ctoken_config: &TokenStream, + ctoken_rent_sponsor: &TokenStream, + light_token_program: &TokenStream, + ctoken_cpi_authority: &TokenStream, +) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + let rentfree_count = parsed.rentfree_fields.len() as u8; + let pda_count = parsed.rentfree_fields.len(); + + // Get the first PDA's output tree index (for the state tree output queue) + let first_pda_output_tree = &parsed.rentfree_fields[0].output_tree; + + // Get the first mint (we only support one mint currently) + let mint = &parsed.light_mint_fields[0]; + let mint_field_ident = &mint.field_ident; + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + + // Use explicit signer_seeds if provided, otherwise empty + let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { + quote! { #seeds } + } else { + quote! { &[] as &[&[u8]] } + }; + + // Build freeze_authority expression + let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { + quote! { Some(*self.#freeze_auth.to_account_info().key) } + } else { + quote! { None } + }; + + // rent_payment defaults to 2 epochs (u8) + let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { + quote! { #rent } + } else { + quote! { 2u8 } + }; + + // write_top_up defaults to 0 (u32) + let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { + quote! { #top_up } + } else { + quote! { 0u32 } + }; + + // assigned_account_index for mint is after PDAs + let mint_assigned_index = pda_count as u8; + + quote! { + // Build CPI accounts WITH CPI context for batching + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( + &self.#fee_payer, + _remaining, + light_sdk_types::cpi_accounts::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree PDA accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Step 1: Write PDAs to CPI context + let cpi_context_account = cpi_accounts.cpi_context()?; + let cpi_context_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_context_account, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(cpi_context_accounts)?; + + // Step 2: Build and invoke mint_action with decompress + CPI context + { + let __tree_info = &#address_tree_info; + let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; + // Output queue is the state tree queue (same as the PDAs' output tree) + let __output_tree_index = #first_pda_output_tree; + let output_queue = cpi_accounts.get_tree_account_info(__output_tree_index as usize)?; + let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); + + let mint_signer_key = self.#mint_signer.to_account_info().key; + let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); + + let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + .expect("proof is required for mint creation"); + + let __freeze_authority: Option = #freeze_authority_tokens; + + // Build compressed mint instruction data + let compressed_mint_data = light_token_interface::instructions::mint_action::CompressedMintInstructionData { + supply: 0, + decimals: #decimals, + metadata: light_token_interface::state::CompressedMintMetadata { + version: 3, + mint: mint_pda.to_bytes().into(), + cmint_decompressed: false, + mint_signer: mint_signer_key.to_bytes(), + bump: _cmint_bump, + }, + mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), + freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + }; + + // Build mint action instruction data with decompress + let mut instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + __tree_info.root_index, + __proof, + compressed_mint_data, + ) + .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { + rent_payment: #rent_payment_tokens, + write_top_up: #write_top_up_tokens, + }) + .with_cpi_context(light_token_interface::instructions::mint_action::CpiContext { + address_tree_pubkey: __tree_pubkey.to_bytes(), + set_context: false, + first_set_context: false, // PDAs already wrote to context + // in_tree_index is 1-indexed and points to the state queue (for CPI context validation) + // The Light System Program does `in_tree_index - 1` and uses queue's associated_merkle_tree + in_tree_index: __output_tree_index + 1, // +1 because 1-indexed + in_queue_index: __output_tree_index, + out_queue_index: __output_tree_index, // Output state queue + token_out_queue_index: 0, + assigned_account_index: #mint_assigned_index, + read_only_address_trees: [0; 4], + }); + + // Build account metas with compressible CMint + let mut meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + *self.#fee_payer.to_account_info().key, + *self.#authority.to_account_info().key, + *mint_signer_key, + __tree_pubkey, + *output_queue.key, + ) + .with_compressible_mint( + mint_pda, + *self.#ctoken_config.to_account_info().key, + *self.#ctoken_rent_sponsor.to_account_info().key, + ); + + meta_config.cpi_context = Some(*cpi_accounts.cpi_context()?.key); + + let account_metas = meta_config.to_account_metas(); + + use light_compressed_account::instruction_data::traits::LightInstructionData; + let ix_data = instruction_data.data() + .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + + let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + // Build account infos and invoke + // Include all accounts needed for mint_action with decompress + let mut account_infos = cpi_accounts.to_account_infos(); + // Add ctoken-specific accounts that aren't in the Light System CPI accounts + account_infos.push(self.#light_token_program.to_account_info()); + account_infos.push(self.#ctoken_cpi_authority.to_account_info()); + account_infos.push(self.#mint_field_ident.to_account_info()); + account_infos.push(self.#ctoken_config.to_account_info()); + account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); + account_infos.push(self.#authority.to_account_info()); + account_infos.push(self.#mint_signer.to_account_info()); + account_infos.push(self.#fee_payer.to_account_info()); + + let signer_seeds: &[&[u8]] = #signer_seeds_tokens; + if signer_seeds.is_empty() { + anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; + } else { + anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; + } + } + + Ok(true) + } +} + +/// Generate LightPreInit body for mints-only (no PDAs): +/// Invoke mint_action with decompress directly +/// After this, CMint is "hot" and usable in instruction body +#[allow(clippy::too_many_arguments)] +fn generate_pre_init_mints_only( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + ctoken_config: &TokenStream, + ctoken_rent_sponsor: &TokenStream, + light_token_program: &TokenStream, + ctoken_cpi_authority: &TokenStream, +) -> TokenStream { + // Get the first mint (we only support one mint currently) + let mint = &parsed.light_mint_fields[0]; + let mint_field_ident = &mint.field_ident; + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + + // Use explicit signer_seeds if provided, otherwise empty + let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { + quote! { #seeds } + } else { + quote! { &[] as &[&[u8]] } + }; + + // Build freeze_authority expression + let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { + quote! { Some(*self.#freeze_auth.to_account_info().key) } + } else { + quote! { None } + }; + + // rent_payment defaults to 2 epochs (u8) + let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { + quote! { #rent } + } else { + quote! { 2u8 } + }; + + // write_top_up defaults to 0 (u32) + let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { + quote! { #top_up } + } else { + quote! { 0u32 } + }; + + quote! { + // Build CPI accounts (no CPI context needed for mints-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Build and invoke mint_action with decompress + { + let __tree_info = &#address_tree_info; + let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; + let output_queue = cpi_accounts.get_tree_account_info(__tree_info.address_queue_pubkey_index as usize)?; + let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); + + let mint_signer_key = self.#mint_signer.to_account_info().key; + let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); + + let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + .expect("proof is required for mint creation"); + + let __freeze_authority: Option = #freeze_authority_tokens; + + // Build compressed mint instruction data + let compressed_mint_data = light_token_interface::instructions::mint_action::CompressedMintInstructionData { + supply: 0, + decimals: #decimals, + metadata: light_token_interface::state::CompressedMintMetadata { + version: 3, + mint: mint_pda.to_bytes().into(), + cmint_decompressed: false, + mint_signer: mint_signer_key.to_bytes(), + bump: _cmint_bump, + }, + mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), + freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + }; + + // Build mint action instruction data with decompress (no CPI context) + let instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + __tree_info.root_index, + __proof, + compressed_mint_data, + ) + .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { + rent_payment: #rent_payment_tokens, + write_top_up: #write_top_up_tokens, + }); + + // Build account metas with compressible CMint + let meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + *self.#fee_payer.to_account_info().key, + *self.#authority.to_account_info().key, + *mint_signer_key, + __tree_pubkey, + *output_queue.key, + ) + .with_compressible_mint( + mint_pda, + *self.#ctoken_config.to_account_info().key, + *self.#ctoken_rent_sponsor.to_account_info().key, + ); + + let account_metas = meta_config.to_account_metas(); + + use light_compressed_account::instruction_data::traits::LightInstructionData; + let ix_data = instruction_data.data() + .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + + let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + // Build account infos and invoke + let mut account_infos = cpi_accounts.to_account_infos(); + // Add ctoken-specific accounts + account_infos.push(self.#light_token_program.to_account_info()); + account_infos.push(self.#ctoken_cpi_authority.to_account_info()); + account_infos.push(self.#mint_field_ident.to_account_info()); + account_infos.push(self.#ctoken_config.to_account_info()); + account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); + account_infos.push(self.#authority.to_account_info()); + account_infos.push(self.#mint_signer.to_account_info()); + account_infos.push(self.#fee_payer.to_account_info()); + + let signer_seeds: &[&[u8]] = #signer_seeds_tokens; + if signer_seeds.is_empty() { + anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; + } else { + anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; + } + } + + Ok(true) + } +} + +/// Generate LightPreInit body for PDAs only (no mints) +/// After this, compressed addresses are registered +fn generate_pre_init_pdas_only( + parsed: &ParsedCompressibleStruct, + params_ident: &syn::Ident, + fee_payer: &TokenStream, + compression_config: &TokenStream, +) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + let rentfree_count = parsed.rentfree_fields.len() as u8; + + quote! { + // Build CPI accounts (no CPI context needed for PDAs-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Execute Light System Program CPI directly with proof + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .invoke(cpi_accounts)?; + + Ok(true) + } +} + +/// Generate compression blocks for PDA fields +fn generate_pda_compress_blocks( + fields: &[RentFreeField], + _params_ident: &syn::Ident, +) -> (Vec, Vec) { + let mut blocks = Vec::new(); + let mut addr_idents = Vec::new(); + + for (idx, field) in fields.iter().enumerate() { + let idx_lit = idx as u8; + let ident = &field.ident; + let addr_tree_info = &field.address_tree_info; + let output_tree = &field.output_tree; + let acc_ty_path = extract_inner_account_type(&field.ty); + + let new_addr_params_ident = format_ident!("__new_addr_params_{}", idx); + let compressed_infos_ident = format_ident!("__compressed_infos_{}", idx); + let address_ident = format_ident!("__address_{}", idx); + let account_info_ident = format_ident!("__account_info_{}", idx); + let account_key_ident = format_ident!("__account_key_{}", idx); + let account_data_ident = format_ident!("__account_data_{}", idx); + + // Generate correct deref pattern: ** for Box>, * for Account + let deref_expr = if field.is_boxed { + quote! { &mut **self.#ident } + } else { + quote! { &mut *self.#ident } + }; + + addr_idents.push(quote! { #new_addr_params_ident }); + + blocks.push(quote! { + // Get account info early before any mutable borrows + let #account_info_ident = self.#ident.to_account_info(); + let #account_key_ident = #account_info_ident.key.to_bytes(); + + let #new_addr_params_ident = { + let tree_info = &#addr_tree_info; + light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked { + seed: #account_key_ident, + address_merkle_tree_account_index: tree_info.address_merkle_tree_pubkey_index, + address_queue_account_index: tree_info.address_queue_pubkey_index, + address_merkle_tree_root_index: tree_info.root_index, + assigned_to_account: true, + assigned_account_index: #idx_lit, + } + }; + + // Derive the compressed address + let #address_ident = light_compressed_account::address::derive_address( + &#new_addr_params_ident.seed, + &cpi_accounts + .get_tree_account_info(#new_addr_params_ident.address_merkle_tree_account_index as usize)? + .key() + .to_bytes(), + &crate::ID.to_bytes(), + ); + + // Get mutable reference to inner account data + let #account_data_ident = #deref_expr; + + let #compressed_infos_ident = light_sdk::compressible::prepare_compressed_account_on_init::<#acc_ty_path>( + &#account_info_ident, + #account_data_ident, + &compression_config_data, + #address_ident, + #new_addr_params_ident, + #output_tree, + &cpi_accounts, + &compression_config_data.address_space, + false, // at init, we do not compress_and_close the pda, we just "register" the empty compressed account with the derived address. + )?; + all_compressed_infos.push(#compressed_infos_ident); + }); + } + + (blocks, addr_idents) +} + +/// Extract the inner type T from Account<'info, T> or Box> +fn extract_inner_account_type(ty: &syn::Type) -> TokenStream { + match ty { + syn::Type::Path(type_path) => { + let path = &type_path.path; + if let Some(segment) = path.segments.last() { + let ident_str = segment.ident.to_string(); + + if ident_str == "Account" { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(inner_ty) = arg { + return quote! { #inner_ty }; + } + } + } + } + + if ident_str == "Box" { + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { + return extract_inner_account_type(inner); + } + } + } + } + quote! { #ty } + } + _ => quote! { #ty }, + } +} diff --git a/sdk-libs/macros/src/finalize/mod.rs b/sdk-libs/macros/src/finalize/mod.rs new file mode 100644 index 0000000000..2f6b3991fc --- /dev/null +++ b/sdk-libs/macros/src/finalize/mod.rs @@ -0,0 +1,18 @@ +//! RentFree derive macro for Accounts structs. +//! +//! This module provides: +//! - `#[derive(RentFree)]` - Generates the LightFinalize trait impl for accounts structs +//! with fields marked `#[rentfree(...)]` +//! +//! Note: Instruction handlers are auto-wrapped by `#[rentfree_program]`. + +mod codegen; +mod parse; + +use proc_macro2::TokenStream; +use syn::DeriveInput; + +pub fn derive_light_finalize(input: DeriveInput) -> Result { + let parsed = parse::parse_compressible_struct(&input)?; + Ok(codegen::generate_finalize_impl(&parsed)) +} diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs new file mode 100644 index 0000000000..869073ba13 --- /dev/null +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -0,0 +1,389 @@ +//! Parsing logic for #[rentfree(...)] and #[light_mint(...)] attributes. + +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + DeriveInput, Error, Expr, Ident, Token, Type, +}; + +/// Parsed representation of a struct with rentfree and light_mint fields. +pub struct ParsedCompressibleStruct { + pub struct_name: Ident, + pub generics: syn::Generics, + pub rentfree_fields: Vec, + pub light_mint_fields: Vec, + pub instruction_args: Option>, + pub fee_payer_field: Option, + pub compression_config_field: Option, + /// CToken compressible config account (for decompress mint) + pub ctoken_config_field: Option, + /// CToken rent sponsor account (for decompress mint) + pub ctoken_rent_sponsor_field: Option, + /// CToken program account (for decompress mint CPI) + pub ctoken_program_field: Option, + /// CToken CPI authority PDA (for decompress mint CPI) + pub ctoken_cpi_authority_field: Option, +} + +/// A field marked with #[rentfree(...)] +pub struct RentFreeField { + pub ident: Ident, + pub ty: Type, + pub address_tree_info: Expr, + pub output_tree: Expr, + /// True if the field is Box>, false if Account + pub is_boxed: bool, +} + +/// A field marked with #[light_mint(...)] +pub struct LightMintField { + /// The field name where #[light_mint] is attached (CMint account) + pub field_ident: Ident, + /// The mint_signer field (AccountInfo that seeds the mint PDA) + pub mint_signer: Expr, + /// The authority for mint operations + pub authority: Expr, + /// Decimals for the mint + pub decimals: Expr, + /// Address tree info expression + pub address_tree_info: Expr, + /// Optional freeze authority + pub freeze_authority: Option, + /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) + pub signer_seeds: Option, + /// Rent payment epochs for decompression (default: 2) + pub rent_payment: Option, + /// Write top-up lamports for decompression (default: 0) + pub write_top_up: Option, +} + +/// Instruction argument from #[instruction(...)] +pub struct InstructionArg { + pub name: Ident, + pub ty: Type, +} + +/// Arguments inside #[rentfree(...)] +struct RentFreeArgs { + address_tree_info: Option, + output_tree: Option, +} + +impl Parse for RentFreeArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = RentFreeArgs { + address_tree_info: None, + output_tree: None, + }; + + let content: Punctuated = Punctuated::parse_terminated(input)?; + + for arg in content { + match arg.name.to_string().as_str() { + "address_tree_info" => args.address_tree_info = Some(arg.value), + "output_tree" => args.output_tree = Some(arg.value), + other => { + return Err(Error::new( + arg.name.span(), + format!("unknown rentfree attribute: {}", other), + )) + } + } + } + + Ok(args) + } +} + +/// Arguments inside #[light_mint(...)] +struct LightMintArgs { + mint_signer: Option, + authority: Option, + decimals: Option, + address_tree_info: Option, + freeze_authority: Option, + signer_seeds: Option, + rent_payment: Option, + write_top_up: Option, +} + +impl Parse for LightMintArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = LightMintArgs { + mint_signer: None, + authority: None, + decimals: None, + address_tree_info: None, + freeze_authority: None, + signer_seeds: None, + rent_payment: None, + write_top_up: None, + }; + + let content: Punctuated = Punctuated::parse_terminated(input)?; + + for arg in content { + match arg.name.to_string().as_str() { + "mint_signer" => args.mint_signer = Some(arg.value), + "authority" => args.authority = Some(arg.value), + "decimals" => args.decimals = Some(arg.value), + "address_tree_info" => args.address_tree_info = Some(arg.value), + "freeze_authority" => args.freeze_authority = Some(arg.value), + "signer_seeds" => args.signer_seeds = Some(arg.value), + "rent_payment" => args.rent_payment = Some(arg.value), + "write_top_up" => args.write_top_up = Some(arg.value), + other => { + return Err(Error::new( + arg.name.span(), + format!("unknown light_mint attribute: {}", other), + )) + } + } + } + + Ok(args) + } +} + +/// Generic key = value argument parser +struct KeyValueArg { + name: Ident, + value: Expr, +} + +impl Parse for KeyValueArg { + fn parse(input: ParseStream) -> syn::Result { + let name: Ident = input.parse()?; + input.parse::()?; + let value: Expr = input.parse()?; + Ok(KeyValueArg { name, value }) + } +} + +/// A single instruction argument: `name: Type` +struct InstructionArgParsed { + name: Ident, + _colon: Token![:], + ty: Type, +} + +impl Parse for InstructionArgParsed { + fn parse(input: ParseStream) -> syn::Result { + Ok(InstructionArgParsed { + name: input.parse()?, + _colon: input.parse()?, + ty: input.parse()?, + }) + } +} + +/// Parse #[instruction(...)] attribute from struct +fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Option> { + for attr in attrs { + if attr.path().is_ident("instruction") { + if let Ok(args) = attr.parse_args_with(|input: ParseStream| { + let content: Punctuated = + Punctuated::parse_terminated(input)?; + Ok(content + .into_iter() + .map(|arg| InstructionArg { + name: arg.name, + ty: arg.ty, + }) + .collect::>()) + }) { + return Some(args); + } + } + } + None +} + +/// Check if a type is Account<...> or Box> +fn extract_account_type(ty: &Type) -> Option<(bool, &syn::Path)> { + match ty { + Type::Path(type_path) => { + let path = &type_path.path; + if let Some(segment) = path.segments.last() { + let ident_str = segment.ident.to_string(); + if ident_str == "Account" { + return Some((false, path)); + } + if ident_str == "Box" { + // Check for Box> + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + if let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = + args.args.first() + { + if let Some(inner_seg) = inner_path.path.segments.last() { + if inner_seg.ident == "Account" { + return Some((true, &inner_path.path)); + } + } + } + } + } + } + None + } + _ => None, + } +} + +/// Parse a struct to extract rentfree and light_mint fields +pub fn parse_compressible_struct(input: &DeriveInput) -> Result { + let struct_name = input.ident.clone(); + let generics = input.generics.clone(); + + let instruction_args = parse_instruction_attr(&input.attrs); + + let fields = match &input.data { + syn::Data::Struct(data) => match &data.fields { + syn::Fields::Named(fields) => &fields.named, + _ => return Err(Error::new_spanned(input, "expected named fields")), + }, + _ => return Err(Error::new_spanned(input, "expected struct")), + }; + + let mut rentfree_fields = Vec::new(); + let mut light_mint_fields = Vec::new(); + let mut fee_payer_field = None; + let mut compression_config_field = None; + let mut ctoken_config_field = None; + let mut ctoken_rent_sponsor_field = None; + let mut ctoken_program_field = None; + let mut ctoken_cpi_authority_field = None; + + for field in fields { + let field_ident = field.ident.clone().unwrap(); + let field_name = field_ident.to_string(); + + // Track special fields by name + if field_name == "fee_payer" || field_name == "payer" || field_name == "creator" { + fee_payer_field = Some(field_ident.clone()); + } + if field_name == "compression_config" { + compression_config_field = Some(field_ident.clone()); + } + // Track ctoken-related fields for decompress mint + if field_name == "ctoken_compressible_config" + || field_name == "ctoken_config" + || field_name == "light_token_config_account" + { + ctoken_config_field = Some(field_ident.clone()); + } + if field_name == "ctoken_rent_sponsor" || field_name == "light_token_rent_sponsor" { + ctoken_rent_sponsor_field = Some(field_ident.clone()); + } + if field_name == "ctoken_program" || field_name == "light_token_program" { + ctoken_program_field = Some(field_ident.clone()); + } + if field_name == "ctoken_cpi_authority" + || field_name == "light_token_program_cpi_authority" + || field_name == "compress_token_program_cpi_authority" + { + ctoken_cpi_authority_field = Some(field_ident.clone()); + } + + // Look for #[rentfree] or #[rentfree(...)] attribute + for attr in &field.attrs { + if attr.path().is_ident("rentfree") { + // Handle both #[rentfree] and #[rentfree(...)] + let args: RentFreeArgs = match &attr.meta { + syn::Meta::Path(_) => { + // No parentheses: #[rentfree] + RentFreeArgs { + address_tree_info: None, + output_tree: None, + } + } + syn::Meta::List(_) => { + // Has parentheses: #[rentfree(...)] + attr.parse_args()? + } + syn::Meta::NameValue(_) => { + return Err(Error::new_spanned( + attr, + "expected #[rentfree] or #[rentfree(...)]", + )); + } + }; + + // Use defaults if not specified: + // - address_tree_info defaults to params.create_accounts_proof.address_tree_info + // - output_tree defaults to params.create_accounts_proof.output_state_tree_index + let address_tree_info = args.address_tree_info.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + let output_tree = args.output_tree.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.output_state_tree_index) + }); + + // Validate this is an Account type and check if it's boxed + let (is_boxed, _) = extract_account_type(&field.ty).ok_or_else(|| { + Error::new_spanned( + &field.ty, + "#[rentfree] can only be applied to Account<...> fields", + ) + })?; + + rentfree_fields.push(RentFreeField { + ident: field_ident.clone(), + ty: field.ty.clone(), + address_tree_info, + output_tree, + is_boxed, + }); + break; + } + + // Look for #[light_mint(...)] attribute + if attr.path().is_ident("light_mint") { + let args: LightMintArgs = attr.parse_args()?; + + // Validate required fields + let mint_signer = args + .mint_signer + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires mint_signer"))?; + let authority = args + .authority + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires authority"))?; + let decimals = args + .decimals + .ok_or_else(|| Error::new_spanned(attr, "light_mint requires decimals"))?; + + // address_tree_info defaults to params.create_accounts_proof.address_tree_info + let address_tree_info = args.address_tree_info.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + + light_mint_fields.push(LightMintField { + field_ident: field_ident.clone(), + mint_signer, + authority, + decimals, + address_tree_info, + freeze_authority: args.freeze_authority, + signer_seeds: args.signer_seeds, + rent_payment: args.rent_payment, + write_top_up: args.write_top_up, + }); + break; + } + } + } + + Ok(ParsedCompressibleStruct { + struct_name, + generics, + rentfree_fields, + light_mint_fields, + instruction_args, + fee_payer_field, + compression_config_field, + ctoken_config_field, + ctoken_rent_sponsor_field, + ctoken_program_field, + ctoken_cpi_authority_field, + }) +} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 4161dbea34..64755504ef 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -1,148 +1,18 @@ extern crate proc_macro; -use accounts::{process_light_accounts, process_light_system_accounts}; use discriminator::discriminator; use hasher::{derive_light_hasher, derive_light_hasher_sha}; use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput, ItemStruct}; -use traits::process_light_traits; use utils::into_token_stream; mod account; -mod accounts; mod compressible; mod discriminator; +mod finalize; mod hasher; -mod program; mod rent_sponsor; -mod traits; mod utils; -/// Adds required fields to your anchor instruction for applying a zk-compressed -/// state transition. -/// -/// ## Usage -/// Add `#[light_system_accounts]` to your struct. Ensure it's applied before Anchor's -/// `#[derive(Accounts)]` and Light's `#[derive(LightTraits)]`. -/// -/// ## Example -/// Note: You will have to build your program IDL using Anchor's `idl-build` -/// feature, otherwise your IDL won't include these accounts. -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::light_system_accounts; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// #[program] -/// pub mod my_program { -/// use super::*; -/// } -/// -/// #[light_system_accounts] -/// #[derive(Accounts)] -/// pub struct ExampleInstruction<'info> { -/// pub my_program: Program<'info, MyProgram>, -/// } -/// ``` -/// This will expand to add the following fields to your struct: -/// - `light_system_program`: Verifies and applies zk-compression -/// state transitions. -/// - `registered_program_pda`: A light protocol PDA to authenticate -/// state tree updates. -/// - `noop_program`: The SPL noop program to write -/// compressed-account state as calldata to -/// the Solana ledger. -/// - `account_compression_authority`: The authority for account compression -/// operations. -/// - `account_compression_program`: Called by light_system_program. Updates -/// state trees. -/// - `system_program`: The Solana System program. -#[proc_macro_attribute] -pub fn light_system_accounts(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(process_light_system_accounts(input)) -} - -#[proc_macro_attribute] -pub fn light_accounts(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(process_light_accounts(input)) -} - -#[proc_macro_derive(LightAccounts, attributes(light_account))] -pub fn light_accounts_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(accounts::process_light_accounts_derive(input)) -} - -/// Implements traits on the given struct required for invoking The Light system -/// program via CPI. -/// -/// ## Usage -/// Add `#[derive(LightTraits)]` to your struct which specifies the accounts -/// required for your Anchor program instruction. Specify the attributes -/// `self_program`, `fee_payer`, `authority`, and optionally `cpi_context` to -/// the relevant fields. -/// -/// ### Attributes -/// - `self_program`: Marks the field that represents the program invoking the -/// light system program, i.e. your program. You need to -/// list your program as part of the struct. -/// - `fee_payer`: Marks the field that represents the account responsible -/// for paying transaction fees. (Signer) -/// -/// - `authority`: TODO: explain authority. -/// - `cpi_context`: TODO: explain cpi_context. -/// -/// ### Required accounts (must specify exact name). -/// -/// - `light_system_program`: Light systemprogram. verifies & applies -/// compression state transitions. -/// - `registered_program_pda`: Light Systemprogram PDA -/// - `noop_program`: SPL noop program -/// - `account_compression_authority`: TODO: explain. -/// - `account_compression_program`: Account Compression program. -/// - `system_program`: The Solana Systemprogram. -/// -/// ### Example -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::LightTraits; -/// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); -/// -/// #[program] -/// pub mod my_program { -/// use super::*; -/// } -/// -/// #[derive(Accounts, LightTraits)] -/// pub struct ExampleInstruction<'info> { -/// #[self_program] -/// pub my_program: Program<'info, MyProgram>, -/// #[fee_payer] -/// pub payer: Signer<'info>, -/// #[authority] -/// pub user: AccountInfo<'info>, -/// #[cpi_context] -/// pub cpi_context_account: AccountInfo<'info>, -/// pub light_system_program: AccountInfo<'info>, -/// pub registered_program_pda: AccountInfo<'info>, -/// pub noop_program: AccountInfo<'info>, -/// pub account_compression_authority: AccountInfo<'info>, -/// pub account_compression_program: AccountInfo<'info>, -/// pub system_program: Program<'info, System>, -/// } -/// ``` -#[proc_macro_derive( - LightTraits, - attributes(self_program, fee_payer, authority, cpi_context) -)] -pub fn light_traits_derive(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - into_token_stream(process_light_traits(input)) -} - #[proc_macro_derive(LightDiscriminator)] pub fn light_discriminator(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); @@ -224,13 +94,6 @@ pub fn light_hasher_sha(input: TokenStream) -> TokenStream { into_token_stream(derive_light_hasher_sha(input)) } -/// Alias of `LightHasher`. -#[proc_macro_derive(DataHasher, attributes(skip, hash))] -pub fn data_hasher(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as ItemStruct); - into_token_stream(derive_light_hasher_sha(input)) -} - /// Automatically implements the HasCompressionInfo trait for structs that have a /// `compression_info: Option` field. /// @@ -305,38 +168,42 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { into_token_stream(compressible::traits::derive_compress_as(input)) } -/// Adds compressible account support with automatic seed generation. -/// -/// This macro generates everything needed for compressible accounts: -/// - CompressedAccountVariant enum with all trait implementations -/// - Compress and decompress instructions with auto-generated seed derivation -/// - CTokenSeedProvider implementation for token accounts -/// - All required account structs and functions +/// Auto-discovering rent-free program macro that reads external module files. /// -/// ## Usage -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::add_compressible_instructions; +/// This macro automatically discovers #[rentfree] fields in Accounts structs +/// by reading external module files. No explicit type list needed! /// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); +/// It also **automatically wraps** instruction handlers that use rentfree Accounts +/// structs with `light_pre_init`/`light_finalize` logic - no separate attribute needed! /// -/// #[add_compressible_instructions( -/// UserRecord = ("user_record", data.owner), -/// GameSession = ("game_session", data.session_id.to_le_bytes()), -/// CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.mint) -/// )] +/// Usage: +/// ```ignore +/// #[rentfree_program] /// #[program] /// pub mod my_program { -/// use super::*; -/// // Your regular instructions here - everything else is auto-generated! -/// // CTokenAccountVariant enum is automatically generated with: -/// // - CTokenSigner = 0 +/// pub mod instruction_accounts; // Macro reads this file! +/// pub mod state; +/// +/// use instruction_accounts::*; +/// use state::*; +/// +/// pub fn create_user(ctx: Context, params: Params) -> Result<()> { +/// // Your business logic +/// } /// } /// ``` +/// +/// The macro: +/// 1. Scans the crate's `src/` directory for `#[derive(Accounts)]` structs +/// 2. Extracts seeds from `#[account(seeds = [...])]` on `#[rentfree]` fields +/// 3. Auto-wraps instruction handlers that use those Accounts structs +/// 4. Generates all necessary types, enums, and instruction handlers +/// +/// Seeds are declared ONCE in Anchor attributes - no duplication! #[proc_macro_attribute] -pub fn add_compressible_instructions(args: TokenStream, input: TokenStream) -> TokenStream { +pub fn rentfree_program(args: TokenStream, input: TokenStream) -> TokenStream { let module = syn::parse_macro_input!(input as syn::ItemMod); - into_token_stream(compressible::instructions::add_compressible_instructions( + into_token_stream(compressible::instructions::compressible_program_impl( args.into(), module, )) @@ -421,94 +288,52 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { into_token_stream(compressible::pack_unpack::derive_compressible_pack(input)) } -// DEPRECATED: compressed_account_variant macro is now integrated into add_compressible_instructions -// Use add_compressible_instructions instead for complete automation - -/// Generates complete compressible instructions with auto-generated seed derivation. +/// Consolidates all required traits for rent-free state accounts into a single derive. /// -/// This is a drop-in replacement for manual decompress_accounts_idempotent and -/// compress_accounts_idempotent instructions. It reads #[light_seeds(...)] attributes -/// from account types and generates complete instructions with inline seed derivation. +/// This macro is equivalent to deriving: +/// - `LightHasherSha` (SHA256/ShaFlat hashing - type 3) +/// - `LightDiscriminator` (unique discriminator) +/// - `Compressible` (HasCompressionInfo + CompressAs + Size + CompressedInitSpace) +/// - `CompressiblePack` (Pack + Unpack + Packed struct generation) /// /// ## Example /// -/// Add #[light_seeds(...)] to your account types: /// ```ignore -/// use light_sdk_macros::{Compressible, CompressiblePack}; +/// use light_sdk_macros::RentFreeAccount; +/// use light_sdk::compressible::CompressionInfo; /// use solana_pubkey::Pubkey; /// -/// #[derive(Compressible, CompressiblePack)] -/// #[light_seeds(b"user_record", owner.as_ref())] +/// #[derive(Default, Debug, InitSpace, RentFreeAccount)] +/// #[account] /// pub struct UserRecord { /// pub owner: Pubkey, -/// // ... -/// } -/// -/// #[derive(Compressible, CompressiblePack)] -/// #[light_seeds(b"game_session", session_id.to_le_bytes().as_ref())] -/// pub struct GameSession { -/// pub session_id: u64, -/// // ... +/// #[max_len(32)] +/// pub name: String, +/// pub score: u64, +/// pub compression_info: Option, /// } /// ``` /// -/// Then generate complete instructions: +/// This is equivalent to: /// ```ignore -/// # macro_rules! compressed_account_variant_with_instructions { ($($t:ty),*) => {} } -/// compressed_account_variant_with_instructions!(UserRecord, GameSession, PlaceholderRecord); +/// #[derive(Default, Debug, InitSpace, LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +/// #[account] +/// pub struct UserRecord { ... } /// ``` /// -/// This generates: -/// - CompressedAccountVariant enum + all trait implementations -/// - Complete decompress_accounts_idempotent instruction with auto-generated seed derivation -/// - Complete compress_accounts_idempotent instruction with auto-generated seed derivation -/// - CompressedAccountData struct -/// -/// The generated instructions automatically handle seed derivation for each account type -/// without requiring manual seed function calls. -/// -/// Derive DecompressContext trait implementation. -/// -/// This generates the full DecompressContext trait implementation for -/// decompression account structs. Can be used standalone or is automatically -/// used by add_compressible_instructions. -/// /// ## Attributes -/// - `#[pda_types(Type1, Type2, ...)]` - List of PDA account types -/// - `#[token_variant(CTokenAccountVariant)]` - The token variant enum name /// -/// ## Example -/// -/// ```ignore -/// use anchor_lang::prelude::*; -/// use light_sdk_macros::DecompressContext; +/// - `#[compress_as(...)]` - Optional: specify field values to reset during compression /// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); +/// ## Notes /// -/// struct UserRecord; -/// struct GameSession; -/// enum CTokenAccountVariant {} -/// -/// #[derive(Accounts, DecompressContext)] -/// #[pda_types(UserRecord, GameSession)] -/// #[token_variant(CTokenAccountVariant)] -/// pub struct DecompressAccountsIdempotent<'info> { -/// #[account(mut)] -/// pub fee_payer: Signer<'info>, -/// pub config: AccountInfo<'info>, -/// #[account(mut)] -/// pub rent_sponsor: Signer<'info>, -/// #[account(mut)] -/// pub ctoken_rent_sponsor: AccountInfo<'info>, -/// pub ctoken_program: UncheckedAccount<'info>, -/// pub ctoken_cpi_authority: UncheckedAccount<'info>, -/// pub ctoken_config: UncheckedAccount<'info>, -/// } -/// ``` -#[proc_macro_derive(DecompressContext, attributes(pda_types, token_variant))] -pub fn derive_decompress_context(input: TokenStream) -> TokenStream { +/// - The `compression_info` field is auto-detected and handled (no `#[skip]` needed) +/// - SHA256 (ShaFlat) hashes the entire serialized struct (no `#[hash]` needed) +/// - The struct must have a `compression_info: Option` field +#[proc_macro_derive(RentFreeAccount, attributes(compress_as))] +pub fn rent_free_account(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(compressible::decompress_context::derive_decompress_context( + into_token_stream(compressible::light_compressible::derive_light_compressible( input, )) } @@ -546,30 +371,90 @@ pub fn derive_light_rent_sponsor_pda(input: TokenStream) -> TokenStream { pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { rent_sponsor::derive_light_rent_sponsor(input) } -/// Generates a Light program for the given module. + +/// Generates `RentFree` trait implementation for rent-free accounts and light-mints. /// -/// ## Example +/// This derive macro works alongside Anchor's `#[derive(Accounts)]` to add +/// compression finalize logic for: +/// - Accounts marked with `#[rentfree]` (rent-free PDAs) +/// - Accounts marked with `#[rentfree_token(...)]` (rent-free token accounts) +/// - Accounts marked with `#[light_mint(...)]` (light-mint creation) +/// +/// The trait is defined in `light_sdk::compressible::LightFinalize`. +/// +/// ## Usage - Rent-free PDAs /// /// ```ignore -/// use light_sdk_macros::light_program; -/// use anchor_lang::prelude::*; +/// #[derive(Accounts, RentFree)] +/// #[instruction(params: CompressionParams)] +/// pub struct CreateRentFree<'info> { +/// #[account(mut)] +/// pub fee_payer: Signer<'info>, +/// +/// #[account( +/// init, payer = fee_payer, space = 8 + MyData::INIT_SPACE, +/// seeds = [b"my_data", authority.key().as_ref()], +/// bump +/// )] +/// #[rentfree] +/// pub my_account: Account<'info, MyData>, +/// +/// /// CHECK: Compression config +/// pub compression_config: AccountInfo<'info>, +/// } +/// ``` /// -/// declare_id!("Fg6PaFpoGXkYsidMpWxTWKGNpKK39H3UKo7wjRZnq89u"); +/// ## Usage - Rent-free Token Accounts /// -/// #[derive(Accounts)] -/// pub struct MyInstruction {} +/// ```ignore +/// #[derive(Accounts, RentFree)] +/// pub struct CreateVault<'info> { +/// #[account( +/// mut, +/// seeds = [b"vault", cmint.key().as_ref()], +/// bump +/// )] +/// // Variant name derived from field name: vault -> Vault +/// #[rentfree_token(authority = [b"vault_authority"])] +/// pub vault: UncheckedAccount<'info>, +/// } +/// ``` /// -/// #[light_program] -/// pub mod my_program { -/// use super::*; -/// pub fn my_instruction(ctx: Context) -> Result<()> { -/// // Your instruction logic here -/// Ok(()) -/// } +/// ## Usage - Light Mints +/// +/// ```ignore +/// #[derive(Accounts, RentFree)] +/// #[instruction(params: MintParams)] +/// pub struct CreateMint<'info> { +/// #[account(mut)] +/// pub fee_payer: Signer<'info>, +/// +/// #[account(mut)] +/// #[light_mint( +/// mint_signer = mint_signer, +/// authority = authority, +/// decimals = 9, +/// signer_seeds = &[...] +/// )] +/// pub mint: UncheckedAccount<'info>, +/// +/// pub mint_signer: Signer<'info>, +/// pub authority: Signer<'info>, /// } /// ``` -#[proc_macro_attribute] -pub fn light_program(_: TokenStream, input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as syn::ItemMod); - into_token_stream(program::program(input)) +/// +/// ## Requirements +/// +/// Your program must define: +/// - `LIGHT_CPI_SIGNER`: CPI signer pubkey constant +/// - `ID`: Program ID (from declare_id!) +/// +/// The struct should have fields named `fee_payer` (or `payer`) and `compression_config`. +#[proc_macro_derive( + RentFree, + attributes(rentfree, rentfree_token, light_mint, instruction) +)] +pub fn rent_free_derive(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + into_token_stream(finalize::derive_light_finalize(input)) } diff --git a/sdk-libs/macros/src/program.rs b/sdk-libs/macros/src/program.rs deleted file mode 100644 index 5df0375b1a..0000000000 --- a/sdk-libs/macros/src/program.rs +++ /dev/null @@ -1,301 +0,0 @@ -use std::{ - collections::HashMap, - ops::{Deref, DerefMut}, -}; - -use proc_macro2::{Span, TokenStream}; -use quote::{quote, ToTokens}; -use syn::{ - parse_quote, visit_mut::VisitMut, Attribute, FnArg, GenericArgument, Ident, Item, ItemFn, - ItemMod, ItemStruct, Pat, PathArguments, Result, Stmt, Token, Type, -}; - -// A single instruction parameter provided as an argument to the Anchor program -// function. It consists of the name an the type, e.g.: `name: String`. -#[derive(Clone)] -struct InstructionParam { - name: Ident, - ty: Type, -} - -impl ToTokens for InstructionParam { - fn to_tokens(&self, tokens: &mut TokenStream) { - self.name.to_tokens(tokens); - Token![:](self.name.span()).to_tokens(tokens); - self.ty.to_tokens(tokens); - } -} - -/// Map which stores instruction parameters for all instructions in the parsed -/// program. -/// -/// # Example -/// -/// For the program with the following instructions: -/// -/// ```ignore -/// #[light_program] -/// pub mode my_program { -/// use super::*; -/// -/// pub fn instruction_one( -/// ctx: LightContext<'_, '_, '_, 'info, InstructionOne<'info>>, -/// name: String, -/// num: u32, -/// ) -> Result<()> {} -/// -/// pub fn instruction_two( -/// ctx: LightContext<'_, '_, '_, 'info, InstructionTwo<'info>>, -/// num_one: u32, -/// num_two: u64, -/// ) -> Result<()> {} -/// } -/// ``` -/// -/// The mapping is going to look like: -/// -/// ```ignore -/// instruction_one -> - name: name -/// ty: String -/// - name: num -/// ty: u32 -/// -/// instruction_two -> - name: num_one -/// ty: u32 -/// - name: num_two -/// ty: u64 -/// ``` -#[derive(Default)] -struct InstructionParams(HashMap>); - -impl Deref for InstructionParams { - type Target = HashMap>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InstructionParams { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -/// Implementation of `ToTokens` which allows to convert the -/// instruction-parameter mapping to structs, which we later use for packing -/// of parameters for convenient usage in `LightContext` extensions produced in -/// `accounts.rs` - precisely, in the `check_constraints` and -/// `derive_address_seeds` methods. -impl ToTokens for InstructionParams { - fn to_tokens(&self, tokens: &mut TokenStream) { - for (name, inputs) in self.0.iter() { - let name = Ident::new(name, Span::call_site()); - let strct: ItemStruct = parse_quote! { - pub struct #name { - #(#inputs),* - } - }; - strct.to_tokens(tokens); - } - } -} - -#[derive(Default)] -struct LightProgramTransform { - /// Mapping of instructions to their parameters in the program. - instruction_params: InstructionParams, -} - -impl VisitMut for LightProgramTransform { - fn visit_item_fn_mut(&mut self, i: &mut ItemFn) { - // Add `#[allow(clippy::too_many_arguments)]` attribute in case. We are - // injecting many arguments in this macro and they can easily go over - // the limit. - let clippy_attr: Attribute = parse_quote! { - #[allow(clippy::too_many_arguments)] - }; - i.attrs.push(clippy_attr); - - // Gather names instruction parameters (arguments other than `ctx`). - // They are going to be used to generate `Inputs*` structs. - let mut instruction_params = Vec::with_capacity(i.sig.inputs.len() - 1); - for input in i.sig.inputs.iter().skip(1) { - if let FnArg::Typed(input) = input { - if let Pat::Ident(ref pat_ident) = *input.pat { - instruction_params.push(InstructionParam { - name: pat_ident.ident.clone(), - ty: (*input.ty).clone(), - }); - } - } - } - - // Find the `ctx` argument. - let ctx_arg = i.sig.inputs.first_mut().unwrap(); - - // Retrieve the type of `ctx`. - let pat_type = match ctx_arg { - FnArg::Typed(pat_type) => pat_type, - _ => return, - }; - - // Get the last path segment of `ctx` type. - let type_path = match pat_type.ty.as_mut() { - Type::Path(type_path) => type_path, - _ => return, - }; - let ctx_segment = &mut type_path.path.segments.last_mut().unwrap(); - // If the `ctx` is of type `LightContext`, that means that the given - // instruction uses compressed accounts and we need to inject our code - // for handling them. - // Otherwise, stop processing the instruction and assume it's just a - // regular instruction using only regular accounts. - if ctx_segment.ident != "LightContext" { - return; - } - - // Swap the type of `ctx` to Anchor's `Context` to keep the instruction - // signature correct. We are going to inject the code converting it to - // `LightContext` later. - ctx_segment.ident = Ident::new("Context", Span::call_site()); - - // Figure out what's are the names of: - // - // - The struct with Anchor accounts (implementing `anchor_lang::Accounts`) - - // it's specified as the last generic argument in `ctx`, e.g. `MyInstruction`. - // - The struct with compressed accounts (implementing `LightAccounts`) - - // it's derived by adding the `Light` prefix to the previous struct name, - // e.g. `LightMyInstruction`. - let arguments = match &ctx_segment.arguments { - PathArguments::AngleBracketed(arguments) => arguments, - _ => return, - }; - let last_arg = arguments.args.last().unwrap(); - let last_arg_type = match last_arg { - GenericArgument::Type(last_arg_type) => last_arg_type, - _ => return, - }; - let last_arg_type_path = match last_arg_type { - Type::Path(last_arg_type_path) => last_arg_type_path, - _ => return, - }; - let accounts_segment = &last_arg_type_path.path.segments.last().unwrap(); - let accounts_ident = accounts_segment.ident.clone(); - let light_accounts_name = format!("Light{}", accounts_segment.ident); - let light_accounts_ident = Ident::new(&light_accounts_name, Span::call_site()); - - // Add the previously gathered instruction inputs to the mapping of - // instructions to their parameters (`self.instruction_inputs`). - let params_name = format!("Params{}", accounts_segment.ident); - self.instruction_params - .insert(params_name.clone(), instruction_params.clone()); - let inputs_ident = Ident::new(¶ms_name, Span::call_site()); - - // Inject an `inputs: Vec>` argument to all instructions. The - // purpose of that additional argument is passing compressed accounts. - let inputs_arg: FnArg = parse_quote! { inputs: Vec> }; - i.sig.inputs.insert(1, inputs_arg); - - // Inject Merkle context related arguments. - let proof_arg: FnArg = parse_quote! { proof: ::light_sdk::proof::CompressedProof }; - i.sig.inputs.insert(2, proof_arg); - let merkle_context_arg: FnArg = - parse_quote! { merkle_context: ::light_sdk::merkle_context::PackedMerkleContext }; - i.sig.inputs.insert(3, merkle_context_arg); - let merkle_tree_root_index_arg: FnArg = parse_quote! { merkle_tree_root_index: u16 }; - i.sig.inputs.insert(4, merkle_tree_root_index_arg); - let address_merkle_context_arg: FnArg = - parse_quote! { address_merkle_context: ::light_sdk::tree_info::PackedAddressTreeInfo }; - i.sig.inputs.insert(5, address_merkle_context_arg); - let address_merkle_tree_root_index_arg: FnArg = - parse_quote! { address_merkle_tree_root_index: u16 }; - i.sig.inputs.insert(6, address_merkle_tree_root_index_arg); - - // Inject a `LightContext` into the function body. - let light_context_stmt: Stmt = parse_quote! { - let mut ctx: ::light_sdk::context::LightContext< - #accounts_ident, - #light_accounts_ident - > = ::light_sdk::context::LightContext::new( - ctx, - inputs, - merkle_context, - merkle_tree_root_index, - address_merkle_context, - address_merkle_tree_root_index, - )?; - }; - i.block.stmts.insert(0, light_context_stmt); - - // Pack all instruction inputs in a struct, which then can be used in - // `check_constrants` and `derive_address_seeds`. - // - // We do that, because passing one reference to these methods is more - // comfortable. Passing references to each input separately would - // require even messier code... - // - // We move the inputs to that struct, so no copies are being made. - let input_idents = instruction_params - .iter() - .map(|input| input.name.clone()) - .collect::>(); - let inputs_pack_stmt: Stmt = parse_quote! { - let inputs = #inputs_ident { #(#input_idents),* }; - }; - i.block.stmts.insert(1, inputs_pack_stmt); - - // Inject `check_constraints` and `derive_address_seeds` calls right - // after. - let check_constraints_stmt: Stmt = parse_quote! { - ctx.check_constraints(&inputs)?; - }; - i.block.stmts.insert(2, check_constraints_stmt); - let derive_address_seed_stmt: Stmt = parse_quote! { - ctx.derive_address_seeds(address_merkle_context, &inputs); - }; - i.block.stmts.insert(3, derive_address_seed_stmt); - - // Once we are done with calling `check_constraints` and - // `derive_address_seeds`, we can unpack the inputs, so developers can - // use them as regular variables in their code. - // - // Unpacking of the struct means moving the values and no copies are - // being made. - let inputs_unpack_stmt: Stmt = parse_quote! { - let #inputs_ident { #(#input_idents),* } = inputs; - }; - i.block.stmts.insert(4, inputs_unpack_stmt); - - // Inject `verify` statements at the end of the function. - let stmts_len = i.block.stmts.len(); - let verify_stmt: Stmt = parse_quote! { - ctx.verify(proof)?; - }; - i.block.stmts.insert(stmts_len - 1, verify_stmt); - } - - fn visit_item_mod_mut(&mut self, i: &mut ItemMod) { - // Search for all functions inside the annotated `mod` and visit them. - if let Some((_, ref mut content)) = i.content { - for item in content.iter_mut() { - if let Item::Fn(item_fn) = item { - self.visit_item_fn_mut(item_fn) - } - } - } - } -} - -pub(crate) fn program(mut input: ItemMod) -> Result { - let mut transform = LightProgramTransform::default(); - transform.visit_item_mod_mut(&mut input); - - let instruction_params = transform.instruction_params; - - Ok(quote! { - #instruction_params - - #input - }) -} diff --git a/sdk-libs/macros/src/traits.rs b/sdk-libs/macros/src/traits.rs deleted file mode 100644 index 6699ec172b..0000000000 --- a/sdk-libs/macros/src/traits.rs +++ /dev/null @@ -1,401 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{Data, DeriveInput, Fields, FieldsNamed, Ident, Result}; - -pub(crate) fn process_light_traits(input: DeriveInput) -> Result { - let name = &input.ident; - - let trait_impls = match input.data { - Data::Struct(data_struct) => match data_struct.fields { - Fields::Named(fields) => process_fields_and_attributes(name, fields), - _ => quote! { - compile_error!("Error: Expected named fields but found unnamed or no fields."); - }, - }, - _ => quote! {}, - }; - - let expanded = quote! { - #trait_impls - }; - - Ok(expanded) -} - -fn process_fields_and_attributes(name: &Ident, fields: FieldsNamed) -> TokenStream { - let mut self_program_field = None; - let mut fee_payer_field = None; - let mut authority_field = None; - let mut light_system_program_field = None; - let mut cpi_context_account_field = None; - - // base impl - let mut registered_program_pda_field = None; - let mut noop_program_field = None; - let mut account_compression_authority_field = None; - let mut account_compression_program_field = None; - let mut system_program_field = None; - - let compressed_sol_pda_field = fields - .named - .iter() - .find_map(|f| { - if f.ident - .as_ref() - .map(|id| id == "compressed_sol_pda") - .unwrap_or(false) - { - Some(quote! { self.#f.ident.as_ref() }) - } else { - None - } - }) - .unwrap_or(quote! { None }); - - let compression_recipient_field = fields - .named - .iter() - .find_map(|f| { - if f.ident - .as_ref() - .map(|id| id == "compression_recipient") - .unwrap_or(false) - { - Some(quote! { self.#f.ident.as_ref() }) - } else { - None - } - }) - .unwrap_or(quote! { None }); - - for f in fields.named.iter() { - for attr in &f.attrs { - if attr.path().is_ident("self_program") { - self_program_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("fee_payer") { - fee_payer_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("authority") { - authority_field = Some(f.ident.as_ref().unwrap()); - } - if attr.path().is_ident("cpi_context") { - cpi_context_account_field = Some(f.ident.as_ref().unwrap()); - } - } - if f.ident - .as_ref() - .map(|id| id == "light_system_program") - .unwrap_or(false) - { - light_system_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "registered_program_pda") - .unwrap_or(false) - { - registered_program_pda_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "noop_program") - .unwrap_or(false) - { - noop_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "account_compression_authority") - .unwrap_or(false) - { - account_compression_authority_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "account_compression_program") - .unwrap_or(false) - { - account_compression_program_field = Some(f.ident.as_ref().unwrap()); - } - if f.ident - .as_ref() - .map(|id| id == "system_program") - .unwrap_or(false) - { - system_program_field = Some(f.ident.as_ref().unwrap()); - } - } - - // optional: compressed_sol_pda, compression_recipient, cpi_context_account - let missing_required_fields = [ - if light_system_program_field.is_none() { - "light_system_program" - } else { - "" - }, - if registered_program_pda_field.is_none() { - "registered_program_pda" - } else { - "" - }, - if noop_program_field.is_none() { - "noop_program" - } else { - "" - }, - if account_compression_authority_field.is_none() { - "account_compression_authority" - } else { - "" - }, - if account_compression_program_field.is_none() { - "account_compression_program" - } else { - "" - }, - if system_program_field.is_none() { - "system_program" - } else { - "" - }, - ] - .iter() - .filter(|&field| !field.is_empty()) - .cloned() - .collect::>(); - - let missing_required_attributes = [ - if self_program_field.is_none() { - "self_program" - } else { - "" - }, - if fee_payer_field.is_none() { - "fee_payer" - } else { - "" - }, - if authority_field.is_none() { - "authority" - } else { - "" - }, - ] - .iter() - .filter(|&attr| !attr.is_empty()) - .cloned() - .collect::>(); - - if !missing_required_fields.is_empty() || !missing_required_attributes.is_empty() { - let error_message = format!( - "Error: Missing required fields: [{}], Missing required attributes: [{}]", - missing_required_fields.join(", "), - missing_required_attributes.join(", ") - ); - quote! { - compile_error!(#error_message); - } - } else { - let base_impls = quote! { - impl<'info> ::light_sdk::legacy::InvokeCpiAccounts<'info> for #name<'info> { - fn get_invoking_program(&self) -> AccountInfo<'info> { - self.#self_program_field.to_account_info() - } - } - impl<'info> ::light_sdk::legacy::SignerAccounts<'info> for #name<'info> { - fn get_fee_payer(&self) -> ::anchor_lang::prelude::AccountInfo<'info> { - self.#fee_payer_field.to_account_info() - } - fn get_authority(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#authority_field - } - } - impl<'info> ::light_sdk::legacy::LightSystemAccount<'info> for #name<'info> { - fn get_light_system_program(&self) -> ::anchor_lang::prelude::AccountInfo<'info> { - self.#light_system_program_field.to_account_info() - } - } - }; - let invoke_accounts_impl = quote! { - impl<'info> ::light_sdk::legacy::InvokeAccounts<'info> for #name<'info> { - fn get_registered_program_pda(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#registered_program_pda_field - } - fn get_noop_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#noop_program_field - } - fn get_account_compression_authority(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#account_compression_authority_field - } - fn get_account_compression_program(&self) -> &::anchor_lang::prelude::AccountInfo<'info> { - &self.#account_compression_program_field - } - fn get_system_program(&self) ->::anchor_lang::prelude::AccountInfo<'info> { - self.#system_program_field.to_account_info() - } - fn get_compressed_sol_pda(&self) -> Option<&::anchor_lang::prelude::AccountInfo<'info>> { - #compressed_sol_pda_field - } - fn get_compression_recipient(&self) -> Option<&::anchor_lang::prelude::AccountInfo<'info>> { - #compression_recipient_field - } - } - }; - if cpi_context_account_field.is_none() { - quote! { - #base_impls - #invoke_accounts_impl - impl<'info> ::light_sdk::legacy::InvokeCpiContextAccount<'info> for #name<'info> { - fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::AccountInfo<'info> - > { - None - } - } - } - } else { - quote! { - #base_impls - #invoke_accounts_impl - impl<'info> ::light_sdk::legacy::InvokeCpiContextAccount<'info> for #name<'info> { - fn get_cpi_context_account(&self) -> Option< - &::anchor_lang::prelude::AccountInfo<'info> - > { - Some(&self.#cpi_context_account_field) - } - } - } - } - } -} - -#[cfg(test)] -mod tests { - use syn::{parse_quote, DeriveInput, FieldsNamed}; - - use super::*; - - #[test] - fn test_process_light_traits() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let output = process_light_traits(input).unwrap(); - let output_string = output.to_string(); - - assert!(output_string.contains("InvokeCpiAccounts")); - assert!(output_string.contains("SignerAccounts")); - assert!(output_string.contains("LightSystemAccount")); - assert!(output_string.contains("InvokeAccounts")); - assert!(output_string.contains("InvokeCpiContextAccount")); - } - - #[test] - fn test_process_fields_and_attributes() { - let fields: FieldsNamed = parse_quote! { - { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let name = syn::Ident::new("TestStruct", proc_macro2::Span::call_site()); - let output = process_fields_and_attributes(&name, fields); - let output_string = output.to_string(); - - assert!(output_string.contains("InvokeCpiAccounts")); - assert!(output_string.contains("SignerAccounts")); - assert!(output_string.contains("LightSystemAccount")); - assert!(output_string.contains("InvokeAccounts")); - assert!(output_string.contains("InvokeCpiContextAccount")); - } - - #[test] - fn test_process_light_traits_missing_fields() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - #[authority] - pub user: AccountInfo<'info>, - // Missing required fields - } - }; - - let result = process_light_traits(input); - let output_string = result.unwrap().to_string(); - - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [light_system_program, registered_program_pda, noop_program, account_compression_authority, account_compression_program, system_program], Missing required attributes: []")); - } - - #[test] - fn test_process_light_traits_missing_attributes() { - let input: DeriveInput = parse_quote! { - struct TestStruct { - pub my_program: Program<'info, MyProgram>, // Missing #[self_program] - pub payer: Signer<'info>, // Missing #[fee_payer] - pub user: AccountInfo<'info>, // Missing #[authority] - pub light_system_program: AccountInfo<'info>, - pub registered_program_pda: AccountInfo<'info>, - pub noop_program: AccountInfo<'info>, - pub account_compression_authority: AccountInfo<'info>, - pub account_compression_program: AccountInfo<'info>, - pub system_program: Program<'info, System>, - } - }; - - let result = process_light_traits(input); - let output_string = result.unwrap().to_string(); - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [], Missing required attributes: [self_program, fee_payer, authority]")); - } - - #[test] - fn test_process_fields_and_attributes_missing_fields() { - let fields: FieldsNamed = parse_quote! { - { - #[self_program] - pub my_program: Program<'info, MyProgram>, - #[fee_payer] - pub payer: Signer<'info>, - pub user: AccountInfo<'info>, // missing #[authority] - // Missing required fields - } - }; - - let name = syn::Ident::new("TestStruct", proc_macro2::Span::call_site()); - let output = process_fields_and_attributes(&name, fields); - let output_string = output.to_string(); - - assert!(output_string.contains("compile_error")); - assert!(output_string.contains("Error: Missing required fields: [light_system_program, registered_program_pda, noop_program, account_compression_authority, account_compression_program, system_program], Missing required attributes: [authority]")); - } -} diff --git a/sdk-libs/program-test/Cargo.toml b/sdk-libs/program-test/Cargo.toml index 15324540df..d4c3073d07 100644 --- a/sdk-libs/program-test/Cargo.toml +++ b/sdk-libs/program-test/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [features] default = [] -devenv = ["v2","light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-token-interface", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] +devenv = ["v2","light-client/devenv", "light-prover-client/devenv", "dep:account-compression", "dep:light-compressed-token", "dep:light-compressible", "dep:light-registry", "dep:light-batched-merkle-tree", "dep:light-concurrent-merkle-tree"] v2 = ["light-client/v2"] [dependencies] @@ -18,7 +18,7 @@ light-merkle-tree-reference = { workspace = true } light-merkle-tree-metadata = { workspace = true, features = ["anchor"] } light-concurrent-merkle-tree = { workspace = true, optional = true } light-hasher = { workspace = true, features = ["poseidon", "sha256", "keccak", "std"] } -light-token-interface = { workspace = true, optional = true } +light-token-interface = { workspace = true } light-compressible = { workspace = true, optional = true } light-token-sdk = { workspace = true } light-compressed-account = { workspace = true, features = ["anchor", "poseidon"] } diff --git a/sdk-libs/program-test/src/compressible.rs b/sdk-libs/program-test/src/compressible.rs index 92a8461778..6406f1b58b 100644 --- a/sdk-libs/program-test/src/compressible.rs +++ b/sdk-libs/program-test/src/compressible.rs @@ -42,32 +42,44 @@ fn determine_account_type(data: &[u8]) -> Option { } } -/// Extracts CompressionInfo and account type from account data, handling both Token and CMint. -/// Returns (CompressionInfo, account_type) or None if parsing fails. +/// Extracts CompressionInfo, account type, and compression_only from account data. +/// Returns (CompressionInfo, account_type, compression_only) or None if parsing fails. #[cfg(feature = "devenv")] -fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8)> { - use light_token_interface::state::extensions::ExtensionStruct; +fn extract_compression_info(data: &[u8]) -> Option<(CompressionInfo, u8, bool)> { + use light_zero_copy::traits::ZeroCopyAt; let account_type = determine_account_type(data)?; match account_type { ACCOUNT_TYPE_TOKEN_ACCOUNT => { - let ctoken = Token::deserialize(&mut &data[..]).ok()?; - // Get CompressionInfo from Compressible extension - let compression_info = - ctoken - .extensions - .as_ref()? - .iter() - .find_map(|ext| match ext { - ExtensionStruct::Compressible(comp) => Some(comp.info), - _ => None, - })?; - Some((compression_info, account_type)) + let (ctoken, _) = Token::zero_copy_at(data).ok()?; + let ext = ctoken.get_compressible_extension()?; + + let compression_info = CompressionInfo { + config_account_version: ext.info.config_account_version.into(), + compress_to_pubkey: ext.info.compress_to_pubkey, + account_version: ext.info.account_version, + lamports_per_write: ext.info.lamports_per_write.into(), + compression_authority: ext.info.compression_authority, + rent_sponsor: ext.info.rent_sponsor, + last_claimed_slot: ext.info.last_claimed_slot.into(), + rent_exemption_paid: ext.info.rent_exemption_paid.into(), + _reserved: ext.info._reserved.into(), + rent_config: RentConfig { + base_rent: ext.info.rent_config.base_rent.into(), + compression_cost: ext.info.rent_config.compression_cost.into(), + lamports_per_byte_per_epoch: ext.info.rent_config.lamports_per_byte_per_epoch, + max_funded_epochs: ext.info.rent_config.max_funded_epochs, + max_top_up: ext.info.rent_config.max_top_up.into(), + }, + }; + let compression_only = ext.compression_only != 0; + Some((compression_info, account_type, compression_only)) } ACCOUNT_TYPE_MINT => { let cmint = CompressedMint::deserialize(&mut &data[..]).ok()?; - Some((cmint.compression, account_type)) + // CMint accounts don't have compression_only, default to false + Some((cmint.compression, account_type, false)) } _ => None, } @@ -84,6 +96,8 @@ pub struct StoredCompressibleAccount { pub compression: CompressionInfo, /// Account type: ACCOUNT_TYPE_TOKEN_ACCOUNT (2) or ACCOUNT_TYPE_MINT (1) pub account_type: u8, + /// Whether this is a compression-only account (affects batching) + pub compression_only: bool, } #[cfg(feature = "devenv")] @@ -141,12 +155,15 @@ pub async fn claim_and_compress( .context .get_program_accounts(&light_compressed_token::ID); + // CToken base accounts are 165 bytes, filter above that to exclude empty/minimal accounts for account in compressible_ctoken_accounts .iter() - .filter(|e| e.1.data.len() > 200 && e.1.lamports > 0) + .filter(|e| e.1.data.len() >= 165 && e.1.lamports > 0) { - // Extract compression info and account type, handling both Token and CMint - let Some((compression, account_type)) = extract_compression_info(&account.1.data) else { + // Extract compression info, account type, and compression_only + let Some((compression, account_type, compression_only)) = + extract_compression_info(&account.1.data) + else { continue; }; @@ -169,13 +186,17 @@ pub async fn claim_and_compress( last_paid_slot: last_funded_slot, compression, account_type, + compression_only, }, ); } let current_slot = rpc.get_slot().await?; - let mut compress_accounts = Vec::new(); + // Separate accounts by type and compression_only setting + let mut compress_accounts_compression_only = Vec::new(); + let mut compress_accounts_normal = Vec::new(); + let mut compress_cmint_accounts = Vec::new(); let mut claim_accounts = Vec::new(); // For each stored account, determine action using AccountRentState @@ -201,10 +222,16 @@ pub async fn claim_and_compress( match state.calculate_claimable_rent(&compression.rent_config, rent_exemption) { None => { // Account is compressible (has rent deficit) - // Only Token accounts can be compressed via compress_and_close_forester - // CMint accounts have a different compression flow if stored_account.account_type == ACCOUNT_TYPE_TOKEN_ACCOUNT { - compress_accounts.push(*pubkey); + // CToken accounts - separate by compression_only + if stored_account.compression_only { + compress_accounts_compression_only.push(*pubkey); + } else { + compress_accounts_normal.push(*pubkey); + } + } else if stored_account.account_type == ACCOUNT_TYPE_MINT { + // CMint accounts - use mint_action flow + compress_cmint_accounts.push(*pubkey); } } Some(claimable_amount) if claimable_amount > 0 => { @@ -224,17 +251,32 @@ pub async fn claim_and_compress( claim_forester(rpc, token_accounts, &forester_keypair, &payer).await?; } - // Process compressible accounts in batches + // Process compressible accounts in batches, separated by compression_only setting + // This prevents TlvExtensionLengthMismatch errors when batching accounts together const BATCH_SIZE: usize = 10; - for chunk in compress_accounts.chunks(BATCH_SIZE) { + + // Process compression_only=true CToken accounts + for chunk in compress_accounts_compression_only.chunks(BATCH_SIZE) { compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?; + for account_pubkey in chunk { + stored_compressible_accounts.remove(account_pubkey); + } + } - // Remove compressed accounts from HashMap + // Process compression_only=false CToken accounts + for chunk in compress_accounts_normal.chunks(BATCH_SIZE) { + compress_and_close_forester(rpc, chunk, &forester_keypair, &payer, None).await?; for account_pubkey in chunk { stored_compressible_accounts.remove(account_pubkey); } } + // Process CMint accounts via mint_action + for cmint_pubkey in compress_cmint_accounts { + compress_cmint_forester(rpc, cmint_pubkey, &payer).await?; + stored_compressible_accounts.remove(&cmint_pubkey); + } + Ok(()) } @@ -257,7 +299,7 @@ pub async fn auto_compress_program_pdas( let cfg = CpdaCompressibleConfig::try_from_slice(&cfg_acc.data) .map_err(|e| RpcError::CustomError(format!("config deserialize: {e:?}")))?; let rent_sponsor = cfg.rent_sponsor; - // TODO: add coverage for external compression_authority + // compression_authority is the payer by default for auto-compress let compression_authority = payer.pubkey(); let address_tree = cfg.address_space[0]; @@ -267,11 +309,16 @@ pub async fn auto_compress_program_pdas( return Ok(()); } + // CompressAccountsIdempotent struct expects 4 accounts: + // 1. fee_payer (signer, writable) + // 2. config (read-only) + // 3. rent_sponsor (writable) + // 4. compression_authority (writable - per generated struct) let program_metas = vec![ AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(config_pda, false), AccountMeta::new(rent_sponsor, false), - AccountMeta::new_readonly(compression_authority, false), + AccountMeta::new(compression_authority, false), ]; const BATCH_SIZE: usize = 5; @@ -309,6 +356,7 @@ async fn try_compress_chunk( // Attempt compression per-account idempotently. for (pda, acc) in chunk.iter() { + // v2 address derive using PDA as seed let addr = derive_address( &pda.to_bytes(), &address_tree.to_bytes(), @@ -354,3 +402,110 @@ async fn try_compress_chunk( .await; } } + +/// Compress and close a CMint account via mint_action instruction. +/// CMint uses MintAction::CompressAndCloseCMint flow instead of registry compress_and_close. +#[cfg(feature = "devenv")] +async fn compress_cmint_forester( + rpc: &mut LightProgramTest, + cmint_pubkey: Pubkey, + payer: &solana_sdk::signature::Keypair, +) -> Result<(), RpcError> { + use light_client::indexer::Indexer; + use light_compressed_account::instruction_data::traits::LightInstructionData; + use light_compressible::config::CompressibleConfig; + use light_token_interface::{ + instructions::mint_action::{ + CompressAndCloseCMintAction, CompressedMintWithContext, + MintActionCompressedInstructionData, + }, + LIGHT_TOKEN_PROGRAM_ID, + }; + use light_token_sdk::compressed_token::mint_action::MintActionMetaConfig; + use solana_sdk::signature::Signer; + + // Get CMint account data + let cmint_account = rpc.get_account(cmint_pubkey).await?.ok_or_else(|| { + RpcError::CustomError(format!("CMint account {} not found", cmint_pubkey)) + })?; + + // Deserialize CMint to get compressed_address and rent_sponsor + let cmint: CompressedMint = + BorshDeserialize::deserialize(&mut cmint_account.data.as_slice()) + .map_err(|e| RpcError::CustomError(format!("Failed to deserialize CMint: {:?}", e)))?; + + let compressed_mint_address = cmint.metadata.compressed_address(); + let rent_sponsor = Pubkey::from(cmint.compression.rent_sponsor); + + // Get the compressed mint account from indexer + let compressed_mint_account = rpc + .get_compressed_account(compressed_mint_address, None) + .await? + .value + .ok_or(RpcError::AccountDoesNotExist(format!( + "Compressed mint {:?}", + compressed_mint_address + )))?; + + // Get validity proof + let rpc_proof_result = rpc + .get_validity_proof(vec![compressed_mint_account.hash], vec![], None) + .await? + .value; + + // Build compressed mint inputs + // IMPORTANT: Set mint to None when CMint is decompressed + // This tells on-chain code to read mint data from CMint Solana account + // (not from instruction data which would have stale compression_info) + let compressed_mint_inputs = CompressedMintWithContext { + prove_by_index: rpc_proof_result.accounts[0].root_index.proof_by_index(), + leaf_index: compressed_mint_account.leaf_index, + root_index: rpc_proof_result.accounts[0] + .root_index + .root_index() + .unwrap_or_default(), + address: compressed_mint_address, + mint: None, // CMint is decompressed, data lives in CMint account + }; + + // Build instruction data with CompressAndCloseCMint action + let instruction_data = MintActionCompressedInstructionData::new( + compressed_mint_inputs, + rpc_proof_result.proof.into(), + ) + .with_compress_and_close_cmint(CompressAndCloseCMintAction { idempotent: 1 }); + + // Get state tree info + let state_tree_info = rpc_proof_result.accounts[0].tree_info; + + // Build account metas - authority can be anyone for permissionless CompressAndCloseCMint + let config_address = CompressibleConfig::light_token_v1_config_pda(); + let meta_config = MintActionMetaConfig::new( + payer.pubkey(), + payer.pubkey(), // authority doesn't matter for CompressAndCloseCMint + state_tree_info.tree, + state_tree_info.queue, + state_tree_info.queue, + ) + .with_compressible_mint(cmint_pubkey, config_address, rent_sponsor); + + let account_metas = meta_config.to_account_metas(); + + // Serialize instruction data + let data = instruction_data + .data() + .map_err(|e| RpcError::CustomError(format!("Failed to serialize instruction: {:?}", e)))?; + + // Build instruction + let instruction = solana_instruction::Instruction { + program_id: Pubkey::from(LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data, + }; + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) + .await?; + + Ok(()) +} diff --git a/sdk-libs/program-test/src/program_test/light_program_test.rs b/sdk-libs/program-test/src/program_test/light_program_test.rs index 08b4680f26..3591ec8a77 100644 --- a/sdk-libs/program-test/src/program_test/light_program_test.rs +++ b/sdk-libs/program-test/src/program_test/light_program_test.rs @@ -430,6 +430,279 @@ impl LightProgramTest { self.auto_mine_cold_state_programs .retain(|&pid| pid != program_id); } + + /// Fetches ATA interface for a (mint, owner) pair. + /// + /// Checks on-chain first, then compressed state. + /// Always returns `AtaInterface` with `data` populated so clients can + /// access `amount`, `delegate`, etc. regardless of hot/cold state. + /// + /// Fetches raw ATA account interface with Account bytes always present. + /// + /// For hot accounts: actual on-chain bytes. + /// For cold accounts: synthetic SPL Token Account format bytes. + /// + /// Use `parse_token_account_interface()` to extract typed `TokenData`. + /// + /// # Example + /// ```ignore + /// // 1. Fetch raw account interface (async) + /// let account = rpc.get_ata_account_interface(&mint, &owner).await?; + /// + /// // 2. Parse into token account interface (sync) + /// let parsed = parse_token_account_interface(&account)?; + /// + /// // 3. Check if cold and act accordingly + /// if parsed.is_cold { + /// let proof = rpc.get_validity_proof(vec![parsed.hash().unwrap()], vec![], None).await?; + /// let ixs = build_decompress_token_accounts(&[parsed], fee_payer, Some(proof.value))?; + /// } + /// ``` + pub async fn get_ata_account_interface( + &self, + mint: &solana_sdk::pubkey::Pubkey, + owner: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use light_client::indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer}; + use light_compressible_client::{ + pack_token_data_to_spl_bytes, AtaAccountInterface, AtaDecompressionContext, + }; + use light_sdk::constants::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::derive_token_ata; + + let (ata, bump) = derive_token_ata(owner, mint); + + // Check on-chain first + if let Some(account) = self.context.get_account(&ata) { + return Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: false, + decompression_context: None, + }); + } + + // Check compressed state + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); + let result = self + .get_compressed_token_accounts_by_owner(&ata, options, None) + .await?; + + if let Some(compressed) = result.value.items.into_iter().next() { + // Synthesize SPL Token Account bytes from TokenData + let token_data = &compressed.token; + let data = pack_token_data_to_spl_bytes(mint, &ata, token_data).to_vec(); + + // Create synthetic Account + let account = solana_sdk::account::Account { + lamports: 0, // Compressed accounts don't have lamports + data, + owner: LIGHT_TOKEN_PROGRAM_ID.into(), + executable: false, + rent_epoch: 0, + }; + + return Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: true, + decompression_context: Some(AtaDecompressionContext { + compressed, + wallet_owner: *owner, + mint: *mint, + bump, + }), + }); + } + + // Doesn't exist - return empty synthetic account + let data = vec![0u8; 165]; + let account = solana_sdk::account::Account { + lamports: 0, + data, + owner: LIGHT_TOKEN_PROGRAM_ID.into(), + executable: false, + rent_epoch: 0, + }; + + Ok(AtaAccountInterface { + pubkey: ata, + account, + is_cold: false, + decompression_context: None, + }) + } + + /// Legacy: Fetches AtaInterface (unified ATA representation). + /// Prefer `get_ata_account_interface()` + `parse_token_account_interface()` for new code. + pub async fn get_ata_interface( + &self, + mint: &solana_sdk::pubkey::Pubkey, + owner: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use light_client::indexer::{GetCompressedTokenAccountsByOwnerOrDelegateOptions, Indexer}; + use light_compressible_client::{AtaInterface, DecompressionContext, TokenData}; + use light_token_sdk::{compat::AccountState, token::derive_token_ata}; + + let (ata, bump) = derive_token_ata(owner, mint); + + // Check on-chain first + if let Some(account) = self.context.get_account(&ata) { + use solana_sdk::program_pack::Pack; + let token_data = if account.data.len() >= 165 { + let spl_account = spl_token_2022::state::Account::unpack(&account.data[..165]) + .unwrap_or_default(); + TokenData { + mint: spl_account.mint, + owner: spl_account.owner, + amount: spl_account.amount, + delegate: spl_account.delegate.into(), + state: match spl_account.state { + spl_token_2022::state::AccountState::Frozen => AccountState::Frozen, + _ => AccountState::Initialized, + }, + tlv: None, + } + } else { + TokenData { + mint: *mint, + owner: ata, + ..Default::default() + } + }; + + return Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: false, + token_data, + raw_account: Some(account), + decompression: None, + }); + } + + // Check compressed state + let options = Some(GetCompressedTokenAccountsByOwnerOrDelegateOptions::new( + Some(*mint), + )); + let result = self + .get_compressed_token_accounts_by_owner(&ata, options, None) + .await?; + + if let Some(compressed) = result.value.items.into_iter().next() { + let token_data = compressed.token.clone(); + + return Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: true, + token_data, + raw_account: None, + decompression: Some(DecompressionContext { compressed }), + }); + } + + // Doesn't exist + Ok(AtaInterface { + ata, + owner: *owner, + mint: *mint, + bump, + is_cold: false, + token_data: TokenData { + mint: *mint, + owner: ata, + ..Default::default() + }, + raw_account: None, + decompression: None, + }) + } + + /// Fetches MintInterface for a mint signer pubkey. + /// + /// Checks on-chain first, then compressed state. + /// Returns `MintInterface` with state: + /// - `Hot` if CMint exists on-chain + /// - `Cold` if CMint is compressed (needs decompression) + /// - `None` if CMint doesn't exist + /// + /// # Example + /// ```ignore + /// let mint = rpc.get_mint_interface(&signer).await?; + /// if mint.is_cold() { + /// // Need to decompress + /// } + /// ``` + pub async fn get_mint_interface( + &self, + signer: &solana_sdk::pubkey::Pubkey, + ) -> Result { + use borsh::BorshDeserialize; + use light_client::indexer::Indexer; + use light_compressible_client::{MintInterface, MintState}; + use light_token_interface::{state::CompressedMint, CMINT_ADDRESS_TREE}; + use light_token_sdk::{ + compressed_token::create_compressed_mint::derive_mint_compressed_address, + token::find_mint_address, + }; + + let (cmint, _) = find_mint_address(signer); + let address_tree = solana_sdk::pubkey::Pubkey::new_from_array(CMINT_ADDRESS_TREE); + let compressed_address = derive_mint_compressed_address(signer, &address_tree); + + // Check on-chain first + if let Some(account) = self.context.get_account(&cmint) { + return Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::Hot { account }, + }); + } + + // Check compressed state + let result = self + .get_compressed_account(compressed_address, None) + .await?; + + if let Some(compressed) = result.value { + // Parse mint data if available + if let Some(data) = compressed.data.as_ref() { + if !data.data.is_empty() { + if let Ok(mint_data) = CompressedMint::try_from_slice(&data.data) { + return Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::Cold { + compressed, + mint_data, + }, + }); + } + } + } + // Empty data = already decompressed (return None, not Cold) + } + + // Doesn't exist + Ok(MintInterface { + cmint, + signer: *signer, + address_tree, + compressed_address, + state: MintState::None, + }) + } } impl MerkleTreeExt for LightProgramTest {} diff --git a/sdk-libs/sdk/src/compressible/compress_account.rs b/sdk-libs/sdk/src/compressible/compress_account.rs index 77e7c46a6a..20ded890cc 100644 --- a/sdk-libs/sdk/src/compressible/compress_account.rs +++ b/sdk-libs/sdk/src/compressible/compress_account.rs @@ -53,6 +53,7 @@ where { use light_compressed_account::address::derive_address; + // v2 address derive using PDA as seed let derived_c_pda = derive_address( &account_info.key.to_bytes(), &address_space[0].to_bytes(), diff --git a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs index e5af904bc9..24c46eb102 100644 --- a/sdk-libs/sdk/src/compressible/compress_account_on_init.rs +++ b/sdk-libs/sdk/src/compressible/compress_account_on_init.rs @@ -52,6 +52,13 @@ where + Clone + HasCompressionInfo, { + // TODO: consider not supporting yet. + // Fail-fast: with_data=true is not yet supported in macro-generated code + // if with_data { + // msg!("with_data=true is not supported yet"); + // return Err(LightSdkError::ConstraintViolation.into()); + // } + let tree = cpi_accounts .get_tree_account_info(new_address_param.address_merkle_tree_account_index as usize) .map_err(|_| { diff --git a/sdk-libs/sdk/src/compressible/decompress_runtime.rs b/sdk-libs/sdk/src/compressible/decompress_runtime.rs index 10ece4b6ae..d5e36d3387 100644 --- a/sdk-libs/sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/decompress_runtime.rs @@ -1,9 +1,7 @@ //! Traits and processor for decompress_accounts_idempotent instruction. use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; -#[cfg(feature = "cpi-context")] -use light_sdk_types::cpi_context_write::CpiContextWriteAccounts; use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, + cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, CpiSigner, }; use solana_account_info::AccountInfo; @@ -19,7 +17,7 @@ use crate::{ AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; -/// Trait for account variants that can be checked for token vs PDA type. +/// Trait for account variants that can be checked for token or PDA type. pub trait HasTokenVariant { /// Returns true if this variant represents a token account (PackedTokenData). fn is_packed_token(&self) -> bool; @@ -27,23 +25,16 @@ pub trait HasTokenVariant { /// Trait for token seed providers. /// -/// Also defined in compressed-token-sdk for token-specific runtime helpers. +/// After Phase 8 refactor: The variant itself contains resolved seed pubkeys, +/// so no accounts struct is needed for seed derivation. pub trait TokenSeedProvider: Copy { - /// Type of accounts struct needed for seed derivation. - type Accounts<'info>; - /// Get seeds for the token account PDA (used for decompression). - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; + fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError>; /// Get authority seeds for signing during compression. - fn get_authority_seeds<'a, 'info>( + fn get_authority_seeds( &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], + program_id: &Pubkey, ) -> Result<(Vec>, Pubkey), ProgramError>; } @@ -71,8 +62,6 @@ pub trait DecompressContext<'info> { fn token_config(&self) -> Option<&AccountInfo<'info>>; /// Collect and unpack compressed accounts into PDAs and tokens. - /// - /// Caller program-specific: handles variant matching and PDA seed derivation. #[allow(clippy::type_complexity)] #[allow(clippy::too_many_arguments)] fn collect_pda_and_token<'b>( @@ -88,8 +77,6 @@ pub trait DecompressContext<'info> { ), ProgramError>; /// Process token decompression. - /// - /// Caller program-specific: handles token account creation and seed derivation. #[allow(clippy::too_many_arguments)] fn process_tokens<'b>( &self, @@ -104,20 +91,11 @@ pub trait DecompressContext<'info> { proof: crate::instruction::ValidityProof, cpi_accounts: &CpiAccounts<'b, 'info>, post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, + has_prior_context: bool, ) -> Result<(), ProgramError>; } /// Trait for PDA types that can derive seeds with full account context access. -/// -/// - A: The accounts struct type (typically DecompressAccountsIdempotent<'info>) -/// - S: The SeedParams struct containing data.* field values from instruction data -/// -/// This allows PDA seeds to reference: -/// - `data.*` fields from instruction parameters (seed_params.field) -/// - `ctx.*` accounts from the instruction context (accounts.field) -/// -/// For off-chain PDA derivation, use the generated client helper functions (get_*_seeds). pub trait PdaSeedDerivation { fn derive_pda_seeds_with_accounts( &self, @@ -127,7 +105,8 @@ pub trait PdaSeedDerivation { ) -> Result<(Vec>, Pubkey), ProgramError>; } -/// Check compressed accounts to determine if we have tokens and/or PDAs. +/// Check what types of accounts are in the batch. +/// Returns (has_tokens, has_pdas). #[inline(never)] pub fn check_account_types(compressed_accounts: &[T]) -> (bool, bool) { let (mut has_tokens, mut has_pdas) = (false, false); @@ -176,9 +155,6 @@ where { let data: T = P::unpack(packed, post_system_accounts)?; - // CHECK: pda match - // Call the method with account context and seed params - // Note: Some implementations may use S::default() when seed_params is None for static seeds let (seeds_vec, derived_pda) = if let Some(params) = seed_params { data.derive_pda_seeds_with_accounts(program_id, seed_accounts, params)? } else { @@ -198,7 +174,6 @@ where )); } - // prepare decompression let compressed_infos = { let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v| v.as_slice()).collect(); crate::compressible::decompress_idempotent::prepare_account_for_decompression_idempotent::( @@ -221,6 +196,10 @@ where } /// Processor for decompress_accounts_idempotent. +/// +/// CPI context batching rules: +/// - Can use inputs from N trees +/// - All inputs must use the FIRST CPI context account of the FIRST input #[inline(never)] #[allow(clippy::too_many_arguments)] pub fn process_decompress_accounts_idempotent<'info, Ctx>( @@ -250,7 +229,10 @@ where return Err(ProgramError::NotEnoughAccountKeys); } - let cpi_accounts = if has_tokens { + // Use CPI context batching when we have both PDAs and tokens + // CPI context can handle inputs from N trees - all use FIRST cpi context of FIRST input + let needs_cpi_context = has_tokens && has_pdas; + let cpi_accounts = if needs_cpi_context { CpiAccounts::new_with_config( ctx.fee_payer(), &remaining_accounts[system_accounts_offset_usize..], @@ -271,13 +253,7 @@ where let solana_accounts = remaining_accounts .get(pda_accounts_start..) .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = all_infos - .get(post_system_offset..) - .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; - // Call trait method for program-specific collection let (compressed_pda_infos, compressed_token_accounts) = ctx.collect_pda_and_token( &cpi_accounts, address_space, @@ -288,49 +264,51 @@ where let has_pdas = !compressed_pda_infos.is_empty(); let has_tokens = !compressed_token_accounts.is_empty(); + if !has_pdas && !has_tokens { return Ok(()); } let fee_payer = ctx.fee_payer(); - // Decompress PDAs via LightSystemProgram - #[cfg(feature = "cpi-context")] - if has_pdas && has_tokens { - let authority = cpi_accounts - .authority() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let system_cpi_accounts = CpiContextWriteAccounts { - fee_payer, - authority, - cpi_context, - cpi_signer, - }; - - LightSystemProgramCpi::new_cpi(cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(system_cpi_accounts)?; - } else if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; - } - - // TODO: fix this - #[cfg(not(feature = "cpi-context"))] + // Process PDAs (if any) if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; + if !has_tokens { + // PDAs only - execute directly + LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .invoke(cpi_accounts.clone())?; + } else { + // PDAs + tokens - write to CPI context first, tokens will execute + let authority = cpi_accounts + .authority() + .map_err(|_| ProgramError::MissingRequiredSignature)?; + let cpi_context = cpi_accounts + .cpi_context() + .map_err(|_| ProgramError::MissingRequiredSignature)?; + let system_cpi_accounts = CpiContextWriteAccounts { + fee_payer, + authority, + cpi_context, + cpi_signer, + }; + + LightSystemProgramCpi::new_cpi(cpi_signer, proof) + .with_account_infos(&compressed_pda_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(system_cpi_accounts)?; + } } - // Decompress tokens via trait method + // Process tokens (if any) - executes and consumes CPI context if PDAs wrote to it if has_tokens { - let token_program = ctx + let post_system_offset = cpi_accounts.system_accounts_end_offset(); + let all_infos = cpi_accounts.account_infos(); + let post_system_accounts = all_infos + .get(post_system_offset..) + .ok_or_else(|| ProgramError::from(crate::error::LightSdkError::ConstraintViolation))?; + + let light_token_program = ctx .token_program() .ok_or(ProgramError::NotEnoughAccountKeys)?; let token_rent_sponsor = ctx @@ -346,7 +324,7 @@ where ctx.process_tokens( remaining_accounts, fee_payer, - token_program, + light_token_program, token_rent_sponsor, token_cpi_authority, token_config, @@ -355,7 +333,7 @@ where proof, &cpi_accounts, post_system_accounts, - has_pdas, + has_pdas, // has_prior_context: PDAs wrote to CPI context )?; } diff --git a/sdk-libs/sdk/src/compressible/finalize.rs b/sdk-libs/sdk/src/compressible/finalize.rs new file mode 100644 index 0000000000..5e24ba2daf --- /dev/null +++ b/sdk-libs/sdk/src/compressible/finalize.rs @@ -0,0 +1,71 @@ +//! LightFinalize and LightPreInit traits for compression operations. +//! +//! These traits are implemented by the `#[derive(LightFinalize)]` macro from light-sdk-macros. +//! They provide hooks for running compression operations at different points in an instruction: +//! +//! - `LightPreInit`: Called at START of instruction - creates mints via CPI context write +//! - `LightFinalize`: Called at END of instruction - compresses PDAs and executes with proof +//! +//! This two-phase design allows mints to be created BEFORE the instruction body runs, +//! so they can be used during the instruction (e.g., for vault creation, minting tokens). + +use solana_account_info::AccountInfo; + +/// Trait for pre-initialization operations (mint creation). +/// +/// This is generated by `#[derive(LightFinalize)]` when `#[light_mint]` fields exist. +/// Called at the START of an instruction to write mint creation to CPI context. +/// +/// The mints are written to CPI context but NOT executed yet - execution happens +/// in `light_finalize()` at the end, allowing the shared proof to cover both +/// mints and PDAs. +/// +/// # Type Parameters +/// * `'info` - The account info lifetime +/// * `P` - The instruction params type (from `#[instruction(params: P)]`) +pub trait LightPreInit<'info, P> { + /// Execute pre-initialization operations (mint creation). + /// + /// This writes mint creation operations to CPI context. The actual execution + /// with proof happens in `light_finalize()`. + /// + /// # Arguments + /// * `remaining_accounts` - The remaining accounts from the context, used for CPI + /// * `params` - The instruction parameters containing compression data + /// + /// # Returns + /// `true` if mints were written to CPI context and `light_finalize` should execute + /// with CPI context. `false` if no mints exist and normal flow should proceed. + fn light_pre_init( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &P, + ) -> Result; +} + +/// Trait for finalizing compression operations on accounts. +/// +/// # Type Parameters +/// * `'info` - The account info lifetime +/// * `P` - The instruction params type (from `#[instruction(params: P)]`) +/// +pub trait LightFinalize<'info, P> { + /// Execute compression finalization. + /// + /// This method is called at the end of an instruction to batch and execute + /// all compression CPIs for accounts marked with `#[compressible(...)]`. + /// + /// # Arguments + /// * `remaining_accounts` - The remaining accounts from the context, used for CPI + /// * `params` - The instruction parameters containing compression data + /// * `has_pre_init` - Whether `light_pre_init` was called and wrote to CPI context + /// + /// # Errors + /// Returns an error if the compression CPI fails. + fn light_finalize( + &mut self, + remaining_accounts: &[AccountInfo<'info>], + params: &P, + has_pre_init: bool, + ) -> Result<(), crate::error::LightSdkError>; +} diff --git a/sdk-libs/sdk/src/compressible/mod.rs b/sdk-libs/sdk/src/compressible/mod.rs index 666751140d..a9e0ab9392 100644 --- a/sdk-libs/sdk/src/compressible/mod.rs +++ b/sdk-libs/sdk/src/compressible/mod.rs @@ -1,6 +1,11 @@ pub mod close; pub mod compression_info; pub mod config; +pub mod finalize; +pub mod traits; + +pub use finalize::{LightFinalize, LightPreInit}; +pub use traits::{IntoCTokenVariant, IntoVariant}; #[cfg(feature = "v2")] pub mod compress_account; @@ -10,7 +15,7 @@ pub mod compress_account_on_init; pub mod compress_runtime; #[cfg(feature = "v2")] pub mod decompress_idempotent; -#[cfg(feature = "v2")] +#[cfg(all(feature = "v2", feature = "cpi-context"))] pub mod decompress_runtime; #[cfg(feature = "v2")] pub use close::close; @@ -33,7 +38,7 @@ pub use config::{ pub use decompress_idempotent::{ into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, }; -#[cfg(feature = "v2")] +#[cfg(all(feature = "v2", feature = "cpi-context"))] pub use decompress_runtime::{ check_account_types, handle_packed_pda_variant, process_decompress_accounts_idempotent, DecompressContext, HasTokenVariant, PdaSeedDerivation, TokenSeedProvider, diff --git a/sdk-libs/sdk/src/compressible/traits.rs b/sdk-libs/sdk/src/compressible/traits.rs new file mode 100644 index 0000000000..093658b6a4 --- /dev/null +++ b/sdk-libs/sdk/src/compressible/traits.rs @@ -0,0 +1,64 @@ +//! Traits for decompression variant construction. +//! +//! These traits enable ergonomic client-side construction of `RentFreeDecompressAccount` +//! from seeds and compressed account data. + +#[cfg(feature = "anchor")] +use anchor_lang::error::Error; +#[cfg(not(feature = "anchor"))] +use solana_program_error::ProgramError as Error; + +/// Trait for seeds that can construct a compressed account variant. +/// +/// Implemented by generated `XxxSeeds` structs (e.g., `UserRecordSeeds`). +/// The macro generates impls that deserialize account data and verify seeds match. +/// +/// # Example (generated code) +/// ```ignore +/// impl IntoVariant for UserRecordSeeds { +/// fn into_variant(self, data: &[u8]) -> Result { +/// RentFreeAccountVariant::user_record(data, self) +/// } +/// } +/// ``` +pub trait IntoVariant { + /// Construct variant from compressed account data bytes and these seeds. + /// + /// # Arguments + /// * `data` - Raw compressed account data bytes + /// + /// # Returns + /// The constructed variant on success, or an error if: + /// - Deserialization fails + /// - Seed verification fails (data.* seeds don't match account data) + fn into_variant(self, data: &[u8]) -> Result; +} + +/// Trait for CToken account variant types that can construct a full variant with token data. +/// +/// Implemented by generated `TokenAccountVariant` enum. +/// The macro generates the impl that wraps variant + token_data into `RentFreeAccountVariant`. +/// +/// # Example (generated code) +/// ```ignore +/// impl IntoCTokenVariant for TokenAccountVariant { +/// fn into_ctoken_variant(self, token_data: TokenData) -> RentFreeAccountVariant { +/// RentFreeAccountVariant::CTokenData(CTokenData { +/// variant: self, +/// token_data, +/// }) +/// } +/// } +/// ``` +/// +/// Type parameter `T` is typically `light_token_sdk::compat::TokenData`. +pub trait IntoCTokenVariant { + /// Construct variant from CToken variant and token data. + /// + /// # Arguments + /// * `token_data` - The parsed `TokenData` from compressed account bytes + /// + /// # Returns + /// The constructed variant containing both CToken variant and token data + fn into_ctoken_variant(self, token_data: T) -> V; +} diff --git a/sdk-libs/sdk/src/lib.rs b/sdk-libs/sdk/src/lib.rs index c673edbdec..cc6369988b 100644 --- a/sdk-libs/sdk/src/lib.rs +++ b/sdk-libs/sdk/src/lib.rs @@ -174,8 +174,8 @@ pub use light_hasher; use light_hasher::DataHasher; pub use light_macros::{derive_light_cpi_signer, derive_light_cpi_signer_pda}; pub use light_sdk_macros::{ - derive_light_rent_sponsor, derive_light_rent_sponsor_pda, light_system_accounts, - LightDiscriminator, LightHasher, LightHasherSha, LightTraits, + derive_light_rent_sponsor, derive_light_rent_sponsor_pda, LightDiscriminator, LightHasher, + LightHasherSha, }; pub use light_sdk_types::{constants, CpiSigner}; use solana_account_info::AccountInfo; diff --git a/sdk-libs/token-sdk/Cargo.toml b/sdk-libs/token-sdk/Cargo.toml index 5abd8a7cbd..11b1f61435 100644 --- a/sdk-libs/token-sdk/Cargo.toml +++ b/sdk-libs/token-sdk/Cargo.toml @@ -9,7 +9,7 @@ repository = "https://github.com/Lightprotocol/light-protocol" [features] default = [] v1 = [] -compressible = [] +compressible = ["cpi-context"] anchor = ["anchor-lang", "light-token-types/anchor", "light-token-interface/anchor"] cpi-context = ["light-sdk/cpi-context"] diff --git a/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs b/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs index 2e217b8f97..81829891c6 100644 --- a/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs +++ b/sdk-libs/token-sdk/src/compressed_token/v2/decompress_full.rs @@ -33,6 +33,9 @@ pub struct DecompressFullIndices { /// TLV extensions for this compressed account (e.g., CompressedOnly extension). /// Used to transfer extension state during decompress. pub tlv: Option>, + /// Whether this is an ATA decompression. For ATAs, the source.owner is the ATA address + /// (not the wallet), so it should NOT be marked as a signer - the wallet signs the tx instead. + pub is_ata: bool, } /// Decompress full balance from compressed token accounts with pre-computed indices @@ -90,7 +93,11 @@ pub fn decompress_full_token_accounts_with_indices<'info>( if owner_idx >= signer_flags.len() { return Err(TokenSdkError::InvalidAccountData); } - signer_flags[owner_idx] = true; + // For ATAs, the owner is the ATA address (a PDA that can't sign). + // The wallet signs the transaction instead, so don't mark the owner as signer. + if !idx.is_ata { + signer_flags[owner_idx] = true; + } } let mut packed_account_metas = Vec::with_capacity(packed_accounts.len()); @@ -187,6 +194,7 @@ pub fn pack_for_decompress_full( source, destination_index: packed_accounts.insert_or_get(destination), tlv, + is_ata: false, // Non-ATA: owner is a signer } } @@ -235,6 +243,7 @@ pub fn pack_for_decompress_full_with_ata( source, destination_index: packed_accounts.insert_or_get(destination), tlv, + is_ata, } } diff --git a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs index e87be187c2..8441d81f17 100644 --- a/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/token-sdk/src/compressible/decompress_runtime.rs @@ -1,4 +1,6 @@ //! Runtime helpers for token decompression. +// Re-export TokenSeedProvider from sdk (canonical definition). +pub use light_sdk::compressible::TokenSeedProvider; use light_sdk::{cpi::v2::CpiAccounts, instruction::ValidityProof}; use light_sdk_types::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; use light_token_interface::instructions::{ @@ -9,36 +11,24 @@ use solana_msg::msg; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; -use crate::compat::PackedCTokenData; - -/// Trait for getting token account seeds. -pub trait TokenSeedProvider: Copy { - /// Type of accounts struct needed for seed derivation. - type Accounts<'info>; - - /// Get seeds for the token account PDA (used for decompression). - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; - - /// Get authority seeds for signing during compression. - /// - /// TODO: consider removing. - fn get_authority_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - remaining_accounts: &'a [AccountInfo<'info>], - ) -> Result<(Vec>, Pubkey), ProgramError>; -} +use crate::{compat::PackedCTokenData, pack::Unpack}; /// Token decompression processor. +/// +/// Handles both program-owned tokens and ATAs in unified flow. +/// - Program-owned tokens: program signs via CPI with seeds +/// - ATAs: wallet owner signs on transaction (no program signing needed) +/// +/// CPI context usage: +/// - has_prior_context=true: PDAs/Mints already wrote to CPI context, tokens CONSUME it +/// - has_prior_context=false: tokens-only flow, no CPI context needed +/// +/// After Phase 8 refactor: V is `PackedTokenAccountVariant` which unpacks to +/// `TokenAccountVariant` containing resolved seed Pubkeys. No accounts struct needed. #[inline(never)] #[allow(clippy::too_many_arguments)] -pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( - accounts_for_seeds: &A, - remaining_accounts: &[AccountInfo<'info>], +pub fn process_decompress_tokens_runtime<'info, 'b, V>( + _remaining_accounts: &[AccountInfo<'info>], fee_payer: &AccountInfo<'info>, token_program: &AccountInfo<'info>, token_rent_sponsor: &AccountInfo<'info>, @@ -52,30 +42,36 @@ pub fn process_decompress_tokens_runtime<'info, 'a, 'b, V, A>( proof: ValidityProof, cpi_accounts: &CpiAccounts<'b, 'info>, post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, + has_prior_context: bool, program_id: &Pubkey, ) -> Result<(), ProgramError> where - V: TokenSeedProvider = A>, - A: 'info, + V: Unpack + Copy, + V::Unpacked: TokenSeedProvider, { + if token_accounts.is_empty() { + return Ok(()); + } + let mut token_decompress_indices: Vec< crate::compressed_token::decompress_full::DecompressFullIndices, > = Vec::with_capacity(token_accounts.len()); + // Only program-owned tokens need signer seeds let mut token_signers_seed_groups: Vec>> = Vec::with_capacity(token_accounts.len()); let packed_accounts = post_system_accounts; - let authority = cpi_accounts - .authority() - .map_err(|_| ProgramError::MissingRequiredSignature)?; - let cpi_context_pubkey = if has_pdas { - Some( - *cpi_accounts - .cpi_context() - .map_err(|_| ProgramError::MissingRequiredSignature)? - .key, - ) + // CPI context usage for token decompression: + // - If has_prior_context: PDAs/Mints already wrote to CPI context, tokens CONSUME it + // - If !has_prior_context: tokens-only flow, execute directly without CPI context + // + // Note: CPI context supports cross-tree batching. Writes from different trees + // are stored without validation. The only constraint is the executor's first + // input/output must match the CPI context account's associated_merkle_tree. + let cpi_context_pubkey = if has_prior_context { + // PDAs/Mints wrote to context, tokens consume it + cpi_accounts.cpi_context().ok().map(|ctx| *ctx.key) } else { + // Tokens-only: execute directly without CPI context None }; @@ -105,10 +101,12 @@ where } let owner_info = &packed_accounts[owner_index_usize]; - // Use trait method to get seeds (program-specific) - let (token_signer_seeds, derived_token_account_address) = token_data - .variant - .get_seeds(accounts_for_seeds, remaining_accounts)?; + // Unpack the variant to get resolved seed Pubkeys + let unpacked_variant = token_data.variant.unpack(post_system_accounts)?; + + // Program-owned token: use program-derived seeds + let (ctoken_signer_seeds, derived_token_account_address) = + unpacked_variant.get_seeds(program_id)?; if derived_token_account_address != *owner_info.key { msg!( @@ -119,17 +117,22 @@ where return Err(ProgramError::InvalidAccountData); } - let seed_refs: Vec<&[u8]> = token_signer_seeds.iter().map(|s| s.as_slice()).collect(); + // Derive the authority PDA that will own this CToken account (like cp-swap's vault_authority) + let (_authority_seeds, derived_authority_pda) = + unpacked_variant.get_authority_seeds(program_id)?; + + let seed_refs: Vec<&[u8]> = ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); let seeds_slice: &[&[u8]] = &seed_refs; - // Build CompressToPubkey from the signer seeds if bump is present - let compress_to_pubkey = token_signer_seeds + // Build CompressToPubkey from the token account seeds + // This ensures compressed TokenData.owner = token account address (not authority) + let compress_to_pubkey = ctoken_signer_seeds .last() .and_then(|b| b.first().copied()) .map(|bump| { - let seeds_without_bump: Vec> = token_signer_seeds + let seeds_without_bump: Vec> = ctoken_signer_seeds .iter() - .take(token_signer_seeds.len().saturating_sub(1)) + .take(ctoken_signer_seeds.len().saturating_sub(1)) .cloned() .collect(); CompressToPubkey { @@ -143,8 +146,10 @@ where payer: fee_payer.clone(), account: (*owner_info).clone(), mint: (*mint_info).clone(), - owner: *authority.key, - compressible: crate::token::CompressibleParamsCpi { + owner: derived_authority_pda, // Use derived authority PDA (like cp-swap's vault_authority) + } + .invoke_signed_with( + crate::token::CompressibleParamsCpi { compressible_config: token_config.clone(), rent_sponsor: token_rent_sponsor.clone(), system_program: cpi_accounts @@ -157,8 +162,8 @@ where token_account_version: light_token_interface::state::TokenDataVersion::ShaFlat, compression_only: false, }, - } - .invoke_signed(&[seeds_slice])?; + &[seeds_slice], + )?; let source = MultiInputTokenDataWithContext { owner: token_data.token_data.owner, @@ -174,12 +179,17 @@ where source, destination_index: owner_index, tlv: None, + is_ata: false, // Program-owned token: owner is a signer (via CPI seeds) }; token_decompress_indices.push(decompress_index); - token_signers_seed_groups.push(token_signer_seeds); + token_signers_seed_groups.push(ctoken_signer_seeds); } - let token_ix = + if token_decompress_indices.is_empty() { + return Ok(()); + } + + let ctoken_ix = crate::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( *fee_payer.key, proof, @@ -189,26 +199,78 @@ where ) .map_err(ProgramError::from)?; + // Build account infos for CPI. Must include all accounts needed by the transfer2 instruction: + // - System accounts (light_system_program, registered_program_pda, etc.) + // - Fee payer, ctoken accounts + // - CPI context (if present) + // - All packed accounts (post_system_accounts) let mut all_account_infos: Vec> = - Vec::with_capacity(1 + post_system_accounts.len() + 3); + Vec::with_capacity(12 + post_system_accounts.len()); all_account_infos.push(fee_payer.clone()); all_account_infos.push(token_cpi_authority.clone()); all_account_infos.push(token_program.clone()); all_account_infos.push(token_rent_sponsor.clone()); all_account_infos.push(config.clone()); + + // Add required system accounts for transfer2 instruction + // Light system program is at index 0 in the cpi_accounts slice + all_account_infos.push( + cpi_accounts + .account_infos() + .first() + .ok_or(ProgramError::NotEnoughAccountKeys)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .registered_program_pda() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .account_compression_authority() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .account_compression_program() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + all_account_infos.push( + cpi_accounts + .system_program() + .map_err(|_| ProgramError::InvalidAccountData)? + .clone(), + ); + + // Add CPI context if present + if let Ok(cpi_context) = cpi_accounts.cpi_context() { + all_account_infos.push(cpi_context.clone()); + } + all_account_infos.extend_from_slice(post_system_accounts); - let signer_seed_refs: Vec> = token_signers_seed_groups - .iter() - .map(|group| group.iter().map(|s| s.as_slice()).collect()) - .collect(); - let signer_seed_slices: Vec<&[&[u8]]> = signer_seed_refs.iter().map(|g| g.as_slice()).collect(); - - solana_cpi::invoke_signed( - &token_ix, - all_account_infos.as_slice(), - signer_seed_slices.as_slice(), - )?; + // Only include signer seeds for program-owned tokens + if token_signers_seed_groups.is_empty() { + // All tokens were ATAs - no program signing needed + solana_cpi::invoke(&ctoken_ix, all_account_infos.as_slice())?; + } else { + let signer_seed_refs: Vec> = token_signers_seed_groups + .iter() + .map(|group| group.iter().map(|s| s.as_slice()).collect()) + .collect(); + let signer_seed_slices: Vec<&[&[u8]]> = + signer_seed_refs.iter().map(|g| g.as_slice()).collect(); + + solana_cpi::invoke_signed( + &ctoken_ix, + all_account_infos.as_slice(), + signer_seed_slices.as_slice(), + )?; + } Ok(()) } diff --git a/sdk-libs/token-sdk/src/pack.rs b/sdk-libs/token-sdk/src/pack.rs index b220ef3c4b..43b0810b4d 100644 --- a/sdk-libs/token-sdk/src/pack.rs +++ b/sdk-libs/token-sdk/src/pack.rs @@ -11,7 +11,9 @@ use solana_program_error::ProgramError; use crate::{AnchorDeserialize, AnchorSerialize}; -// We define the traits here to circumvent the orphan rule. +// Note: We define Pack/Unpack traits locally to circumvent the orphan rule. +// This allows implementing them for external types like TokenData from ctoken-interface. +// The sdk has identical trait definitions in light_sdk::compressible. pub trait Pack { type Packed; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; @@ -24,36 +26,6 @@ pub trait Unpack { ) -> std::result::Result; } -impl Pack for TokenData { - type Packed = light_token_interface::instructions::transfer2::MultiTokenTransferOutputData; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - Self::Packed { - owner: remaining_accounts.insert_or_get(self.owner.to_bytes().into()), - mint: remaining_accounts.insert_or_get_read_only(self.mint.to_bytes().into()), - amount: self.amount, - has_delegate: self.delegate.is_some(), - delegate: if let Some(delegate) = self.delegate { - remaining_accounts.insert_or_get(delegate.to_bytes().into()) - } else { - 0 - }, - version: TokenDataVersion::ShaFlat as u8, - } - } -} - -impl Unpack for TokenData { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - /// Solana-compatible token types using `solana_pubkey::Pubkey` pub mod compat { use solana_pubkey::Pubkey; @@ -144,7 +116,7 @@ pub mod compat { } } - impl From for crate::pack::TokenData { + impl From for light_token_interface::state::TokenData { fn from(data: TokenData) -> Self { use light_token_interface::state::CompressedTokenAccountState; @@ -162,8 +134,8 @@ pub mod compat { } } - impl From for TokenData { - fn from(data: crate::pack::TokenData) -> Self { + impl From for TokenData { + fn from(data: light_token_interface::state::TokenData) -> Self { Self { mint: Pubkey::new_from_array(data.mint.to_bytes()), owner: Pubkey::new_from_array(data.owner.to_bytes()), @@ -246,7 +218,7 @@ pub mod compat { } #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] - pub struct PackedCTokenDataWithVariant { + pub struct PackedTokenDataWithVariant { pub variant: V, pub token_data: InputTokenDataCompressible, } @@ -259,13 +231,14 @@ pub mod compat { impl Pack for CTokenDataWithVariant where - V: AnchorSerialize + Clone + std::fmt::Debug, + V: Pack, + V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedCTokenDataWithVariant { - variant: self.variant.clone(), + PackedTokenDataWithVariant { + variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } } @@ -281,6 +254,8 @@ pub mod compat { &self, remaining_accounts: &[AccountInfo], ) -> std::result::Result { + // Note: This impl assumes V is already unpacked (has Pubkeys). + // For packed variants, use PackedTokenDataWithVariant::unpack instead. Ok(TokenDataWithVariant { variant: self.variant.clone(), token_data: self.token_data.unpack(remaining_accounts)?, @@ -290,30 +265,31 @@ pub mod compat { impl Pack for TokenDataWithVariant where - V: AnchorSerialize + Clone + std::fmt::Debug, + V: Pack, + V::Packed: AnchorSerialize + Clone + std::fmt::Debug, { - type Packed = PackedCTokenDataWithVariant; + type Packed = PackedTokenDataWithVariant; fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedCTokenDataWithVariant { - variant: self.variant.clone(), + PackedTokenDataWithVariant { + variant: self.variant.pack(remaining_accounts), token_data: self.token_data.pack(remaining_accounts), } } } - impl Unpack for PackedCTokenDataWithVariant + impl Unpack for PackedTokenDataWithVariant where - V: Clone, + V: Unpack, { - type Unpacked = TokenDataWithVariant; + type Unpacked = TokenDataWithVariant; fn unpack( &self, remaining_accounts: &[AccountInfo], ) -> std::result::Result { Ok(TokenDataWithVariant { - variant: self.variant.clone(), + variant: self.variant.unpack(remaining_accounts)?, token_data: self.token_data.unpack(remaining_accounts)?, }) } @@ -323,7 +299,7 @@ pub mod compat { pub type InputTokenDataCompressible = light_token_interface::instructions::transfer2::MultiTokenTransferOutputData; pub type CompressibleTokenDataWithVariant = CTokenDataWithVariant; - pub type PackedCompressibleTokenDataWithVariant = PackedCTokenDataWithVariant; + pub type PackedCompressibleTokenDataWithVariant = PackedTokenDataWithVariant; pub type CTokenData = CTokenDataWithVariant; - pub type PackedCTokenData = PackedCTokenDataWithVariant; + pub type PackedCTokenData = PackedTokenDataWithVariant; } diff --git a/sdk-libs/token-sdk/src/token/create.rs b/sdk-libs/token-sdk/src/token/create.rs index cd0c070120..b41f716138 100644 --- a/sdk-libs/token-sdk/src/token/create.rs +++ b/sdk-libs/token-sdk/src/token/create.rs @@ -1,7 +1,7 @@ use borsh::BorshSerialize; use light_token_interface::instructions::{ create_token_account::CreateTokenAccountInstructionData, - extensions::CompressibleExtensionInstructionData, + extensions::{CompressToPubkey, CompressibleExtensionInstructionData}, }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -84,56 +84,189 @@ impl CreateTokenAccount { } } -/// # Create a ctoken account via CPI: -/// ```rust,no_run -/// # use light_token_sdk::token::{CreateTokenAccountCpi, CompressibleParamsCpi}; -/// # use solana_account_info::AccountInfo; -/// # use solana_pubkey::Pubkey; -/// # let payer: AccountInfo = todo!(); -/// # let account: AccountInfo = todo!(); -/// # let mint: AccountInfo = todo!(); -/// # let owner: Pubkey = todo!(); -/// # let compressible: CompressibleParamsCpi = todo!(); +/// CPI builder for creating CToken accounts (vaults). +/// +/// # Example - Rent-free vault with PDA signing +/// ```rust,ignore /// CreateTokenAccountCpi { -/// payer, -/// account, -/// mint, -/// owner, -/// compressible, +/// payer: ctx.accounts.payer.to_account_info(), +/// account: ctx.accounts.vault.to_account_info(), +/// mint: ctx.accounts.mint.to_account_info(), +/// owner: ctx.accounts.vault_authority.key(), /// } -/// .invoke()?; -/// # Ok::<(), solana_program_error::ProgramError>(()) +/// .rent_free( +/// ctx.accounts.ctoken_config.to_account_info(), +/// ctx.accounts.rent_sponsor.to_account_info(), +/// ctx.accounts.system_program.to_account_info(), +/// &crate::ID, +/// ) +/// .invoke_signed(&[b"vault", mint.key().as_ref(), &[bump]])?; /// ``` pub struct CreateTokenAccountCpi<'info> { pub payer: AccountInfo<'info>, pub account: AccountInfo<'info>, pub mint: AccountInfo<'info>, pub owner: Pubkey, - pub compressible: CompressibleParamsCpi<'info>, } impl<'info> CreateTokenAccountCpi<'info> { - pub fn new( - payer: AccountInfo<'info>, - account: AccountInfo<'info>, - mint: AccountInfo<'info>, - owner: Pubkey, + /// Enable rent-free mode with compressible config. + /// + /// Returns a builder that can call `.invoke()` or `.invoke_signed(seeds)`. + /// When using `invoke_signed`, the seeds are used for both PDA signing + /// and deriving the compress_to address. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + program_id: &Pubkey, + ) -> CreateTokenAccountRentFreeCpi<'info> { + CreateTokenAccountRentFreeCpi { + base: self, + config, + sponsor, + system_program, + program_id: *program_id, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, compressible: CompressibleParamsCpi<'info>, - ) -> Self { - Self { - payer, - account, - mint, - owner, + ) -> Result<(), ProgramError> { + LegacyCreateTokenAccountCpi { + payer: self.payer, + account: self.account, + mint: self.mint, + owner: self.owner, compressible, } + .invoke() } - pub fn instruction(&self) -> Result { - CreateTokenAccount::from(self).instruction() + /// Invoke with signing, without rent-free (requires manually constructed compressible params). + pub fn invoke_signed_with( + self, + compressible: CompressibleParamsCpi<'info>, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), ProgramError> { + LegacyCreateTokenAccountCpi { + payer: self.payer, + account: self.account, + mint: self.mint, + owner: self.owner, + compressible, + } + .invoke_signed(signer_seeds) } +} + +/// Rent-free enabled CToken account creation CPI. +pub struct CreateTokenAccountRentFreeCpi<'info> { + base: CreateTokenAccountCpi<'info>, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + program_id: Pubkey, +} +impl<'info> CreateTokenAccountRentFreeCpi<'info> { + /// Invoke CPI for non-program-owned accounts. pub fn invoke(self) -> Result<(), ProgramError> { + let defaults = CompressibleParams::default(); + + let cpi = LegacyCreateTokenAccountCpi { + payer: self.base.payer, + account: self.base.account, + mint: self.base.mint, + owner: self.base.owner, + compressible: CompressibleParamsCpi { + compressible_config: self.config, + rent_sponsor: self.sponsor, + system_program: self.system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: None, + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + }, + }; + cpi.invoke() + } + + /// Invoke CPI with PDA signing for program-owned accounts. + /// + /// Seeds are used for both signing AND deriving the compress_to address. + pub fn invoke_signed(self, seeds: &[&[u8]]) -> Result<(), ProgramError> { + let defaults = CompressibleParams::default(); + + // Build CompressToPubkey from signer seeds + let bump = seeds.last().and_then(|s| s.first()).copied().unwrap_or(0); + + let seed_vecs: Vec> = seeds + .iter() + .take(seeds.len().saturating_sub(1)) + .map(|s| s.to_vec()) + .collect(); + + let compress_to = CompressToPubkey { + bump, + program_id: self.program_id.to_bytes(), + seeds: seed_vecs, + }; + + let cpi = LegacyCreateTokenAccountCpi { + payer: self.base.payer, + account: self.base.account, + mint: self.base.mint, + owner: self.base.owner, + compressible: CompressibleParamsCpi { + compressible_config: self.config, + rent_sponsor: self.sponsor, + system_program: self.system_program, + pre_pay_num_epochs: defaults.pre_pay_num_epochs, + lamports_per_write: defaults.lamports_per_write, + compress_to_account_pubkey: Some(compress_to), + token_account_version: defaults.token_account_version, + compression_only: defaults.compression_only, + }, + }; + cpi.invoke_signed(&[seeds]) + } +} + +/// Internal legacy CPI struct with full compressible params. +struct LegacyCreateTokenAccountCpi<'info> { + payer: AccountInfo<'info>, + account: AccountInfo<'info>, + mint: AccountInfo<'info>, + owner: Pubkey, + compressible: CompressibleParamsCpi<'info>, +} + +impl<'info> LegacyCreateTokenAccountCpi<'info> { + fn instruction(&self) -> Result { + CreateTokenAccount { + payer: *self.payer.key, + account: *self.account.key, + mint: *self.mint.key, + owner: self.owner, + compressible: CompressibleParams { + compressible_config: *self.compressible.compressible_config.key, + rent_sponsor: *self.compressible.rent_sponsor.key, + pre_pay_num_epochs: self.compressible.pre_pay_num_epochs, + lamports_per_write: self.compressible.lamports_per_write, + compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(), + token_account_version: self.compressible.token_account_version, + compression_only: self.compressible.compression_only, + }, + } + .instruction() + } + + fn invoke(self) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.account, @@ -146,7 +279,7 @@ impl<'info> CreateTokenAccountCpi<'info> { invoke(&instruction, &account_infos) } - pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.account, @@ -159,26 +292,3 @@ impl<'info> CreateTokenAccountCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } - -impl<'info> From<&CreateTokenAccountCpi<'info>> for CreateTokenAccount { - fn from(account_infos: &CreateTokenAccountCpi<'info>) -> Self { - Self { - payer: *account_infos.payer.key, - account: *account_infos.account.key, - mint: *account_infos.mint.key, - owner: account_infos.owner, - compressible: CompressibleParams { - compressible_config: *account_infos.compressible.compressible_config.key, - rent_sponsor: *account_infos.compressible.rent_sponsor.key, - pre_pay_num_epochs: account_infos.compressible.pre_pay_num_epochs, - lamports_per_write: account_infos.compressible.lamports_per_write, - compress_to_account_pubkey: account_infos - .compressible - .compress_to_account_pubkey - .clone(), - token_account_version: account_infos.compressible.token_account_version, - compression_only: account_infos.compressible.compression_only, - }, - } - } -} diff --git a/sdk-libs/token-sdk/src/token/create_ata.rs b/sdk-libs/token-sdk/src/token/create_ata.rs index bf4515937a..b2b983f87f 100644 --- a/sdk-libs/token-sdk/src/token/create_ata.rs +++ b/sdk-libs/token-sdk/src/token/create_ata.rs @@ -132,47 +132,213 @@ impl CreateAssociatedTokenAccount { } } -/// # Create an associated ctoken account via CPI: -/// ```rust,no_run -/// # use light_token_sdk::token::{CreateAssociatedAccountCpi, CompressibleParamsCpi}; -/// # use solana_account_info::AccountInfo; -/// # let owner: AccountInfo = todo!(); -/// # let mint: AccountInfo = todo!(); -/// # let payer: AccountInfo = todo!(); -/// # let associated_token_account: AccountInfo = todo!(); -/// # let system_program: AccountInfo = todo!(); -/// # let bump: u8 = todo!(); -/// # let compressible: CompressibleParamsCpi = todo!(); -/// CreateAssociatedAccountCpi { -/// owner, -/// mint, -/// payer, -/// associated_token_account, -/// system_program, -/// bump, -/// compressible, -/// idempotent: true, +/// CPI builder for creating CToken ATAs. +/// +/// # Example - Rent-free ATA (idempotent) +/// ```rust,ignore +/// CreateCTokenAtaCpi { +/// payer: ctx.accounts.payer.to_account_info(), +/// owner: ctx.accounts.owner.to_account_info(), +/// mint: ctx.accounts.mint.to_account_info(), +/// ata: ctx.accounts.user_ata.to_account_info(), +/// bump: params.user_ata_bump, /// } +/// .idempotent() +/// .rent_free( +/// ctx.accounts.ctoken_config.to_account_info(), +/// ctx.accounts.rent_sponsor.to_account_info(), +/// ctx.accounts.system_program.to_account_info(), +/// ) /// .invoke()?; -/// # Ok::<(), solana_program_error::ProgramError>(()) /// ``` -pub struct CreateAssociatedAccountCpi<'info> { +pub struct CreateCTokenAtaCpi<'info> { + pub payer: AccountInfo<'info>, pub owner: AccountInfo<'info>, pub mint: AccountInfo<'info>, - pub payer: AccountInfo<'info>, - pub associated_token_account: AccountInfo<'info>, - pub system_program: AccountInfo<'info>, + pub ata: AccountInfo<'info>, pub bump: u8, - pub compressible: CompressibleParamsCpi<'info>, - pub idempotent: bool, } -impl<'info> CreateAssociatedAccountCpi<'info> { - pub fn instruction(&self) -> Result { - CreateAssociatedTokenAccount::from(self).instruction() +impl<'info> CreateCTokenAtaCpi<'info> { + /// Make this an idempotent create (won't fail if ATA already exists). + pub fn idempotent(self) -> CreateCTokenAtaCpiIdempotent<'info> { + CreateCTokenAtaCpiIdempotent { base: self } + } + + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + ) -> CreateCTokenAtaRentFreeCpi<'info> { + CreateCTokenAtaRentFreeCpi { + payer: self.payer, + owner: self.owner, + mint: self.mint, + ata: self.ata, + bump: self.bump, + idempotent: false, + config, + sponsor, + system_program, + } } + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + system_program: AccountInfo<'info>, + ) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program, + bump: self.bump, + compressible, + idempotent: false, + } + .invoke() + } +} + +/// Idempotent ATA creation (intermediate type). +pub struct CreateCTokenAtaCpiIdempotent<'info> { + base: CreateCTokenAtaCpi<'info>, +} + +impl<'info> CreateCTokenAtaCpiIdempotent<'info> { + /// Enable rent-free mode with compressible config. + pub fn rent_free( + self, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, + ) -> CreateCTokenAtaRentFreeCpi<'info> { + CreateCTokenAtaRentFreeCpi { + payer: self.base.payer, + owner: self.base.owner, + mint: self.base.mint, + ata: self.base.ata, + bump: self.base.bump, + idempotent: true, + config, + sponsor, + system_program, + } + } + + /// Invoke without rent-free (requires manually constructed compressible params). + pub fn invoke_with( + self, + compressible: CompressibleParamsCpi<'info>, + system_program: AccountInfo<'info>, + ) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.base.owner, + mint: self.base.mint, + payer: self.base.payer, + associated_token_account: self.base.ata, + system_program, + bump: self.base.bump, + compressible, + idempotent: true, + } + .invoke() + } +} + +/// Rent-free enabled CToken ATA creation CPI. +pub struct CreateCTokenAtaRentFreeCpi<'info> { + payer: AccountInfo<'info>, + owner: AccountInfo<'info>, + mint: AccountInfo<'info>, + ata: AccountInfo<'info>, + bump: u8, + idempotent: bool, + config: AccountInfo<'info>, + sponsor: AccountInfo<'info>, + system_program: AccountInfo<'info>, +} + +impl<'info> CreateCTokenAtaRentFreeCpi<'info> { + /// Invoke CPI. pub fn invoke(self) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program: self.system_program.clone(), + bump: self.bump, + compressible: CompressibleParamsCpi::new_ata( + self.config, + self.sponsor, + self.system_program, + ), + idempotent: self.idempotent, + } + .invoke() + } + + /// Invoke CPI with signer seeds (when caller needs to sign for another account). + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + InternalCreateAtaCpi { + owner: self.owner, + mint: self.mint, + payer: self.payer, + associated_token_account: self.ata, + system_program: self.system_program.clone(), + bump: self.bump, + compressible: CompressibleParamsCpi::new_ata( + self.config, + self.sponsor, + self.system_program, + ), + idempotent: self.idempotent, + } + .invoke_signed(signer_seeds) + } +} + +/// Internal CPI struct for ATAs with full params. +struct InternalCreateAtaCpi<'info> { + owner: AccountInfo<'info>, + mint: AccountInfo<'info>, + payer: AccountInfo<'info>, + associated_token_account: AccountInfo<'info>, + system_program: AccountInfo<'info>, + bump: u8, + compressible: CompressibleParamsCpi<'info>, + idempotent: bool, +} + +impl<'info> InternalCreateAtaCpi<'info> { + fn instruction(&self) -> Result { + CreateAssociatedTokenAccount { + payer: *self.payer.key, + owner: *self.owner.key, + mint: *self.mint.key, + associated_token_account: *self.associated_token_account.key, + bump: self.bump, + compressible: CompressibleParams { + compressible_config: *self.compressible.compressible_config.key, + rent_sponsor: *self.compressible.rent_sponsor.key, + pre_pay_num_epochs: self.compressible.pre_pay_num_epochs, + lamports_per_write: self.compressible.lamports_per_write, + compress_to_account_pubkey: self.compressible.compress_to_account_pubkey.clone(), + token_account_version: self.compressible.token_account_version, + compression_only: self.compressible.compression_only, + }, + idempotent: self.idempotent, + } + .instruction() + } + + fn invoke(self) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.owner, @@ -186,7 +352,7 @@ impl<'info> CreateAssociatedAccountCpi<'info> { invoke(&instruction, &account_infos) } - pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { let instruction = self.instruction()?; let account_infos = [ self.owner, @@ -200,28 +366,3 @@ impl<'info> CreateAssociatedAccountCpi<'info> { invoke_signed(&instruction, &account_infos, signer_seeds) } } - -impl<'info> From<&CreateAssociatedAccountCpi<'info>> for CreateAssociatedTokenAccount { - fn from(account_infos: &CreateAssociatedAccountCpi<'info>) -> Self { - Self { - payer: *account_infos.payer.key, - owner: *account_infos.owner.key, - mint: *account_infos.mint.key, - associated_token_account: *account_infos.associated_token_account.key, - bump: account_infos.bump, - compressible: CompressibleParams { - compressible_config: *account_infos.compressible.compressible_config.key, - rent_sponsor: *account_infos.compressible.rent_sponsor.key, - pre_pay_num_epochs: account_infos.compressible.pre_pay_num_epochs, - lamports_per_write: account_infos.compressible.lamports_per_write, - compress_to_account_pubkey: account_infos - .compressible - .compress_to_account_pubkey - .clone(), - token_account_version: account_infos.compressible.token_account_version, - compression_only: account_infos.compressible.compression_only, - }, - idempotent: account_infos.idempotent, - } - } -} diff --git a/sdk-libs/token-sdk/src/token/decompress_mint.rs b/sdk-libs/token-sdk/src/token/decompress_mint.rs index 35eb188483..bb48714d31 100644 --- a/sdk-libs/token-sdk/src/token/decompress_mint.rs +++ b/sdk-libs/token-sdk/src/token/decompress_mint.rs @@ -2,7 +2,8 @@ use light_compressed_account::instruction_data::{ compressed_proof::ValidityProof, traits::LightInstructionData, }; use light_token_interface::instructions::mint_action::{ - CompressedMintWithContext, DecompressMintAction, MintActionCompressedInstructionData, + CompressedMintWithContext, CpiContext, DecompressMintAction, + MintActionCompressedInstructionData, }; use solana_account_info::AccountInfo; use solana_cpi::{invoke, invoke_signed}; @@ -229,3 +230,237 @@ impl<'info> TryFrom<&DecompressMintCpi<'info>> for DecompressMint { }) } } + +/// Decompress a compressed mint with CPI context support. +/// +/// For use in multi-operation ixns where mints are decompressed +/// along with PDAs and token accounts using a single proof. +#[derive(Debug, Clone)] +pub struct DecompressCMintWithCpiContext { + /// Mint seed pubkey (used to derive CMint PDA) + pub mint_seed_pubkey: Pubkey, + /// Fee payer + pub payer: Pubkey, + /// Mint authority (must sign) + pub authority: Pubkey, + /// State tree for the compressed mint + pub state_tree: Pubkey, + /// Input queue for reading compressed mint + pub input_queue: Pubkey, + /// Output queue for updated compressed mint + pub output_queue: Pubkey, + /// Compressed mint with context (from indexer) + pub compressed_mint_with_context: CompressedMintWithContext, + /// Validity proof for the compressed mint + pub proof: ValidityProof, + /// Rent payment in epochs (must be >= 2) + pub rent_payment: u8, + /// Lamports for future write operations + pub write_top_up: u32, + /// CPI context account + pub cpi_context_pubkey: Pubkey, + /// CPI context flags + pub cpi_context: CpiContext, + /// Compressible config account (ctoken's config) + pub compressible_config: Pubkey, + /// Rent sponsor account (ctoken's rent sponsor) + pub rent_sponsor: Pubkey, +} + +impl DecompressCMintWithCpiContext { + pub fn instruction(self) -> Result { + // Derive CMint PDA + let (cmint_pda, _cmint_bump) = crate::token::find_mint_address(&self.mint_seed_pubkey); + + // Build DecompressMintAction + let action = DecompressMintAction { + rent_payment: self.rent_payment, + write_top_up: self.write_top_up, + }; + + // Build instruction data with CPI context + let instruction_data = MintActionCompressedInstructionData::new( + self.compressed_mint_with_context, + self.proof.0, + ) + .with_decompress_mint(action) + .with_cpi_context(self.cpi_context.clone()); + + // Build account metas with compressible CMint and CPI context + // Use provided config/rent_sponsor instead of hardcoded defaults + let mut meta_config = MintActionMetaConfig::new( + self.payer, + self.authority, + self.state_tree, + self.input_queue, + self.output_queue, + ) + .with_compressible_mint(cmint_pda, self.compressible_config, self.rent_sponsor); + + meta_config.cpi_context = Some(self.cpi_context_pubkey); + + let account_metas = meta_config.to_account_metas(); + + let data = instruction_data + .data() + .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; + + Ok(Instruction { + program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data, + }) + } +} + +/// CPI struct for decompressing a mint with CPI context. +pub struct DecompressCMintCpiWithContext<'info> { + /// Mint seed account (used to derive CMint PDA, does not sign) + pub mint_seed: AccountInfo<'info>, + /// Mint authority (must sign) + pub authority: AccountInfo<'info>, + /// Fee payer + pub payer: AccountInfo<'info>, + /// CMint PDA account (writable) + pub cmint: AccountInfo<'info>, + /// CompressibleConfig account + pub compressible_config: AccountInfo<'info>, + /// Rent sponsor PDA account + pub rent_sponsor: AccountInfo<'info>, + /// State tree for the compressed mint + pub state_tree: AccountInfo<'info>, + /// Input queue for reading compressed mint + pub input_queue: AccountInfo<'info>, + /// Output queue for updated compressed mint + pub output_queue: AccountInfo<'info>, + /// CPI context account + pub cpi_context_account: AccountInfo<'info>, + /// System accounts for Light Protocol + pub system_accounts: SystemAccountInfos<'info>, + /// CToken program's CPI authority (GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy) + /// This is separate from system_accounts.cpi_authority_pda which is the calling program's authority + pub ctoken_cpi_authority: AccountInfo<'info>, + /// Compressed mint with context (from indexer) + pub compressed_mint_with_context: CompressedMintWithContext, + /// Validity proof for the compressed mint + pub proof: ValidityProof, + /// Rent payment in epochs (must be >= 2) + pub rent_payment: u8, + /// Lamports for future write operations + pub write_top_up: u32, + /// CPI context flags + pub cpi_context: CpiContext, +} + +impl<'info> DecompressCMintCpiWithContext<'info> { + pub fn instruction(&self) -> Result { + DecompressCMintWithCpiContext { + mint_seed_pubkey: *self.mint_seed.key, + payer: *self.payer.key, + authority: *self.authority.key, + state_tree: *self.state_tree.key, + input_queue: *self.input_queue.key, + output_queue: *self.output_queue.key, + compressed_mint_with_context: self.compressed_mint_with_context.clone(), + proof: self.proof, + rent_payment: self.rent_payment, + write_top_up: self.write_top_up, + cpi_context_pubkey: *self.cpi_context_account.key, + cpi_context: self.cpi_context.clone(), + compressible_config: *self.compressible_config.key, + rent_sponsor: *self.rent_sponsor.key, + } + .instruction() + } + + pub fn invoke(self) -> Result<(), ProgramError> { + let instruction = self.instruction()?; + let account_infos = self.build_account_infos(); + invoke(&instruction, &account_infos) + } + + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + let instruction = self.instruction()?; + let account_infos = self.build_account_infos(); + invoke_signed(&instruction, &account_infos, signer_seeds) + } + + fn build_account_infos(&self) -> Vec> { + vec![ + self.system_accounts.light_system_program.clone(), + self.mint_seed.clone(), + self.authority.clone(), + self.compressible_config.clone(), + self.cmint.clone(), + self.rent_sponsor.clone(), + self.payer.clone(), + // Use ctoken's CPI authority for the CPI, not the calling program's authority + self.ctoken_cpi_authority.clone(), + self.system_accounts.registered_program_pda.clone(), + self.system_accounts.account_compression_authority.clone(), + self.system_accounts.account_compression_program.clone(), + self.system_accounts.system_program.clone(), + self.cpi_context_account.clone(), + self.output_queue.clone(), + self.state_tree.clone(), + self.input_queue.clone(), + ] + } +} + +/// Helper to create CPI context for first write (first_set_context = true) +pub fn create_decompress_mint_cpi_context_first( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: true, + set_context: false, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} + +/// Helper to create CPI context for subsequent writes (set_context = true) +pub fn create_decompress_mint_cpi_context_set( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: false, + set_context: true, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} + +/// Helper to create CPI context for execution (both false - consumes context) +pub fn create_decompress_mint_cpi_context_execute( + address_tree_pubkey: [u8; 32], + tree_index: u8, + queue_index: u8, +) -> CpiContext { + CpiContext { + first_set_context: false, + set_context: false, + in_tree_index: tree_index, + in_queue_index: queue_index, + out_queue_index: queue_index, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey, + } +} diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index 9094faa6b0..e016cad387 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -3,9 +3,9 @@ //! //! ## Account Creation //! -//! - [`CreateAssociatedTokenAccount`] - Create associated ctoken account (ATA) instruction -//! - [`CreateAssociatedTokenAccountCpi`] - Create associated ctoken account (ATA) via CPI -//! - [`CreateTokenAccount`] - Create ctoken account instruction +//! - [`CreateAssociatedCTokenAccount`] - Create associated ctoken account (ATA) instruction +//! - [`CreateCTokenAtaCpi`] - Create associated ctoken account (ATA) via CPI +//! - [`CreateCTokenAccount`] - Create ctoken account instruction //! - [`CreateTokenAccountCpi`] - Create ctoken account via CPI //! //! ## Transfers @@ -49,28 +49,47 @@ //! # Ok::<(), solana_program_error::ProgramError>(()) //! ``` //! -//! # Example: Create cToken Account CPI +//! # Example: Create rent-free ATA via CPI //! //! ```rust,ignore -//! use light_token_sdk::token::{CreateAssociatedTokenAccountCpi, CompressibleParamsCpi}; +//! use light_token_sdk::token::CreateCTokenAtaCpi; //! -//! CreateAssociatedTokenAccountCpi { +//! CreateCTokenAtaCpi { +//! payer: ctx.accounts.payer.to_account_info(), //! owner: ctx.accounts.owner.to_account_info(), //! mint: ctx.accounts.mint.to_account_info(), -//! payer: ctx.accounts.payer.to_account_info(), -//! associated_token_account: ctx.accounts.ctoken_account.to_account_info(), -//! system_program: ctx.accounts.system_program.to_account_info(), +//! ata: ctx.accounts.user_ata.to_account_info(), //! bump, -//! compressible: Some(CompressibleParamsCpi::default_with_accounts( -//! ctx.accounts.compressible_config.to_account_info(), -//! ctx.accounts.rent_sponsor.to_account_info(), -//! ctx.accounts.system_program.to_account_info(), -//! )), -//! idempotent: true, //! } +//! .idempotent() +//! .rent_free( +//! ctx.accounts.ctoken_config.to_account_info(), +//! ctx.accounts.rent_sponsor.to_account_info(), +//! ctx.accounts.system_program.to_account_info(), +//! ) //! .invoke()?; //! ``` //! +//! # Example: Create rent-free vault via CPI (with PDA signing) +//! +//! ```rust,ignore +//! use light_token_sdk::token::CreateTokenAccountCpi; +//! +//! CreateTokenAccountCpi { +//! payer: ctx.accounts.payer.to_account_info(), +//! account: ctx.accounts.vault.to_account_info(), +//! mint: ctx.accounts.mint.to_account_info(), +//! owner: ctx.accounts.vault_authority.key(), +//! } +//! .rent_free( +//! ctx.accounts.ctoken_config.to_account_info(), +//! ctx.accounts.rent_sponsor.to_account_info(), +//! ctx.accounts.system_program.to_account_info(), +//! &crate::ID, +//! ) +//! .invoke_signed(&[b"vault", mint.key().as_ref(), &[bump]])?; +//! ``` +//! mod approve; mod approve_checked; @@ -101,7 +120,10 @@ pub use burn_checked::*; pub use close::{CloseAccount, CloseAccountCpi}; pub use compressible::{CompressibleParams, CompressibleParamsCpi}; pub use create::*; -pub use create_ata::{derive_token_ata, CreateAssociatedAccountCpi, CreateAssociatedTokenAccount}; +pub use create_ata::{ + derive_token_ata, CreateAssociatedTokenAccount, + CreateCTokenAtaCpi as CreateAssociatedAccountCpi, CreateCTokenAtaCpi, +}; pub use create_mint::*; pub use decompress::Decompress; pub use decompress_mint::*; @@ -139,6 +161,7 @@ pub use transfer_to_spl::{TransferToSpl, TransferToSplCpi}; /// - `account_compression_authority` - Compression authority /// - `account_compression_program` - Account Compression Program /// - `system_program` - Solana System Program +#[derive(Clone)] pub struct SystemAccountInfos<'info> { pub light_system_program: AccountInfo<'info>, pub cpi_authority_pda: AccountInfo<'info>, diff --git a/sdk-libs/token-sdk/tests/pack_test.rs b/sdk-libs/token-sdk/tests/pack_test.rs index 2f3039c725..7c52d45c53 100644 --- a/sdk-libs/token-sdk/tests/pack_test.rs +++ b/sdk-libs/token-sdk/tests/pack_test.rs @@ -2,7 +2,7 @@ use light_sdk::instruction::PackedAccounts; use light_token_sdk::{ - compat::{PackedCTokenDataWithVariant, TokenData, TokenDataWithVariant}, + compat::{PackedCompressibleTokenDataWithVariant, TokenData, TokenDataWithVariant}, pack::Pack, }; use solana_pubkey::Pubkey; @@ -52,6 +52,14 @@ fn test_token_data_with_variant_packing() { TypeB = 1, } + impl Pack for MyVariant { + type Packed = Self; + + fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { + *self + } + } + let mut remaining_accounts = PackedAccounts::default(); let token_with_variant = TokenDataWithVariant { @@ -67,7 +75,7 @@ fn test_token_data_with_variant_packing() { }; // Pack the wrapper - let packed: PackedCTokenDataWithVariant = + let packed: PackedCompressibleTokenDataWithVariant = token_with_variant.pack(&mut remaining_accounts); // Verify variant is unchanged diff --git a/sdk-tests/csdk-anchor-derived-test/Anchor.toml b/sdk-tests/csdk-anchor-derived-test/Anchor.toml deleted file mode 100644 index 3237e0c97f..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Anchor.toml +++ /dev/null @@ -1,18 +0,0 @@ -[features] -resolution = true -skip-lint = false - -[programs.localnet] -csdk_anchor_derived_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "~/.config/solana/id.json" - -[scripts] -test = "cargo test-sbf -p csdk-anchor-derived-test -- --nocapture" - - diff --git a/sdk-tests/csdk-anchor-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-derived-test/Cargo.toml deleted file mode 100644 index 8e4fcdfd87..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Cargo.toml +++ /dev/null @@ -1,59 +0,0 @@ -[package] -name = "csdk-anchor-derived-test" -version = "0.1.0" -description = "Anchor program test using add_compressible_instructions-derived instructions" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "csdk_anchor_derived_test" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] -test-sbf = [] - -[dependencies] -light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } -light-hasher = { workspace = true, features = ["solana"] } -solana-program = { workspace = true } -solana-pubkey = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } -light-sdk-macros = { workspace = true } -borsh = { workspace = true } -light-compressed-account = { workspace = true, features = ["solana"] } -anchor-lang = { workspace = true, features = ["idl-build"] } -anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } -light-token-interface = { workspace = true, features = ["anchor"] } -light-token-sdk = { workspace = true, features = ["anchor", "compressible"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } - -[dev-dependencies] -light-token-client = { workspace = true } -light-program-test = { workspace = true, features = ["devenv", "v2"] } -light-client = { workspace = true, features = ["v2"] } -light-compressible-client = { workspace = true, features = ["anchor"] } -light-test-utils = { workspace = true } -tokio = { workspace = true } -solana-sdk = { workspace = true } -solana-logger = { workspace = true } -solana-instruction = { workspace = true } -solana-pubkey = { workspace = true } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-keypair = { workspace = true } -solana-account = { workspace = true } -bincode = "1.3" - -[lints.rust.unexpected_cfgs] -level = "allow" -check-cfg = [ - 'cfg(target_os, values("solana"))', - 'cfg(feature, values("frozen-abi", "no-entrypoint"))', -] diff --git a/sdk-tests/csdk-anchor-derived-test/Xargo.toml b/sdk-tests/csdk-anchor-derived-test/Xargo.toml deleted file mode 100644 index 4f10b17d74..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/Xargo.toml +++ /dev/null @@ -1,4 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] - - diff --git a/sdk-tests/csdk-anchor-derived-test/package.json b/sdk-tests/csdk-anchor-derived-test/package.json deleted file mode 100644 index 9f27c61933..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@lightprotocol/csdk-anchor-derived-test", - "version": "0.1.0", - "license": "Apache-2.0", - "scripts": { - "build": "cargo build-sbf", - "test": "cargo test-sbf -p csdk-anchor-derived-test -- --nocapture" - }, - "nx": {} -} - diff --git a/sdk-tests/csdk-anchor-derived-test/src/errors.rs b/sdk-tests/csdk-anchor-derived-test/src/errors.rs deleted file mode 100644 index e7bdc66a08..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/errors.rs +++ /dev/null @@ -1,12 +0,0 @@ -use anchor_lang::prelude::ProgramError; - -#[repr(u32)] -pub enum ErrorCode { - RentRecipientMismatch, -} - -impl From for ProgramError { - fn from(e: ErrorCode) -> Self { - ProgramError::Custom(e as u32) - } -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs deleted file mode 100644 index eae19d13f8..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/instruction_accounts.rs +++ /dev/null @@ -1,109 +0,0 @@ -use anchor_lang::prelude::*; - -use crate::state::*; - -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - space = 8 + UserRecord::INIT_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - #[account( - init, - payer = user, - space = 8 + GameSession::INIT_SPACE, - seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, - - /// The mint authority used for PDA derivation - pub mint_authority: Signer<'info>, - - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, - - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, - - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - - /// Global compressible config - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Program data account is validated by the SDK - pub program_data: AccountInfo<'info>, - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateCompressionConfig<'info> { - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - pub authority: Signer<'info>, -} - -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: checked by SDK - pub config: AccountInfo<'info>, - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: anyone can pay (optional - only needed if decompressing tokens) - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - /// CHECK: checked by SDK (optional - only needed if decompressing tokens) - pub ctoken_config: Option>, - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub ctoken_program: Option>, - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option>, - /// CHECK: checked by SDK - pub some_mint: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-derived-test/src/lib.rs deleted file mode 100644 index b267dc487e..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/lib.rs +++ /dev/null @@ -1,291 +0,0 @@ -#![allow(deprecated)] - -use anchor_lang::prelude::*; -use light_sdk::derive_light_cpi_signer; -use light_sdk_types::CpiSigner; - -pub mod errors; -pub mod instruction_accounts; -pub mod processor; -pub mod seeds; -pub mod state; -pub mod variant; - -pub use instruction_accounts::*; -pub use state::{ - AccountCreationData, CompressionParams, GameSession, PlaceholderRecord, UserRecord, -}; -pub use variant::{CTokenAccountVariant, CompressedAccountData, CompressedAccountVariant}; - -declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); - -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -#[program] -pub mod csdk_anchor_derived_test { - use anchor_lang::solana_program::{program::invoke, sysvar::clock::Clock}; - use light_compressed_account::instruction_data::traits::LightInstructionData; - use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - }; - use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, - }; - use light_token_interface::instructions::mint_action::{ - MintActionCompressedInstructionData, MintToCompressedAction, Recipient, - }; - use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, - }; - - use super::*; - use crate::{ - errors::ErrorCode, - seeds::get_ctoken_signer_seeds, - state::{GameSession, UserRecord}, - LIGHT_CPI_SIGNER, - }; - - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, - ) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - user_record.owner = ctx.accounts.user.key(); - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(user_compressed_info); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; - - // Build instruction data using the correct API - let proof = compression_params.proof.0.unwrap_or_default(); - let instruction_data = MintActionCompressedInstructionData::new_mint( - 0, // root_index for new addresses - proof, - compression_params.mint_with_context.mint.clone().unwrap(), - ) - .with_mint_to_compressed(MintToCompressedAction { - token_account_version: 3, - recipients: vec![Recipient::new(token_account_address, 1000)], - }) - .with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); - - // Build account metas - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), // authority (mint authority) - ctx.accounts.mint_signer.key(), // mint_signer - address_tree_pubkey, - output_queue, - ) - .with_mint_compressed_tokens(); - - config.cpi_context = Some(cpi_context_pubkey); - - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().map_err(ProgramError::from)?; - - // Build mint action instruction - let mint_action_instruction = solana_program::instruction::Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts: account_metas, - data, - }; - - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - invoke(&mint_action_instruction, &account_infos)?; - - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) - } - - pub fn initialize_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, - rent_sponsor: Pubkey, - address_space: Vec, - ) -> Result<()> { - let compression_authority = ctx.accounts.authority.key(); - let rent_config = light_compressible::rent::RentConfig::default(); - let write_top_up: u32 = 5_000; - light_sdk::compressible::process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - Ok(()) - } - - pub fn update_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - light_sdk::compressible::process_update_compression_config( - ctx.accounts.config.as_ref(), - ctx.accounts.authority.as_ref(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - Ok(()) - } - - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - crate::processor::process_decompress_accounts_idempotent( - ctx.accounts, - ctx.remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - ) - } - - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - _proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec< - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - >, - system_accounts_offset: u8, - ) -> Result<()> { - crate::processor::process_compress_accounts_idempotent( - ctx.accounts, - ctx.remaining_accounts, - compressed_accounts, - system_accounts_offset, - ) - } -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/processor.rs b/sdk-tests/csdk-anchor-derived-test/src/processor.rs deleted file mode 100644 index 82c3e3847e..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/processor.rs +++ /dev/null @@ -1,326 +0,0 @@ -use anchor_lang::prelude::*; -use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; -use light_sdk::{ - compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, - cpi::v2::CpiAccounts, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, - LightDiscriminator, -}; -use light_token_sdk::compat::PackedCTokenData; - -use crate::{ - instruction_accounts::{CompressAccountsIdempotent, DecompressAccountsIdempotent}, - state::{GameSession, PlaceholderRecord, UserRecord}, - variant::{CTokenAccountVariant, CompressedAccountData, CompressedAccountVariant}, - LIGHT_CPI_SIGNER, -}; - -impl light_sdk::compressible::HasTokenVariant for CompressedAccountData { - fn is_packed_token(&self) -> bool { - matches!(self.data, CompressedAccountVariant::PackedCTokenData(_)) - } -} - -/// Empty struct since this test doesn't use data.* fields in PDA seeds -#[derive(Default)] -pub struct SeedParams; - -impl<'info> light_sdk::compressible::DecompressContext<'info> - for DecompressAccountsIdempotent<'info> -{ - type CompressedData = CompressedAccountData; - type PackedTokenData = PackedCTokenData; - type CompressedMeta = CompressedAccountMetaNoLamportsNoAddress; - type SeedParams = SeedParams; - - fn fee_payer(&self) -> &AccountInfo<'info> { - self.fee_payer.as_ref() - } - - fn config(&self) -> &AccountInfo<'info> { - &self.config - } - - fn rent_sponsor(&self) -> &AccountInfo<'info> { - self.rent_sponsor.as_ref() - } - - fn token_rent_sponsor(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_rent_sponsor.as_ref() - } - - fn token_program(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_program.as_ref() - } - - fn token_cpi_authority(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_cpi_authority.as_ref() - } - - fn token_config(&self) -> Option<&AccountInfo<'info>> { - self.ctoken_config.as_ref() - } - - fn collect_pda_and_token<'b>( - &self, - cpi_accounts: &CpiAccounts<'b, 'info>, - address_space: Pubkey, - compressed_accounts: Vec, - solana_accounts: &[AccountInfo<'info>], - _seed_params: Option<&Self::SeedParams>, - ) -> std::result::Result< - ( - Vec, - Vec<(Self::PackedTokenData, Self::CompressedMeta)>, - ), - ProgramError, - > { - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - - let mut compressed_pda_infos = Vec::new(); - let mut compressed_token_accounts = Vec::new(); - - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let meta = compressed_data.meta; - match compressed_data.data { - CompressedAccountVariant::PackedUserRecord(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - UserRecord, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedGameSession(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - GameSession, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedPlaceholderRecord(packed) => { - light_sdk::compressible::handle_packed_pda_variant::< - PlaceholderRecord, - _, - DecompressAccountsIdempotent<'info>, - SeedParams, - >( - self.rent_sponsor.as_ref(), - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &crate::ID, - self, - None, - )?; - } - CompressedAccountVariant::PackedCTokenData(mut data) => { - data.token_data.version = 3; - compressed_token_accounts.push((data, meta)); - } - CompressedAccountVariant::UserRecord(_) - | CompressedAccountVariant::GameSession(_) - | CompressedAccountVariant::PlaceholderRecord(_) - | CompressedAccountVariant::CTokenData(_) => { - unreachable!("Unpacked variants should not appear during decompression") - } - } - } - - Ok((compressed_pda_infos, compressed_token_accounts)) - } - - fn process_tokens<'b>( - &self, - _remaining_accounts: &[AccountInfo<'info>], - _fee_payer: &AccountInfo<'info>, - _token_program: &AccountInfo<'info>, - _token_rent_sponsor: &AccountInfo<'info>, - _token_cpi_authority: &AccountInfo<'info>, - _token_config: &AccountInfo<'info>, - _config: &AccountInfo<'info>, - token_accounts: Vec<(Self::PackedTokenData, Self::CompressedMeta)>, - proof: light_sdk::instruction::ValidityProof, - cpi_accounts: &CpiAccounts<'b, 'info>, - post_system_accounts: &[AccountInfo<'info>], - has_pdas: bool, - ) -> std::result::Result<(), ProgramError> { - if token_accounts.is_empty() { - return Ok(()); - } - - light_token_sdk::compressible::process_decompress_tokens_runtime::( - self, - _remaining_accounts, - _fee_payer, - _token_program, - _token_rent_sponsor, - _token_cpi_authority, - _token_config, - _config, - token_accounts, - proof, - cpi_accounts, - post_system_accounts, - has_pdas, - &crate::ID, - )?; - - Ok(()) - } -} - -#[inline(never)] -pub fn process_decompress_accounts_idempotent<'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[AccountInfo<'info>], - compressed_accounts: Vec, - proof: ValidityProof, - system_accounts_offset: u8, -) -> Result<()> { - light_sdk::compressible::process_decompress_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - None, // No seed params needed for manual implementation - ) - .map_err(|e| e.into()) -} - -impl<'info> light_sdk::compressible::CompressContext<'info> for CompressAccountsIdempotent<'info> { - fn fee_payer(&self) -> &AccountInfo<'info> { - self.fee_payer.as_ref() - } - - fn config(&self) -> &AccountInfo<'info> { - &self.config - } - - fn rent_sponsor(&self) -> &AccountInfo<'info> { - &self.rent_sponsor - } - - fn compression_authority(&self) -> &AccountInfo<'info> { - &self.compression_authority - } - - fn compress_pda_account( - &self, - account_info: &AccountInfo<'info>, - meta: &CompressedAccountMetaNoLamportsNoAddress, - cpi_accounts: &CpiAccounts<'_, 'info>, - compression_config: &CompressibleConfig, - program_id: &Pubkey, - ) -> std::result::Result, ProgramError> { - let data = account_info.try_borrow_data()?; - let discriminator = &data[0..8]; - - match discriminator { - d if d == UserRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - d if d == GameSession::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - Ok(Some(compressed_info)) - } - _ => Err(ProgramError::InvalidAccountData), - } - } -} - -#[inline(never)] -pub fn process_compress_accounts_idempotent<'info>( - accounts: &CompressAccountsIdempotent<'info>, - remaining_accounts: &[AccountInfo<'info>], - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - light_sdk::compressible::process_compress_pda_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e| e.into()) -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/seeds.rs b/sdk-tests/csdk-anchor-derived-test/src/seeds.rs deleted file mode 100644 index 532aef3ef8..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/seeds.rs +++ /dev/null @@ -1,47 +0,0 @@ -use anchor_lang::prelude::Pubkey; - -pub fn get_user_record_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { - let seeds: &[&[u8]] = &[b"user_record", owner.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_game_session_seeds(session_id: u64) -> (Vec>, Pubkey) { - let session_id_bytes = session_id.to_le_bytes(); - let seeds: &[&[u8]] = &[b"game_session", session_id_bytes.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_placeholder_record_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { - let placeholder_id_bytes = placeholder_id.to_le_bytes(); - let seeds: &[&[u8]] = &[b"placeholder_record", placeholder_id_bytes.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} - -pub fn get_ctoken_signer_seeds(user: &Pubkey, mint: &Pubkey) -> (Vec>, Pubkey) { - let seeds: &[&[u8]] = &[b"ctoken_signer", user.as_ref(), mint.as_ref()]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - for seed in seeds { - seeds_vec.push(seed.to_vec()); - } - seeds_vec.push(vec![bump]); - (seeds_vec, pda) -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/state.rs b/sdk-tests/csdk-anchor-derived-test/src/state.rs deleted file mode 100644 index 262667ff61..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/state.rs +++ /dev/null @@ -1,126 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, - instruction::{PackedAddressTreeInfo, ValidityProof}, - LightDiscriminator, LightHasher, -}; -use light_sdk_macros::{Compressible, CompressiblePack}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[account] -pub struct UserRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub score: u64, -} - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[compress_as(start_time = 0, end_time = None, score = 0)] -#[account] -pub struct GameSession { - #[skip] - pub compression_info: Option, - pub session_id: u64, - #[hash] - pub player: Pubkey, - #[max_len(32)] - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] -#[account] -pub struct PlaceholderRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub placeholder_id: u64, -} - -// Implement PdaSeedDerivation for UserRecord -impl light_sdk::compressible::PdaSeedDerivation for UserRecord { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_user_record_seeds(&self.owner)) - } -} - -// Implement PdaSeedDerivation for GameSession -impl light_sdk::compressible::PdaSeedDerivation for GameSession { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_game_session_seeds(self.session_id)) - } -} - -// Implement PdaSeedDerivation for PlaceholderRecord -impl light_sdk::compressible::PdaSeedDerivation for PlaceholderRecord { - fn derive_pda_seeds_with_accounts( - &self, - _program_id: &Pubkey, - _accounts: &A, - _seed_params: &S, - ) -> std::result::Result<(Vec>, Pubkey), anchor_lang::prelude::ProgramError> { - Ok(crate::seeds::get_placeholder_record_seeds( - self.placeholder_id, - )) - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct AccountCreationData { - pub user_name: String, - pub session_id: u64, - pub game_type: String, - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} diff --git a/sdk-tests/csdk-anchor-derived-test/src/variant.rs b/sdk-tests/csdk-anchor-derived-test/src/variant.rs deleted file mode 100644 index 4dada3bd76..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/src/variant.rs +++ /dev/null @@ -1,173 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - account::Size, - compressible::{CompressionInfo, HasCompressionInfo, Pack as SdkPack, Unpack as SdkUnpack}, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts}, - LightDiscriminator, -}; -use light_token_sdk::{ - compat::{CTokenData, PackedCTokenData}, - pack::Pack as TokenPack, -}; - -use crate::{ - instruction_accounts::DecompressAccountsIdempotent, - seeds::get_ctoken_signer_seeds, - state::{ - GameSession, PackedGameSession, PackedPlaceholderRecord, PackedUserRecord, - PlaceholderRecord, UserRecord, - }, -}; - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -#[repr(u8)] -pub enum CTokenAccountVariant { - CTokenSigner = 0, -} - -impl light_token_sdk::compressible::TokenSeedProvider for CTokenAccountVariant { - type Accounts<'info> = DecompressAccountsIdempotent<'info>; - - fn get_seeds<'a, 'info>( - &self, - accounts: &'a Self::Accounts<'info>, - _remaining_accounts: &'a [AccountInfo<'info>], - ) -> std::result::Result<(Vec>, Pubkey), ProgramError> { - match self { - CTokenAccountVariant::CTokenSigner => { - // Use the same convention as the mint/init path: ("ctoken_signer", user, mint) - std::result::Result::<(Vec>, Pubkey), ProgramError>::Ok( - get_ctoken_signer_seeds(&accounts.fee_payer.key(), &accounts.some_mint.key()), - ) - } - } - } - - fn get_authority_seeds<'a, 'info>( - &self, - _accounts: &'a Self::Accounts<'info>, - _remaining_accounts: &'a [AccountInfo<'info>], - ) -> std::result::Result<(Vec>, Pubkey), ProgramError> { - // Not used by the decompression runtime in this test. - std::result::Result::<(Vec>, Pubkey), ProgramError>::Err( - ProgramError::InvalidAccountData, - ) - } -} - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - PlaceholderRecord(PlaceholderRecord), - PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), -} - -impl Default for CompressedAccountVariant { - fn default() -> Self { - Self::UserRecord(UserRecord::default()) - } -} - -impl LightDiscriminator for CompressedAccountVariant { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; -} - -impl HasCompressionInfo for CompressedAccountVariant { - fn compression_info(&self) -> &CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info(), - Self::GameSession(data) => data.compression_info(), - Self::PlaceholderRecord(data) => data.compression_info(), - _ => unreachable!(), - } - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info_mut(), - Self::GameSession(data) => data.compression_info_mut(), - Self::PlaceholderRecord(data) => data.compression_info_mut(), - _ => unreachable!(), - } - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - match self { - Self::UserRecord(data) => data.compression_info_mut_opt(), - Self::GameSession(data) => data.compression_info_mut_opt(), - Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), - _ => unreachable!(), - } - } - - fn set_compression_info_none(&mut self) { - match self { - Self::UserRecord(data) => data.set_compression_info_none(), - Self::GameSession(data) => data.set_compression_info_none(), - Self::PlaceholderRecord(data) => data.set_compression_info_none(), - _ => unreachable!(), - } - } -} - -impl Size for CompressedAccountVariant { - fn size(&self) -> usize { - match self { - Self::UserRecord(data) => data.size(), - Self::GameSession(data) => data.size(), - Self::PlaceholderRecord(data) => data.size(), - _ => unreachable!(), - } - } -} - -impl SdkPack for CompressedAccountVariant { - type Packed = Self; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - match self { - Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), - Self::GameSession(data) => Self::PackedGameSession(data.pack(remaining_accounts)), - Self::PlaceholderRecord(data) => { - Self::PackedPlaceholderRecord(data.pack(remaining_accounts)) - } - Self::CTokenData(data) => { - Self::PackedCTokenData(TokenPack::pack(data, remaining_accounts)) - } - _ => unreachable!(), - } - } -} - -impl SdkUnpack for CompressedAccountVariant { - type Unpacked = Self; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - match self { - Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), - Self::PackedGameSession(data) => { - Ok(Self::GameSession(data.unpack(remaining_accounts)?)) - } - Self::PackedPlaceholderRecord(data) => { - Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) - } - Self::PackedCTokenData(data) => Ok(Self::PackedCTokenData(data.clone())), - _ => unreachable!(), - } - } -} - -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct CompressedAccountData { - pub meta: CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, -} diff --git a/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs deleted file mode 100644 index 7a6c487966..0000000000 --- a/sdk-tests/csdk-anchor-derived-test/tests/basic_test.rs +++ /dev/null @@ -1,706 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, InstructionData, ToAccountMetas}; -use csdk_anchor_derived_test::{AccountCreationData, CompressionParams, GameSession, UserRecord}; -use light_compressed_account::address::derive_address; -use light_macros::pubkey; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::compressed_token::create_compressed_mint::{ - derive_mint_compressed_address, find_mint_address, -}; -use light_token_types::CPI_AUTHORITY_PDA; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; -const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); - -#[tokio::test] -async fn test_create_decompress_compress() { - let program_id = csdk_anchor_derived_test::ID; - let mut config = - ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_derived_test", program_id)])); - config = config.with_light_protocol_events(); - - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &light_compressible_client::compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let session_id = 42424u64; - let (user_record_pda, _user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - - let compressed_game_session = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Test Game"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); - - let spl_mint = find_mint_address(&mint_signer_pubkey).0; - let (_, token_account_address) = - csdk_anchor_derived_test::seeds::get_ctoken_signer_seeds(&payer.pubkey(), &spl_mint); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have compressed token accounts" - ); - - // Test decompress PDAs (UserRecord + GameSession) - // Note: Light Token decompression works but requires manual instruction building - // because the client helper doesn't handle mixed PDA+token packing correctly - rpc.warp_to_slot(100).unwrap(); - - decompress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - 100, - ) - .await; - - // Test compress PDAs after decompression - rpc.warp_to_slot(200).unwrap(); - - compress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; -} - -#[tokio::test] -async fn test_auto_compress_on_warp_forward() { - use light_compressible::rent::SLOTS_PER_EPOCH; - let program_id = csdk_anchor_derived_test::ID; - let config = - ProgramTestConfig::new_v2(true, Some(vec![("csdk_anchor_derived_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - // Initialize compressible config - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &light_compressible_client::compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await - .expect("Initialize config should succeed"); - - // PDAs - let session_id = 5555u64; - let (user_record_pda, _) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - let (game_session_pda, _) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - // Create + compress initial state via helper (combined create path) - let _mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - session_id, - ) - .await; - - // Decompress both PDAs - rpc.warp_to_slot(100).unwrap(); - decompress_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - 100, - ) - .await; - - // Warp two epochs to ensure PDAs are compressible - rpc.warp_slot_forward(SLOTS_PER_EPOCH * 2).await.unwrap(); - - // Also invoke auto-compress directly to ensure it's executed in this test context - light_program_test::compressible::auto_compress_program_pdas(&mut rpc, program_id) - .await - .unwrap(); - - // After auto-compress, PDAs should be closed or emptied - let user_acc = rpc.get_account(user_record_pda).await.unwrap(); - let game_acc = rpc.get_account(game_session_pda).await.unwrap(); - let user_closed = user_acc.is_none() - || user_acc - .as_ref() - .map(|a| a.data.is_empty() || a.lamports == 0) - .unwrap_or(true); - let game_closed = game_acc.is_none() - || game_acc - .as_ref() - .map(|a| a.data.is_empty() || a.lamports == 0) - .unwrap_or(true); - assert!( - user_closed && game_closed, - "Auto-compress should close PDAs" - ); -} - -#[allow(clippy::too_many_arguments)] -async fn decompress_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - // Get compressed PDA accounts - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &light_compressible_client::compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda, *game_session_pda], - &[ - ( - c_user_pda.clone(), - csdk_anchor_derived_test::CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda.clone(), - csdk_anchor_derived_test::CompressedAccountVariant::GameSession(c_game_session), - ), - ], - &csdk_anchor_derived_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Decompress PDAs transaction should succeed"); - - // Verify user record decompressed - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User PDA should exist after decompression" - ); - let decompressed_user_record = - UserRecord::try_deserialize(&mut &user_pda_account.unwrap().data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, "Combined User"); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - // Verify game session decompressed - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.is_some(), - "Game PDA should exist after decompression" - ); - let decompressed_game_session = - GameSession::try_deserialize(&mut &game_pda_account.unwrap().data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, "Test Game"); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - // Verify compressed PDA accounts are empty - let compressed_user = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!( - compressed_user.data.unwrap().data.is_empty(), - "Compressed user should be empty after decompression" - ); - - let compressed_game = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!( - compressed_game.data.unwrap().data.is_empty(), - "Compressed game should be empty after decompression" - ); -} - -#[allow(clippy::too_many_arguments)] -async fn compress_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - // Get PDA accounts - let _user_pda_account = rpc - .get_account(*user_record_pda) - .await - .unwrap() - .expect("User PDA should exist before compression"); - let _game_pda_account = rpc - .get_account(*game_session_pda) - .await - .unwrap() - .expect("Game PDA should exist before compression"); - - // Get compressed account hashes for proof - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_user = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let compressed_game = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let _rpc_result = rpc - .get_validity_proof( - vec![compressed_user.hash, compressed_game.hash], - vec![], - None, - ) - .await - .unwrap() - .value; - - // TODO: remove in separate pr - // let instruction = - // light_compressible_client::compressible_instruction::compress_accounts_idempotent( - // program_id, - // csdk_anchor_derived_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - // &[*user_record_pda, *game_session_pda], - // &[user_pda_account, game_pda_account], - // &csdk_anchor_derived_test::accounts::CompressAccountsIdempotent { - // fee_payer: payer.pubkey(), - // config: CompressibleConfig::derive_pda(program_id, 0).0, - // rent_sponsor: RENT_SPONSOR, - // compression_authority: payer.pubkey(), - // } - // .to_account_metas(None), - // rpc_result, - // ) - // .unwrap(); - - // let result = rpc - // .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - // .await; - - // assert!(result.is_ok(), "Compress PDAs transaction should succeed"); - - rpc.warp_slot_forward(light_compressible::rent::SLOTS_PER_EPOCH * 2) - .await - .unwrap(); - - // Verify PDAs are closed - let user_pda_after = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_after.is_none(), - "User PDA should be closed after compression" - ); - - let game_pda_after = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_after.is_none(), - "Game PDA should be closed after compression" - ); - - // Verify compressed PDA accounts have data - let compressed_user_after = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert_eq!(compressed_user_after.address, Some(user_compressed_address)); - let user_buf = compressed_user_after.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - - let compressed_game_after = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert_eq!(compressed_game_after.address, Some(game_compressed_address)); - let game_buf = compressed_game_after.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Test Game"); - assert!(game_session.compression_info.is_none()); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) -> Pubkey { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority_keypair = Keypair::new(); - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = csdk_anchor_derived_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - mint_signer: mint_signer.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - mint_authority, - compress_token_program_cpi_authority: CPI_AUTHORITY_PDA.into(), - }; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, - ], - None, - ) - .await - .unwrap() - .value; - - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = csdk_anchor_derived_test::instruction::CreateUserRecordAndGameSession { - account_data: AccountCreationData { - user_name: "Combined User".to_string(), - session_id, - game_type: "Test Game".to_string(), - mint_name: "Test Game Token".to_string(), - mint_symbol: "TGT".to_string(), - mint_uri: "https://example.com/token.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, - }, - compression_params: CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, &mint_authority_keypair], - ) - .await; - - assert!( - result.is_ok(), - "Combined creation transaction should succeed: {:?}", - result - ); - - mint_signer.pubkey() -} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs index 1e1db1be0d..2b806739de 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs @@ -1,72 +1,118 @@ use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; use crate::state::*; -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { +#[derive(AnchorSerialize, AnchorDeserialize)] +pub struct FullAutoWithMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub category_id: u64, + pub session_id: u64, + pub mint_signer_bump: u8, + pub vault_bump: u8, + pub user_ata_bump: u8, + pub vault_mint_amount: u64, + pub user_ata_mint_amount: u64, +} + +pub const LP_MINT_SIGNER_SEED: &[u8] = b"lp_mint_signer"; +pub const AUTO_VAULT_SEED: &[u8] = b"auto_vault"; +pub const AUTO_VAULT_AUTHORITY_SEED: &[u8] = b"auto_vault_authority"; + +#[derive(Accounts, RentFree)] +#[instruction(params: FullAutoWithMintParams)] +pub struct CreatePdasAndMintAuto<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + #[account(mut)] - pub user: Signer<'info>, + pub mint_authority: Signer<'info>, - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, + /// CHECK: PDA derived from authority + #[account( + seeds = [LP_MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, #[account( init, - payer = user, - // Space: discriminator(8) + owner(32) + name_len(4) + name(32) + score(8) + category_id(8) = 92 bytes - space = 8 + 32 + 4 + 32 + 8 + 8, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, seeds = [ b"user_record", authority.key().as_ref(), mint_authority.key().as_ref(), - account_data.owner.as_ref(), - account_data.category_id.to_le_bytes().as_ref() + params.owner.as_ref(), + params.category_id.to_le_bytes().as_ref() ], bump, )] + #[rentfree] pub user_record: Account<'info, UserRecord>, + #[account( init, - payer = user, - // Space: discriminator(8) + session_id(8) + player(32) + game_type_len(4) + - // game_type(32) + start_time(8) + end_time(1+8) + score(8) = 109 bytes - space = 8 + 8 + 32 + 4 + 32 + 8 + 9 + 8, + payer = fee_payer, + space = 8 + GameSession::INIT_SPACE, seeds = [ b"game_session", - crate::max_key(&user.key(), &authority.key()).as_ref(), - account_data.session_id.to_le_bytes().as_ref() + crate::max_key(&fee_payer.key(), &authority.key()).as_ref(), + params.session_id.to_le_bytes().as_ref() ], bump, )] + #[rentfree] pub game_session: Account<'info, GameSession>, - /// Authority signer used in PDA seeds - pub authority: Signer<'info>, - - /// Mint authority signer used in PDA seeds - pub mint_authority: Signer<'info>, + /// CHECK: Initialized by mint_action + #[account(mut)] + #[light_mint( + mint_signer = mint_signer, + authority = mint_authority, + decimals = 9, + signer_seeds = &[LP_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] + )] + pub cmint: UncheckedAccount<'info>, - /// Some account used in PlaceholderRecord PDA seeds - /// CHECK: Used as seed component - pub some_account: AccountInfo<'info>, + /// CHECK: Initialized via CToken CPI + #[account( + mut, + seeds = [VAULT_SEED, cmint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [b"vault_authority"])] + pub vault: UncheckedAccount<'info>, - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, + /// CHECK: PDA used as vault owner + #[account(seeds = [b"vault_authority"], bump)] + pub vault_authority: UncheckedAccount<'info>, - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, + /// CHECK: Initialized via CToken CPI + #[account(mut)] + pub user_ata: UncheckedAccount<'info>, - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, - /// Global compressible config - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, + /// CHECK: CToken config + pub ctoken_compressible_config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config + /// CHECK: CToken rent sponsor #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, + pub ctoken_rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub ctoken_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, } + +pub const VAULT_SEED: &[u8] = b"vault"; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index 08563dc6b1..9cbd0f3849 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; -use light_sdk_macros::add_compressible_instructions; +use light_sdk_macros::rentfree_program; use light_sdk_types::CpiSigner; pub mod errors; @@ -10,12 +10,8 @@ pub mod instruction_accounts; pub mod state; pub use instruction_accounts::*; -pub use state::{ - AccountCreationData, CompressionParams, GameSession, PackedGameSession, - PackedPlaceholderRecord, PackedUserRecord, PlaceholderRecord, UserRecord, -}; +pub use state::{GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord}; -// Example helper expression usable in seeds #[inline] pub fn max_key(left: &Pubkey, right: &Pubkey) -> [u8; 32] { if left > right { @@ -30,223 +26,108 @@ declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); pub const LIGHT_CPI_SIGNER: CpiSigner = derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -/// Derive a program-owned rent sponsor PDA (version = 1 by default). pub const PROGRAM_RENT_SPONSOR_DATA: ([u8; 32], u8) = derive_light_rent_sponsor_pda!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah", 1); -/// Returns the program's rent sponsor PDA as a Pubkey. #[inline] pub fn program_rent_sponsor() -> Pubkey { Pubkey::from(PROGRAM_RENT_SPONSOR_DATA.0) } -#[add_compressible_instructions( - // Complex PDA account types with seed specifications using BOTH ctx.accounts.* AND data.* - // UserRecord: uses ctx accounts (authority, mint_authority) + data fields (owner, category_id) - UserRecord = ("user_record", ctx.authority, ctx.mint_authority, data.owner, data.category_id.to_le_bytes()), - // GameSession: uses max_key expression with ctx.accounts + data.session_id - GameSession = ("game_session", max_key(&ctx.user.key(), &ctx.authority.key()), data.session_id.to_le_bytes()), - // PlaceholderRecord: mixes ctx accounts and data for seeds - PlaceholderRecord = ("placeholder_record", ctx.authority, ctx.some_account, data.placeholder_id.to_le_bytes(), data.counter.to_le_bytes()), - // Token variant (Light Token account) with authority for compression signing - CTokenSigner = (is_token, "ctoken_signer", ctx.fee_payer, ctx.mint, authority = LIGHT_CPI_SIGNER), - // Instruction data fields used in seed expressions above - owner = Pubkey, - category_id = u64, - session_id = u64, - placeholder_id = u64, - counter = u32, -)] +pub const GAME_SESSION_SEED: &str = "game_session"; + +#[rentfree_program] #[program] pub mod csdk_anchor_full_derived_test { #![allow(clippy::too_many_arguments)] - use anchor_lang::solana_program::{program::invoke, sysvar::clock::Clock}; - use light_compressed_account::instruction_data::traits::LightInstructionData; - use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - }; - use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, - }; - use light_token_interface::instructions::mint_action::{ - MintActionCompressedInstructionData, MintToCompressedAction, Recipient, - }; - use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, - }; use super::*; use crate::{ - errors::ErrorCode, + instruction_accounts::CreatePdasAndMintAuto, state::{GameSession, UserRecord}, - LIGHT_CPI_SIGNER, + FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, + pub fn create_pdas_and_mint_auto<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePdasAndMintAuto<'info>>, + params: FullAutoWithMintParams, ) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ErrorCode::RentRecipientMismatch.into()); - } + use anchor_lang::solana_program::sysvar::clock::Clock; + use light_token_sdk::token::{ + CreateCTokenAtaCpi, CreateTokenAccountCpi, MintToCpi as CTokenMintToCpi, + }; - // Populate UserRecord - user_record.owner = account_data.owner; - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - user_record.category_id = account_data.category_id; + let user_record = &mut ctx.accounts.user_record; + user_record.owner = params.owner; + user_record.name = "Auto Created User With Mint".to_string(); + user_record.score = 0; + user_record.category_id = params.category_id; - // Populate GameSession - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); + let game_session = &mut ctx.accounts.game_session; + game_session.session_id = params.session_id; + game_session.player = ctx.accounts.fee_payer.key(); + game_session.game_type = "Auto Game With Mint".to_string(); game_session.start_time = Clock::get()?.unix_timestamp as u64; game_session.end_time = None; game_session.score = 0; - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(user_compressed_info); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - - // Use the generated client seed function for Light Token signer (generated by add_compressible_instructions macro) - let (_, token_account_address) = get_ctokensigner_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; - - // Build instruction data using the correct API - let proof = compression_params.proof.0.unwrap_or_default(); - let instruction_data = MintActionCompressedInstructionData::new_mint( - 0, // root_index for new addresses - proof, - compression_params.mint_with_context.mint.clone().unwrap(), + let cmint_key = ctx.accounts.cmint.key(); + CreateTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.vault.to_account_info(), + mint: ctx.accounts.cmint.to_account_info(), + owner: ctx.accounts.vault_authority.key(), + } + .rent_free( + ctx.accounts.ctoken_compressible_config.to_account_info(), + ctx.accounts.ctoken_rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + &crate::ID, ) - .with_mint_to_compressed(MintToCompressedAction { - token_account_version: 3, - recipients: vec![Recipient::new(token_account_address, 1000)], - }) - .with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); - - // Build account metas - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), // authority (mint authority) - ctx.accounts.mint_signer.key(), // mint_signer - address_tree_pubkey, - output_queue, + .invoke_signed(&[ + crate::instruction_accounts::VAULT_SEED, + cmint_key.as_ref(), + &[params.vault_bump], + ])?; + + CreateCTokenAtaCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + owner: ctx.accounts.fee_payer.to_account_info(), + mint: ctx.accounts.cmint.to_account_info(), + ata: ctx.accounts.user_ata.to_account_info(), + bump: params.user_ata_bump, + } + .idempotent() + .rent_free( + ctx.accounts.ctoken_compressible_config.to_account_info(), + ctx.accounts.ctoken_rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), ) - .with_mint_compressed_tokens(); - - config.cpi_context = Some(cpi_context_pubkey); - - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().map_err(ProgramError::from)?; - - // Build mint action instruction - let mint_action_instruction = solana_program::instruction::Instruction { - program_id: light_token_interface::LIGHT_TOKEN_PROGRAM_ID.into(), - accounts: account_metas, - data, - }; + .invoke()?; + + if params.vault_mint_amount > 0 { + CTokenMintToCpi { + cmint: ctx.accounts.cmint.to_account_info(), + destination: ctx.accounts.vault.to_account_info(), + amount: params.vault_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke()?; + } - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - invoke(&mint_action_instruction, &account_infos)?; - - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; + if params.user_ata_mint_amount > 0 { + CTokenMintToCpi { + cmint: ctx.accounts.cmint.to_account_info(), + destination: ctx.accounts.user_ata.to_account_info(), + amount: params.user_ata_mint_amount, + authority: ctx.accounts.mint_authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke()?; + } Ok(()) } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs index 27ce2224eb..975d41282c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state.rs @@ -1,20 +1,11 @@ use anchor_lang::prelude::*; -use light_sdk::{ - compressible::CompressionInfo, - instruction::{PackedAddressTreeInfo, ValidityProof}, - LightDiscriminator, LightHasher, -}; -use light_sdk_macros::{Compressible, CompressiblePack}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[account] pub struct UserRecord { - #[skip] pub compression_info: Option, - #[hash] pub owner: Pubkey, #[max_len(32)] pub name: String, @@ -22,16 +13,12 @@ pub struct UserRecord { pub category_id: u64, } -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[compress_as(start_time = 0, end_time = None, score = 0)] #[account] pub struct GameSession { - #[skip] pub compression_info: Option, pub session_id: u64, - #[hash] pub player: Pubkey, #[max_len(32)] pub game_type: String, @@ -40,56 +27,13 @@ pub struct GameSession { pub score: u64, } -#[derive( - Default, Debug, LightHasher, LightDiscriminator, InitSpace, Compressible, CompressiblePack, -)] +#[derive(Default, Debug, InitSpace, RentFreeAccount)] #[account] pub struct PlaceholderRecord { - #[skip] pub compression_info: Option, - #[hash] pub owner: Pubkey, #[max_len(32)] pub name: String, pub placeholder_id: u64, pub counter: u32, } - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct AccountCreationData { - // Instruction data fields (accounts come from ctx.accounts.*) - pub owner: Pubkey, - pub category_id: u64, - pub user_name: String, - pub session_id: u64, - pub game_type: String, - pub placeholder_id: u64, - pub counter: u32, - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs index fbe723e9a6..f43bb8afd2 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/basic_test.rs @@ -1,36 +1,73 @@ -use anchor_lang::{AnchorDeserialize, InstructionData, ToAccountMetas}; -use csdk_anchor_full_derived_test::{ - AccountCreationData, CompressionParams, GameSession, UserRecord, +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible::rent::SLOTS_PER_EPOCH; +use light_compressible_client::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, }; -use light_compressed_account::address::derive_address; use light_macros::pubkey; use light_program_test::{ - program_test::{setup_mock_program_data, LightProgramTest}, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + Indexer, ProgramTestConfig, Rpc, }; use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::compressed_token::create_compressed_mint::{ - derive_mint_compressed_address, find_mint_address, -}; -use light_token_types::CPI_AUTHORITY_PDA; +use light_token_sdk::token::find_mint_address as find_cmint_address; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; -const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); +/// 2 PDAs + 1 CMint + 1 Vault + 1 User ATA, all in one instruction with single proof. +/// After init: all accounts on-chain + parseable. +/// After warp: all cold (auto-compressed) with non-empty compressed data. #[tokio::test] -async fn test_create_with_complex_seeds() { +async fn test_create_pdas_and_mint_auto() { + use csdk_anchor_full_derived_test::{ + instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}, + FullAutoWithMintParams, + }; + use light_token_interface::state::Token; + use light_token_sdk::token::{ + get_associated_token_address_and_bump, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, + }; + + // Helpers + async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey) { + assert!(rpc.get_account(*pda).await.unwrap().is_some()); + } + async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey) { + let acc = rpc.get_account(*pda).await.unwrap(); + assert!(acc.is_none() || acc.unwrap().lamports == 0); + } + fn parse_token(data: &[u8]) -> Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() + } + async fn assert_compressed_exists_with_data(rpc: &mut LightProgramTest, addr: [u8; 32]) { + let acc = rpc + .get_compressed_account(addr, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!(acc.address.unwrap(), addr); + assert!(!acc.data.as_ref().unwrap().data.is_empty()); + } + async fn assert_compressed_token_exists( + rpc: &mut LightProgramTest, + owner: &Pubkey, + expected_amount: u64, + ) { + let accs = rpc + .get_compressed_token_accounts_by_owner(owner, None, None) + .await + .unwrap() + .value + .items; + assert!(!accs.is_empty()); + assert_eq!(accs[0].token.amount, expected_amount); + } + let program_id = csdk_anchor_full_derived_test::ID; let mut config = ProgramTestConfig::new_v2( true, @@ -41,326 +78,415 @@ async fn test_create_with_complex_seeds() { let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - // Initialize compression config using the macro-generated instruction - let config_instruction = - csdk_anchor_full_derived_test::instruction::InitializeCompressionConfig { - rent_sponsor: RENT_SPONSOR, - compression_authority: payer.pubkey(), - rent_config: light_compressible::rent::RentConfig::default(), - write_top_up: 5_000, - address_space: vec![ADDRESS_SPACE[0]], - }; - let config_accounts = csdk_anchor_full_derived_test::accounts::InitializeCompressionConfig { - payer: payer.pubkey(), - config: config_pda, - program_data: _program_data_pda, - authority: payer.pubkey(), - system_program: solana_sdk::system_program::ID, - }; - let instruction = Instruction { - program_id, - accounts: config_accounts.to_account_metas(None), - data: config_instruction.data(), - }; - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - assert!( - result.is_ok(), - "Initialize config should succeed: {:?}", - result - ); + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); - // Create additional signers for complex seeds let authority = Keypair::new(); - let mint_authority_keypair = Keypair::new(); - let some_account = Keypair::new(); + let mint_authority = Keypair::new(); - let session_id = 42424u64; - let category_id = 777u64; + let owner = payer.pubkey(); + let category_id = 111u64; + let session_id = 222u64; + let vault_mint_amount = 100u64; + let user_ata_mint_amount = 50u64; - // Calculate PDAs with complex seeds using ctx accounts - let (user_record_pda, _user_record_bump) = Pubkey::find_program_address( + // Derive PDAs + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[LP_MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + let (vault_pda, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, cmint_pda.as_ref()], &program_id); + let (vault_authority_pda, _) = Pubkey::find_program_address(&[b"vault_authority"], &program_id); + let (user_ata_pda, user_ata_bump) = + get_associated_token_address_and_bump(&payer.pubkey(), &cmint_pda); + + let (user_record_pda, _) = Pubkey::find_program_address( &[ b"user_record", authority.pubkey().as_ref(), - mint_authority_keypair.pubkey().as_ref(), - payer.pubkey().as_ref(), // owner from instruction data + mint_authority.pubkey().as_ref(), + owner.as_ref(), category_id.to_le_bytes().as_ref(), ], &program_id, ); - // GameSession uses max_key(ctx.user, ctx.authority) for the seed let max_key_result = csdk_anchor_full_derived_test::max_key(&payer.pubkey(), &authority.pubkey()); - let (game_session_pda, _game_bump) = Pubkey::find_program_address( + let (game_session_pda, _) = Pubkey::find_program_address( &[ - b"game_session", + csdk_anchor_full_derived_test::GAME_SESSION_SEED.as_bytes(), max_key_result.as_ref(), session_id.to_le_bytes().as_ref(), ], &program_id, ); - let mint_signer_pubkey = create_user_record_and_game_session( - &mut rpc, - &payer, + let proof_result = get_create_accounts_proof( + &rpc, &program_id, - &config_pda, - &user_record_pda, - &game_session_pda, - &authority, - &mint_authority_keypair, - &some_account, - session_id, - category_id, + vec![ + CreateAccountsProofInput::pda(user_record_pda), + CreateAccountsProofInput::pda(game_session_pda), + CreateAccountsProofInput::mint(mint_signer_pda), + ], ) - .await; + .await + .unwrap(); + // Derive compressed addresses for later assertions let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( + let user_compressed_address = light_compressed_account::address::derive_address( &user_record_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes(), ); - let game_compressed_address = derive_address( + let game_compressed_address = light_compressed_account::address::derive_address( &game_session_pda.to_bytes(), &address_tree_pubkey.to_bytes(), &program_id.to_bytes(), ); + let mint_compressed_address = + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( + &mint_signer_pda, + &address_tree_pubkey, + ); + + let accounts = csdk_anchor_full_derived_test::accounts::CreatePdasAndMintAuto { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_authority: mint_authority.pubkey(), + mint_signer: mint_signer_pda, + user_record: user_record_pda, + game_session: game_session_pda, + cmint: cmint_pda, + vault: vault_pda, + vault_authority: vault_authority_pda, + user_ata: user_ata_pda, + compression_config: config_pda, + ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, + ctoken_rent_sponsor: CTOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + ctoken_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; - // Verify compressed user record - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) + // Simplified instruction data - just pass create_accounts_proof directly + let instruction_data = csdk_anchor_full_derived_test::instruction::CreatePdasAndMintAuto { + params: FullAutoWithMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + category_id, + session_id, + mint_signer_bump, + vault_bump, + user_ata_bump, + vault_mint_amount, + user_ata_mint_amount, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + rpc.create_and_send_transaction( + &[instruction], + &payer.pubkey(), + &[&payer, &authority, &mint_authority], + ) + .await + .unwrap(); + + // PHASE 1: After init - all accounts on-chain and parseable + assert_onchain_exists(&mut rpc, &user_record_pda).await; + assert_onchain_exists(&mut rpc, &game_session_pda).await; + assert_onchain_exists(&mut rpc, &cmint_pda).await; + assert_onchain_exists(&mut rpc, &vault_pda).await; + assert_onchain_exists(&mut rpc, &user_ata_pda).await; + + // Parse and verify CToken data + let vault_data = parse_token(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + assert_eq!(vault_data.owner, vault_authority_pda.to_bytes()); + assert_eq!(vault_data.amount, vault_mint_amount); + + let user_ata_data = parse_token(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + assert_eq!(user_ata_data.owner, payer.pubkey().to_bytes()); + assert_eq!(user_ata_data.amount, user_ata_mint_amount); + + // Verify compressed addresses registered (empty data - decompressed to on-chain) + let compressed_cmint = rpc + .get_compressed_account(mint_compressed_address, None) .await .unwrap() .value .unwrap(); + assert_eq!(compressed_cmint.address.unwrap(), mint_compressed_address); + assert!(compressed_cmint.data.as_ref().unwrap().data.is_empty()); + + // PHASE 2: Warp to trigger auto-compression + rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); + + // After warp: all on-chain accounts should be closed + assert_onchain_closed(&mut rpc, &user_record_pda).await; + assert_onchain_closed(&mut rpc, &game_session_pda).await; + assert_onchain_closed(&mut rpc, &cmint_pda).await; + assert_onchain_closed(&mut rpc, &vault_pda).await; + assert_onchain_closed(&mut rpc, &user_ata_pda).await; + + // Compressed accounts should exist with non-empty data + assert_compressed_exists_with_data(&mut rpc, user_compressed_address).await; + assert_compressed_exists_with_data(&mut rpc, game_compressed_address).await; + assert_compressed_exists_with_data(&mut rpc, mint_compressed_address).await; + + // Compressed token accounts should exist with correct balances + assert_compressed_token_exists(&mut rpc, &vault_pda, vault_mint_amount).await; + assert_compressed_token_exists(&mut rpc, &user_ata_pda, user_ata_mint_amount).await; + + // PHASE 3: Decompress PDAs + vault via build_decompress_idempotent + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + GameSessionSeeds, TokenAccountVariant, UserRecordSeeds, + }; + use light_compressible_client::{ + compressible_instruction, AccountInterface, RentFreeDecompressAccount, + }; - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Complex User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert_eq!(user_record.category_id, category_id); - assert!(user_record.compression_info.is_none()); + // Fetch compressed PDA accounts + let compressed_user = rpc + .get_compressed_account(user_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); - // Verify compressed game session - let compressed_game_session = rpc + let compressed_game = rpc .get_compressed_account(game_compressed_address, None) .await .unwrap() .value .unwrap(); - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Complex Game"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); - - // Verify Light Token account - let spl_mint = find_mint_address(&mint_signer_pubkey).0; - let (_, token_account_address) = - csdk_anchor_full_derived_test::get_ctokensigner_seeds(&payer.pubkey(), &spl_mint); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) + // Fetch compressed vault token account + let compressed_vault_accounts = rpc + .get_compressed_token_accounts_by_owner(&vault_pda, None, None) .await .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have compressed token accounts" - ); -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - authority: &Keypair, - mint_authority_keypair: &Keypair, - some_account: &Keypair, - session_id: u64, - category_id: u64, -) -> Pubkey { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = csdk_anchor_full_derived_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - mint_signer: mint_signer.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - authority: authority.pubkey(), - mint_authority, - some_account: some_account.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - compress_token_program_cpi_authority: CPI_AUTHORITY_PDA.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); + .value + .items; + let compressed_vault = &compressed_vault_accounts[0]; + // Get validity proof for PDAs + vault let rpc_result = rpc .get_validity_proof( - vec![], vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, + compressed_user.hash, + compressed_game.hash, + compressed_vault.account.hash, ], + vec![], None, ) .await .unwrap() .value; - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); + // Build RentFreeDecompressAccount using from_seeds and from_ctoken helpers + let decompress_accounts = vec![ + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(user_record_pda, compressed_user.clone()), + UserRecordSeeds { + authority: authority.pubkey(), + mint_authority: mint_authority.pubkey(), + owner, + category_id, + }, + ) + .expect("UserRecord seed verification failed"), + RentFreeDecompressAccount::from_seeds( + AccountInterface::cold(game_session_pda, compressed_game.clone()), + GameSessionSeeds { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + session_id, + }, + ) + .expect("GameSession seed verification failed"), + RentFreeDecompressAccount::from_ctoken( + AccountInterface::cold(vault_pda, compressed_vault.account.clone()), + TokenAccountVariant::Vault { cmint: cmint_pda }, + ) + .expect("CToken variant construction failed"), + ]; + + // Build decompress instruction + // No SeedParams needed - data.* seeds from unpacked account, ctx.* from variant idx + let decompress_instruction = compressible_instruction::build_decompress_idempotent( + &program_id, + decompress_accounts, + compressible_instruction::decompress::accounts(payer.pubkey(), config_pda, payer.pubkey()), + rpc_result, + ) + .unwrap() + .expect("Should have cold accounts to decompress"); - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); + rpc.create_and_send_transaction(&[decompress_instruction], &payer.pubkey(), &[&payer]) + .await + .expect("PDA + vault decompression should succeed"); - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; + // Assert PDAs are back on-chain + assert_onchain_exists(&mut rpc, &user_record_pda).await; + assert_onchain_exists(&mut rpc, &game_session_pda).await; - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); + // Assert vault is back on-chain with correct balance + assert_onchain_exists(&mut rpc, &vault_pda).await; + let vault_after = parse_token(&rpc.get_account(vault_pda).await.unwrap().unwrap().data); + assert_eq!(vault_after.amount, vault_mint_amount); - let instruction_data = - csdk_anchor_full_derived_test::instruction::CreateUserRecordAndGameSession { - account_data: AccountCreationData { - // Instruction data fields (accounts come from ctx.accounts.*) - owner: user.pubkey(), - category_id, - user_name: "Complex User".to_string(), - session_id, - game_type: "Complex Game".to_string(), - placeholder_id: 0, - counter: 0, - mint_name: "Complex Token".to_string(), - mint_symbol: "CPLX".to_string(), - mint_uri: "https://example.com/complex.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, - }, - compression_params: CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; + // Verify compressed vault token is consumed (no more compressed token accounts for vault) + let remaining_vault = rpc + .get_compressed_token_accounts_by_owner(&vault_pda, None, None) + .await + .unwrap() + .value + .items; + assert!(remaining_vault.is_empty()); - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), + // PHASE 4: Decompress user ATA via new high-performance API pattern + use light_compressible_client::{ + build_decompress_token_accounts, decompress_mint, decompress_token_accounts, + parse_token_account_interface, }; - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, mint_authority_keypair, authority], - ) - .await; + // Step 1: Fetch raw account interface (Account bytes always present) + let account_interface = rpc + .get_ata_account_interface(&cmint_pda, &payer.pubkey()) + .await + .expect("get_ata_account_interface should succeed"); + + // Verify raw bytes are present (even for cold accounts) + assert_eq!(account_interface.account.data.len(), 165); + + // Step 2: Parse into TokenAccountInterface (sync, no RPC) + let parsed = parse_token_account_interface(&account_interface) + .expect("parse_token_account_interface should succeed"); + + // Verify it's cold (compressed) + assert!(parsed.is_cold, "ATA should be cold after warp"); + assert!( + parsed.decompression_context.is_some(), + "Cold ATA should have decompression_context" + ); + + // Amount accessible via TokenData + assert_eq!(parsed.amount(), user_ata_mint_amount); + + // Step 3: Get proof and build instructions (sync after proof) + let cold_hash = parsed.hash().expect("Cold ATA should have hash"); + let proof = rpc + .get_validity_proof(vec![cold_hash], vec![], None) + .await + .expect("get_validity_proof should succeed") + .value; + + // Step 4: Build decompress instructions (sync) + let ata_instructions = build_decompress_token_accounts(&[parsed], payer.pubkey(), Some(proof)) + .expect("build_decompress_token_accounts should succeed"); + + assert!(!ata_instructions.is_empty(), "Should have instructions"); + + rpc.create_and_send_transaction(&ata_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("ATA decompression should succeed"); + + // Assert user ATA is back on-chain with correct balance + assert_onchain_exists(&mut rpc, &user_ata_pda).await; + let user_ata_after = parse_token(&rpc.get_account(user_ata_pda).await.unwrap().unwrap().data); + assert_eq!(user_ata_after.amount, user_ata_mint_amount); + + // Verify idempotency: calling again should return empty vec + let account_interface_again = rpc + .get_ata_account_interface(&cmint_pda, &payer.pubkey()) + .await + .expect("get_ata_account_interface should succeed"); + + let parsed_again = parse_token_account_interface(&account_interface_again) + .expect("parse_token_account_interface should succeed"); + + assert!( + !parsed_again.is_cold, + "ATA should be hot after decompression" + ); + assert!( + parsed_again.decompression_context.is_none(), + "Hot ATA should not have decompression_context" + ); + // Using async wrapper (alternative pattern) + let ata_instructions_again = decompress_token_accounts(&[parsed_again], payer.pubkey(), &rpc) + .await + .expect("decompress_token_accounts should succeed"); assert!( - result.is_ok(), - "Complex seed creation transaction should succeed: {:?}", - result + ata_instructions_again.is_empty(), + "Should return empty vec when already decompressed" ); - mint_signer.pubkey() + // PHASE 5: Decompress CMint via decompress_mint (lean wrapper) + let mint_interface = rpc + .get_mint_interface(&mint_signer_pda) + .await + .expect("get_mint_interface should succeed"); + + // Verify it's cold (compressed) + assert!(mint_interface.is_cold(), "Mint should be cold after warp"); + + // Decompress using lean wrapper (fetches proof internally) + let mint_instructions = decompress_mint(&mint_interface, payer.pubkey(), &rpc) + .await + .expect("decompress_mint should succeed"); + + if !mint_instructions.is_empty() { + rpc.create_and_send_transaction(&mint_instructions, &payer.pubkey(), &[&payer]) + .await + .expect("Mint decompression should succeed"); + } + + // Assert CMint is back on-chain + assert_onchain_exists(&mut rpc, &cmint_pda).await; + + // Verify calling again returns empty vec (idempotent) + let mint_interface_again = rpc + .get_mint_interface(&mint_signer_pda) + .await + .expect("get_mint_interface should succeed"); + assert!( + mint_interface_again.is_hot(), + "Mint should be hot after decompression" + ); + let mint_instructions_again = decompress_mint(&mint_interface_again, payer.pubkey(), &rpc) + .await + .expect("decompress_mint should succeed"); + assert!( + mint_instructions_again.is_empty(), + "Should return empty vec when mint already decompressed" + ); } diff --git a/sdk-tests/sdk-compressible-test/Anchor.toml b/sdk-tests/sdk-compressible-test/Anchor.toml deleted file mode 100644 index 7225c40f12..0000000000 --- a/sdk-tests/sdk-compressible-test/Anchor.toml +++ /dev/null @@ -1,19 +0,0 @@ -[toolchain] - -[features] -resolution = true -skip-lint = false - -[programs.localnet] -sdk_compressible_test = "FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah" - -[registry] -url = "https://api.apr.dev" - -[provider] -cluster = "Localnet" -wallet = "~/.config/solana/id.json" - -[scripts] -test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" - diff --git a/sdk-tests/sdk-compressible-test/Cargo.toml b/sdk-tests/sdk-compressible-test/Cargo.toml deleted file mode 100644 index ccb30f3186..0000000000 --- a/sdk-tests/sdk-compressible-test/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "sdk-compressible-test" -version = "0.1.0" -description = "Simple Anchor program template with user records" -edition = "2021" - -[lib] -crate-type = ["cdylib", "lib"] -name = "sdk_compressible_test" - -[features] -no-entrypoint = [] -no-idl = [] -no-log-ix-name = [] -cpi = ["no-entrypoint"] -default = [] -idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] -test-sbf = [] - -[dependencies] -light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } -light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } -light-hasher = { workspace = true, features = ["solana"] } -solana-program = { workspace = true } -solana-system-interface = { workspace = true } -light-macros = { workspace = true, features = ["solana"] } -borsh = { workspace = true } -light-compressed-account = { workspace = true, features = ["solana"] } -anchor-lang = { workspace = true, features = ["idl-build"] } -anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } -light-token-interface = { workspace = true, features = ["anchor"] } -light-token-sdk = { workspace = true, features = ["anchor", "compressible"] } -light-token-types = { workspace = true, features = ["anchor"] } -light-compressible = { workspace = true, features = ["anchor"] } - -[dev-dependencies] -light-token-client = { workspace = true } -light-program-test = { workspace = true, features = ["devenv", "v2"] } -light-client = { workspace = true, features = ["v2"] } -light-compressible-client = { workspace = true, features = ["anchor"] } -light-test-utils = { workspace = true} -tokio = { workspace = true } -solana-sdk = { workspace = true } -solana-logger = { workspace = true } -solana-instruction = { workspace = true } -solana-pubkey = { workspace = true } -solana-signature = { workspace = true } -solana-signer = { workspace = true } -solana-keypair = { workspace = true } -solana-account = { workspace = true } -bincode = "1.3" - -[lints.rust.unexpected_cfgs] -level = "allow" -check-cfg = [ - 'cfg(target_os, values("solana"))', - 'cfg(feature, values("frozen-abi", "no-entrypoint"))', -] diff --git a/sdk-tests/sdk-compressible-test/Xargo.toml b/sdk-tests/sdk-compressible-test/Xargo.toml deleted file mode 100644 index 1744f098ae..0000000000 --- a/sdk-tests/sdk-compressible-test/Xargo.toml +++ /dev/null @@ -1,2 +0,0 @@ -[target.bpfel-unknown-unknown.dependencies.std] -features = [] \ No newline at end of file diff --git a/sdk-tests/sdk-compressible-test/package.json b/sdk-tests/sdk-compressible-test/package.json deleted file mode 100644 index 3711173d19..0000000000 --- a/sdk-tests/sdk-compressible-test/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@lightprotocol/sdk-compressible-test", - "version": "0.1.0", - "license": "Apache-2.0", - "scripts": { - "build": "cargo build-sbf", - "test": "cargo test-sbf -p sdk-compressible-test -- --nocapture" - }, - "nx": {} -} - diff --git a/sdk-tests/sdk-compressible-test/src/constants.rs b/sdk-tests/sdk-compressible-test/src/constants.rs deleted file mode 100644 index 2fcae0aa3a..0000000000 --- a/sdk-tests/sdk-compressible-test/src/constants.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub const POOL_VAULT_SEED: &str = "pool_vault"; -pub const USER_RECORD_SEED: &str = "user_record"; -pub const CTOKEN_SIGNER_SEED: &str = "ctoken_signer"; diff --git a/sdk-tests/sdk-compressible-test/src/errors.rs b/sdk-tests/sdk-compressible-test/src/errors.rs deleted file mode 100644 index cef4dc033f..0000000000 --- a/sdk-tests/sdk-compressible-test/src/errors.rs +++ /dev/null @@ -1,92 +0,0 @@ -use anchor_lang::prelude::*; - -#[repr(u32)] -pub enum ErrorCode { - InvalidAccountCount, - InvalidRentRecipient, - MintCreationFailed, - MissingCompressedTokenProgram, - MissingCompressedTokenProgramAuthorityPDA, - RentRecipientMismatch, - InvalidAccountDiscriminator, - DerivedTokenAccountMismatch, - MissingAuthority, - MissingCpiContext, -} - -#[automatically_derived] -impl ::core::fmt::Debug for ErrorCode { - #[inline] - fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result { - ::core::fmt::Formatter::write_str( - f, - match self { - ErrorCode::InvalidAccountCount => "InvalidAccountCount", - ErrorCode::InvalidRentRecipient => "InvalidRentRecipient", - ErrorCode::MintCreationFailed => "MintCreationFailed", - ErrorCode::MissingCompressedTokenProgram => "MissingCompressedTokenProgram", - ErrorCode::MissingCompressedTokenProgramAuthorityPDA => { - "MissingCompressedTokenProgramAuthorityPDA" - } - ErrorCode::RentRecipientMismatch => "RentRecipientMismatch", - ErrorCode::InvalidAccountDiscriminator => "InvalidAccountDiscriminator", - ErrorCode::DerivedTokenAccountMismatch => "DerivedTokenAccountMismatch", - ErrorCode::MissingAuthority => "MissingAuthority", - ErrorCode::MissingCpiContext => "MissingCpiContext", - }, - ) - } -} - -impl std::fmt::Display for ErrorCode { - fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { - match self { - ErrorCode::InvalidAccountCount => fmt.write_fmt(format_args!( - "Invalid account count: PDAs and compressed accounts must match", - )), - ErrorCode::InvalidRentRecipient => { - fmt.write_fmt(format_args!("Rent recipient does not match config")) - } - ErrorCode::MintCreationFailed => { - fmt.write_fmt(format_args!("Failed to create compressed mint")) - } - ErrorCode::MissingCompressedTokenProgram => fmt.write_fmt(format_args!( - "Compressed token program account not found in remaining accounts", - )), - ErrorCode::MissingCompressedTokenProgramAuthorityPDA => fmt.write_fmt(format_args!( - "Compressed token program authority PDA account not found in remaining accounts", - )), - ErrorCode::RentRecipientMismatch => { - fmt.write_fmt(format_args!("Rent recipient does not match config")) - } - ErrorCode::InvalidAccountDiscriminator => fmt.write_fmt(format_args!( - "Trying to compress account with invalid discriminator" - )), - ErrorCode::DerivedTokenAccountMismatch => fmt.write_fmt(format_args!( - "Derived token account address must match owner_info.key" - )), - ErrorCode::MissingAuthority => fmt.write_fmt(format_args!( - "Authority account is missing from CPI accounts" - )), - ErrorCode::MissingCpiContext => fmt.write_fmt(format_args!( - "CPI context account is missing from CPI accounts" - )), - } - } -} - -impl From for ProgramError { - fn from(e: ErrorCode) -> Self { - ProgramError::Custom(e as u32) - } -} - -#[repr(u32)] -pub enum CompressibleInstructionError { - InvalidRentRecipient, - CTokenDecompressionNotImplemented, - PdaDecompressionNotImplemented, - TokenCompressionNotImplemented, - PdaCompressionNotImplemented, - MissingSeedAccount, -} diff --git a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs b/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs deleted file mode 100644 index aaa5492cbb..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instruction_accounts.rs +++ /dev/null @@ -1,223 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::compressible::OPTION_COMPRESSION_INFO_SPACE; - -/// CompressAccountsIdempotent, DecompressAccountsIdempotent, -/// InitializeCompressionConfig, UpdateCompressionConfig accounts are all -/// auto-generated by compressible_instructions macro. -use crate::state::*; - -#[derive(Accounts)] -pub struct CreateRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + owner + string len + name + score + - // option. Note that in the onchain space - // CompressionInfo is always Some. - space = 8 + 32 + 4 + 32 + 8 + OPTION_COMPRESSION_INFO_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(placeholder_id: u64)] -pub struct CreatePlaceholderRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + compression_info + owner + string len + name + placeholder_id - space = 8 + OPTION_COMPRESSION_INFO_SPACE + 32 + 4 + 32 + 8, - seeds = [b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - bump, - )] - pub placeholder_record: Account<'info, PlaceholderRecord>, - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(account_data: AccountCreationData)] -pub struct CreateUserRecordAndGameSession<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - init, - payer = user, - // discriminator + owner + string len + name + score + - // option. Note that in the onchain space - // CompressionInfo is always Some. - space = 8 + 32 + 4 + 32 + 8 + OPTION_COMPRESSION_INFO_SPACE, - seeds = [b"user_record", user.key().as_ref()], - bump, - )] - pub user_record: Account<'info, UserRecord>, - #[account( - init, - payer = user, - // discriminator + option + session_id + player + - // string len + game_type + start_time + end_time(Option) + score - space = 8 + OPTION_COMPRESSION_INFO_SPACE + 8 + 32 + 4 + 32 + 8 + 9 + 8, - seeds = [b"game_session", account_data.session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - - // Compressed mint creation accounts - only token-specific ones needed - /// The mint signer used for PDA derivation - pub mint_signer: Signer<'info>, - - /// The mint authority used for PDA derivation - pub mint_authority: Signer<'info>, - - /// Compressed token program - /// CHECK: Program ID validated using LIGHT_TOKEN_PROGRAM_ID constant - pub ctoken_program: UncheckedAccount<'info>, - - /// CHECK: CPI authority of the compressed token program - pub compress_token_program_cpi_authority: UncheckedAccount<'info>, - - /// Needs to be here for the init anchor macro to work. - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -#[instruction(session_id: u64)] -pub struct CreateGameSession<'info> { - #[account(mut)] - pub player: Signer<'info>, - #[account( - init, - payer = player, - space = 8 + 24 + 8 + 32 + 4 + 32 + 8 + 9 + 8, // discriminator + compression_info + session_id + player + string len + game_type + start_time + end_time(Option) + score - seeds = [b"game_session", session_id.to_le_bytes().as_ref()], - bump, - )] - pub game_session: Account<'info, GameSession>, - pub system_program: Program<'info, System>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct UpdateRecord<'info> { - #[account(mut)] - pub user: Signer<'info>, - #[account( - mut, - seeds = [b"user_record", user.key().as_ref()], - bump, - constraint = user_record.owner == user.key() - )] - pub user_record: Account<'info, UserRecord>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -#[instruction(session_id: u64)] -pub struct UpdateGameSession<'info> { - #[account(mut)] - pub player: Signer<'info>, - #[account( - mut, - seeds = [b"game_session", session_id.to_le_bytes().as_ref()], - bump, - constraint = game_session.player == player.key() - )] - pub game_session: Account<'info, GameSession>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// The global config account - /// CHECK: Config is validated by the SDK's load_checked method - pub config: AccountInfo<'info>, - /// Rent recipient - must match config - /// CHECK: Rent recipient is validated against the config - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, -} - -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// The global config account - /// CHECK: load_checked. - pub config: AccountInfo<'info>, - /// UNCHECKED: Anyone can pay to init PDAs. - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: anyone can pay (optional - only needed if decompressing tokens) - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - /// CHECK: checked by SDK - pub ctoken_config: Option>, - /// CHECK: - pub ctoken_program: Option>, - /// CHECK: - pub ctoken_cpi_authority: Option>, - /// CHECK: unchecked. - pub some_mint: UncheckedAccount<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// The program's data account - /// CHECK: Program data account is validated by the SDK - pub program_data: AccountInfo<'info>, - /// The program's upgrade authority (must sign) - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, -} - -#[derive(Accounts)] -pub struct UpdateCompressionConfig<'info> { - /// CHECK: Config PDA is created and validated by the SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// Must match the update authority stored in config - pub authority: Signer<'info>, -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs deleted file mode 100644 index 4e8a41acdb..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/compress_accounts_idempotent.rs +++ /dev/null @@ -1,135 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{compress_account::prepare_account_for_compression, CompressibleConfig}, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{account_meta::CompressedAccountMetaNoLamportsNoAddress, ValidityProof}, - LightDiscriminator, -}; - -/// Auto-generated by compressible_instructions macro. -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - let compression_config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - if ctx.accounts.rent_sponsor.key() != compression_config.rent_sponsor { - msg!( - "rent recipient passed: {:?}", - ctx.accounts.rent_sponsor.key() - ); - msg!( - "rent recipient config: {:?}", - compression_config.rent_sponsor - ); - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - let cpi_accounts = CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - LIGHT_CPI_SIGNER, - ); - - let system_accounts_end = cpi_accounts.system_accounts_end_offset(); - let solana_accounts = &cpi_accounts.to_account_infos()[system_accounts_end..]; - - let mut compressed_pda_infos = Vec::new(); - let mut pda_indices_to_close: Vec = Vec::new(); - let mut compressed_account_idx = 0; - - for (i, account_info) in solana_accounts.iter().enumerate() { - if account_info.data_is_empty() { - msg!("No data. Account already compressed or uninitialized. Skipping."); - continue; - } - if account_info.owner == &crate::ID { - let data = account_info.try_borrow_data()?; - let discriminator = &data[0..8]; - let meta = compressed_accounts[compressed_account_idx]; - compressed_account_idx += 1; - - // TODO: consider CHECKING seeds. - match discriminator { - d if d == UserRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = UserRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - d if d == GameSession::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = GameSession::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - d if d == PlaceholderRecord::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data()?; - let mut account_data = - PlaceholderRecord::try_deserialize(&mut &data_borrow[..])?; - drop(data_borrow); - - let compressed_info = prepare_account_for_compression::( - &crate::ID, - account_info, - &mut account_data, - &meta, - &cpi_accounts, - &compression_config.address_space, - )?; - - compressed_pda_infos.push(compressed_info); - pda_indices_to_close.push(i); - } - _ => { - return Err(ProgramError::from(ErrorCode::InvalidAccountDiscriminator).into()); - } - } - } - } - let has_pdas = !compressed_pda_infos.is_empty(); - if has_pdas { - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts)?; - - // Close - for idx in pda_indices_to_close.into_iter() { - let mut info = solana_accounts[idx].clone(); - light_sdk::compressible::close::close(&mut info, ctx.accounts.rent_sponsor.clone()) - .map_err(anchor_lang::prelude::ProgramError::from)?; - } - } - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs deleted file mode 100644 index c478821ecd..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_game_session.rs +++ /dev/null @@ -1,77 +0,0 @@ -use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, - session_id: u64, - game_type: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let game_session = &mut ctx.accounts.game_session; - - // Load config from the config account - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - // Set your account data. - game_session.session_id = session_id; - game_session.player = ctx.accounts.player.key(); - game_session.game_type = game_type; - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - // Check that rent recipient matches your config. - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Create CPI accounts. - let player_account_info = ctx.accounts.player.to_account_info(); - let cpi_accounts = CpiAccounts::new( - &player_account_info, - ctx.remaining_accounts, - LIGHT_CPI_SIGNER, - ); - - // Prepare new address params. The cpda takes the address of the - // compressible pda account as seed. - let new_address_params = address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(0)); - - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs deleted file mode 100644 index f3ee307cdd..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_placeholder_record.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_placeholder_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, - placeholder_id: u64, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let placeholder_record = &mut ctx.accounts.placeholder_record; - - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - placeholder_record.owner = ctx.accounts.user.key(); - placeholder_record.name = name; - placeholder_record.placeholder_id = placeholder_id; - - // Verify rent recipient matches config - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Create CPI accounts - let user_account_info = ctx.accounts.user.to_account_info(); - let cpi_accounts = - CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); - - let new_address_params = address_tree_info.into_new_address_params_assigned_packed( - placeholder_record.key().to_bytes().into(), - Some(0), - ); - - let placeholder_info = placeholder_record.to_account_info(); - let placeholder_data_mut = &mut **placeholder_record; - let compressed_info = prepare_compressed_account_on_init::( - &placeholder_info, - placeholder_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - false, // with_data = false for empty compressed account - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - // Note: PDA is NOT closed in this example (compression_info is set, account remains) - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs deleted file mode 100644 index 81d918c412..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_record.rs +++ /dev/null @@ -1,68 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, - instruction::{PackedAddressTreeInfo, ValidityProof}, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; - -pub fn create_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, -) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - - // 1. Load config from the config account - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - user_record.owner = ctx.accounts.user.key(); - user_record.name = name; - user_record.score = 11; - - // 2. Verify rent recipient matches config - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // 3. Create CPI accounts - let user_account_info = ctx.accounts.user.to_account_info(); - let cpi_accounts = - CpiAccounts::new(&user_account_info, ctx.remaining_accounts, LIGHT_CPI_SIGNER); - - let new_address_params = address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compressed_address, - new_address_params, - output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_new_addresses(&[new_address_params]) - .with_account_infos(&[compressed_info]) - .invoke(cpi_accounts)?; - - // Close the PDA - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs deleted file mode 100644 index c5079a9612..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/create_user_record_and_game_session.rs +++ /dev/null @@ -1,218 +0,0 @@ -use anchor_lang::{ - prelude::*, - solana_program::{instruction::Instruction, program::invoke, sysvar::clock::Clock}, -}; -use light_compressed_account::instruction_data::traits::LightInstructionData; -use light_sdk::{ - compressible::{ - compress_account_on_init::prepare_compressed_account_on_init, CompressibleConfig, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, -}; -use light_sdk_types::{ - cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, -}; -use light_token_interface::instructions::mint_action::{MintToCompressedAction, Recipient}; -use light_token_sdk::compressed_token::{ - create_compressed_mint::find_mint_address, mint_action::MintActionMetaConfig, -}; - -use crate::{errors::ErrorCode, instruction_accounts::*, seeds::*, state::*, LIGHT_CPI_SIGNER}; -pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, -) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - let game_session = &mut ctx.accounts.game_session; - - // Load your config checked. - let config = CompressibleConfig::load_checked(&ctx.accounts.config, &crate::ID)?; - - // Check that rent recipient matches your config. - if ctx.accounts.rent_sponsor.key() != config.rent_sponsor { - return Err(ProgramError::from(ErrorCode::RentRecipientMismatch).into()); - } - - // Set your account data. - user_record.owner = ctx.accounts.user.key(); - user_record.name = account_data.user_name.clone(); - user_record.score = 11; - - game_session.session_id = account_data.session_id; - game_session.player = ctx.accounts.user.key(); - game_session.game_type = account_data.game_type.clone(); - game_session.start_time = Clock::get()?.unix_timestamp as u64; - game_session.end_time = None; - game_session.score = 0; - - // Create CPI accounts from remaining accounts - let cpi_accounts = CpiAccounts::new_with_config( - ctx.accounts.user.as_ref(), - ctx.remaining_accounts, - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ); - let cpi_context_pubkey = cpi_accounts.cpi_context().unwrap().key(); - let cpi_context_account = cpi_accounts.cpi_context().unwrap(); - - // Prepare new address params. One per pda account. - let user_new_address_params = compression_params - .user_address_tree_info - .into_new_address_params_assigned_packed(user_record.key().to_bytes().into(), Some(0)); - let game_new_address_params = compression_params - .game_address_tree_info - .into_new_address_params_assigned_packed(game_session.key().to_bytes().into(), Some(1)); - - let mut all_compressed_infos = Vec::new(); - - // Prepare user record for compression - let user_record_info = user_record.to_account_info(); - let user_record_data_mut = &mut **user_record; - let user_compressed_info = prepare_compressed_account_on_init::( - &user_record_info, - user_record_data_mut, - &config, - compression_params.user_compressed_address, - user_new_address_params, - compression_params.user_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - - all_compressed_infos.push(user_compressed_info); - - // Prepare game session for compression - let game_session_info = game_session.to_account_info(); - let game_session_data_mut = &mut **game_session; - let game_compressed_info = prepare_compressed_account_on_init::( - &game_session_info, - game_session_data_mut, - &config, - compression_params.game_compressed_address, - game_new_address_params, - compression_params.game_output_state_tree_index, - &cpi_accounts, - &config.address_space, - true, // with_data - )?; - all_compressed_infos.push(game_compressed_info); - - let cpi_context_accounts = CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority().unwrap(), - cpi_context: cpi_context_account, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, compression_params.proof) - .with_new_addresses(&[user_new_address_params, game_new_address_params]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - // these are custom seeds of the caller program that are used to derive the program owned onchain tokenb account PDA. - // dual use: as owner of the compressed token account. - let mint = find_mint_address(&ctx.accounts.mint_signer.key()).0; - let (_, token_account_address) = get_ctoken_signer_seeds(&ctx.accounts.user.key(), &mint); - - let output_queue = *cpi_accounts.tree_accounts().unwrap()[0].key; // Same tree as PDA - let address_tree_pubkey = *cpi_accounts.tree_accounts().unwrap()[1].key; // Same tree as PDA - - let proof = compression_params.proof.0.unwrap_or_default(); - let mut instruction_data = - light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( - 0, // root_index - proof, - compression_params.mint_with_context.mint.clone().unwrap(), - ) - .with_mint_to_compressed(MintToCompressedAction::new(vec![ - Recipient::new( - token_account_address, // TRY: THE DECOMPRESS TOKEN ACCOUNT ADDRESS IS THE OWNER OF ITS COMPRESSIBLED VERSION. - 1000, // Mint the full supply to the user - ), - Recipient::new( - get_ctoken_signer2_seeds(&ctx.accounts.user.key()).1, - 1000, - ), - Recipient::new( - get_ctoken_signer3_seeds(&ctx.accounts.user.key()).1, - 1000, - ), - Recipient::new( - get_ctoken_signer4_seeds( - &ctx.accounts.user.key(), - &ctx.accounts.user.key(), - ) - .1, // user as fee_payer - 1000, - ), - Recipient::new( - get_ctoken_signer5_seeds(&ctx.accounts.user.key(), &mint, 42).1, // Fixed index 42 - 1000, - ), - ])); - - instruction_data = instruction_data.with_cpi_context( - light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: address_tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: 1, // address tree - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 2, - read_only_address_trees: [0; 4], - }, - ); - - // Build account meta config - let mut config = MintActionMetaConfig::new_create_mint( - ctx.accounts.user.key(), // fee_payer - ctx.accounts.mint_authority.key(), - ctx.accounts.mint_signer.key(), - address_tree_pubkey, - output_queue, - ) - .with_mint_compressed_tokens(); - - // Set CPI context - config.cpi_context = Some(cpi_context_pubkey); - - // Get account metas - let account_metas = config.to_account_metas(); - - // Serialize instruction data - let data = instruction_data.data().unwrap(); - - // Build instruction - let mint_action_instruction = Instruction { - program_id: Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), - accounts: account_metas, - data, - }; - - // Get all account infos needed for the mint action - let mut account_infos = cpi_accounts.to_account_infos(); - account_infos.push( - ctx.accounts - .compress_token_program_cpi_authority - .to_account_info(), - ); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); - account_infos.push(ctx.accounts.mint_authority.to_account_info()); - account_infos.push(ctx.accounts.mint_signer.to_account_info()); - account_infos.push(ctx.accounts.user.to_account_info()); - - // Invoke the mint action instruction directly - invoke(&mint_action_instruction, &account_infos)?; - - // at the end of the instruction we always clean up all onchain pdas that we compressed - user_record.close(ctx.accounts.rent_sponsor.to_account_info())?; - game_session.close(ctx.accounts.rent_sponsor.to_account_info())?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs b/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs deleted file mode 100644 index 89cc0024ce..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/decompress_accounts_idempotent.rs +++ /dev/null @@ -1,472 +0,0 @@ -// Auto-generated by compressible_instructions macro. -use anchor_lang::prelude::*; -use light_sdk::{ - compressible::{ - decompress_idempotent::{ - into_compressed_meta_with_address, prepare_account_for_decompression_idempotent, - }, - Unpack, - }, - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, -}; -use light_sdk_types::cpi_accounts::CpiAccountsConfig; -use light_token_sdk::token::{CompressibleParamsCpi, CreateTokenAccountCpi}; -use solana_program::program_error::ProgramError; - -use crate::{constants::*, errors::ErrorCode, instruction_accounts::*, state::*, LIGHT_CPI_SIGNER}; -pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - // Helper functions to handle each account type - kept out of main frame - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_user_record<'b, 'info>( - data: UserRecord, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seeds_vec = { - let seeds: &[&[u8]] = &[USER_RECORD_SEED.as_bytes(), (data.owner).as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_game_session<'b, 'info>( - data: GameSession, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seed_binding_1 = data.session_id.to_le_bytes(); - let seeds_vec = { - let seeds: &[&[u8]] = &["game_session".as_bytes(), seed_binding_1.as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - #[allow(clippy::too_many_arguments)] - fn handle_placeholder_record<'b, 'info>( - data: PlaceholderRecord, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - solana_accounts: &[AccountInfo<'info>], - i: usize, - address_space: Pubkey, - cpi_accounts: &CpiAccounts<'b, 'info>, - rent_sponsor: &AccountInfo<'info>, - out: &mut Vec< - light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo, - >, - ) -> Result<()> { - let seed_binding_1 = data.placeholder_id.to_le_bytes(); - let seeds_vec = { - let seeds: &[&[u8]] = &["placeholder_record".as_bytes(), seed_binding_1.as_ref()]; - let (_pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - vec![seeds[0].to_vec(), seeds[1].to_vec(), vec![bump]] - }; - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v: &Vec| v.as_slice()).collect(); - let infos = prepare_account_for_decompression_idempotent::( - &crate::ID, - data, - into_compressed_meta_with_address(meta, &solana_accounts[i], address_space, &crate::ID), - &solana_accounts[i], - rent_sponsor, - cpi_accounts, - seed_refs.as_slice(), - ) - .map_err(ProgramError::from)?; - out.extend(infos); - Ok(()) - } - - #[inline(never)] - fn check_account_types(compressed_accounts: &[CompressedAccountData]) -> (bool, bool) { - let (mut has_tokens, mut has_pdas) = (false, false); - for c in compressed_accounts { - match c.data { - CompressedAccountVariant::PackedCTokenData(_) => { - has_tokens = true; - } - _ => has_pdas = true, - } - if has_tokens && has_pdas { - break; - } - } - (has_tokens, has_pdas) - } - /// Helper function to process token decompression - separated to avoid stack overflow - #[inline(never)] - #[allow(clippy::too_many_arguments, clippy::extra_unused_lifetimes)] - fn process_tokens<'a, 'b, 'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[anchor_lang::prelude::AccountInfo<'info>], - fee_payer: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_program: &anchor_lang::prelude::UncheckedAccount<'info>, - ctoken_rent_sponsor: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_cpi_authority: &anchor_lang::prelude::UncheckedAccount<'info>, - ctoken_config: &anchor_lang::prelude::AccountInfo<'info>, - config: &anchor_lang::prelude::AccountInfo<'info>, - ctoken_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )>, - proof: light_sdk::instruction::ValidityProof, - cpi_accounts: &CpiAccounts<'b, 'info>, - post_system_accounts: &[anchor_lang::prelude::AccountInfo<'info>], - has_pdas: bool, - ) -> Result<()> { - let mut token_decompress_indices: Box< - Vec, - > = Box::new(Vec::with_capacity(ctoken_accounts.len())); - // Collect per-owner signer seed groups; invoke_signed requires one seed group per PDA signer - let mut token_signers_seed_groups: Vec>> = - Vec::with_capacity(ctoken_accounts.len()); - let packed_accounts = post_system_accounts; - use crate::seeds::ctoken_seed_system::{CTokenSeedContext, CTokenSeedProvider}; - let seed_context = CTokenSeedContext { - accounts, - remaining_accounts, - }; - let authority = cpi_accounts - .authority() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; - - for (token_data, meta) in ctoken_accounts.into_iter() { - let owner_index: u8 = token_data.token_data.owner; - let mint_index: u8 = token_data.token_data.mint; - let mint_info = packed_accounts[mint_index as usize].to_account_info(); - let owner_info = packed_accounts[owner_index as usize].to_account_info(); - let (ctoken_signer_seeds, derived_token_account_address) = - token_data.variant.get_seeds(&seed_context); - { - if derived_token_account_address != *owner_info.key { - msg!( - "derived_token_account_address: {:?}", - derived_token_account_address - ); - msg!("owner_info.key: {:?}", owner_info.key); - return Err(ProgramError::from(ErrorCode::DerivedTokenAccountMismatch).into()); - } - - let seed_refs: Vec<&[u8]> = - ctoken_signer_seeds.iter().map(|s| s.as_slice()).collect(); - let seeds_slice: &[&[u8]] = &seed_refs; - - // Build CompressToPubkey from the signer seeds - // The last element is the bump, all preceding elements are the seeds - let bump = ctoken_signer_seeds - .last() - .and_then(|b| b.first().copied()) - .unwrap_or(0); - let seeds_without_bump: Vec> = ctoken_signer_seeds - .iter() - .take(ctoken_signer_seeds.len().saturating_sub(1)) - .cloned() - .collect(); - let compress_to_pubkey = - light_token_interface::instructions::extensions::CompressToPubkey { - bump, - program_id: crate::ID.to_bytes(), - seeds: seeds_without_bump, - }; - - CreateTokenAccountCpi { - payer: fee_payer.clone().to_account_info(), - account: owner_info.clone(), - mint: mint_info.clone(), - owner: *authority.clone().to_account_info().key, - compressible: CompressibleParamsCpi { - compressible_config: ctoken_config.to_account_info(), - rent_sponsor: ctoken_rent_sponsor.clone().to_account_info(), - system_program: accounts.system_program.to_account_info(), - pre_pay_num_epochs: 2, - lamports_per_write: None, - compress_to_account_pubkey: Some(compress_to_pubkey), - token_account_version: - light_token_interface::state::TokenDataVersion::ShaFlat, - compression_only: false, - }, - } - .invoke_signed(&[seeds_slice])?; - } - - // Construct MultiInputTokenDataWithContext from token data and meta - let source = - light_token_interface::instructions::transfer2::MultiInputTokenDataWithContext { - owner: token_data.token_data.owner, - amount: token_data.token_data.amount, - has_delegate: token_data.token_data.has_delegate, - delegate: token_data.token_data.delegate, - mint: token_data.token_data.mint, - version: token_data.token_data.version, - merkle_context: meta.tree_info.into(), - root_index: meta.tree_info.root_index, - }; - let decompress_index = - light_token_sdk::compressed_token::decompress_full::DecompressFullIndices { - source, - destination_index: owner_index, - tlv: None, - }; - token_decompress_indices.push(decompress_index); - token_signers_seed_groups.push(ctoken_signer_seeds); - } - - let ctoken_ix = - light_token_sdk::compressed_token::decompress_full::decompress_full_token_accounts_with_indices( - fee_payer.key(), - proof, - if has_pdas { - Some(cpi_context.key()) - } else { - None - }, - &token_decompress_indices, - packed_accounts, - ) - .map_err(anchor_lang::prelude::ProgramError::from)?; - { - let mut all_account_infos = <[_]>::into_vec(Box::new([fee_payer.to_account_info()])); - all_account_infos.extend(ctoken_cpi_authority.to_account_infos()); - all_account_infos.extend(ctoken_program.to_account_infos()); - all_account_infos.extend(ctoken_rent_sponsor.to_account_infos()); - all_account_infos.extend(config.to_account_infos()); - all_account_infos.extend(cpi_accounts.to_account_infos()); - // Build &[&[&[u8]]] where each inner slice is a distinct PDA seed group - let signer_seed_refs: Vec> = token_signers_seed_groups - .iter() - .map(|group| group.iter().map(|s| s.as_slice()).collect()) - .collect(); - let signer_seed_slices: Vec<&[&[u8]]> = - signer_seed_refs.iter().map(|g| g.as_slice()).collect(); - - anchor_lang::solana_program::program::invoke_signed( - &ctoken_ix, - all_account_infos.as_slice(), - signer_seed_slices.as_slice(), - )?; - } - Ok(()) - } - - let compression_config = light_sdk::compressible::CompressibleConfig::load_checked( - &ctx.accounts.config, - &crate::ID, - )?; - let address_space = compression_config.address_space[0]; - - let (has_tokens, has_pdas) = check_account_types(&compressed_accounts); - if !has_tokens && !has_pdas { - return Ok(()); - } - - // Pre-count for exact alloc. - let (mut token_count, mut pda_count) = (0usize, 0usize); - for c in &compressed_accounts { - match c.data { - CompressedAccountVariant::PackedCTokenData(_) => token_count += 1, - _ => pda_count += 1, - } - } - - let mut ctoken_accounts: Vec<( - light_token_sdk::compat::PackedCTokenData, - light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - )> = Vec::with_capacity(token_count); - let mut compressed_pda_infos = Vec::with_capacity(pda_count); - - let cpi_accounts = if has_tokens { - CpiAccounts::new_with_config( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - CpiAccountsConfig::new_with_cpi_context(LIGHT_CPI_SIGNER), - ) - } else { - CpiAccounts::new( - ctx.accounts.fee_payer.as_ref(), - &ctx.remaining_accounts[system_accounts_offset as usize..], - LIGHT_CPI_SIGNER, - ) - }; - - let pda_accounts_start = ctx.remaining_accounts.len() - compressed_accounts.len(); - let solana_accounts = &ctx.remaining_accounts[pda_accounts_start..]; - let post_system_offset = cpi_accounts.system_accounts_end_offset(); - let all_infos = cpi_accounts.account_infos(); - let post_system_accounts = &all_infos[post_system_offset..]; - for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { - let unpacked_data = compressed_data.data.unpack(post_system_accounts)?; - match unpacked_data { - CompressedAccountVariant::UserRecord(data) => { - handle_user_record( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::GameSession(data) => { - handle_game_session( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::PlaceholderRecord(data) => { - handle_placeholder_record( - data, - &compressed_data.meta, - solana_accounts, - i, - address_space, - &cpi_accounts, - &ctx.accounts.rent_sponsor, - &mut compressed_pda_infos, - )?; - } - CompressedAccountVariant::PackedCTokenData(data) => { - ctoken_accounts.push((data, compressed_data.meta)); - } - CompressedAccountVariant::PackedUserRecord(_) - | CompressedAccountVariant::PackedGameSession(_) - | CompressedAccountVariant::PackedPlaceholderRecord(_) - | CompressedAccountVariant::CTokenData(_) => { - panic!("internal error: entered unreachable code"); - } - } - } - // return if no uninitialized accounts. - let has_pdas = !compressed_pda_infos.is_empty(); - let has_tokens = !ctoken_accounts.is_empty(); - if !has_pdas && !has_tokens { - return Ok(()); - } - let fee_payer = ctx.accounts.fee_payer.as_ref(); - - // init PDAs. - if has_pdas && has_tokens { - let authority = cpi_accounts - .authority() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingAuthority))?; - let cpi_context = cpi_accounts - .cpi_context() - .map_err(|_| anchor_lang::prelude::ProgramError::from(ErrorCode::MissingCpiContext))?; - let system_cpi_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { - fee_payer, - authority, - cpi_context, - cpi_signer: LIGHT_CPI_SIGNER, - }; - LightSystemProgramCpi::new_cpi(LIGHT_CPI_SIGNER, proof) - .with_account_infos(&compressed_pda_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(system_cpi_accounts)?; - } else if has_pdas { - LightSystemProgramCpi::new_cpi(cpi_accounts.config().cpi_signer, proof) - .with_account_infos(&compressed_pda_infos) - .invoke(cpi_accounts.clone())?; - } - - // init tokens. - if has_tokens { - let ctoken_program = ctx - .accounts - .ctoken_program - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_rent_sponsor = ctx - .accounts - .ctoken_rent_sponsor - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_cpi_authority = ctx - .accounts - .ctoken_cpi_authority - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - let ctoken_config = ctx - .accounts - .ctoken_config - .as_ref() - .ok_or(ProgramError::NotEnoughAccountKeys)?; - - process_tokens( - ctx.accounts, - ctx.remaining_accounts, - fee_payer, - ctoken_program, - ctoken_rent_sponsor, - ctoken_cpi_authority, - ctoken_config, - &ctx.accounts.config, - ctoken_accounts, - proof, - &cpi_accounts, - post_system_accounts, - has_pdas, - )?; - } - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs deleted file mode 100644 index 9be151ef7c..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/initialize_compression_config.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Auto-generated by compressible_instructions macro. - -use anchor_lang::prelude::*; -use light_sdk::compressible::process_initialize_compression_config_checked; - -use crate::instruction_accounts::*; - -pub fn initialize_compression_config( - ctx: Context, - rent_sponsor: Pubkey, - address_space: Vec, -) -> Result<()> { - // For tests, set compression_authority to the program's authority (can be a PDA in real apps) - let compression_authority = ctx.accounts.authority.key(); - // Use default rent config for tests - let rent_config = light_compressible::rent::RentConfig::default(); - // Default write_top_up for tests - let write_top_up: u32 = 5_000; - process_initialize_compression_config_checked( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - &ctx.accounts.program_data.to_account_info(), - &rent_sponsor, - &compression_authority, - rent_config, - write_top_up, - address_space, - 0, // one global config for now, so bump is 0. - &ctx.accounts.payer.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - &crate::ID, - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs b/sdk-tests/sdk-compressible-test/src/instructions/mod.rs deleted file mode 100644 index 5c7bd74407..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod compress_accounts_idempotent; -pub mod create_game_session; -pub mod create_placeholder_record; -pub mod create_record; -pub mod create_user_record_and_game_session; -pub mod decompress_accounts_idempotent; -pub mod initialize_compression_config; -pub mod update_compression_config; -pub mod update_game_session; -pub mod update_record; diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs deleted file mode 100644 index 5b505ab161..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_compression_config.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Auto-generated by compressible_instructions macro. -use anchor_lang::prelude::*; -use light_sdk::compressible::process_update_compression_config; - -use crate::instruction_accounts::*; - -pub fn update_compression_config( - ctx: Context, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, -) -> Result<()> { - process_update_compression_config( - &ctx.accounts.config.to_account_info(), - &ctx.accounts.authority.to_account_info(), - new_update_authority.as_ref(), - new_rent_sponsor.as_ref(), - new_compression_authority.as_ref(), - new_rent_config, - new_write_top_up, - new_address_space, - &crate::ID, - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs deleted file mode 100644 index 0b831ee0a9..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_game_session.rs +++ /dev/null @@ -1,23 +0,0 @@ -use anchor_lang::{prelude::*, solana_program::sysvar::clock::Clock}; -use light_sdk::compressible::HasCompressionInfo; - -use crate::instruction_accounts::*; -pub fn update_game_session( - ctx: Context, - _session_id: u64, - new_score: u64, -) -> Result<()> { - let game_session = &mut ctx.accounts.game_session; - - game_session.score = new_score; - game_session.end_time = Some(Clock::get()?.unix_timestamp as u64); - - // Rent top-up on write using the abstracted method - game_session.compression_info().top_up_rent( - &game_session.to_account_info(), - &ctx.accounts.player.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs b/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs deleted file mode 100644 index 8f76d4282d..0000000000 --- a/sdk-tests/sdk-compressible-test/src/instructions/update_record.rs +++ /dev/null @@ -1,19 +0,0 @@ -use anchor_lang::prelude::*; -use light_sdk::compressible::HasCompressionInfo; - -use crate::instruction_accounts::*; - -pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { - let user_record = &mut ctx.accounts.user_record; - - user_record.name = name; - user_record.score = score; - - user_record.compression_info().top_up_rent( - &user_record.to_account_info(), - &ctx.accounts.user.to_account_info(), - &ctx.accounts.system_program.to_account_info(), - )?; - - Ok(()) -} diff --git a/sdk-tests/sdk-compressible-test/src/lib.rs b/sdk-tests/sdk-compressible-test/src/lib.rs deleted file mode 100644 index cae42204e0..0000000000 --- a/sdk-tests/sdk-compressible-test/src/lib.rs +++ /dev/null @@ -1,178 +0,0 @@ -#![allow(deprecated)] - -use anchor_lang::{prelude::*, solana_program::pubkey::Pubkey}; -use light_sdk::derive_light_cpi_signer; -use light_sdk_types::CpiSigner; - -pub mod constants; -pub mod errors; -pub mod instruction_accounts; -pub mod instructions; -pub mod seeds; -pub mod state; - -pub use constants::*; -pub use errors::*; -pub use instruction_accounts::*; -// Re-export types needed by Anchor's macro expansion -pub use light_sdk::instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, ValidityProof, -}; -pub use seeds::*; -pub use state::*; - -declare_id!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); -pub const LIGHT_CPI_SIGNER: CpiSigner = - derive_light_cpi_signer!("FAMipfVEhN4hjCLpKCvjDXXfzLsoVTqQccXzePz1L1ah"); - -#[program] -pub mod sdk_compressible_test { - use light_sdk::instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAddressTreeInfo, - ValidityProof, - }; - - use super::*; - - pub fn create_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreateRecord<'info>>, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_record::create_record( - ctx, - name, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn create_user_record_and_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateUserRecordAndGameSession<'info>>, - account_data: AccountCreationData, - compression_params: CompressionParams, - ) -> Result<()> { - instructions::create_user_record_and_game_session::create_user_record_and_game_session( - ctx, - account_data, - compression_params, - ) - } - - pub fn update_game_session( - ctx: Context, - _session_id: u64, - new_score: u64, - ) -> Result<()> { - instructions::update_game_session::update_game_session(ctx, _session_id, new_score) - } - - pub fn create_game_session<'info>( - ctx: Context<'_, '_, '_, 'info, CreateGameSession<'info>>, - session_id: u64, - game_type: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_game_session::create_game_session( - ctx, - session_id, - game_type, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn initialize_compression_config( - ctx: Context, - rent_sponsor: Pubkey, - address_space: Vec, - ) -> Result<()> { - instructions::initialize_compression_config::initialize_compression_config( - ctx, - rent_sponsor, - address_space, - ) - } - - pub fn update_record(ctx: Context, name: String, score: u64) -> Result<()> { - instructions::update_record::update_record(ctx, name, score) - } - - pub fn create_placeholder_record<'info>( - ctx: Context<'_, '_, '_, 'info, CreatePlaceholderRecord<'info>>, - placeholder_id: u64, - name: String, - proof: ValidityProof, - compressed_address: [u8; 32], - address_tree_info: PackedAddressTreeInfo, - output_state_tree_index: u8, - ) -> Result<()> { - instructions::create_placeholder_record::create_placeholder_record( - ctx, - placeholder_id, - name, - proof, - compressed_address, - address_tree_info, - output_state_tree_index, - ) - } - - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - instructions::decompress_accounts_idempotent::decompress_accounts_idempotent( - ctx, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, 'info, 'info, CompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - instructions::compress_accounts_idempotent::compress_accounts_idempotent( - ctx, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - - pub fn update_compression_config( - ctx: Context, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, - ) -> Result<()> { - instructions::update_compression_config::update_compression_config( - ctx, - new_rent_sponsor, - new_compression_authority, - new_rent_config, - new_write_top_up, - new_address_space, - new_update_authority, - ) - } -} diff --git a/sdk-tests/sdk-compressible-test/src/seeds.rs b/sdk-tests/sdk-compressible-test/src/seeds.rs deleted file mode 100644 index 414bbd6d5b..0000000000 --- a/sdk-tests/sdk-compressible-test/src/seeds.rs +++ /dev/null @@ -1,219 +0,0 @@ -// Auto-generated by macro. Seed getter implementations. - -use anchor_lang::prelude::Pubkey; - -use crate::constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED, USER_RECORD_SEED}; - -pub fn get_userrecord_seeds(owner: &Pubkey) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push((USER_RECORD_SEED.as_bytes()).to_vec()); - seed_values.push((owner.as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_gamesession_seeds(session_id: u64) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push(("game_session".as_bytes()).to_vec()); - seed_values.push((session_id.to_le_bytes().as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_placeholderrecord_seeds(placeholder_id: u64) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(2usize + 1); - seed_values.push(("placeholder_record".as_bytes()).to_vec()); - seed_values.push((placeholder_id.to_le_bytes().as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_ctokensigner_seeds(fee_payer: &Pubkey, some_mint: &Pubkey) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(3usize + 1); - seed_values.push((CTOKEN_SIGNER_SEED.as_bytes()).to_vec()); - seed_values.push((fee_payer.as_ref()).to_vec()); - seed_values.push((some_mint.as_ref()).to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(<[_]>::into_vec(Box::new([bump]))); - (seed_values, pda) -} - -pub fn get_ctoken_signer_seeds<'a>(user: &'a Pubkey, mint: &'a Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"ctoken_signer".to_vec(), - user.to_bytes().to_vec(), - mint.to_bytes().to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer2_seeds(user: &Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![b"user_vault".to_vec(), user.to_bytes().to_vec()]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer3_seeds(user: &Pubkey) -> (Vec>, Pubkey) { - let mut seeds = vec![ - POOL_VAULT_SEED.as_bytes().to_vec(), - user.to_bytes().to_vec(), - b"liquidity".to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner_authority_seeds() -> (Vec>, Pubkey) { - let mut seeds = vec![b"cpi_authority".to_vec()]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner2_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner3_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctoken_signer4_seeds<'a>( - user: &'a Pubkey, - fee_payer: &'a Pubkey, -) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"multi_account".to_vec(), - user.to_bytes().to_vec(), - fee_payer.to_bytes().to_vec(), - crate::ID.to_bytes().to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctoken_signer5_seeds<'a>( - user: &'a Pubkey, - mint: &'a Pubkey, - index: u64, -) -> (Vec>, Pubkey) { - let mut seeds = vec![ - b"indexed_vault".to_vec(), - user.to_bytes().to_vec(), - mint.to_bytes().to_vec(), - index.to_le_bytes().to_vec(), - b"final".to_vec(), - ]; - let seeds_slice = seeds.iter().map(|s| s.as_slice()).collect::>(); - let (pda, bump) = Pubkey::find_program_address(seeds_slice.as_slice(), &crate::ID); - seeds.push(vec![bump]); - (seeds, pda) -} - -pub fn get_ctokensigner4_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub fn get_ctokensigner5_authority_seeds() -> (Vec>, Pubkey) { - get_ctokensigner_authority_seeds() -} - -pub mod ctoken_seed_system { - use anchor_lang::prelude::{AccountInfo, Pubkey}; - - use super::super::{ - constants::{CTOKEN_SIGNER_SEED, POOL_VAULT_SEED}, - instruction_accounts::DecompressAccountsIdempotent, - state::CTokenAccountVariant, - }; - - pub struct CTokenSeedContext<'a, 'info> { - pub accounts: &'a DecompressAccountsIdempotent<'info>, - pub remaining_accounts: &'a [AccountInfo<'info>], - } - - pub trait CTokenSeedProvider { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> (Vec>, Pubkey); - } - - impl CTokenSeedProvider for CTokenAccountVariant { - fn get_seeds<'a, 'info>( - &self, - ctx: &CTokenSeedContext<'a, 'info>, - ) -> (Vec>, Pubkey) { - match self { - CTokenAccountVariant::CTokenSigner => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.some_mint.key.to_bytes(); - let seeds: &[&[u8]] = &[CTOKEN_SIGNER_SEED.as_bytes(), &seed_1, &seed_2]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner2 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seeds: &[&[u8]] = &[b"user_vault", &seed_1]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner3 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seeds: &[&[u8]] = &[POOL_VAULT_SEED.as_bytes(), &seed_1, b"liquidity"]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner4 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.fee_payer.key.to_bytes(); - let program_id_bytes = crate::ID.to_bytes(); - let seeds: &[&[u8]] = &[b"multi_account", &seed_1, &seed_2, &program_id_bytes]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - CTokenAccountVariant::CTokenSigner5 => { - let seed_1 = ctx.accounts.fee_payer.key.to_bytes(); - let seed_2 = ctx.accounts.some_mint.key.to_bytes(); - let index_bytes = 42u64.to_le_bytes(); - let seeds: &[&[u8]] = - &[b"indexed_vault", &seed_1, &seed_2, &index_bytes, b"final"]; - let (pda, bump) = Pubkey::find_program_address(seeds, &crate::ID); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - seeds_vec.extend(seeds.iter().map(|s| s.to_vec())); - seeds_vec.push(<[_]>::into_vec(Box::new([bump]))); - (seeds_vec, pda) - } - } - } - } -} diff --git a/sdk-tests/sdk-compressible-test/src/state.rs b/sdk-tests/sdk-compressible-test/src/state.rs deleted file mode 100644 index 3cca9dc377..0000000000 --- a/sdk-tests/sdk-compressible-test/src/state.rs +++ /dev/null @@ -1,522 +0,0 @@ -// Only CompressionParams is custom to the caller program. All other structs are -// auto-generated by macro. Compressed account variant, pack, unpack, -// hasCompressionInfo implementions. - -use anchor_lang::prelude::*; -use light_sdk::{ - account::Size, - compressible::{ - CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo, Pack as SdkPack, - Unpack as SdkUnpack, - }, - instruction::{ - account_meta::CompressedAccountMetaNoLamportsNoAddress, PackedAccounts, - PackedAddressTreeInfo, ValidityProof, - }, - LightDiscriminator, LightHasher, -}; -use light_token_interface::instructions::mint_action::CompressedMintWithContext; -use light_token_sdk::pack::Pack as _TokenPack; - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -#[repr(u8)] -pub enum CTokenAccountVariant { - CTokenSigner = 0, - CTokenSigner2 = 1, - CTokenSigner3 = 2, - CTokenSigner4 = 3, - CTokenSigner5 = 4, -} - -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum CompressedAccountVariant { - UserRecord(UserRecord), - PackedUserRecord(PackedUserRecord), - GameSession(GameSession), - PackedGameSession(PackedGameSession), - PlaceholderRecord(PlaceholderRecord), - PackedPlaceholderRecord(PackedPlaceholderRecord), - PackedCTokenData(light_token_sdk::compat::PackedCTokenData), - CTokenData(light_token_sdk::compat::CTokenData), -} - -impl Default for CompressedAccountVariant { - fn default() -> Self { - Self::UserRecord(UserRecord::default()) - } -} - -impl LightDiscriminator for CompressedAccountVariant { - const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; // This won't be used directly - const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; -} - -impl HasCompressionInfo for CompressedAccountVariant { - fn compression_info(&self) -> &CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info(), - Self::PlaceholderRecord(data) => data.compression_info(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - match self { - Self::UserRecord(data) => data.compression_info_mut(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info_mut(), - Self::PlaceholderRecord(data) => data.compression_info_mut(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - match self { - Self::UserRecord(data) => data.compression_info_mut_opt(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.compression_info_mut_opt(), - Self::PlaceholderRecord(data) => data.compression_info_mut_opt(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } - - fn set_compression_info_none(&mut self) { - match self { - Self::UserRecord(data) => data.set_compression_info_none(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.set_compression_info_none(), - Self::PlaceholderRecord(data) => data.set_compression_info_none(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -impl Size for CompressedAccountVariant { - fn size(&self) -> usize { - match self { - Self::UserRecord(data) => data.size(), - Self::PackedUserRecord(_) => unreachable!(), - Self::GameSession(data) => data.size(), - Self::PlaceholderRecord(data) => data.size(), - Self::PackedCTokenData(_) => unreachable!(), - Self::CTokenData(_) => unreachable!(), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -// Pack implementation for CompressedAccountVariant -// This delegates to the underlying type's Pack implementation -impl SdkPack for CompressedAccountVariant { - type Packed = Self; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - match self { - Self::PackedUserRecord(_) => unreachable!(), - Self::UserRecord(data) => Self::PackedUserRecord(data.pack(remaining_accounts)), - Self::GameSession(data) => Self::GameSession(data.pack(remaining_accounts)), - Self::PlaceholderRecord(data) => Self::PlaceholderRecord(data.pack(remaining_accounts)), - Self::PackedCTokenData(_) => { - unreachable!() - } - Self::CTokenData(data) => Self::PackedCTokenData(data.pack(remaining_accounts)), - Self::PackedGameSession(_) => unreachable!(), - Self::PackedPlaceholderRecord(_) => unreachable!(), - } - } -} - -// Unpack implementation for CompressedAccountVariant -// This delegates to the underlying type's Unpack implementation -impl SdkUnpack for CompressedAccountVariant { - type Unpacked = Self; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - match self { - Self::PackedUserRecord(data) => Ok(Self::UserRecord(data.unpack(remaining_accounts)?)), - Self::UserRecord(_) => unreachable!(), - Self::GameSession(data) => Ok(Self::GameSession(data.unpack(remaining_accounts)?)), - Self::PlaceholderRecord(data) => { - Ok(Self::PlaceholderRecord(data.unpack(remaining_accounts)?)) - } - Self::PackedCTokenData(_data) => Ok(self.clone()), // as-is - Self::CTokenData(_data) => unreachable!(), // as-is - Self::PackedGameSession(_data) => unreachable!(), - Self::PackedPlaceholderRecord(_data) => unreachable!(), - } - } -} - -// Auto-derived via macro. Ix data implemented for Variant. -#[derive(Clone, Debug, AnchorDeserialize, AnchorSerialize)] -pub struct CompressedAccountData { - pub meta: CompressedAccountMetaNoLamportsNoAddress, - pub data: CompressedAccountVariant, -} - -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct UserRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub score: u64, -} - -// Auto-derived via macro. -impl HasCompressionInfo for UserRecord { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl CompressedInitSpace for UserRecord { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl CompressedInitSpace for GameSession { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl CompressedInitSpace for PlaceholderRecord { - const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; -} - -impl Size for UserRecord { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for UserRecord { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - // Simple case: return owned data with compression_info = None - // We can't return Cow::Borrowed because compression_info must always be None for compressed storage - std::borrow::Cow::Owned(Self { - compression_info: None, // ALWAYS None for compressed storage - owner: self.owner, - name: self.name.clone(), - score: self.score, - }) - } -} - -#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] -pub struct PackedUserRecord { - pub compression_info: Option, - pub owner: u8, - pub name: String, - pub score: u64, -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for UserRecord { - type Packed = PackedUserRecord; - - fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { - PackedUserRecord { - compression_info: None, - owner: remaining_accounts.insert_or_get(self.owner), - name: self.name.clone(), - score: self.score, - } - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for UserRecord { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for PackedUserRecord { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for PackedUserRecord { - type Unpacked = UserRecord; - - fn unpack( - &self, - remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(UserRecord { - compression_info: None, - owner: *remaining_accounts[self.owner as usize].key, - name: self.name.clone(), - score: self.score, - }) - } -} - -// Your existing account structs must be manually extended: -// 1. Add compression_info field to the struct, with type -// Option. -// 2. add a #[skip] field for the compression_info field. -// 3. Add LightHasher, LightDiscriminator. -// 4. Add #[hash] attribute to ALL fields that can be >31 bytes. (eg Pubkeys, -// Strings) -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct GameSession { - #[skip] - pub compression_info: Option, - pub session_id: u64, - #[hash] - pub player: Pubkey, - #[max_len(32)] - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -// Auto-derived via macro. -impl HasCompressionInfo for GameSession { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl Size for GameSession { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for GameSession { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - // Custom compression: return owned data with modified fields - std::borrow::Cow::Owned(Self { - compression_info: None, // ALWAYS None for compressed storage - session_id: self.session_id, // KEEP - identifier - player: self.player, // KEEP - identifier - game_type: self.game_type.clone(), // KEEP - core property - start_time: 0, // RESET - clear timing - end_time: None, // RESET - clear timing - score: 0, // RESET - clear progress - }) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for GameSession { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for GameSession { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -// PlaceholderRecord - demonstrates empty compressed account creation -// The PDA remains intact while an empty compressed account is created -#[derive(Default, Debug, LightHasher, LightDiscriminator, InitSpace)] -#[account] -pub struct PlaceholderRecord { - #[skip] - pub compression_info: Option, - #[hash] - pub owner: Pubkey, - #[max_len(32)] - pub name: String, - pub placeholder_id: u64, -} - -impl HasCompressionInfo for PlaceholderRecord { - fn compression_info(&self) -> &CompressionInfo { - self.compression_info - .as_ref() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut(&mut self) -> &mut CompressionInfo { - self.compression_info - .as_mut() - .expect("CompressionInfo must be Some on-chain") - } - - fn compression_info_mut_opt(&mut self) -> &mut Option { - &mut self.compression_info - } - - fn set_compression_info_none(&mut self) { - self.compression_info = None; - } -} - -impl Size for PlaceholderRecord { - fn size(&self) -> usize { - Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE - } -} - -impl CompressAs for PlaceholderRecord { - type Output = Self; - - fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { - std::borrow::Cow::Owned(Self { - compression_info: None, - owner: self.owner, - name: self.name.clone(), - placeholder_id: self.placeholder_id, - }) - } -} - -// Identity Pack implementation - no custom packing needed for PDA types -impl SdkPack for PlaceholderRecord { - type Packed = Self; - - fn pack(&self, _remaining_accounts: &mut PackedAccounts) -> Self::Packed { - self.clone() - } -} - -// Identity Unpack implementation - PDA types are sent unpacked -impl SdkUnpack for PlaceholderRecord { - type Unpacked = Self; - - fn unpack( - &self, - _remaining_accounts: &[AccountInfo], - ) -> std::result::Result { - Ok(self.clone()) - } -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedGameSession { - pub compression_info: Option, - pub session_id: u64, - pub player: u8, - pub game_type: String, - pub start_time: u64, - pub end_time: Option, - pub score: u64, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct PackedPlaceholderRecord { - pub compression_info: Option, - pub owner: u8, - pub name: String, - pub placeholder_id: u64, -} - -// Add these struct definitions before the program module -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct AccountCreationData { - pub user_name: String, - pub session_id: u64, - pub game_type: String, - // TODO: Add mint metadata fields when implementing mint functionality - pub mint_name: String, - pub mint_symbol: String, - pub mint_uri: String, - pub mint_decimals: u8, - pub mint_supply: u64, - pub mint_update_authority: Option, - pub mint_freeze_authority: Option, - pub additional_metadata: Option>, -} - -/// Information about a token account to compress -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct TokenAccountInfo { - pub user: Pubkey, - pub mint: Pubkey, -} - -#[derive(AnchorSerialize, AnchorDeserialize)] -pub struct CompressionParams { - pub proof: ValidityProof, - pub user_compressed_address: [u8; 32], - pub user_address_tree_info: PackedAddressTreeInfo, - pub user_output_state_tree_index: u8, - pub game_compressed_address: [u8; 32], - pub game_address_tree_info: PackedAddressTreeInfo, - pub game_output_state_tree_index: u8, - pub mint_bump: u8, - pub mint_with_context: CompressedMintWithContext, -} diff --git a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs b/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs deleted file mode 100644 index f1ad826dcd..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/game_session_tests.rs +++ /dev/null @@ -1,228 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, Discriminator, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::compressible::{CompressAs, CompressibleConfig}; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_game_session, ADDRESS_SPACE, RENT_SPONSOR}; - -// Test: create, decompress game session, compress with custom data at -// compression -#[tokio::test] -async fn test_custom_compression_game_session() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let session_id = 42424u64; - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &game_session_pda, - session_id, - None, - ) - .await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_game_session( - &mut rpc, - &payer, - &program_id, - &game_session_pda, - &_game_bump, - session_id, - "Battle Royale", - 100, - 0, - ) - .await; - - rpc.warp_to_slot(250).unwrap(); - - compress_game_session_with_custom_data( - &mut rpc, - &payer, - &program_id, - &game_session_pda, - session_id, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_single_game_session( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - game_session_pda: &Pubkey, - _game_bump: &u8, - session_id: u64, - expected_game_type: &str, - expected_slot: u64, - expected_score: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = - sdk_compressible_test::GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*game_session_pda], - &[( - c_game_pda, - sdk_compressible_test::CompressedAccountVariant::GameSession(c_game_session), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, expected_score); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); -} - -pub async fn compress_game_session_with_custom_data( - rpc: &mut LightProgramTest, - _payer: &Keypair, - _program_id: &Pubkey, - game_session_pda: &Pubkey, - _session_id: u64, -) { - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); - let game_pda_data = game_pda_account.data; - let original_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - - let custom_compressed_data = match original_game_session.compress_as() { - std::borrow::Cow::Borrowed(data) => data.clone(), - std::borrow::Cow::Owned(data) => data, - }; - - assert_eq!( - custom_compressed_data.session_id, original_game_session.session_id, - "Session ID should be kept" - ); - assert_eq!( - custom_compressed_data.player, original_game_session.player, - "Player should be kept" - ); - assert_eq!( - custom_compressed_data.game_type, original_game_session.game_type, - "Game type should be kept" - ); - assert_eq!( - custom_compressed_data.start_time, 0, - "Start time should be RESET to 0" - ); - assert_eq!( - custom_compressed_data.end_time, None, - "End time should be RESET to None" - ); - assert_eq!( - custom_compressed_data.score, 0, - "Score should be RESET to 0" - ); -} diff --git a/sdk-tests/sdk-compressible-test/tests/helpers.rs b/sdk-tests/sdk-compressible-test/tests/helpers.rs deleted file mode 100644 index 38ae537ec5..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/helpers.rs +++ /dev/null @@ -1,334 +0,0 @@ -// Common test helpers and constants for all test files -#![allow(dead_code)] - -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_macros::pubkey; -use light_program_test::{program_test::LightProgramTest, AddressWithTree, Indexer, Rpc}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use sdk_compressible_test::{CompressedAccountVariant, GameSession, UserRecord}; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -pub const ADDRESS_SPACE: [Pubkey; 1] = [pubkey!("amt2kaJA14v3urZbZvnc5v2np8jqvc4Z8zDep5wbtzx")]; -pub const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); -pub const TOKEN_PROGRAM_ID: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); - -pub const CTOKEN_RENT_SPONSOR: Pubkey = pubkey!("r18WwUxfG8kQ69bQPAB2jV6zGNKy3GosFGctjQoV4ti"); -pub const CTOKEN_RENT_AUTHORITY: Pubkey = pubkey!("8r3QmazwoLHYppYWysXPgUxYJ3Khn7vh3e313jYDcCKy"); - -// Helper functions used across multiple test files - -pub async fn create_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - state_tree_queue: Option, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreateRecord { - user: payer.pubkey(), - user_record: *user_record_pda, - system_program: solana_sdk::system_program::ID, - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = remaining_accounts.insert_or_get( - state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), - ); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateRecord { - name: "Test User".to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_user_record.address, Some(compressed_address)); - assert!(compressed_user_record.data.is_some()); - - let buf = compressed_user_record.data.unwrap().data; - - let user_record = UserRecord::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(user_record.name, "Test User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); -} - -pub async fn decompress_single_user_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - _user_record_bump: &u8, - expected_user_name: &str, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda], - &[( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - - let compressed_account = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - assert!(compressed_account.data.unwrap().data.is_empty()); - - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); -} - -pub async fn create_game_session( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - state_tree_queue: Option, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreateGameSession { - player: payer.pubkey(), - game_session: *game_session_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = remaining_accounts.insert_or_get( - state_tree_queue.unwrap_or_else(|| rpc.get_random_state_tree_info().unwrap().queue), - ); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateGameSession { - session_id, - game_type: "Battle Royale".to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!(result.is_ok(), "Transaction should succeed"); - - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_session_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_game_session = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_game_session.address, Some(compressed_address)); - assert!(compressed_game_session.data.is_some()); - - let buf = compressed_game_session.data.as_ref().unwrap().data.clone(); - - let game_session = GameSession::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Battle Royale"); - assert_eq!(game_session.player, payer.pubkey()); - assert_eq!(game_session.score, 0); - assert!(game_session.compression_info.is_none()); -} diff --git a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs b/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs deleted file mode 100644 index af75d13450..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/idempotency_tests.rs +++ /dev/null @@ -1,137 +0,0 @@ -use anchor_lang::{AccountDeserialize, AnchorDeserialize, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::compressible::CompressibleConfig; -use sdk_compressible_test::{CompressedAccountVariant, UserRecord}; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; - -#[tokio::test] -async fn test_double_decompression_attack() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let c_user_record = - UserRecord::deserialize(&mut &compressed_user_record.data.unwrap().data[..]).unwrap(); - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA should be decompressed after first operation" - ); - - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - &program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[user_record_pda], - &[( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - )], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(&program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - - assert!( - result.is_ok(), - "Second decompression should succeed idempotently" - ); - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - let user_pda_data = user_pda_account.unwrap().data; - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - - assert_eq!(decompressed_user_record.name, "Test User"); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); -} diff --git a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs b/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs deleted file mode 100644 index 8e037f4533..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/multi_account_tests.rs +++ /dev/null @@ -1,1379 +0,0 @@ -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_client::indexer::CompressedAccount; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - AddressWithTree, Indexer, ProgramTestConfig, Rpc, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; -use light_token_interface::{ - instructions::mint_action::{CompressedMintInstructionData, CompressedMintWithContext}, - state::CompressedMintMetadata, -}; -use light_token_sdk::{ - compressed_token::create_compressed_mint::{derive_mint_compressed_address, find_mint_address}, - pack::compat::CTokenDataWithVariant, - token, -}; -use light_token_types::CPI_AUTHORITY_PDA; -use sdk_compressible_test::{ - get_ctoken_signer2_seeds, get_ctoken_signer3_seeds, get_ctoken_signer4_seeds, - get_ctoken_signer5_seeds, get_ctoken_signer_seeds, CTokenAccountVariant, - CompressedAccountVariant, GameSession, UserRecord, -}; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{create_game_session, create_record, ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests -// 1. create and decompress two accounts and compress token accounts after -// decompression -// 2. create and decompress accounts with different state trees -#[tokio::test] -async fn test_create_and_decompress_two_accounts() { - let program_id = sdk_compressible_test::ID; - let mut config = - ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - config = config.with_light_protocol_events(); - - let mut rpc = LightProgramTest::new(config).await.unwrap(); - - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![crate::helpers::ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let combined_user = Keypair::new(); - let fund_user_ix = solana_sdk::system_instruction::transfer( - &payer.pubkey(), - &combined_user.pubkey(), - 1e9 as u64, - ); - let fund_result = rpc - .create_and_send_transaction(&[fund_user_ix], &payer.pubkey(), &[&payer]) - .await; - assert!(fund_result.is_ok(), "Funding combined user should succeed"); - let combined_session_id = 99999u64; - let (combined_user_record_pda, _combined_user_record_bump) = Pubkey::find_program_address( - &[b"user_record", combined_user.pubkey().as_ref()], - &program_id, - ); - let (combined_game_session_pda, _combined_game_bump) = Pubkey::find_program_address( - &[b"game_session", combined_session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let ( - ctoken_account, - _mint_signer, - ctoken_account_2, - ctoken_account_3, - ctoken_account_4, - ctoken_account_5, - ) = create_user_record_and_game_session( - &mut rpc, - &combined_user, - &program_id, - &config_pda, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - ) - .await; - - rpc.warp_to_slot(200).unwrap(); - - let (_, ctoken_account_address) = sdk_compressible_test::get_ctoken_signer_seeds( - &combined_user.pubkey(), - &ctoken_account.token.mint, - ); - - let (_, ctoken_account_address_2) = - sdk_compressible_test::get_ctoken_signer2_seeds(&combined_user.pubkey()); - - let (_, ctoken_account_address_3) = - sdk_compressible_test::get_ctoken_signer3_seeds(&combined_user.pubkey()); - - let (_, ctoken_account_address_4) = sdk_compressible_test::get_ctoken_signer4_seeds( - &combined_user.pubkey(), - &combined_user.pubkey(), - ); - - let (_, ctoken_account_address_5) = sdk_compressible_test::get_ctoken_signer5_seeds( - &combined_user.pubkey(), - &ctoken_account.token.mint, - 42, - ); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let compressed_user_record_address = derive_address( - &combined_user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_game_session_address = derive_address( - &combined_game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let user_record_before_decompression: CompressedAccount = rpc - .get_compressed_account(compressed_user_record_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_session_before_decompression: CompressedAccount = rpc - .get_compressed_account(compressed_game_session_address, None) - .await - .unwrap() - .value - .unwrap(); - - decompress_multiple_pdas_with_ctoken( - &mut rpc, - &combined_user, - &program_id, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - "Combined User", - "Combined Game", - 200, - ctoken_account.clone(), - ctoken_account_address, - ctoken_account_2.clone(), - ctoken_account_address_2, - ctoken_account_3.clone(), - ctoken_account_address_3, - ctoken_account_4.clone(), - ctoken_account_address_4, - ctoken_account_5.clone(), - ctoken_account_address_5, - ) - .await; - - rpc.warp_epoch_forward(1).await.unwrap(); - - compress_token_account_after_decompress( - &mut rpc, - &combined_user, - &program_id, - &config_pda, - ctoken_account_address, - ctoken_account_address_2, - ctoken_account_address_3, - ctoken_account_address_4, - ctoken_account_address_5, - ctoken_account.token.mint, - ctoken_account.token.amount, - &combined_user_record_pda, - &combined_game_session_pda, - combined_session_id, - user_record_before_decompression.hash, - game_session_before_decompression.hash, - ) - .await; -} - -#[allow(clippy::too_many_arguments)] -pub async fn create_user_record_and_game_session( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, -) -> ( - light_client::indexer::CompressedTokenAccount, - Pubkey, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, - light_client::indexer::CompressedTokenAccount, -) { - let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new_with_cpi_context( - *program_id, - state_tree_info.cpi_context.unwrap(), - ); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let decimals = 6u8; - let mint_authority_keypair = Keypair::new(); - let mint_authority = mint_authority_keypair.pubkey(); - let freeze_authority = mint_authority; - let mint_signer = Keypair::new(); - let compressed_mint_address = - derive_mint_compressed_address(&mint_signer.pubkey(), &address_tree_pubkey); - - let (spl_mint, mint_bump) = find_mint_address(&mint_signer.pubkey()); - let accounts = sdk_compressible_test::accounts::CreateUserRecordAndGameSession { - user: user.pubkey(), - user_record: *user_record_pda, - game_session: *game_session_pda, - mint_signer: mint_signer.pubkey(), - ctoken_program: LIGHT_TOKEN_PROGRAM_ID.into(), - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - mint_authority, - compress_token_program_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), - }; - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![ - AddressWithTree { - address: user_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: game_compressed_address, - tree: address_tree_pubkey, - }, - AddressWithTree { - address: compressed_mint_address, - tree: address_tree_pubkey, - }, - ], - None, - ) - .await - .unwrap() - .value; - - let user_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let game_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - let _mint_output_state_tree_index = remaining_accounts.insert_or_get(state_tree_info.queue); - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let user_address_tree_info = packed_tree_infos.address_trees[0]; - let game_address_tree_info = packed_tree_infos.address_trees[1]; - let mint_address_tree_info = packed_tree_infos.address_trees[2]; - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreateUserRecordAndGameSession { - account_data: sdk_compressible_test::AccountCreationData { - user_name: "Combined User".to_string(), - session_id, - game_type: "Combined Game".to_string(), - mint_name: "Test Game Token".to_string(), - mint_symbol: "TGT".to_string(), - mint_uri: "https://example.com/token.json".to_string(), - mint_decimals: 9, - mint_supply: 1_000_000_000, - mint_update_authority: Some(mint_authority), - mint_freeze_authority: Some(freeze_authority), - additional_metadata: None, - }, - compression_params: sdk_compressible_test::CompressionParams { - proof: rpc_result.proof, - user_compressed_address, - user_address_tree_info, - user_output_state_tree_index, - game_compressed_address, - game_address_tree_info, - game_output_state_tree_index, - mint_bump, - mint_with_context: CompressedMintWithContext { - leaf_index: 0, - prove_by_index: false, - root_index: mint_address_tree_info.root_index, - address: compressed_mint_address, - mint: Some(CompressedMintInstructionData { - supply: 0, - decimals, - metadata: CompressedMintMetadata { - version: 3, - mint: spl_mint.into(), - cmint_decompressed: false, - mint_signer: mint_signer.pubkey().to_bytes(), - bump: mint_bump, - }, - mint_authority: Some(mint_authority.into()), - freeze_authority: Some(freeze_authority.into()), - extensions: None, - }), - }, - }, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction( - &[instruction], - &user.pubkey(), - &[user, &mint_signer, &mint_authority_keypair], - ) - .await; - - assert!( - result.is_ok(), - "Combined creation transaction should succeed" - ); - - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_record_account.is_none(), - "User record account should not exist after compression" - ); - - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_session_account.is_none(), - "Game session account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_user_record.address, - Some(user_compressed_address) - ); - assert!(compressed_user_record.data.is_some()); - - let user_buf = compressed_user_record.data.unwrap().data; - - let user_record = UserRecord::deserialize(&mut &user_buf[..]).unwrap(); - - assert_eq!(user_record.name, "Combined User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, user.pubkey()); - - let compressed_game_session = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_game_session.address, - Some(game_compressed_address) - ); - assert!(compressed_game_session.data.is_some()); - - let game_buf = compressed_game_session.data.unwrap().data; - let game_session = GameSession::deserialize(&mut &game_buf[..]).unwrap(); - assert_eq!(game_session.session_id, session_id); - assert_eq!(game_session.game_type, "Combined Game"); - assert_eq!(game_session.player, user.pubkey()); - assert_eq!(game_session.score, 0); - - let token_account_address = - get_ctoken_signer_seeds(&user.pubkey(), &find_mint_address(&mint_signer.pubkey()).0).1; - - let mint = find_mint_address(&mint_signer.pubkey()).0; - let token_account_address_2 = get_ctoken_signer2_seeds(&user.pubkey()).1; - let token_account_address_3 = get_ctoken_signer3_seeds(&user.pubkey()).1; - let token_account_address_4 = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()).1; - let token_account_address_5 = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42).1; - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_2 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_3 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_4 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_5 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have at least one compressed token account" - ); - assert!( - !ctoken_accounts_2.items.is_empty(), - "Should have at least one compressed token account 2" - ); - assert!( - !ctoken_accounts_3.items.is_empty(), - "Should have at least one compressed token account 3" - ); - assert!( - !ctoken_accounts_4.items.is_empty(), - "Should have at least one compressed token account 4" - ); - assert!( - !ctoken_accounts_5.items.is_empty(), - "Should have at least one compressed token account 5" - ); - - let ctoken_account = ctoken_accounts.items[0].clone(); - let ctoken_account_2 = ctoken_accounts_2.items[0].clone(); - let ctoken_account_3 = ctoken_accounts_3.items[0].clone(); - let ctoken_account_4 = ctoken_accounts_4.items[0].clone(); - let ctoken_account_5 = ctoken_accounts_5.items[0].clone(); - - ( - ctoken_account, - mint_signer.pubkey(), - ctoken_account_2, - ctoken_account_3, - ctoken_account_4, - ctoken_account_5, - ) -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_multiple_pdas_with_ctoken( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_user_name: &str, - expected_game_type: &str, - expected_slot: u64, - ctoken_account: light_client::indexer::CompressedTokenAccount, - native_token_account: Pubkey, - ctoken_account_2: light_client::indexer::CompressedTokenAccount, - native_token_account_2: Pubkey, - ctoken_account_3: light_client::indexer::CompressedTokenAccount, - native_token_account_3: Pubkey, - ctoken_account_4: light_client::indexer::CompressedTokenAccount, - native_token_account_4: Pubkey, - ctoken_account_5: light_client::indexer::CompressedTokenAccount, - native_token_account_5: Pubkey, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_account_data = c_game_pda.data.as_ref().unwrap(); - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof( - vec![ - c_user_pda.hash, - c_game_pda.hash, - ctoken_account.clone().account.hash, - ctoken_account_2.clone().account.hash, - ctoken_account_3.clone().account.hash, - ctoken_account_4.clone().account.hash, - ctoken_account_5.clone().account.hash, - ], - vec![], - None, - ) - .await - .unwrap() - .value; - - let ctoken_config = token::config_pda(); - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[ - *user_record_pda, - *game_session_pda, - native_token_account, - native_token_account_2, - native_token_account_3, - native_token_account_4, - native_token_account_5, - ], - &[ - ( - c_user_pda.clone(), - CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda.clone(), - CompressedAccountVariant::GameSession(c_game_session), - ), - ( - { - let acc = ctoken_account.clone().account; - let _token = ctoken_account.clone().token; - acc - }, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner, - token_data: ctoken_account.clone().token, - }), - ), - ( - ctoken_account_2.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner2, - token_data: ctoken_account_2.clone().token, - }), - ), - ( - ctoken_account_3.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner3, - token_data: ctoken_account_3.clone().token, - }), - ), - ( - ctoken_account_4.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner4, - token_data: ctoken_account_4.clone().token, - }), - ), - ( - ctoken_account_5.clone().account, - CompressedAccountVariant::CTokenData(CTokenDataWithVariant::< - CTokenAccountVariant, - > { - variant: CTokenAccountVariant::CTokenSigner5, - token_data: ctoken_account_5.clone().token, - }), - ), - ], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: Some(token::rent_sponsor_pda()), - ctoken_config: Some(ctoken_config), - ctoken_program: Some(token::id()), - ctoken_cpi_authority: Some(token::cpi_authority()), - some_mint: ctoken_account.token.mint, - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert_eq!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "Game PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, 0); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let token_account_data = rpc - .get_account(native_token_account) - .await - .unwrap() - .unwrap(); - assert!( - !token_account_data.data.is_empty(), - "Token account should have data" - ); - assert_eq!(token_account_data.owner, LIGHT_TOKEN_PROGRAM_ID.into()); - - let compressed_user_record_data = rpc - .get_compressed_account(c_user_pda.clone().address.unwrap(), None) - .await - .unwrap() - .value - .unwrap(); - let compressed_game_session_data = rpc - .get_compressed_account(c_game_pda.clone().address.unwrap(), None) - .await - .unwrap() - .value - .unwrap(); - for ctoken in [ - &ctoken_account, - &ctoken_account_2, - &ctoken_account_3, - &ctoken_account_4, - &ctoken_account_5, - ] { - let response = rpc - .get_compressed_account_by_hash(ctoken.clone().account.hash, None) - .await - .unwrap(); - assert!( - response.value.is_none(), - "Compressed token account should have value == None after being closed" - ); - } - - assert!( - compressed_user_record_data.data.unwrap().data.is_empty(), - "Compressed user record should be closed/empty after decompression" - ); - assert!( - compressed_game_session_data.data.unwrap().data.is_empty(), - "Compressed game session should be closed/empty after decompression" - ); -} - -#[allow(clippy::too_many_arguments)] -pub async fn decompress_multiple_pdas( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - expected_user_name: &str, - expected_game_type: &str, - expected_slot: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let user_compressed_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_user_pda = rpc - .get_compressed_account(user_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_account_data = c_user_pda.data.as_ref().unwrap(); - - let c_user_record = UserRecord::deserialize(&mut &user_account_data.data[..]).unwrap(); - - let game_compressed_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_account_data = c_game_pda.data.as_ref().unwrap(); - - let c_game_session = GameSession::deserialize(&mut &game_account_data.data[..]).unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![c_user_pda.hash, c_game_pda.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::decompress_accounts_idempotent( - program_id, - &compressible_instruction::DECOMPRESS_ACCOUNTS_IDEMPOTENT_DISCRIMINATOR, - &[*user_record_pda, *game_session_pda], - &[ - ( - c_user_pda, - CompressedAccountVariant::UserRecord(c_user_record), - ), - ( - c_game_pda, - CompressedAccountVariant::GameSession(c_game_session), - ), - ], - &sdk_compressible_test::accounts::DecompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: payer.pubkey(), - ctoken_rent_sponsor: None, - ctoken_config: None, - ctoken_program: None, - ctoken_cpi_authority: None, - some_mint: payer.pubkey(), - system_program: Pubkey::default(), - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert_eq!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "User PDA account data len must be 0 before decompression" - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert_eq!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0), - 0, - "Game PDA account data len must be 0 before decompression" - ); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - assert!(result.is_ok(), "Decompress transaction should succeed"); - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "User PDA account data len must be > 0 after decompression" - ); - - let user_pda_data = user_pda_account.unwrap().data; - assert_eq!( - &user_pda_data[0..8], - UserRecord::DISCRIMINATOR, - "User account anchor discriminator mismatch" - ); - - let decompressed_user_record = UserRecord::try_deserialize(&mut &user_pda_data[..]).unwrap(); - assert_eq!(decompressed_user_record.name, expected_user_name); - assert_eq!(decompressed_user_record.score, 11); - assert_eq!(decompressed_user_record.owner, payer.pubkey()); - assert!(!decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let game_pda_account = rpc.get_account(*game_session_pda).await.unwrap(); - assert!( - game_pda_account.as_ref().map(|a| a.data.len()).unwrap_or(0) > 0, - "Game PDA account data len must be > 0 after decompression" - ); - - let game_pda_data = game_pda_account.unwrap().data; - assert_eq!( - &game_pda_data[0..8], - sdk_compressible_test::GameSession::DISCRIMINATOR, - "Game account anchor discriminator mismatch" - ); - - let decompressed_game_session = - sdk_compressible_test::GameSession::try_deserialize(&mut &game_pda_data[..]).unwrap(); - assert_eq!(decompressed_game_session.session_id, session_id); - assert_eq!(decompressed_game_session.game_type, expected_game_type); - assert_eq!(decompressed_game_session.player, payer.pubkey()); - assert_eq!(decompressed_game_session.score, 0); - assert!(!decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .is_compressed()); - assert_eq!( - decompressed_game_session - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - expected_slot - ); - - let c_game_pda = rpc - .get_compressed_account(game_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert!(c_game_pda.data.is_some()); - assert_eq!(c_game_pda.data.unwrap().data.len(), 0); -} - -#[allow(clippy::too_many_arguments)] -pub async fn compress_token_account_after_decompress( - rpc: &mut LightProgramTest, - user: &Keypair, - program_id: &Pubkey, - _config_pda: &Pubkey, - token_account_address: Pubkey, - _token_account_address_2: Pubkey, - _token_account_address_3: Pubkey, - _token_account_address_4: Pubkey, - _token_account_address_5: Pubkey, - mint: Pubkey, - amount: u64, - user_record_pda: &Pubkey, - game_session_pda: &Pubkey, - session_id: u64, - user_record_hash_before_decompression: [u8; 32], - game_session_hash_before_decompression: [u8; 32], -) { - let token_account_data = rpc.get_account(token_account_address).await.unwrap(); - assert!( - token_account_data.is_some(), - "Token account should exist before compression" - ); - - let account = token_account_data.unwrap(); - - assert!( - account.lamports > 0, - "Token account should have lamports before compression" - ); - assert!( - !account.data.is_empty(), - "Token account should have data before compression" - ); - - let (_user_record_seeds, user_record_pubkey) = - sdk_compressible_test::get_userrecord_seeds(&user.pubkey()); - let (_game_session_seeds, game_session_pubkey) = - sdk_compressible_test::get_gamesession_seeds(session_id); - let (_, token_account_address) = get_ctoken_signer_seeds(&user.pubkey(), &mint); - - let (_, token_account_address_2) = get_ctoken_signer2_seeds(&user.pubkey()); - let (_, token_account_address_3) = get_ctoken_signer3_seeds(&user.pubkey()); - let (_, token_account_address_4) = get_ctoken_signer4_seeds(&user.pubkey(), &user.pubkey()); - let (_, token_account_address_5) = get_ctoken_signer5_seeds(&user.pubkey(), &mint, 42); - let (_token_signer_seeds, _ctoken_1_authority_pda) = - sdk_compressible_test::get_ctokensigner_authority_seeds(); - - let (_token_signer_seeds_2, _ctoken_2_authority_pda) = - sdk_compressible_test::get_ctokensigner2_authority_seeds(); - - let (_token_signer_seeds_3, _ctoken_3_authority_pda) = - sdk_compressible_test::get_ctokensigner3_authority_seeds(); - - let (_token_signer_seeds_4, _ctoken_4_authority_pda) = - sdk_compressible_test::get_ctokensigner4_authority_seeds(); - - let (_token_signer_seeds_5, _ctoken_5_authority_pda) = - sdk_compressible_test::get_ctokensigner5_authority_seeds(); - - let _cpisigner = Pubkey::new_from_array(sdk_compressible_test::LIGHT_CPI_SIGNER.cpi_signer); - - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap().unwrap(); - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap().unwrap(); - let _token_account = rpc - .get_account(token_account_address) - .await - .unwrap() - .unwrap(); - let _token_account_2 = rpc - .get_account(token_account_address_2) - .await - .unwrap() - .unwrap(); - let _token_account_3 = rpc - .get_account(token_account_address_3) - .await - .unwrap() - .unwrap(); - let _token_account_4 = rpc - .get_account(token_account_address_4) - .await - .unwrap() - .unwrap(); - let _token_account_5 = rpc - .get_account(token_account_address_5) - .await - .unwrap() - .unwrap(); - - assert_eq!(*user_record_pda, user_record_pubkey); - assert_eq!(*game_session_pda, game_session_pubkey); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let compressed_user_record_address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let compressed_game_session_address = derive_address( - &game_session_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - let user_record: CompressedAccount = rpc - .get_compressed_account(compressed_user_record_address, None) - .await - .unwrap() - .value - .unwrap(); - let game_session: CompressedAccount = rpc - .get_compressed_account(compressed_game_session_address, None) - .await - .unwrap() - .value - .unwrap(); - - let user_record_hash = user_record.hash; - let game_session_hash = game_session.hash; - - assert_ne!( - user_record_hash, user_record_hash_before_decompression, - "User record hash NOT_EQUAL before and after compression" - ); - assert_ne!( - game_session_hash, game_session_hash_before_decompression, - "Game session hash NOT_EQUAL before and after compression" - ); - - let proof_with_context = rpc - .get_validity_proof(vec![user_record_hash, game_session_hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[user_record_pubkey, game_session_pubkey], - &[user_record_account, game_session_account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: user.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - proof_with_context, - ) - .unwrap(); - - for _account in instruction.accounts.iter() {} - - let result = rpc - .create_and_send_transaction(&[instruction], &user.pubkey(), &[user]) - .await; - - assert!( - result.is_ok(), - "PDA compression should succeed: {:?}", - result - ); - - rpc.warp_slot_forward(20000).await.unwrap(); - - let token_account_after = rpc.get_account(token_account_address).await.unwrap(); - assert!( - token_account_after.is_none(), - "Token account should not exist after compression" - ); - let token_account_after_2 = rpc.get_account(token_account_address_2).await.unwrap(); - assert!( - token_account_after_2.is_none(), - "Token account 2 should not exist after compression" - ); - let token_account_after_3 = rpc.get_account(token_account_address_3).await.unwrap(); - assert!( - token_account_after_3.is_none(), - "Token account 3 should not exist after compression" - ); - let token_account_after_4 = rpc.get_account(token_account_address_4).await.unwrap(); - assert!( - token_account_after_4.is_none(), - "Token account 4 should not exist after compression" - ); - let token_account_after_5 = rpc.get_account(token_account_address_5).await.unwrap(); - assert!( - token_account_after_5.is_none(), - "Token account 5 should not exist after compression" - ); - - let ctoken_accounts = rpc - .get_compressed_token_accounts_by_owner(&token_account_address, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_2 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_2, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_3 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_3, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_4 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_4, None, None) - .await - .unwrap() - .value; - let ctoken_accounts_5 = rpc - .get_compressed_token_accounts_by_owner(&token_account_address_5, None, None) - .await - .unwrap() - .value; - - assert!( - !ctoken_accounts.items.is_empty(), - "Should have at least one compressed token account after compression" - ); - assert!( - !ctoken_accounts_2.items.is_empty(), - "Should have at least one compressed token account 2 after compression" - ); - assert!( - !ctoken_accounts_3.items.is_empty(), - "Should have at least one compressed token account 3 after compression" - ); - assert!( - !ctoken_accounts_4.items.is_empty(), - "Should have at least one compressed token account 4 after compression" - ); - assert!( - !ctoken_accounts_5.items.is_empty(), - "Should have at least one compressed token account 5 after compression" - ); - - let ctoken = &ctoken_accounts.items[0]; - assert_eq!( - ctoken.token.mint, mint, - "Compressed token should have the same mint" - ); - assert_eq!( - ctoken.token.owner, token_account_address, - "Compressed token owner should be the token account address" - ); - assert_eq!( - ctoken.token.amount, amount, - "Compressed token should have the same amount" - ); - let ctoken2 = &ctoken_accounts_2.items[0]; - assert_eq!( - ctoken2.token.mint, mint, - "Compressed token 2 should have the same mint" - ); - assert_eq!( - ctoken2.token.owner, token_account_address_2, - "Compressed token 2 owner should be the token account address" - ); - assert_eq!( - ctoken2.token.amount, amount, - "Compressed token 2 should have the same amount" - ); - let ctoken3 = &ctoken_accounts_3.items[0]; - assert_eq!( - ctoken3.token.mint, mint, - "Compressed token 3 should have the same mint" - ); - assert_eq!( - ctoken3.token.owner, token_account_address_3, - "Compressed token 3 owner should be the token account address" - ); - assert_eq!( - ctoken3.token.amount, amount, - "Compressed token 3 should have the same amount" - ); - let ctoken4 = &ctoken_accounts_4.items[0]; - assert_eq!( - ctoken4.token.mint, mint, - "Compressed token 4 should have the same mint" - ); - assert_eq!( - ctoken4.token.owner, token_account_address_4, - "Compressed token 4 owner should be the token account address" - ); - assert_eq!( - ctoken4.token.amount, amount, - "Compressed token 4 should have the same amount" - ); - let ctoken5 = &ctoken_accounts_5.items[0]; - assert_eq!( - ctoken5.token.mint, mint, - "Compressed token 5 should have the same mint" - ); - assert_eq!( - ctoken5.token.owner, token_account_address_5, - "Compressed token 5 owner should be the token account address" - ); - assert_eq!( - ctoken5.token.amount, amount, - "Compressed token 5 should have the same amount" - ); - let user_record_account = rpc.get_account(*user_record_pda).await.unwrap(); - let game_session_account = rpc.get_account(*game_session_pda).await.unwrap(); - let token_account = rpc.get_account(token_account_address).await.unwrap(); - let token_account_3 = rpc.get_account(token_account_address_3).await.unwrap(); - let token_account_4 = rpc.get_account(token_account_address_4).await.unwrap(); - let token_account_5 = rpc.get_account(token_account_address_5).await.unwrap(); - - assert!( - user_record_account.is_none(), - "User record account should be None" - ); - assert!( - game_session_account.is_none(), - "Game session account should be None" - ); - assert!(token_account.is_none(), "Token account should be None"); - assert!( - user_record_account - .map(|a| a.data.is_empty()) - .unwrap_or(true), - "User record account should be empty" - ); - assert!( - game_session_account - .map(|a| a.data.is_empty()) - .unwrap_or(true), - "Game session account should be empty" - ); - assert!( - token_account.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account should be empty" - ); - assert!( - token_account_3.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 3 should be empty" - ); - assert!( - token_account_4.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 4 should be empty" - ); - assert!( - token_account_5.map(|a| a.data.is_empty()).unwrap_or(true), - "Token account 5 should be empty" - ); -} - -#[tokio::test] -async fn test_create_and_decompress_accounts_with_different_state_trees() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, _user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - let session_id = 54321u64; - let (game_session_pda, _game_bump) = Pubkey::find_program_address( - &[b"game_session", session_id.to_le_bytes().as_ref()], - &program_id, - ); - - let first_state_tree_info = rpc.get_state_tree_infos()[0]; - let second_state_tree_info = rpc.get_state_tree_infos()[1]; - - create_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - Some(first_state_tree_info.queue), - ) - .await; - - create_game_session( - &mut rpc, - &payer, - &program_id, - &config_pda, - &game_session_pda, - session_id, - Some(second_state_tree_info.queue), - ) - .await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_multiple_pdas( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &game_session_pda, - session_id, - "Test User", - "Battle Royale", - 100, - ) - .await; -} diff --git a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs b/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs deleted file mode 100644 index cb8adc9d87..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/placeholder_tests.rs +++ /dev/null @@ -1,527 +0,0 @@ -use anchor_lang::{AccountDeserialize, Discriminator, InstructionData, ToAccountMetas}; -use light_compressed_account::address::derive_address; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use solana_account::Account; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; - -mod helpers; -use helpers::{ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests for the simplest possible compression flows: -// 1. Create empty compressed account (do not compress at init) -// 2. Idempotent double compression -#[tokio::test] -async fn test_create_empty_compressed_account() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let placeholder_id = 54321u64; - let (placeholder_record_pda, placeholder_record_bump) = Pubkey::find_program_address( - &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - placeholder_id, - "Test Placeholder", - ) - .await; - - let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_account.is_some(), - "Placeholder PDA should exist after empty compression" - ); - let account = placeholder_pda_account.unwrap(); - assert!( - account.lamports > 0, - "Placeholder PDA should have lamports (not closed)" - ); - assert!( - !account.data.is_empty(), - "Placeholder PDA should have data (not closed)" - ); - - let placeholder_data = account.data; - let decompressed_placeholder_record = - sdk_compressible_test::PlaceholderRecord::try_deserialize(&mut &placeholder_data[..]) - .unwrap(); - assert_eq!(decompressed_placeholder_record.name, "Test Placeholder"); - assert_eq!( - decompressed_placeholder_record.placeholder_id, - placeholder_id - ); - assert_eq!(decompressed_placeholder_record.owner, payer.pubkey()); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder.address, - Some(compressed_address), - "Compressed account should exist with correct address" - ); - assert!( - compressed_placeholder.data.is_some(), - "Compressed account should have data field" - ); - - let compressed_data = compressed_placeholder.data.unwrap(); - assert_eq!( - compressed_data.data.len(), - 0, - "Compressed account data should be empty" - ); - - rpc.warp_to_slot(200).unwrap(); - - compress_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - &placeholder_record_bump, - placeholder_id, - ) - .await; -} - -#[tokio::test] -async fn test_double_compression_attack() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let config_pda = CompressibleConfig::derive_pda(&program_id, 0).0; - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let placeholder_id = 99999u64; - let (placeholder_record_pda, _placeholder_record_bump) = Pubkey::find_program_address( - &[b"placeholder_record", placeholder_id.to_le_bytes().as_ref()], - &program_id, - ); - - create_placeholder_record( - &mut rpc, - &payer, - &program_id, - &config_pda, - &placeholder_record_pda, - placeholder_id, - "Double Compression Test", - ) - .await; - - let placeholder_pda_account = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_account.is_some(), - "Placeholder PDA should exist before compression" - ); - let account_before = placeholder_pda_account.unwrap(); - assert!( - account_before.lamports > 0, - "Placeholder PDA should have lamports before compression" - ); - assert!( - !account_before.data.is_empty(), - "Placeholder PDA should have data before compression" - ); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder_before = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder_before.address, - Some(compressed_address), - "Empty compressed account should exist" - ); - assert_eq!( - compressed_placeholder_before - .data - .as_ref() - .unwrap() - .data - .len(), - 0, - "Compressed account should be empty initially" - ); - - rpc.warp_to_slot(200).unwrap(); - - let first_compression_result = compress_placeholder_record_for_double_test( - &mut rpc, - &payer, - &program_id, - &placeholder_record_pda, - placeholder_id, - Some(account_before.clone()), - ) - .await; - assert!( - first_compression_result.is_ok(), - "First compression should succeed: {:?}", - first_compression_result - ); - - let placeholder_pda_after_first = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_after_first.is_none(), - "PDA should not exist after first compression" - ); - - let compressed_placeholder_after_first = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let first_data_len = compressed_placeholder_after_first - .data - .as_ref() - .unwrap() - .data - .len(); - assert!( - first_data_len > 0, - "Compressed account should contain data after first compression" - ); - - let second_compression_result = compress_placeholder_record_for_double_test( - &mut rpc, - &payer, - &program_id, - &placeholder_record_pda, - placeholder_id, - Some(account_before), - ) - .await; - - assert!( - second_compression_result.is_ok(), - "Second compression should succeed idempotently: {:?}", - second_compression_result - ); - - let placeholder_pda_after_second = rpc.get_account(placeholder_record_pda).await.unwrap(); - assert!( - placeholder_pda_after_second.is_none(), - "PDA should still not exist after second compression" - ); - - let compressed_placeholder_after_second = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!( - compressed_placeholder_after_first.hash, compressed_placeholder_after_second.hash, - "Compressed account hash should be unchanged after second compression" - ); - assert_eq!( - compressed_placeholder_after_first - .data - .as_ref() - .unwrap() - .data, - compressed_placeholder_after_second - .data - .as_ref() - .unwrap() - .data, - "Compressed account data should be unchanged after second compression" - ); -} - -pub async fn create_placeholder_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - config_pda: &Pubkey, - placeholder_record_pda: &Pubkey, - placeholder_id: u64, - name: &str, -) { - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let accounts = sdk_compressible_test::accounts::CreatePlaceholderRecord { - user: payer.pubkey(), - placeholder_record: *placeholder_record_pda, - system_program: solana_sdk::system_program::ID, - config: *config_pda, - rent_sponsor: RENT_SPONSOR, - }; - - let compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let rpc_result = rpc - .get_validity_proof( - vec![], - vec![light_program_test::AddressWithTree { - address: compressed_address, - tree: address_tree_pubkey, - }], - None, - ) - .await - .unwrap() - .value; - - let packed_tree_infos = rpc_result.pack_tree_infos(&mut remaining_accounts); - - let address_tree_info = packed_tree_infos.address_trees[0]; - - let output_state_tree_index = - remaining_accounts.insert_or_get(rpc.get_random_state_tree_info().unwrap().queue); - - let (system_accounts, _, _) = remaining_accounts.to_account_metas(); - - let instruction_data = sdk_compressible_test::instruction::CreatePlaceholderRecord { - placeholder_id, - name: name.to_string(), - proof: rpc_result.proof, - compressed_address, - address_tree_info, - output_state_tree_index, - }; - - let instruction = Instruction { - program_id: *program_id, - accounts: [accounts.to_account_metas(None), system_accounts].concat(), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!( - result.is_ok(), - "CreatePlaceholderRecord transaction should succeed" - ); -} - -pub async fn compress_placeholder_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - _config_pda: &Pubkey, - placeholder_record_pda: &Pubkey, - _placeholder_record_bump: &u8, - placeholder_id: u64, -) { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) - .await - .unwrap() - .value; - - let _placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); - - let account = rpc - .get_account(*placeholder_record_pda) - .await - .unwrap() - .unwrap(); - - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &[account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - assert!( - result.is_ok(), - "CompressPlaceholderRecord transaction should succeed: {:?}", - result - ); - - let _account = rpc.get_account(*placeholder_record_pda).await.unwrap(); - - let compressed_placeholder_after = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert!( - compressed_placeholder_after.data.is_some(), - "Compressed account should have data after compression" - ); - - let compressed_data_after = compressed_placeholder_after.data.unwrap(); - - assert!( - !compressed_data_after.data.is_empty(), - "Compressed account should contain the PDA data" - ); -} - -pub async fn compress_placeholder_record_for_double_test( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - placeholder_record_pda: &Pubkey, - placeholder_id: u64, - previous_account: Option, -) -> Result { - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let placeholder_compressed_address = derive_address( - &placeholder_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_placeholder = rpc - .get_compressed_account(placeholder_compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_placeholder.hash], vec![], None) - .await - .unwrap() - .value; - - let _placeholder_seeds = sdk_compressible_test::get_placeholderrecord_seeds(placeholder_id); - - let accounts_to_compress = if let Some(account) = previous_account { - vec![account] - } else { - panic!("Previous account should be provided"); - }; - let instruction = - light_compressible_client::compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*placeholder_record_pda], - &accounts_to_compress, - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await -} diff --git a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs b/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs deleted file mode 100644 index 3dab2f194c..0000000000 --- a/sdk-tests/sdk-compressible-test/tests/user_record_tests.rs +++ /dev/null @@ -1,293 +0,0 @@ -use anchor_lang::{ - AccountDeserialize, AnchorDeserialize, Discriminator, InstructionData, ToAccountMetas, -}; -use light_compressed_account::address::derive_address; -use light_compressible::rent::{RentConfig, SLOTS_PER_EPOCH}; -use light_compressible_client::compressible_instruction; -use light_program_test::{ - program_test::{ - initialize_compression_config, setup_mock_program_data, LightProgramTest, TestRpc, - }, - Indexer, ProgramTestConfig, Rpc, RpcError, -}; -use light_sdk::{ - compressible::CompressibleConfig, - instruction::{PackedAccounts, SystemAccountMetaConfig}, -}; -use sdk_compressible_test::UserRecord; -use solana_instruction::Instruction; -use solana_keypair::Keypair; -use solana_pubkey::Pubkey; -use solana_signer::Signer; -use solana_system_interface::instruction as system_instruction; - -mod helpers; -use helpers::{create_record, decompress_single_user_record, ADDRESS_SPACE, RENT_SPONSOR}; - -// Tests -// 1. init compressed, decompress, and compress -// 2. update_record bumps compression info -#[tokio::test] -async fn test_create_decompress_compress_single_account() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - - rpc.warp_to_slot(100).unwrap(); - - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - // Top up PDA so it's initially NOT compressible (sufficiently funded) - // Fund exactly one epoch of rent plus compression_cost, so after one epoch passes it becomes compressible. - let pda_account = rpc.get_account(user_record_pda).await.unwrap().unwrap(); - let bytes = pda_account.data.len() as u64; - let rent_cfg = RentConfig::default(); - let rent_per_epoch = rent_cfg.rent_curve_per_epoch(bytes); - let compression_cost = rent_cfg.compression_cost as u64; - let top_up = rent_per_epoch + compression_cost; - - let transfer_ix = system_instruction::transfer(&payer.pubkey(), &user_record_pda, top_up); - let res = rpc - .create_and_send_transaction(&[transfer_ix], &payer.pubkey(), &[&payer]) - .await; - assert!(res.is_ok(), "Top-up transfer should succeed"); - - // Immediately try to compress – should FAIL because not compressible yet (sufficiently funded) - let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, true).await; - assert!( - result.is_err(), - "Compression should fail while sufficiently funded" - ); - - // Advance one full epoch so required_epochs increases and the account becomes compressible - rpc.warp_to_slot(SLOTS_PER_EPOCH * 2).unwrap(); - - // Now compression should SUCCEED (account no longer sufficiently funded for current+next epoch) - let result = compress_record(&mut rpc, &payer, &program_id, &user_record_pda, false).await; - assert!( - result.is_ok(), - "Compression should succeed after epochs advance" - ); -} - -#[tokio::test] -async fn test_update_record_compression_info() { - let program_id = sdk_compressible_test::ID; - let config = ProgramTestConfig::new_v2(true, Some(vec![("sdk_compressible_test", program_id)])); - let mut rpc = LightProgramTest::new(config).await.unwrap(); - let payer = rpc.get_payer().insecure_clone(); - - let _program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); - - let result = initialize_compression_config( - &mut rpc, - &payer, - &program_id, - &payer, - RENT_SPONSOR, - vec![ADDRESS_SPACE[0]], - &compressible_instruction::INITIALIZE_COMPRESSION_CONFIG_DISCRIMINATOR, - None, - ) - .await; - assert!(result.is_ok(), "Initialize config should succeed"); - - let (user_record_pda, user_record_bump) = - Pubkey::find_program_address(&[b"user_record", payer.pubkey().as_ref()], &program_id); - - create_record(&mut rpc, &payer, &program_id, &user_record_pda, None).await; - - rpc.warp_to_slot(100).unwrap(); - decompress_single_user_record( - &mut rpc, - &payer, - &program_id, - &user_record_pda, - &user_record_bump, - "Test User", - 100, - ) - .await; - - rpc.warp_to_slot(150).unwrap(); - - let accounts = sdk_compressible_test::accounts::UpdateRecord { - user: payer.pubkey(), - user_record: user_record_pda, - system_program: solana_sdk::system_program::id(), - }; - - let instruction_data = sdk_compressible_test::instruction::UpdateRecord { - name: "Updated User".to_string(), - score: 42, - }; - - let instruction = Instruction { - program_id, - accounts: accounts.to_account_metas(None), - data: instruction_data.data(), - }; - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer]) - .await; - assert!(result.is_ok(), "Update record transaction should succeed"); - - rpc.warp_to_slot(200).unwrap(); - - let user_pda_account = rpc.get_account(user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User record account should exist after update" - ); - - let account_data = user_pda_account.unwrap().data; - let updated_user_record = UserRecord::try_deserialize(&mut &account_data[..]).unwrap(); - - assert_eq!(updated_user_record.name, "Updated User"); - assert_eq!(updated_user_record.score, 42); - assert_eq!(updated_user_record.owner, payer.pubkey()); - - assert_eq!( - updated_user_record - .compression_info - .as_ref() - .unwrap() - .last_claimed_slot(), - 100 - ); - assert!(!updated_user_record - .compression_info - .as_ref() - .unwrap() - .is_compressed()); -} - -pub async fn compress_record( - rpc: &mut LightProgramTest, - payer: &Keypair, - program_id: &Pubkey, - user_record_pda: &Pubkey, - should_fail: bool, -) -> Result { - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_some(), - "User PDA account should exist before compression" - ); - let account = user_pda_account.unwrap(); - assert!( - account.lamports > 0, - "Account should have lamports before compression" - ); - assert!( - !account.data.is_empty(), - "Account data should not be empty before compression" - ); - - let mut remaining_accounts = PackedAccounts::default(); - let system_config = SystemAccountMetaConfig::new(*program_id); - let _ = remaining_accounts.add_system_accounts_v2(system_config); - - let address_tree_pubkey = rpc.get_address_tree_v2().tree; - - let address = derive_address( - &user_record_pda.to_bytes(), - &address_tree_pubkey.to_bytes(), - &program_id.to_bytes(), - ); - - let compressed_account = rpc - .get_compressed_account(address, None) - .await - .unwrap() - .value - .unwrap(); - let compressed_address = compressed_account.address.unwrap(); - - let rpc_result = rpc - .get_validity_proof(vec![compressed_account.hash], vec![], None) - .await - .unwrap() - .value; - - let instruction = compressible_instruction::compress_accounts_idempotent( - program_id, - sdk_compressible_test::instruction::CompressAccountsIdempotent::DISCRIMINATOR, - &[*user_record_pda], - &[account], - &sdk_compressible_test::accounts::CompressAccountsIdempotent { - fee_payer: payer.pubkey(), - config: CompressibleConfig::derive_pda(program_id, 0).0, - rent_sponsor: RENT_SPONSOR, - } - .to_account_metas(None), - rpc_result, - ) - .unwrap(); - - let result = rpc - .create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer]) - .await; - - if should_fail { - assert!(result.is_err(), "Compress transaction should fail"); - return result; - } else { - assert!(result.is_ok(), "Compress transaction should succeed"); - } - - let user_pda_account = rpc.get_account(*user_record_pda).await.unwrap(); - assert!( - user_pda_account.is_none(), - "Account should not exist after compression" - ); - - let compressed_user_record = rpc - .get_compressed_account(compressed_address, None) - .await - .unwrap() - .value - .unwrap(); - - assert_eq!(compressed_user_record.address, Some(compressed_address)); - assert!(compressed_user_record.data.is_some()); - - let buf = compressed_user_record.data.unwrap().data; - let user_record: UserRecord = UserRecord::deserialize(&mut &buf[..]).unwrap(); - - assert_eq!(user_record.name, "Test User"); - assert_eq!(user_record.score, 11); - assert_eq!(user_record.owner, payer.pubkey()); - assert!(user_record.compression_info.is_none()); - Ok(result.unwrap()) -} diff --git a/sdk-tests/sdk-light-token-test/README.md b/sdk-tests/sdk-light-token-test/README.md index 87681eb6e5..56ae03ca78 100644 --- a/sdk-tests/sdk-light-token-test/README.md +++ b/sdk-tests/sdk-light-token-test/README.md @@ -70,7 +70,7 @@ All instructions use the **builder pattern** from `light-token-sdk::ctoken`: - **create_cmint** (Instruction 0): Create compressed mint using `CreateCMintCpi::invoke()` - **mint_to_ctoken** (Instruction 1): Mint tokens to compressed accounts using `MintToCTokenCpi::invoke()` -- **create_token_account_invoke** (Instruction 2): Create compressible token account using `CreateCTokenAccountCpi` +- **create_token_account_invoke** (Instruction 2): Create compressible token account using `CreateTokenAccountCpi` - **create_token_account_invoke_signed** (Instruction 3): Create with PDA ownership using `invoke_signed()` - **create_ata_invoke** (Instruction 4): Create compressible ATA using `CreateAssociatedTokenAccountCpi` - **create_ata_invoke_signed** (Instruction 5): Create ATA with PDA ownership using `invoke_signed()` @@ -122,6 +122,7 @@ cargo test-sbf ### Compressible Token Accounts Compressible token accounts have a special extension that allows them to be: + - Compressed back into compressed state - Configured with rent payment mechanisms - Automatically closed and compressed @@ -129,11 +130,13 @@ Compressible token accounts have a special extension that allows them to be: ### PDA Patterns (invoke_signed) The `invoke_signed` variants demonstrate how to: + 1. Derive a PDA from the program 2. Use the PDA as the authority/owner for token accounts 3. Sign transactions on behalf of the PDA This is useful for: + - Escrow programs - Vaults - Program-controlled liquidity diff --git a/sdk-tests/sdk-light-token-test/src/approve.rs b/sdk-tests/sdk-light-token-test/src/approve.rs index d8be99d6b0..9a51f82d26 100644 --- a/sdk-tests/sdk-light-token-test/src/approve.rs +++ b/sdk-tests/sdk-light-token-test/src/approve.rs @@ -17,7 +17,7 @@ pub struct ApproveData { /// - accounts[1]: delegate /// - accounts[2]: owner (signer) /// - accounts[3]: system_program -/// - accounts[4]: ctoken_program +/// - accounts[4]: light_token_program pub fn process_approve_invoke( accounts: &[AccountInfo], data: ApproveData, @@ -45,7 +45,7 @@ pub fn process_approve_invoke( /// - accounts[1]: delegate /// - accounts[2]: PDA owner (program signs) /// - accounts[3]: system_program -/// - accounts[4]: ctoken_program +/// - accounts[4]: light_token_program pub fn process_approve_invoke_signed( accounts: &[AccountInfo], data: ApproveData, diff --git a/sdk-tests/sdk-light-token-test/src/burn.rs b/sdk-tests/sdk-light-token-test/src/burn.rs index 8e0c46767a..ca55f300b0 100644 --- a/sdk-tests/sdk-light-token-test/src/burn.rs +++ b/sdk-tests/sdk-light-token-test/src/burn.rs @@ -16,7 +16,7 @@ pub struct BurnData { /// - accounts[0]: source (Light Token account, writable) /// - accounts[1]: cmint (writable) /// - accounts[2]: authority (owner, signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -40,7 +40,7 @@ pub fn process_burn_invoke(accounts: &[AccountInfo], amount: u64) -> Result<(), /// - accounts[0]: source (Light Token account, writable) /// - accounts[1]: cmint (writable) /// - accounts[2]: PDA authority (owner, program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_burn_invoke_signed( accounts: &[AccountInfo], amount: u64, diff --git a/sdk-tests/sdk-light-token-test/src/create_ata.rs b/sdk-tests/sdk-light-token-test/src/create_ata.rs index 541a6c2767..05fec5045e 100644 --- a/sdk-tests/sdk-light-token-test/src/create_ata.rs +++ b/sdk-tests/sdk-light-token-test/src/create_ata.rs @@ -1,5 +1,5 @@ use borsh::{BorshDeserialize, BorshSerialize}; -use light_token_sdk::token::{CompressibleParamsCpi, CreateAssociatedAccountCpi}; +use light_token_sdk::token::CreateCTokenAtaCpi; use solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; use crate::{ATA_SEED, ID}; @@ -30,24 +30,18 @@ pub fn process_create_ata_invoke( return Err(ProgramError::NotEnoughAccountKeys); } - // Build the compressible params using constructor - let compressible_params = CompressibleParamsCpi::new_ata( - accounts[5].clone(), - accounts[6].clone(), - accounts[4].clone(), - ); - - // Use the CreateAssociatedCTokenAccountCpi - owner and mint are AccountInfos - CreateAssociatedAccountCpi { + CreateCTokenAtaCpi { + payer: accounts[2].clone(), owner: accounts[0].clone(), mint: accounts[1].clone(), - payer: accounts[2].clone(), - associated_token_account: accounts[3].clone(), - system_program: accounts[4].clone(), + ata: accounts[3].clone(), bump: data.bump, - compressible: compressible_params, - idempotent: false, } + .rent_free( + accounts[5].clone(), // compressible_config + accounts[6].clone(), // rent_sponsor + accounts[4].clone(), // system_program + ) .invoke()?; Ok(()) @@ -79,28 +73,21 @@ pub fn process_create_ata_invoke_signed( return Err(ProgramError::InvalidSeeds); } - // Build the compressible params using constructor - let compressible_params = CompressibleParamsCpi::new_ata( - accounts[5].clone(), - accounts[6].clone(), - accounts[4].clone(), - ); + let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; - // Use the CreateAssociatedAccountCpi - owner and mint are AccountInfos - let account_infos = CreateAssociatedAccountCpi { + CreateCTokenAtaCpi { + payer: accounts[2].clone(), owner: accounts[0].clone(), mint: accounts[1].clone(), - payer: accounts[2].clone(), - associated_token_account: accounts[3].clone(), - system_program: accounts[4].clone(), + ata: accounts[3].clone(), bump: data.bump, - compressible: compressible_params, - idempotent: false, - }; - - // Invoke with PDA signing - let signer_seeds: &[&[u8]] = &[ATA_SEED, &[bump]]; - account_infos.invoke_signed(&[signer_seeds])?; + } + .rent_free( + accounts[5].clone(), // compressible_config + accounts[6].clone(), // rent_sponsor + accounts[4].clone(), // system_program + ) + .invoke_signed(&[signer_seeds])?; Ok(()) } diff --git a/sdk-tests/sdk-light-token-test/src/create_token_account.rs b/sdk-tests/sdk-light-token-test/src/create_token_account.rs index 0325d6aa2f..43c847386c 100644 --- a/sdk-tests/sdk-light-token-test/src/create_token_account.rs +++ b/sdk-tests/sdk-light-token-test/src/create_token_account.rs @@ -40,15 +40,14 @@ pub fn process_create_token_account_invoke( accounts[4].clone(), ); - // Build the account infos struct + // Build the account infos struct and invoke with custom compressible params CreateTokenAccountCpi { payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), owner: data.owner, - compressible: compressible_params, } - .invoke()?; + .invoke_with(compressible_params)?; Ok(()) } @@ -85,18 +84,15 @@ pub fn process_create_token_account_invoke_signed( accounts[4].clone(), ); - // Build the account infos struct - let account_infos = CreateTokenAccountCpi { + // Invoke with PDA signing and custom compressible params + let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; + CreateTokenAccountCpi { payer: accounts[0].clone(), account: accounts[1].clone(), mint: accounts[2].clone(), owner: data.owner, - compressible: compressible_params, - }; - - // Invoke with PDA signing - let signer_seeds: &[&[u8]] = &[TOKEN_ACCOUNT_SEED, &[bump]]; - account_infos.invoke_signed(&[signer_seeds])?; + } + .invoke_signed_with(compressible_params, &[signer_seeds])?; Ok(()) } diff --git a/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs b/sdk-tests/sdk-light-token-test/src/decompress_mint.rs similarity index 98% rename from sdk-tests/sdk-light-token-test/src/decompress_cmint.rs rename to sdk-tests/sdk-light-token-test/src/decompress_mint.rs index 92ac319e89..d91aa3aa58 100644 --- a/sdk-tests/sdk-light-token-test/src/decompress_cmint.rs +++ b/sdk-tests/sdk-light-token-test/src/decompress_mint.rs @@ -34,7 +34,7 @@ pub struct DecompressCmintData { /// - accounts[12]: account_compression_authority (readonly) /// - accounts[13]: account_compression_program (readonly) /// - accounts[14]: system_program (readonly) -pub fn process_decompress_cmint_invoke_signed( +pub fn process_decompress_mint_invoke_signed( accounts: &[AccountInfo], data: DecompressCmintData, ) -> Result<(), ProgramError> { diff --git a/sdk-tests/sdk-light-token-test/src/freeze.rs b/sdk-tests/sdk-light-token-test/src/freeze.rs index 16183125e8..249c804e01 100644 --- a/sdk-tests/sdk-light-token-test/src/freeze.rs +++ b/sdk-tests/sdk-light-token-test/src/freeze.rs @@ -9,7 +9,7 @@ use crate::{FREEZE_AUTHORITY_SEED, ID}; /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: freeze_authority (signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_freeze_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_freeze_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: PDA freeze_authority (program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_freeze_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/src/lib.rs b/sdk-tests/sdk-light-token-test/src/lib.rs index 840805cdba..4fc437dc61 100644 --- a/sdk-tests/sdk-light-token-test/src/lib.rs +++ b/sdk-tests/sdk-light-token-test/src/lib.rs @@ -7,7 +7,7 @@ mod create_ata; mod create_cmint; mod create_token_account; mod ctoken_mint_to; -mod decompress_cmint; +mod decompress_mint; mod freeze; mod revoke; mod thaw; @@ -30,7 +30,7 @@ pub use create_token_account::{ CreateTokenAccountData, }; pub use ctoken_mint_to::{process_mint_to_invoke, process_mint_to_invoke_signed, MintToData}; -pub use decompress_cmint::{process_decompress_cmint_invoke_signed, DecompressCmintData}; +pub use decompress_mint::{process_decompress_mint_invoke_signed, DecompressCmintData}; pub use freeze::{process_freeze_invoke, process_freeze_invoke_signed}; pub use revoke::{process_revoke_invoke, process_revoke_invoke_signed}; use solana_program::{ @@ -315,7 +315,7 @@ pub fn process_instruction( InstructionType::DecompressCmintInvokeSigned => { let data = DecompressCmintData::try_from_slice(&instruction_data[1..]) .map_err(|_| ProgramError::InvalidInstructionData)?; - process_decompress_cmint_invoke_signed(accounts, data) + process_decompress_mint_invoke_signed(accounts, data) } InstructionType::CTokenTransferCheckedInvoke => { let data = TransferCheckedData::try_from_slice(&instruction_data[1..]) diff --git a/sdk-tests/sdk-light-token-test/src/revoke.rs b/sdk-tests/sdk-light-token-test/src/revoke.rs index cdd84fb7ca..c3bc1d59a6 100644 --- a/sdk-tests/sdk-light-token-test/src/revoke.rs +++ b/sdk-tests/sdk-light-token-test/src/revoke.rs @@ -9,7 +9,7 @@ use crate::{ID, TOKEN_ACCOUNT_SEED}; /// - accounts[0]: token_account (writable) /// - accounts[1]: owner (signer) /// - accounts[2]: system_program -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_revoke_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramErro /// - accounts[0]: token_account (writable) /// - accounts[1]: PDA owner (program signs) /// - accounts[2]: system_program -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_revoke_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/src/thaw.rs b/sdk-tests/sdk-light-token-test/src/thaw.rs index 5cfeffbc33..8ce5a57678 100644 --- a/sdk-tests/sdk-light-token-test/src/thaw.rs +++ b/sdk-tests/sdk-light-token-test/src/thaw.rs @@ -9,7 +9,7 @@ use crate::{FREEZE_AUTHORITY_SEED, ID}; /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: freeze_authority (signer) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_thaw_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); @@ -31,7 +31,7 @@ pub fn process_thaw_invoke(accounts: &[AccountInfo]) -> Result<(), ProgramError> /// - accounts[0]: token_account (writable) /// - accounts[1]: mint /// - accounts[2]: PDA freeze_authority (program signs) -/// - accounts[3]: ctoken_program +/// - accounts[3]: light_token_program pub fn process_thaw_invoke_signed(accounts: &[AccountInfo]) -> Result<(), ProgramError> { if accounts.len() < 4 { return Err(ProgramError::NotEnoughAccountKeys); diff --git a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs index 0574e4e44a..296b4cd222 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_approve_revoke.rs @@ -44,15 +44,15 @@ async fn test_approve_invoke() { }; approve_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(payer.pubkey(), true), // owner (signer) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new(payer.pubkey(), true), // owner (signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -103,15 +103,15 @@ async fn test_approve_invoke_signed() { }; approve_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new_readonly(delegate.pubkey(), false), // delegate - AccountMeta::new(pda_owner, false), // PDA owner (program signs) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new_readonly(delegate.pubkey(), false), // delegate + AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -156,7 +156,7 @@ async fn test_revoke_invoke() { let ata = ata_pubkeys[0]; let delegate = Keypair::new(); let approve_amount = 100u64; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // First approve a delegate let mut approve_instruction_data = vec![InstructionType::ApproveInvoke as u8]; @@ -174,7 +174,7 @@ async fn test_revoke_invoke() { AccountMeta::new_readonly(delegate.pubkey(), false), AccountMeta::new(payer.pubkey(), true), AccountMeta::new_readonly(Pubkey::default(), false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: approve_instruction_data, }; @@ -198,10 +198,10 @@ async fn test_revoke_invoke() { let revoke_instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new(payer.pubkey(), true), // owner (signer) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new(payer.pubkey(), true), // owner (signer) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; @@ -242,7 +242,7 @@ async fn test_revoke_invoke_signed() { let ata = ata_pubkeys[0]; let delegate = Keypair::new(); let approve_amount = 100u64; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // First approve a delegate using invoke_signed let mut approve_instruction_data = vec![InstructionType::ApproveInvokeSigned as u8]; @@ -260,7 +260,7 @@ async fn test_revoke_invoke_signed() { AccountMeta::new_readonly(delegate.pubkey(), false), AccountMeta::new(pda_owner, false), AccountMeta::new_readonly(Pubkey::default(), false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: approve_instruction_data, }; @@ -284,10 +284,10 @@ async fn test_revoke_invoke_signed() { let revoke_instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // token_account - AccountMeta::new(pda_owner, false), // PDA owner (program signs) - AccountMeta::new_readonly(Pubkey::default(), false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // token_account + AccountMeta::new(pda_owner, false), // PDA owner (program signs) + AccountMeta::new_readonly(Pubkey::default(), false), // system_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: revoke_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_burn.rs b/sdk-tests/sdk-light-token-test/tests/test_burn.rs index 052f2ce2ab..fecab7c855 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_burn.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_burn.rs @@ -48,14 +48,14 @@ async fn test_burn_invoke() { }; burn_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // source - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new_readonly(payer.pubkey(), true), // authority (signer) + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -114,14 +114,14 @@ async fn test_burn_invoke_signed() { }; burn_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(ata, false), // source - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new(ata, false), // source + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new_readonly(pda_owner, false), // PDA authority (program signs) + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs b/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs index 288b4d1b83..e1bf29154f 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_create_ata.rs @@ -49,7 +49,7 @@ async fn test_create_ata_invoke() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, ctoken_program + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: ID, accounts: vec![ @@ -135,7 +135,7 @@ async fn test_create_ata_invoke_signed() { let config = config_pda(); let rent_sponsor = rent_sponsor_pda(); - // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, ctoken_program + // Account order: owner, mint, payer, ata, system_program, config, rent_sponsor, light_token_program let instruction = Instruction { program_id: ID, accounts: vec![ diff --git a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs index 6f0667eaa7..064c664197 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_ctoken_mint_to.rs @@ -48,16 +48,16 @@ async fn test_ctoken_mint_to_invoke() { }; mint_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let system_program = Pubkey::default(); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new(ata, false), // destination + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new(ata, false), // destination AccountMeta::new(payer.pubkey(), true), // authority (signer, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -233,7 +233,7 @@ async fn test_ctoken_mint_to_invoke_signed() { ] .concat(); - // Account order matches process_decompress_cmint_invoke_signed: + // Account order matches process_decompress_mint_invoke_signed: // 0: authority (PDA, readonly - program signs) // 1: payer (signer, writable) // 2: cmint (writable) @@ -307,16 +307,16 @@ async fn test_ctoken_mint_to_invoke_signed() { }; mint_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let system_program = Pubkey::default(); let instruction = Instruction { program_id: ID, accounts: vec![ - AccountMeta::new(mint_pda, false), // cmint - AccountMeta::new(ata, false), // destination + AccountMeta::new(mint_pda, false), // cmint + AccountMeta::new(ata, false), // destination AccountMeta::new(pda_mint_authority, false), // PDA authority (program signs, writable for top-up) AccountMeta::new_readonly(system_program, false), // system_program - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs b/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs index a07621a232..a6bd5ef4ea 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_decompress_cmint.rs @@ -14,7 +14,7 @@ use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; /// Test decompressing a compressed mint to CMint account #[tokio::test] -async fn test_decompress_cmint() { +async fn test_decompress_mint() { let config = ProgramTestConfig::new_v2(true, None); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -114,7 +114,7 @@ async fn test_decompress_cmint() { /// Test decompressing a compressed mint with freeze_authority #[tokio::test] -async fn test_decompress_cmint_with_freeze_authority() { +async fn test_decompress_mint_with_freeze_authority() { let config = ProgramTestConfig::new_v2(true, None); let mut rpc = LightProgramTest::new(config).await.unwrap(); let payer = rpc.get_payer().insecure_clone(); @@ -299,7 +299,7 @@ async fn setup_create_compressed_mint_with_freeze_authority_only( /// Test decompressing a compressed mint with TokenMetadata extension #[tokio::test] -async fn test_decompress_cmint_with_token_metadata() { +async fn test_decompress_mint_with_token_metadata() { use light_token_interface::instructions::extensions::{ ExtensionInstructionData, TokenMetadataInstructionData, }; @@ -506,7 +506,7 @@ async fn setup_create_compressed_mint_with_extensions( /// Test decompressing a compressed mint via CPI with PDA authority using invoke_signed #[tokio::test] -async fn test_decompress_cmint_cpi_invoke_signed() { +async fn test_decompress_mint_cpi_invoke_signed() { use borsh::BorshSerialize; use native_ctoken_examples::{ CreateCmintData, DecompressCmintData, InstructionType, ID, MINT_AUTHORITY_SEED, @@ -655,7 +655,7 @@ async fn test_decompress_cmint_cpi_invoke_signed() { ] .concat(); - // Account order matches process_decompress_cmint_invoke_signed: + // Account order matches process_decompress_mint_invoke_signed: // 0: authority (PDA, readonly - program signs) // 1: payer (signer, writable) // 2: cmint (writable) diff --git a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs index f7284e4a38..86ae32e091 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_freeze_thaw.rs @@ -51,14 +51,14 @@ async fn test_freeze_invoke() { // Build freeze instruction via wrapper program let instruction_data = vec![InstructionType::FreezeInvoke as u8]; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(freeze_authority.pubkey(), true), // freeze_authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -110,14 +110,14 @@ async fn test_freeze_invoke_signed() { // Build freeze instruction via wrapper program using invoke_signed let instruction_data = vec![InstructionType::FreezeInvokeSigned as u8]; - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); let instruction = Instruction { program_id: ID, accounts: vec![ AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: instruction_data, }; @@ -146,7 +146,7 @@ async fn test_thaw_invoke() { let payer = rpc.get_payer().insecure_clone(); let freeze_authority = Keypair::new(); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // Create a compressed mint with freeze_authority and an ATA for the payer with 1000 tokens let (mint_pda, _compression_address, ata_pubkeys) = @@ -170,7 +170,7 @@ async fn test_thaw_invoke() { AccountMeta::new(ata, false), AccountMeta::new_readonly(mint_pda, false), AccountMeta::new_readonly(freeze_authority.pubkey(), true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: freeze_instruction_data, }; @@ -200,7 +200,7 @@ async fn test_thaw_invoke() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(freeze_authority.pubkey(), true), // freeze_authority (signer) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: thaw_instruction_data, }; @@ -233,7 +233,7 @@ async fn test_thaw_invoke_signed() { // Derive the PDA that will be the freeze authority let (pda_freeze_authority, _bump) = Pubkey::find_program_address(&[FREEZE_AUTHORITY_SEED], &ID); - let ctoken_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); + let light_token_program = Pubkey::from(LIGHT_TOKEN_PROGRAM_ID); // Create a compressed mint with PDA freeze_authority and an ATA for the payer with 1000 tokens let (mint_pda, _compression_address, ata_pubkeys) = @@ -257,7 +257,7 @@ async fn test_thaw_invoke_signed() { AccountMeta::new(ata, false), AccountMeta::new_readonly(mint_pda, false), AccountMeta::new_readonly(pda_freeze_authority, false), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: freeze_instruction_data, }; @@ -283,7 +283,7 @@ async fn test_thaw_invoke_signed() { AccountMeta::new(ata, false), // token_account AccountMeta::new_readonly(mint_pda, false), // mint AccountMeta::new_readonly(pda_freeze_authority, false), // PDA freeze_authority (program signs) - AccountMeta::new_readonly(ctoken_program, false), // ctoken_program + AccountMeta::new_readonly(light_token_program, false), // light_token_program ], data: thaw_instruction_data, }; diff --git a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs index 393b5ecd6f..fbbfedab88 100644 --- a/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs +++ b/sdk-tests/sdk-light-token-test/tests/test_transfer_checked.rs @@ -144,7 +144,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -152,7 +152,7 @@ async fn test_ctoken_transfer_checked_spl_mint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; @@ -249,7 +249,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -257,7 +257,7 @@ async fn test_ctoken_transfer_checked_t22_mint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; @@ -310,7 +310,7 @@ async fn test_ctoken_transfer_checked_cmint() { let mut instruction_data = vec![InstructionType::CTokenTransferCheckedInvoke as u8]; transfer_data.serialize(&mut instruction_data).unwrap(); - let ctoken_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; + let light_token_program = light_token_sdk::token::LIGHT_TOKEN_PROGRAM_ID; let instruction = Instruction { program_id: ID, accounts: vec![ @@ -318,7 +318,7 @@ async fn test_ctoken_transfer_checked_cmint() { AccountMeta::new_readonly(mint, false), AccountMeta::new(dest_ata, false), AccountMeta::new_readonly(source_owner, true), - AccountMeta::new_readonly(ctoken_program, false), + AccountMeta::new_readonly(light_token_program, false), ], data: instruction_data, }; diff --git a/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs b/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs index ec86393d57..9a327540f1 100644 --- a/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs +++ b/sdk-tests/sdk-token-test/src/ctoken_pda/mod.rs @@ -11,7 +11,7 @@ pub struct CTokenPda<'info> { pub mint_authority: Signer<'info>, pub mint_seed: Signer<'info>, /// CHECK: - pub ctoken_program: UncheckedAccount<'info>, + pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub ctoken_cpi_authority: UncheckedAccount<'info>, } diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs index 794a4c09c9..1e9fa397ea 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mint.rs @@ -52,7 +52,7 @@ pub fn process_mint_action<'a, 'info>( let tree_accounts = cpi_accounts.tree_accounts().unwrap(); let ctoken_accounts_vec = vec![ctx.accounts.token_account.to_account_info()]; let mint_action_accounts = MintActionCpiAccounts { - compressed_token_program: ctx.accounts.ctoken_program.as_ref(), + compressed_token_program: ctx.accounts.light_token_program.as_ref(), light_system_program: cpi_accounts.system_program().unwrap(), mint_signer: Some(ctx.accounts.mint_seed.as_ref()), authority: ctx.accounts.mint_authority.as_ref(), @@ -76,7 +76,7 @@ pub fn process_mint_action<'a, 'info>( // Get all account infos needed for the mint action let mut account_infos = cpi_accounts.to_account_infos(); account_infos.push(ctx.accounts.ctoken_cpi_authority.to_account_info()); - account_infos.push(ctx.accounts.ctoken_program.to_account_info()); + account_infos.push(ctx.accounts.light_token_program.to_account_info()); account_infos.push(ctx.accounts.mint_authority.to_account_info()); account_infos.push(ctx.accounts.mint_seed.to_account_info()); account_infos.push(ctx.accounts.payer.to_account_info()); diff --git a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs index ad861592d2..5158622533 100644 --- a/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs +++ b/sdk-tests/sdk-token-test/src/pda_ctoken/mod.rs @@ -16,7 +16,7 @@ pub struct PdaCToken<'info> { #[account(mut)] pub token_account: UncheckedAccount<'info>, /// CHECK: - pub ctoken_program: UncheckedAccount<'info>, + pub light_token_program: UncheckedAccount<'info>, /// CHECK: pub ctoken_cpi_authority: UncheckedAccount<'info>, } diff --git a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs index 946c72dc4d..b7e1c70b4b 100644 --- a/sdk-tests/sdk-token-test/tests/ctoken_pda.rs +++ b/sdk-tests/sdk-token-test/tests/ctoken_pda.rs @@ -226,7 +226,7 @@ pub async fn create_mint( payer: payer.pubkey(), mint_authority: mint_authority.pubkey(), mint_seed: mint_seed.pubkey(), - ctoken_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), ctoken_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), }; diff --git a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs index d455c0b2f2..9c40aabaa0 100644 --- a/sdk-tests/sdk-token-test/tests/pda_ctoken.rs +++ b/sdk-tests/sdk-token-test/tests/pda_ctoken.rs @@ -297,7 +297,7 @@ pub async fn create_mint( payer: payer.pubkey(), mint_authority: mint_authority.pubkey(), mint_seed: mint_seed.pubkey(), - ctoken_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + light_token_program: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), ctoken_cpi_authority: Pubkey::new_from_array(CPI_AUTHORITY_PDA), token_account, };