From 5562365d9b4744422d3dfbeb9e53804b2eee307b Mon Sep 17 00:00:00 2001 From: ananas Date: Tue, 20 Jan 2026 17:22:59 +0000 Subject: [PATCH 1/3] fix: light_program for only one mint --- .../light_pdas/account/decompress_context.rs | 13 ++ .../macros/src/light_pdas/account/utils.rs | 45 ++++++- .../src/light_pdas/program/decompress.rs | 14 +-- .../src/light_pdas/program/instructions.rs | 119 ++++++++++++++++-- .../macros/src/light_pdas/program/parsing.rs | 2 + 5 files changed, 173 insertions(+), 20 deletions(-) diff --git a/sdk-libs/macros/src/light_pdas/account/decompress_context.rs b/sdk-libs/macros/src/light_pdas/account/decompress_context.rs index da5a460fed..269221f96f 100644 --- a/sdk-libs/macros/src/light_pdas/account/decompress_context.rs +++ b/sdk-libs/macros/src/light_pdas/account/decompress_context.rs @@ -101,6 +101,18 @@ pub fn generate_decompress_context_trait_impl( }) .collect(); + // For mint-only programs (no PDA variants), add an arm for the Empty variant + let empty_variant_arm = if pda_ctx_seeds.is_empty() { + quote! { + // Mint-only programs have an Empty variant that should never be decompressed + LightAccountVariant::Empty => { + return std::result::Result::Err(solana_program_error::ProgramError::InvalidAccountData); + } + } + } else { + quote! {} + }; + let packed_token_variant_ident = format_ident!("Packed{}", token_variant_ident); Ok(quote! { @@ -177,6 +189,7 @@ pub fn generate_decompress_context_trait_impl( LightAccountVariant::CTokenData(_) => { return std::result::Result::Err(light_sdk::error::LightSdkError::UnexpectedUnpackedVariant.into()); } + #empty_variant_arm } } diff --git a/sdk-libs/macros/src/light_pdas/account/utils.rs b/sdk-libs/macros/src/light_pdas/account/utils.rs index 35c09e5d93..06b2bed531 100644 --- a/sdk-libs/macros/src/light_pdas/account/utils.rs +++ b/sdk-libs/macros/src/light_pdas/account/utils.rs @@ -104,13 +104,54 @@ pub(crate) fn is_pubkey_type(ty: &Type) -> bool { } } -/// Generates an empty TokenAccountVariant enum. +/// Generates placeholder TokenAccountVariant and PackedTokenAccountVariant enums. /// /// This is used when no token accounts are specified in compressible instructions. +/// We use a placeholder variant since Rust doesn't support empty enums with #[repr(u8)]. 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 TokenAccountVariant {} + pub enum TokenAccountVariant { + /// Placeholder variant for programs without token accounts + Empty = 0, + } + + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Debug, Clone, Copy)] + #[repr(u8)] + pub enum PackedTokenAccountVariant { + /// Placeholder variant for programs without token accounts + Empty = 0, + } + + impl light_token::pack::Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + Ok(PackedTokenAccountVariant::Empty) + } + } + + impl light_token::pack::Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; + fn unpack(&self, _remaining_accounts: &[solana_account_info::AccountInfo]) -> std::result::Result { + Ok(TokenAccountVariant::Empty) + } + } + + impl light_sdk::interface::TokenSeedProvider for TokenAccountVariant { + fn get_seeds(&self, _program_id: &Pubkey) -> std::result::Result<(Vec>, Pubkey), solana_program_error::ProgramError> { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + + fn get_authority_seeds(&self, _program_id: &Pubkey) -> std::result::Result<(Vec>, Pubkey), solana_program_error::ProgramError> { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + } + + impl light_sdk::interface::IntoCTokenVariant for TokenAccountVariant { + fn into_ctoken_variant(self, _token_data: light_token::compat::TokenData) -> LightAccountVariant { + LightAccountVariant::Empty + } + } } } diff --git a/sdk-libs/macros/src/light_pdas/program/decompress.rs b/sdk-libs/macros/src/light_pdas/program/decompress.rs index d8d32b57cb..3fa8288a52 100644 --- a/sdk-libs/macros/src/light_pdas/program/decompress.rs +++ b/sdk-libs/macros/src/light_pdas/program/decompress.rs @@ -161,15 +161,13 @@ impl DecompressBuilder { } /// Generate PDA seed provider implementations. + /// Returns empty Vec for mint-only or token-only programs that have no PDA seeds. pub fn generate_seed_provider_impls(&self) -> Result> { - let pda_seed_specs = self.pda_seeds.as_ref().ok_or_else(|| { - let span_source = self - .account_types - .first() - .map(|t| quote::quote!(#t)) - .unwrap_or_else(|| quote::quote!(unknown)); - super::parsing::macro_error!(span_source, "No seed specifications provided") - })?; + // For mint-only or token-only programs, there are no PDA seeds - return empty Vec + let pda_seed_specs = match self.pda_seeds.as_ref() { + Some(specs) if !specs.is_empty() => specs, + _ => return Ok(Vec::new()), + }; let mut results = Vec::with_capacity(self.pda_ctx_seeds.len()); diff --git a/sdk-libs/macros/src/light_pdas/program/instructions.rs b/sdk-libs/macros/src/light_pdas/program/instructions.rs index d4ce9d55ea..8f56ec38e1 100644 --- a/sdk-libs/macros/src/light_pdas/program/instructions.rs +++ b/sdk-libs/macros/src/light_pdas/program/instructions.rs @@ -36,6 +36,7 @@ fn codegen( token_seeds: Option>, instruction_data: Vec, crate_ctx: &super::crate_context::CrateContext, + has_mint_fields: bool, ) -> Result { let content = match module.content.as_mut() { Some(content) => content, @@ -105,7 +106,98 @@ fn codegen( }) .unwrap_or_default(); - let enum_and_traits = LightVariantBuilder::new(&pda_ctx_seeds).build()?; + // Generate variant enum and traits only if there are PDA seeds + // For mint-only programs (no PDA state accounts), generate minimal placeholder code + let enum_and_traits = if pda_ctx_seeds.is_empty() { + // Generate minimal code for mint-only programs that matches trait signatures + quote! { + /// Placeholder enum for programs that only use Light mints without state accounts. + #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + pub enum LightAccountVariant { + /// Placeholder variant for mint-only programs + Empty, + PackedCTokenData(light_token::compat::PackedCTokenData), + CTokenData(light_token::compat::CTokenData), + } + + impl Default for LightAccountVariant { + fn default() -> Self { + Self::Empty + } + } + + impl ::light_sdk::hasher::DataHasher for LightAccountVariant { + fn hash(&self) -> std::result::Result<[u8; 32], ::light_sdk::hasher::HasherError> { + match self { + Self::Empty => Err(::light_sdk::hasher::HasherError::EmptyInput), + Self::PackedCTokenData(_) => Err(::light_sdk::hasher::HasherError::EmptyInput), + Self::CTokenData(_) => Err(::light_sdk::hasher::HasherError::EmptyInput), + } + } + } + + impl light_sdk::LightDiscriminator for LightAccountVariant { + const LIGHT_DISCRIMINATOR: [u8; 8] = [0; 8]; + const LIGHT_DISCRIMINATOR_SLICE: &'static [u8] = &Self::LIGHT_DISCRIMINATOR; + } + + impl light_sdk::interface::HasCompressionInfo for LightAccountVariant { + fn compression_info(&self) -> std::result::Result<&light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + + fn compression_info_mut(&mut self) -> std::result::Result<&mut light_sdk::interface::CompressionInfo, solana_program_error::ProgramError> { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + panic!("compression_info_mut_opt not supported for mint-only programs") + } + + fn set_compression_info_none(&mut self) -> std::result::Result<(), solana_program_error::ProgramError> { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + } + + impl light_sdk::account::Size for LightAccountVariant { + fn size(&self) -> std::result::Result { + Err(solana_program_error::ProgramError::InvalidAccountData) + } + } + + impl light_sdk::Pack for LightAccountVariant { + type Packed = Self; + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> std::result::Result { + Ok(Self::Empty) + } + } + + impl light_sdk::Unpack for LightAccountVariant { + type Unpacked = Self; + fn unpack(&self, _remaining_accounts: &[solana_account_info::AccountInfo]) -> std::result::Result { + Ok(Self::Empty) + } + } + + /// Wrapper for compressed account data (mint-only placeholder). + #[derive(Clone, Debug, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] + pub struct LightAccountData { + pub meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + pub data: LightAccountVariant, + } + + impl Default for LightAccountData { + fn default() -> Self { + Self { + meta: light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress::default(), + data: LightAccountVariant::default(), + } + } + } + } + } else { + LightVariantBuilder::new(&pda_ctx_seeds).build()? + }; // Collect all unique params-only seed fields across all variants for SeedParams struct // Use BTreeMap for deterministic ordering @@ -245,14 +337,16 @@ fn codegen( 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); - let instruction_variant = match (has_pda_seeds, has_token_seeds) { - (true, true) => InstructionVariant::Mixed, - (true, false) => InstructionVariant::PdaOnly, - (false, true) => InstructionVariant::TokenOnly, - (false, false) => { + let instruction_variant = match (has_pda_seeds, has_token_seeds, has_mint_fields) { + (true, true, _) => InstructionVariant::Mixed, + (true, false, _) => InstructionVariant::PdaOnly, + (false, true, _) => InstructionVariant::TokenOnly, + (false, false, true) => InstructionVariant::MintOnly, + (false, false, false) => { return Err(macro_error!( module, - "At least one PDA or token seed specification must be provided" + "No #[light_account(init)], #[light_account(init, mint)], or #[light_account(token)] fields found.\n\ + At least one light account field must be provided." )) } }; @@ -502,6 +596,7 @@ pub fn light_program_impl(_args: TokenStream, mut module: ItemMod) -> Result = Vec::new(); let mut token_specs: Vec = Vec::new(); let mut rentfree_struct_names = std::collections::HashSet::new(); + let mut has_any_mint_fields = false; for item_struct in crate_ctx.structs_with_derive("Accounts") { // Parse #[instruction(...)] attribute to get instruction arg names @@ -515,15 +610,18 @@ pub fn light_program_impl(_args: TokenStream, mut module: ItemMod) -> Result Result Date: Tue, 20 Jan 2026 18:40:08 +0000 Subject: [PATCH 2/3] fix: macros for single accounts --- .github/workflows/sdk-tests.yml | 2 +- Cargo.lock | 128 ++++++++++++ Cargo.toml | 4 + .../token-interface/tests/token/spl_compat.rs | 1 + .../tests/token/zero_copy_new.rs | 1 + .../macros/src/light_pdas/account/utils.rs | 5 +- .../src/light_pdas/program/decompress.rs | 7 +- .../src/light_pdas/program/instructions.rs | 8 +- sdk-tests/single-ata-test/Cargo.toml | 57 ++++++ sdk-tests/single-ata-test/src/lib.rs | 75 +++++++ sdk-tests/single-ata-test/tests/test.rs | 181 +++++++++++++++++ sdk-tests/single-mint-test/Cargo.toml | 58 ++++++ sdk-tests/single-mint-test/src/lib.rs | 89 +++++++++ sdk-tests/single-mint-test/tests/test.rs | 121 ++++++++++++ sdk-tests/single-pda-test/Cargo.toml | 57 ++++++ .../src/instruction_accounts.rs | 36 ++++ sdk-tests/single-pda-test/src/lib.rs | 39 ++++ sdk-tests/single-pda-test/src/state.rs | 14 ++ sdk-tests/single-pda-test/tests/test.rs | 109 ++++++++++ sdk-tests/single-token-test/Cargo.toml | 57 ++++++ sdk-tests/single-token-test/src/lib.rs | 90 +++++++++ sdk-tests/single-token-test/tests/test.rs | 186 ++++++++++++++++++ 22 files changed, 1311 insertions(+), 14 deletions(-) create mode 100644 sdk-tests/single-ata-test/Cargo.toml create mode 100644 sdk-tests/single-ata-test/src/lib.rs create mode 100644 sdk-tests/single-ata-test/tests/test.rs create mode 100644 sdk-tests/single-mint-test/Cargo.toml create mode 100644 sdk-tests/single-mint-test/src/lib.rs create mode 100644 sdk-tests/single-mint-test/tests/test.rs create mode 100644 sdk-tests/single-pda-test/Cargo.toml create mode 100644 sdk-tests/single-pda-test/src/instruction_accounts.rs create mode 100644 sdk-tests/single-pda-test/src/lib.rs create mode 100644 sdk-tests/single-pda-test/src/state.rs create mode 100644 sdk-tests/single-pda-test/tests/test.rs create mode 100644 sdk-tests/single-token-test/Cargo.toml create mode 100644 sdk-tests/single-token-test/src/lib.rs create mode 100644 sdk-tests/single-token-test/tests/test.rs diff --git a/.github/workflows/sdk-tests.yml b/.github/workflows/sdk-tests.yml index 34089cfb88..388dc4d8e6 100644 --- a/.github/workflows/sdk-tests.yml +++ b/.github/workflows/sdk-tests.yml @@ -50,7 +50,7 @@ jobs: - program: native sub-tests: '["cargo-test-sbf -p sdk-native-test", "cargo-test-sbf -p sdk-v1-native-test", "cargo-test-sbf -p sdk-light-token-test", "cargo-test-sbf -p client-test"]' - program: anchor & pinocchio - sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test"]' + sub-tests: '["cargo-test-sbf -p sdk-anchor-test", "cargo-test-sbf -p sdk-compressible-test", "cargo-test-sbf -p csdk-anchor-derived-test", "cargo-test-sbf -p csdk-anchor-full-derived-test", "cargo-test-sbf -p sdk-pinocchio-v1-test", "cargo-test-sbf -p sdk-pinocchio-v2-test", "cargo-test-sbf -p pinocchio-nostd-test", "cargo-test-sbf -p single-mint-test", "cargo-test-sbf -p single-pda-test", "cargo-test-sbf -p single-ata-test", "cargo-test-sbf -p single-token-test"]' - program: token test sub-tests: '["cargo-test-sbf -p sdk-token-test"]' - program: sdk-libs diff --git a/Cargo.lock b/Cargo.lock index 4e5d48983e..0e2d449a0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6442,6 +6442,134 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "single-ata-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-heap", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-macros", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-interface", + "light-token-types", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + +[[package]] +name = "single-mint-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-anchor-spl", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-heap", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-macros", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-interface", + "light-token-types", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + +[[package]] +name = "single-pda-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-heap", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-macros", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-types", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + +[[package]] +name = "single-token-test" +version = "0.1.0" +dependencies = [ + "anchor-lang", + "borsh 0.10.4", + "light-client", + "light-compressed-account", + "light-compressible", + "light-hasher", + "light-heap", + "light-macros", + "light-program-test", + "light-sdk", + "light-sdk-macros", + "light-sdk-types", + "light-test-utils", + "light-token", + "light-token-interface", + "light-token-types", + "solana-account-info", + "solana-instruction", + "solana-keypair", + "solana-msg 2.2.1", + "solana-program", + "solana-program-error 2.2.2", + "solana-pubkey 2.4.0", + "solana-sdk", + "solana-signer", + "tokio", +] + [[package]] name = "siphasher" version = "0.3.11" diff --git a/Cargo.toml b/Cargo.toml index 006f5e4318..5330dbe428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,10 @@ members = [ "sdk-tests/sdk-light-token-test", "sdk-tests/csdk-anchor-full-derived-test", "sdk-tests/csdk-anchor-full-derived-test-sdk", + "sdk-tests/single-mint-test", + "sdk-tests/single-pda-test", + "sdk-tests/single-ata-test", + "sdk-tests/single-token-test", "forester-utils", "forester", "sparse-merkle-tree", diff --git a/program-libs/token-interface/tests/token/spl_compat.rs b/program-libs/token-interface/tests/token/spl_compat.rs index db832b2b23..18a3de4879 100644 --- a/program-libs/token-interface/tests/token/spl_compat.rs +++ b/program-libs/token-interface/tests/token/spl_compat.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "test-only")] //! Tests token solana account - spl token account layout compatibility //! //! Tests: diff --git a/program-libs/token-interface/tests/token/zero_copy_new.rs b/program-libs/token-interface/tests/token/zero_copy_new.rs index cdc82db555..5809f3346d 100644 --- a/program-libs/token-interface/tests/token/zero_copy_new.rs +++ b/program-libs/token-interface/tests/token/zero_copy_new.rs @@ -1,3 +1,4 @@ +#![cfg(feature = "test-only")] //! Contains functional zero copy tests for: //! - ZeroCopyNew //! diff --git a/sdk-libs/macros/src/light_pdas/account/utils.rs b/sdk-libs/macros/src/light_pdas/account/utils.rs index 06b2bed531..fbca083bb3 100644 --- a/sdk-libs/macros/src/light_pdas/account/utils.rs +++ b/sdk-libs/macros/src/light_pdas/account/utils.rs @@ -150,7 +150,10 @@ pub(crate) fn generate_empty_ctoken_enum() -> proc_macro2::TokenStream { impl light_sdk::interface::IntoCTokenVariant for TokenAccountVariant { fn into_ctoken_variant(self, _token_data: light_token::compat::TokenData) -> LightAccountVariant { - LightAccountVariant::Empty + // This function should never be called for programs without token accounts. + // The Empty variant only exists in mint-only programs (no PDAs). + // For programs with PDAs but no tokens, this impl exists only to satisfy trait bounds. + unreachable!("into_ctoken_variant called on program without token accounts") } } } diff --git a/sdk-libs/macros/src/light_pdas/program/decompress.rs b/sdk-libs/macros/src/light_pdas/program/decompress.rs index 3fa8288a52..da5b06bede 100644 --- a/sdk-libs/macros/src/light_pdas/program/decompress.rs +++ b/sdk-libs/macros/src/light_pdas/program/decompress.rs @@ -6,7 +6,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Result, Type}; +use syn::{Ident, Result}; use super::{ expr_traversal::transform_expr_for_ctx_seeds, @@ -30,8 +30,6 @@ pub(super) struct DecompressBuilder { pda_ctx_seeds: Vec, /// Token variant identifier (e.g., TokenAccountVariant). token_variant_ident: Ident, - /// Account types that can be decompressed. - account_types: Vec, /// PDA seed specifications. pda_seeds: Option>, } @@ -42,18 +40,15 @@ impl DecompressBuilder { /// # Arguments /// * `pda_ctx_seeds` - PDA context seed information for each variant /// * `token_variant_ident` - Token variant identifier - /// * `account_types` - Account types that can be decompressed /// * `pda_seeds` - PDA seed specifications pub fn new( pda_ctx_seeds: Vec, token_variant_ident: Ident, - account_types: Vec, pda_seeds: Option>, ) -> Self { Self { pda_ctx_seeds, token_variant_ident, - account_types, pda_seeds, } } diff --git a/sdk-libs/macros/src/light_pdas/program/instructions.rs b/sdk-libs/macros/src/light_pdas/program/instructions.rs index 8f56ec38e1..339577a372 100644 --- a/sdk-libs/macros/src/light_pdas/program/instructions.rs +++ b/sdk-libs/macros/src/light_pdas/program/instructions.rs @@ -361,12 +361,8 @@ fn codegen( let token_variant_name = format_ident!("TokenAccountVariant"); // Create DecompressBuilder to generate all decompress-related code - let decompress_builder = DecompressBuilder::new( - pda_ctx_seeds.clone(), - token_variant_name, - account_types.clone(), - pda_seeds.clone(), - ); + let decompress_builder = + DecompressBuilder::new(pda_ctx_seeds.clone(), token_variant_name, pda_seeds.clone()); // Note: DecompressBuilder validation is optional for now since pda_seeds may be empty for TokenOnly let decompress_accounts = decompress_builder.generate_accounts_struct()?; diff --git a/sdk-tests/single-ata-test/Cargo.toml b/sdk-tests/single-ata-test/Cargo.toml new file mode 100644 index 0000000000..3769f189c6 --- /dev/null +++ b/sdk-tests/single-ata-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "single-ata-test" +version = "0.1.0" +description = "Minimal Anchor program test for single ATA macro validation" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "single_ata_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +custom-heap = ["light-heap", "light-sdk/custom-heap"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-heap = { workspace = true, optional = true } +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +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"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true, features = ["anchor"] } +light-compressible = { workspace = true, features = ["anchor"] } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +solana-pubkey = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } +solana-account-info = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token-interface = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/single-ata-test/src/lib.rs b/sdk-tests/single-ata-test/src/lib.rs new file mode 100644 index 0000000000..1bc808326f --- /dev/null +++ b/sdk-tests/single-ata-test/src/lib.rs @@ -0,0 +1,75 @@ +//! Minimal test program for #[light_account(init, associated_token, ...)] macro validation. +//! +//! This program tests ONLY the ATA creation macro in isolation, +//! ensuring the simplest ATA-only program compiles and works correctly. + +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::{CpiSigner, LIGHT_TOKEN_PROGRAM_ID}; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR}; + +declare_id!("AtaT111111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("AtaT111111111111111111111111111111111111111"); + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateAtaParams { + pub create_accounts_proof: CreateAccountsProof, + /// Bump for the ATA PDA + pub ata_bump: u8, +} + +/// Minimal accounts struct for testing single ATA creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateAtaParams)] +pub struct CreateAta<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint for the ATA + pub ata_mint: AccountInfo<'info>, + + /// CHECK: Owner of the ATA + pub ata_owner: AccountInfo<'info>, + + /// ATA account - macro should generate creation code. + #[account(mut)] + #[light_account(init, associated_token, owner = ata_owner, mint = ata_mint, bump = params.ata_bump)] + pub ata: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub light_token_compressible_config: AccountInfo<'info>, + + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light Token Program for CPI + #[account(address = LIGHT_TOKEN_PROGRAM_ID.into())] + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod single_ata_test { + use super::*; + + /// Create a single ATA. + /// The ATA is created by the LightFinalize trait implementation + /// generated by the #[light_account(init, associated_token, ...)] macro. + #[allow(unused_variables)] + pub fn create_ata<'info>( + ctx: Context<'_, '_, '_, 'info, CreateAta<'info>>, + params: CreateAtaParams, + ) -> Result<()> { + // ATA creation is handled by the macro-generated LightFinalize implementation. + // Nothing to do here. + Ok(()) + } +} diff --git a/sdk-tests/single-ata-test/tests/test.rs b/sdk-tests/single-ata-test/tests/test.rs new file mode 100644 index 0000000000..7de9f960e5 --- /dev/null +++ b/sdk-tests/single-ata-test/tests/test.rs @@ -0,0 +1,181 @@ +//! Integration test for single ATA macro validation. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Setup helper: Creates a compressed mint directly using the ctoken SDK. +/// Returns (mint_pda, mint_seed_keypair) +async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + use light_token::instruction::{CreateMint, CreateMintParams}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +/// Test creating a single ATA using the macro. +/// Validates that #[light_account(init, associated_token, ...)] works in isolation. +#[tokio::test] +async fn test_create_single_ata() { + use single_ata_test::CreateAtaParams; + + let program_id = single_ata_test::ID; + let mut config = ProgramTestConfig::new_v2(true, Some(vec![("single_ata_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 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"); + + // Setup mint first + let (mint, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), // mint_authority + 9, // decimals + ) + .await; + + // The ATA owner will be the payer + let ata_owner = payer.pubkey(); + + // Derive the ATA address using Light Token SDK's derivation + let (ata, ata_bump) = light_token::instruction::derive_token_ata(&ata_owner, &mint); + + // Get proof (no PDA accounts for ATA-only instruction) + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + // Build instruction + let accounts = single_ata_test::accounts::CreateAta { + fee_payer: payer.pubkey(), + ata_mint: mint, + ata_owner, + ata, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + light_token_rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = single_ata_test::instruction::CreateAta { + params: CreateAtaParams { + create_accounts_proof: proof_result.create_accounts_proof, + ata_bump, + }, + }; + + 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]) + .await + .expect("CreateAta instruction should succeed"); + + // Verify ATA exists on-chain + let ata_account = rpc + .get_account(ata) + .await + .unwrap() + .expect("ATA should exist on-chain"); + + // Parse and verify token data + use light_token_interface::state::Token; + let token: Token = borsh::BorshDeserialize::deserialize(&mut &ata_account.data[..]) + .expect("Failed to deserialize Token"); + + // Verify owner + assert_eq!(token.owner, ata_owner.to_bytes(), "ATA owner should match"); + + // Verify mint + assert_eq!(token.mint, mint.to_bytes(), "ATA mint should match"); + + // Verify initial amount is 0 + assert_eq!(token.amount, 0, "ATA amount should be 0 initially"); +} diff --git a/sdk-tests/single-mint-test/Cargo.toml b/sdk-tests/single-mint-test/Cargo.toml new file mode 100644 index 0000000000..618df66e5b --- /dev/null +++ b/sdk-tests/single-mint-test/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "single-mint-test" +version = "0.1.0" +description = "Minimal Anchor program test for single mint macro validation" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "single_mint_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +custom-heap = ["light-heap", "light-sdk/custom-heap"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-heap = { workspace = true, optional = true } +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +light-macros = { workspace = true, features = ["solana"] } +light-sdk-macros = { workspace = true } +borsh = { workspace = true } +light-compressed-account = { workspace = true, features = ["solana"] } +light-hasher = { workspace = true, features = ["solana"] } +anchor-lang = { workspace = true, features = ["idl-build"] } +light-anchor-spl = { workspace = true, features = ["metadata", "idl-build"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true, features = ["anchor"] } +light-compressible = { workspace = true, features = ["anchor"] } +solana-program = { workspace = true } +solana-pubkey = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } +solana-account-info = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token-interface = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/single-mint-test/src/lib.rs b/sdk-tests/single-mint-test/src/lib.rs new file mode 100644 index 0000000000..27febcaf24 --- /dev/null +++ b/sdk-tests/single-mint-test/src/lib.rs @@ -0,0 +1,89 @@ +//! Minimal test program for #[light_account(init, mint, ...)] macro validation. +//! +//! This program tests ONLY the mint creation macro in isolation, +//! ensuring the simplest mint-only program compiles and works correctly. + +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::CpiSigner; + +declare_id!("Mint111111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("Mint111111111111111111111111111111111111111"); + +pub const MINT_SIGNER_SEED: &[u8] = b"mint_signer"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, +} + +/// Minimal accounts struct for testing single mint creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateMintParams)] +pub struct CreateMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority + #[account( + seeds = [MINT_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_account(init, mint, + mint_signer = mint_signer, + authority = fee_payer, + decimals = 9, + mint_seeds = &[MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] + )] + pub mint: UncheckedAccount<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + pub light_token_compressible_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod single_mint_test { + use super::*; + + /// Create a single mint. + /// The mint account is created by the LightFinalize trait implementation + /// generated by the #[light_account(init, mint, ...)] macro. + #[allow(unused_variables)] + pub fn create_mint<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMint<'info>>, + params: CreateMintParams, + ) -> Result<()> { + // Mint creation is handled by the macro-generated LightFinalize implementation. + // Nothing to do here. + Ok(()) + } +} diff --git a/sdk-tests/single-mint-test/tests/test.rs b/sdk-tests/single-mint-test/tests/test.rs new file mode 100644 index 0000000000..fb289a6e15 --- /dev/null +++ b/sdk-tests/single-mint-test/tests/test.rs @@ -0,0 +1,121 @@ +//! Integration test for single mint macro validation. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::interface::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{find_mint_address, COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Test creating a single mint using the macro. +/// Validates that #[light_account(init, mint, ...)] works in isolation. +#[tokio::test] +async fn test_create_single_mint() { + use single_mint_test::{CreateMintParams, MINT_SIGNER_SEED}; + + let program_id = single_mint_test::ID; + let mut config = ProgramTestConfig::new_v2(true, Some(vec![("single_mint_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 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"); + + let authority = Keypair::new(); + + // Derive PDA for mint signer + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDA + let (mint_pda, _) = find_mint_address(&mint_signer_pda); + + // Get proof for the mint + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + let accounts = single_mint_test::accounts::CreateMint { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + mint: mint_pda, + compression_config: config_pda, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = single_mint_test::instruction::CreateMint { + params: CreateMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }, + }; + + 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]) + .await + .expect("CreateMint should succeed"); + + // Verify mint exists on-chain + let mint_account = rpc + .get_account(mint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + // Parse and verify mint data + use light_token_interface::state::Mint; + let mint: Mint = borsh::BorshDeserialize::deserialize(&mut &mint_account.data[..]) + .expect("Failed to deserialize Mint"); + + // Verify decimals match what was specified in #[light_account(init)] + assert_eq!(mint.base.decimals, 9, "Mint should have 9 decimals"); + + // Verify mint authority + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); +} diff --git a/sdk-tests/single-pda-test/Cargo.toml b/sdk-tests/single-pda-test/Cargo.toml new file mode 100644 index 0000000000..4d8d9a40b2 --- /dev/null +++ b/sdk-tests/single-pda-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "single-pda-test" +version = "0.1.0" +description = "Minimal Anchor program test for single PDA macro validation" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "single_pda_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +custom-heap = ["light-heap", "light-sdk/custom-heap"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-heap = { workspace = true, optional = true } +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +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"] } +light-compressible = { workspace = true, features = ["anchor"] } +light-hasher = { workspace = true, features = ["solana"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true, features = ["anchor"] } +solana-program = { workspace = true } +solana-pubkey = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } +solana-account-info = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/single-pda-test/src/instruction_accounts.rs b/sdk-tests/single-pda-test/src/instruction_accounts.rs new file mode 100644 index 0000000000..c8ab500621 --- /dev/null +++ b/sdk-tests/single-pda-test/src/instruction_accounts.rs @@ -0,0 +1,36 @@ +//! Accounts module for single-pda-test. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::LightAccounts; + +use crate::state::MinimalRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreatePdaParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Minimal accounts struct for testing single PDA creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreatePdaParams)] +pub struct CreatePda<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + MinimalRecord::INIT_SPACE, + seeds = [b"minimal_record", params.owner.as_ref()], + bump, + )] + #[light_account(init)] + pub record: Account<'info, MinimalRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/single-pda-test/src/lib.rs b/sdk-tests/single-pda-test/src/lib.rs new file mode 100644 index 0000000000..54a2e3618a --- /dev/null +++ b/sdk-tests/single-pda-test/src/lib.rs @@ -0,0 +1,39 @@ +//! Minimal test program for #[light_account(init)] PDA macro validation. +//! +//! This program tests ONLY the compressible PDA creation macro in isolation, +//! ensuring the simplest PDA-only program compiles and works correctly. + +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::light_program; +use light_sdk_types::CpiSigner; + +pub mod instruction_accounts; +pub mod state; + +pub use instruction_accounts::*; +pub use state::*; + +declare_id!("PdaT111111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("PdaT111111111111111111111111111111111111111"); + +#[light_program] +#[program] +pub mod single_pda_test { + use super::*; + + /// Create a single compressible PDA. + /// The account is created by Anchor and made compressible by the + /// LightFinalize trait implementation generated by #[light_account(init)]. + pub fn create_pda<'info>( + ctx: Context<'_, '_, '_, 'info, CreatePda<'info>>, + params: CreatePdaParams, + ) -> Result<()> { + ctx.accounts.record.owner = params.owner; + Ok(()) + } +} diff --git a/sdk-tests/single-pda-test/src/state.rs b/sdk-tests/single-pda-test/src/state.rs new file mode 100644 index 0000000000..4f8bbf6dea --- /dev/null +++ b/sdk-tests/single-pda-test/src/state.rs @@ -0,0 +1,14 @@ +//! State module for single-pda-test. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::LightAccount; + +/// Minimal record struct for testing PDA creation. +/// Contains only compression_info and one field. +#[derive(Default, Debug, InitSpace, LightAccount)] +#[account] +pub struct MinimalRecord { + pub compression_info: Option, + pub owner: Pubkey, +} diff --git a/sdk-tests/single-pda-test/tests/test.rs b/sdk-tests/single-pda-test/tests/test.rs new file mode 100644 index 0000000000..b0bf6c32bc --- /dev/null +++ b/sdk-tests/single-pda-test/tests/test.rs @@ -0,0 +1,109 @@ +//! Integration test for single PDA macro validation. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::interface::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + ProgramTestConfig, Rpc, +}; +use light_token::instruction::RENT_SPONSOR; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Test creating a single compressible PDA using the macro. +/// Validates that #[light_account(init)] works in isolation for PDAs. +#[tokio::test] +async fn test_create_single_pda() { + use single_pda_test::CreatePdaParams; + + let program_id = single_pda_test::ID; + let mut config = ProgramTestConfig::new_v2(true, Some(vec![("single_pda_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 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"); + + let owner = Keypair::new().pubkey(); + + // Derive PDA for record + let (record_pda, _) = + Pubkey::find_program_address(&[b"minimal_record", owner.as_ref()], &program_id); + + // Get proof for the PDA + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::pda(record_pda)], + ) + .await + .unwrap(); + + let accounts = single_pda_test::accounts::CreatePda { + fee_payer: payer.pubkey(), + compression_config: config_pda, + record: record_pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = single_pda_test::instruction::CreatePda { + params: CreatePdaParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + 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]) + .await + .expect("CreatePda should succeed"); + + // Verify PDA exists on-chain + let record_account = rpc + .get_account(record_pda) + .await + .unwrap() + .expect("Record PDA should exist on-chain"); + + // Parse and verify record data + use single_pda_test::MinimalRecord; + let record: MinimalRecord = + borsh::BorshDeserialize::deserialize(&mut &record_account.data[8..]) + .expect("Failed to deserialize MinimalRecord"); + + // Verify owner field + assert_eq!(record.owner, owner, "Record owner should match"); + + // Verify compression_info is set (indicates compressible registration) + assert!( + record.compression_info.is_some(), + "Record should have compression_info set" + ); +} diff --git a/sdk-tests/single-token-test/Cargo.toml b/sdk-tests/single-token-test/Cargo.toml new file mode 100644 index 0000000000..211a65303e --- /dev/null +++ b/sdk-tests/single-token-test/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "single-token-test" +version = "0.1.0" +description = "Minimal Anchor program test for single token vault macro validation" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "single_token_test" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +custom-heap = ["light-heap", "light-sdk/custom-heap"] +default = [] +idl-build = ["anchor-lang/idl-build", "light-sdk/idl-build"] +test-sbf = [] + +[dependencies] +light-heap = { workspace = true, optional = true } +light-sdk = { workspace = true, features = ["anchor", "idl-build", "v2", "anchor-discriminator", "cpi-context"] } +light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } +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"] } +light-token = { workspace = true, features = ["anchor"] } +light-token-types = { workspace = true, features = ["anchor"] } +light-compressible = { workspace = true, features = ["anchor"] } +light-hasher = { workspace = true, features = ["solana"] } +solana-program = { workspace = true } +solana-pubkey = { workspace = true } +solana-msg = { workspace = true } +solana-program-error = { workspace = true } +solana-account-info = { workspace = true } + +[dev-dependencies] +light-program-test = { workspace = true, features = ["devenv"] } +light-client = { workspace = true, features = ["v2", "anchor"] } +light-test-utils = { workspace = true } +light-token-interface = { workspace = true } +tokio = { workspace = true } +solana-sdk = { workspace = true } +solana-instruction = { workspace = true } +solana-pubkey = { workspace = true } +solana-keypair = { workspace = true } +solana-signer = { workspace = true } + +[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/single-token-test/src/lib.rs b/sdk-tests/single-token-test/src/lib.rs new file mode 100644 index 0000000000..a6cad4093a --- /dev/null +++ b/sdk-tests/single-token-test/src/lib.rs @@ -0,0 +1,90 @@ +//! Minimal test program for #[light_account(init, token, ...)] macro validation. +//! +//! This program tests ONLY the token vault creation macro in isolation, +//! ensuring the simplest token-vault-only program compiles and works correctly. + +#![allow(deprecated)] + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk::derive_light_cpi_signer; +use light_sdk_macros::{light_program, LightAccounts}; +use light_sdk_types::CpiSigner; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR}; + +declare_id!("TknT111111111111111111111111111111111111111"); + +pub const LIGHT_CPI_SIGNER: CpiSigner = + derive_light_cpi_signer!("TknT111111111111111111111111111111111111111"); + +/// Seed for the vault authority PDA +pub const VAULT_AUTH_SEED: &[u8] = b"vault_auth"; +/// Seed for the vault token account PDA +pub const VAULT_SEED: &[u8] = b"vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTokenVaultParams { + pub create_accounts_proof: CreateAccountsProof, + /// Bump for the vault PDA (needed for invoke_signed) + pub vault_bump: u8, +} + +/// Minimal accounts struct for testing single token vault creation. +#[derive(Accounts, LightAccounts)] +#[instruction(params: CreateTokenVaultParams)] +pub struct CreateTokenVault<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + #[account( + seeds = [VAULT_AUTH_SEED], + bump, + )] + pub vault_authority: UncheckedAccount<'info>, + + /// Token vault account - macro should generate creation code. + /// The `authority` seeds must match the account's PDA seeds (including bump) for invoke_signed. + #[account( + mut, + seeds = [VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[light_account(init, token, authority = [VAULT_SEED, self.mint.key(), &[params.vault_bump]], mint = mint, owner = vault_authority)] + pub vault: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub light_token_compressible_config: AccountInfo<'info>, + + #[account(mut, address = LIGHT_TOKEN_RENT_SPONSOR)] + pub light_token_rent_sponsor: AccountInfo<'info>, + + /// CHECK: Light token CPI authority (required for token account creation) + pub light_token_cpi_authority: AccountInfo<'info>, + + /// CHECK: Light token program for CPI + pub light_token_program: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} + +#[light_program] +#[program] +pub mod single_token_test { + use super::*; + + /// Create a single token vault. + /// The token vault is created by the LightFinalize trait implementation + /// generated by the #[light_account(init, token, ...)] macro. + #[allow(unused_variables)] + pub fn create_token_vault<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTokenVault<'info>>, + params: CreateTokenVaultParams, + ) -> Result<()> { + // Token vault creation is handled by the macro-generated LightFinalize implementation. + // Nothing to do here. + Ok(()) + } +} diff --git a/sdk-tests/single-token-test/tests/test.rs b/sdk-tests/single-token-test/tests/test.rs new file mode 100644 index 0000000000..a2b7e87708 --- /dev/null +++ b/sdk-tests/single-token-test/tests/test.rs @@ -0,0 +1,186 @@ +//! Integration test for single token vault macro validation. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_client::interface::{get_create_accounts_proof, InitializeRentFreeConfig}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + Indexer, ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Setup helper: Creates a compressed mint directly using the ctoken SDK. +/// Returns (mint_pda, mint_seed_keypair) +async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, +) -> (Pubkey, Keypair) { + use light_token::instruction::{CreateMint, CreateMintParams}; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + let compression_address = light_token::instruction::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token::instruction::find_mint_address(&mint_seed.pubkey()); + + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + (mint, mint_seed) +} + +/// Test creating a single token vault using the macro. +/// Validates that #[light_account(init, token, ...)] works in isolation. +#[tokio::test] +async fn test_create_single_token_vault() { + use single_token_test::{CreateTokenVaultParams, VAULT_AUTH_SEED, VAULT_SEED}; + + let program_id = single_token_test::ID; + let mut config = ProgramTestConfig::new_v2(true, Some(vec![("single_token_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 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"); + + // Setup mint first + let (mint, _mint_seed) = setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), // mint_authority + 9, // decimals + ) + .await; + + // Derive PDAs + let (vault_authority, _auth_bump) = + Pubkey::find_program_address(&[VAULT_AUTH_SEED], &program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[VAULT_SEED, mint.as_ref()], &program_id); + + // Get proof (no PDA accounts for token-only instruction) + let proof_result = get_create_accounts_proof(&rpc, &program_id, vec![]) + .await + .unwrap(); + + // Build instruction + let accounts = single_token_test::accounts::CreateTokenVault { + fee_payer: payer.pubkey(), + mint, + vault_authority, + vault, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + light_token_rent_sponsor: RENT_SPONSOR, + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = single_token_test::instruction::CreateTokenVault { + params: CreateTokenVaultParams { + create_accounts_proof: proof_result.create_accounts_proof, + vault_bump, + }, + }; + + 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]) + .await + .expect("CreateTokenVault instruction should succeed"); + + // Verify token vault exists on-chain + let vault_account = rpc + .get_account(vault) + .await + .unwrap() + .expect("Token vault should exist on-chain"); + + // Parse and verify token data + use light_token_interface::state::Token; + let token: Token = borsh::BorshDeserialize::deserialize(&mut &vault_account.data[..]) + .expect("Failed to deserialize Token"); + + // Verify owner (should be vault_authority PDA) + assert_eq!( + token.owner, + vault_authority.to_bytes(), + "Token vault owner should be vault_authority" + ); + + // Verify mint + assert_eq!(token.mint, mint.to_bytes(), "Token vault mint should match"); + + // Verify initial amount is 0 + assert_eq!(token.amount, 0, "Token vault amount should be 0 initially"); +} From 058c0319aef0fc410cf041eb2ac5fcdede0b2115 Mon Sep 17 00:00:00 2001 From: ananas Date: Wed, 21 Jan 2026 00:10:14 +0000 Subject: [PATCH 3/3] fix ai feedback --- .../src/light_pdas/program/decompress.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/sdk-libs/macros/src/light_pdas/program/decompress.rs b/sdk-libs/macros/src/light_pdas/program/decompress.rs index da5b06bede..95366f1e7c 100644 --- a/sdk-libs/macros/src/light_pdas/program/decompress.rs +++ b/sdk-libs/macros/src/light_pdas/program/decompress.rs @@ -161,7 +161,28 @@ impl DecompressBuilder { // For mint-only or token-only programs, there are no PDA seeds - return empty Vec let pda_seed_specs = match self.pda_seeds.as_ref() { Some(specs) if !specs.is_empty() => specs, - _ => return Ok(Vec::new()), + _ => { + // Fail fast if pda_ctx_seeds has variants but pda_seeds is missing + if !self.pda_ctx_seeds.is_empty() { + let variant_names: Vec<_> = self + .pda_ctx_seeds + .iter() + .map(|v| v.variant_name.to_string()) + .collect(); + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!( + "generate_seed_provider_impls: pda_seeds is None/empty but \ + pda_ctx_seeds contains {} variant(s): [{}]. \ + Each pda_ctx_seeds variant requires a corresponding PDA seed \ + specification in pda_seeds.", + self.pda_ctx_seeds.len(), + variant_names.join(", ") + ), + )); + } + return Ok(Vec::new()); + } }; let mut results = Vec::with_capacity(self.pda_ctx_seeds.len());